'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 = `
${localEntries.filter((val) => !this.finder.filter || (val.author === this.finder.filter || val.channel === this.finder.filter || val.tags.includes(this.finder.filter))).filter((_, id) => id < Number(this.finder.page) * 20 && id >= (Number(this.finder.page) - 1) * 20).reduce((acc, val) => acc + this.templateEntry(val) + '\n', '')}
`
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()
}