3const re_channel = /(\s|^)\/([a-zA-Z0-9]+)/g
4const re_user = /((\s|^)@<[a-zA-Z0-9./:\-_\+~#= ]*>)/g
5const re_tag = /([^&]|^)\#([a-zA-Z0-9]+)/g
6const re_url = /((https?):\/\/(([-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b)([-a-zA-Z0-9@:%_\+.~#?&//=]*)))/g
8function Hallway (sites) {
11 this.el = document.createElement('div')
12 this.el.id = 'hallway'
13 this.filter = window.location.hash.replace('#', '').trim()
16 this.install = function (host) {
17 host.appendChild(this.el)
21 this.start = function () {
22 this.el.innerHTML = 'hello.'
26 this.refresh = function (feeds = this.cache) {
27 const entries = this.findEntries(feeds)
28 const channels = this.findChannels(entries)
29 const users = this.findUsers(entries)
30 const tags = this.findTags(entries)
34 ${entries.filter((val) => { return !this.filter || (val.author === this.filter || val.channel === this.filter || val.tags.includes(this.filter)) }).filter((_, id) => { return id < 50 }).reduce((acc, val) => { return acc + this.templateEntry(val) + '\n' }, '')}
38 <li onclick='filter("")' class='${hallway.filter === '' ? 'selected' : ''}'><a href='#'>hallway <span class='right'>${entries.length}</span></a></li>
39 ${Object.keys(channels).reduce((acc, val) => { return acc + `<li onclick='filter("${val}")' class='${hallway.filter === val ? 'selected' : ''}'><a href='#${val}'>${val} <span class='right'>${channels[val]}</span></a></li>\n` }, '')}
42 ${Object.keys(users).reduce((acc, val) => { return acc + `<li onclick='filter("${val}")' class='${hallway.filter === val ? 'selected' : ''}' href='#${val}'>${val} <span class='right'>${users[val]}</span></li>\n` }, '')}
45 ${Object.keys(tags).reduce((acc, val) => { return acc + `<li onclick='filter("${val}")' class='${hallway.filter === val ? 'selected' : ''}' href='#${val}'>#${val} <span class='right'>${tags[val]}</span></li>\n` }, '')}
48 <p>The <b>Hallway</b> is a decentralized forum, to join the conversation, simply create yourself a <a href="https://twtxt.readthedocs.io/en/stable/user/twtxtfile.html">twtxt</a> feed and <a href="https://github.com/XXIIVV/Webring/">add it</a> to your entry in the <a href="index.html">webring</a>.</p>`
57 this.findChannels = function (entries) {
59 for (const id in entries) {
60 const entry = entries[id]
61 if (!entry.channel) { continue }
62 channels[entry.channel] = channels[entry.channel] ? channels[entry.channel] + 1 : 1
67 this.findUsers = function (entries) {
69 for (const id in entries) {
70 const entry = entries[id]
71 users[entry.author] = users[entry.author] ? users[entry.author] + 1 : 1
76 this.findTags = function (entries){
78 for (const id in entries) {
79 const entry = entries[id]
80 entry.tags.map(tag => {
81 tags[tag] = tags[tag] ? tags[tag] + 1 : 1
87 this.findEntries = function (feeds) {
89 for (const id in feeds) {
90 for (const i in feeds[id].content) {
91 a.push(feeds[id].content[i])
94 return a.sort( (a, b) => a.offset - b.offset )
97 this.findMention = function (found) {
98 const mention = Object.keys(feeds).filter(user => found.indexOf(feeds[user].path) > -1)
99 return mention.length === 1
100 ? ` <span class='user'>@${mention[0]}</span>`
101 : ` <span class='user'>${found}</span>`
104 this.templateEntry = function (entry) {
105 entry.html = entry.body
106 .replace(re_channel, `$1<span class='channel'>/$2</span>`)
107 .replace(re_user, this.findMention)
108 .replace(re_tag, `$1<span class='tag'>#$2</span>`)
109 .replace(re_url, `<a target="_blank" href='$1'>$1</a>`)
111 const filter = window.location.hash.substr(1).replace(/\+/g, ' ').toLowerCase()
112 const highlight = filter === entry.author.toLowerCase()
113 const origin = feeds[entry.author].path
115 return `<li class='entry ${highlight ? 'highlight' : ''}'><span class='date'>${timeAgo(Date.parse(entry.date))}</span> <a class='author' href='${origin}' target='_blank'>${entry.author}</a> <span class='body'>${entry.html}</span></li>`
120 this.findFeeds = function () {
121 console.log('Finding feeds..')
122 for (const id in sites) {
123 const site = sites[id]
124 if (site.feed && site.author) {
125 feeds[site.author] = { path: site.feed }
128 console.log(`Found ${Object.keys(feeds).length} feeds.`)
131 this.fetchFeeds = function () {
132 console.log(`Fetching ${Object.keys(feeds).length} feeds..`)
133 for (const id in feeds) {
134 this.fetchFeed(id, feeds[id])
138 this.fetchFeed = function (id, feed) {
139 console.log(`Fetching ${id}(${feed.path})..`)
140 fetch(feed.path, { cache: 'no-store' }).then(x => x.text()).then((content) => {
141 feeds[id].content = parseFeed(id, content)
144 console.warn(`${id}`, err)
150 function parseFeed (author, feed) {
151 const lines = feed.split('\n').filter((line) => { return line.substr(0,1) !== '#' })
153 for (const id in lines) {
154 const line = lines[id].trim()
155 if (line === '') { continue }
156 const parts = line.replace(' ', '\t').split('\t')
157 const date = parts[0].trim()
158 const body = escapeHtml(parts[1].trim()).trim()
159 const channel = body.substr(0, 1) === '/' ? body.split(' ')[0].substr(1).toLowerCase() : body.substr(0, 1) === '@' ? 'veranda' : 'lobby'
160 const tags = (body.match(re_tag)||[]).map(a => a.substr(a.indexOf('#')+1).toLowerCase())
161 const offset = new Date() - new Date(date)
162 entries.push({ date, body, author, offset, channel, tags })
167 function timeAgo (dateParam) {
168 const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam)
169 const DAY_IN_MS = 86400000 // 24 * 60 * 60 * 1000
170 const today = new Date()
171 const yesterday = new Date(today - DAY_IN_MS)
172 const seconds = Math.round((today - date) / 1000)
173 const minutes = Math.round(seconds / 60)
174 const isToday = today.toDateString() === date.toDateString()
175 const isYesterday = yesterday.toDateString() === date.toDateString()
179 } else if (seconds < 60) {
180 return `${seconds} seconds ago`
181 } else if (seconds < 90) {
182 return 'a minute ago'
183 } else if (minutes < 60) {
184 return `${minutes} minutes ago`
185 } else if (isToday) {
186 return `${Math.floor(minutes / 60)} hours ago`
187 } else if (isYesterday) {
191 return `${Math.floor(minutes / 1440)} days ago`
194 function escapeHtml (unsafe) {
195 return unsafe.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
199function filter (name) {
200 window.location.hash = name
201 hallway.filter = window.location.hash.replace('#', '').trim()
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list