'use strict' const reChannel = /(\s|^)\/([a-zA-Z0-9]+)/g const reUser = /((\s|^)@<[a-zA-Z0-9./:\-_+~#= ]*>)/g const reTag = /(^|\s)(#[a-z\d-]+)/ig const reUrl = /((https?):\/\/(([-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b)([-a-zA-Z0-9@:%_+.~#?&//=]*)))/g function Hallway (sites) { const feeds = {} this.sites = sites this._el = document.createElement('div') this._el.id = 'hallway' const filterAndPage = window.location.hash.split('^') this.finder = { filter: stripHash(filterAndPage[0]), page: filterAndPage[1] || 1 } this.cache = null this._main = document.createElement('main') this._main.id = 'entries' this._footer = document.createElement('footer') this._footer.id = 'footer' this.readme = `

The Hallway is a decentralized forum, to join the conversation, add a feed: field to your entry in the webring.

` this.nav = `

Information | The Portal | The Wiki | OPML

` this._footer.innerHTML = `${this.readme}${this.nav}` this.install = function (host) { this._el.appendChild(this._main) this._el.appendChild(this._footer) host.appendChild(this._el) this.findFeeds() } this.start = function () { this._main.innerHTML = 'Loading..' this.fetchFeeds() } this.refresh = function (feeds = this.cache) { const entries = this.findEntries(feeds) const localEntries = entries.filter(entry => this.filterExternals(entry)) const channels = this.find(localEntries, 'channel') const users = this.find(localEntries, 'author') const tags = this.findTags(localEntries) const relevantEntries = localEntries.filter(val => !this.finder.filter || (val.author === this.finder.filter || val.channel === this.finder.filter || val.tags.includes(this.finder.filter))) const mainHtml = `
` const aside = ` ` this._main.innerHTML = `${mainHtml}${aside}` if (feeds) { this.cache = feeds } } // Entries this.filterExternals = function (entry) { const matches = entry.body.match(reUser) const locals = Object.keys(feeds) return matches ? matches.some(match => locals.some(local => match.indexOf(feeds[local].path) !== -1)) : true } this.find = function (entries, key) { const h = {} for (const entry of entries) { if (entry && entry[key]) { h[entry[key]] = h[entry[key]] ? h[entry[key]] + 1 : 1 } } return h } this.findTags = function (entries) { const tags = {} for (const entry of entries) { entry.tags.map(tag => { tags[tag] = tags[tag] ? tags[tag] + 1 : 1 }) } return tags } this.findEntries = function (feeds) { const a = [] for (const id in feeds) { for (const i in feeds[id].content) { a.push(feeds[id].content[i]) } } return a.sort((a, b) => a.offset - b.offset) } this.findMention = function (found) { const mention = Object.keys(feeds).filter(user => found.indexOf(feeds[user].path) > -1) return ` ${mention[0]}` } this.templateEntry = function (entry) { entry.html = entry.body .replace(reChannel, `$1/$2`) .replace(reUser, this.findMention) .replace(reTag, '$1$2') .replace(reUrl, `$1`) const filter = window.location.hash.substr(1).replace(/\+/g, ' ').toLowerCase() const highlight = filter === entry.author.toLowerCase() const origin = feeds[entry.author].path return `
  • ${timeAgo(Date.parse(entry.date))} ${entry.author} ${entry.html}
  • ` } // Feeds this.findFeeds = function () { console.log('Finding feeds..') for (const site of sites) { if (site.feed && site.author) { feeds[site.author] = { path: site.feed } } } console.log(`Found ${Object.keys(feeds).length} feeds.`) } this.fetchFeeds = function () { console.log(`Fetching ${Object.keys(feeds).length} feeds..`) for (const id in feeds) { this.fetchFeed(id, feeds[id]) } } this.fetchFeed = function (id, feed) { console.log(`Fetching ${id}(${feed.path})..`) fetch(feed.path, { cache: 'no-store' }).then(x => x.text()).then((content) => { feeds[id].content = parseFeed(id, content) this.refresh(feeds) }).catch((err) => { console.warn(`${id}`, err) }) } // Utils function parseFeed (author, feed) { const lines = feed.split('\n').filter((line) => line.substr(0, 1) !== '#') const entries = [] for (const id in lines) { const line = lines[id].trim() if (line === '') { continue } const parts = line.replace(' ', '\t').split('\t') const date = parts[0].trim() const body = escapeHtml(parts[1].trim()).trim() const channel = body.substr(0, 1) === '/' ? body.split(' ')[0].substr(1).toLowerCase() : body.substr(0, 1) === '@' ? 'veranda' : 'lobby' const tags = (body.match(reTag) || []).map(a => a.substr(a.indexOf('#') + 1).toLowerCase()) const offset = new Date() - new Date(date) entries.push({ date, body, author, offset, channel, tags }) } return entries } function timeAgo (dateParam) { const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam) const today = new Date() const yesterday = new Date(today - 86400000) const seconds = Math.round((today - date) / 1000) const minutes = Math.round(seconds / 60) const isToday = today.toDateString() === date.toDateString() const isYesterday = yesterday.toDateString() === date.toDateString() if (seconds < 5) { return 'just now' } else if (seconds < 60) { return `${seconds} seconds ago` } else if (seconds < 90) { return 'a minute ago' } else if (minutes < 60) { return `${minutes} minutes ago` } else if (isToday) { return `${Math.floor(minutes / 60)} hours ago` } else if (isYesterday) { return 'yesterday' } return `${Math.floor(minutes / 1440)} days ago` } } function toggleVisibility (id) { const e = document.getElementById(id) if (e.style.display === 'block') { e.style.display = 'none' } else { e.style.display = 'block' } } function escapeHtml (unsafe) { return unsafe.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''') } function stripHash (hash) { const decoded = decodeURIComponent(hash) return decoded.charAt(0) === '#' ? decoded.substring(1) : decoded } function filter (name) { window.location.hash = name const filterAndPage = window.location.hash.split('^') hallway.finder = { filter: stripHash(filterAndPage[0]), page: filterAndPage[1] || 1 } hallway.refresh() }