on 3d3eec8 help sign in
webring/scripts/hallway.js
203 lines 8.1 kB history raw
1'use strict'
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) {
9 const feeds = {}
10 this.sites = sites
11 this.el = document.createElement('div')
12 this.el.id = 'hallway'
13 const filterAndPage = window.location.hash.split('^')
14 this.finder = { filter: filterAndPage[0].trim().replace('#', ''), page: filterAndPage[1] || 1 }
15 this.cache = null
17 this.install = function (host) {
18 host.appendChild(this.el)
19 this.findFeeds()
20 }
22 this.start = function () {
23 this.el.innerHTML = 'hello.'
24 this.fetchFeeds()
25 }
27 this.refresh = function (feeds = this.cache) {
28 const entries = this.findEntries(feeds)
29 const channels = this.find(entries, 'channel')
30 const users = this.find(entries, 'author')
31 const tags = this.findTags(entries)
32 const relevantEntries = entries.filter(val => !this.finder.filter || (val.author === this.finder.filter || val.channel === this.finder.filter || val.tags.includes(this.finder.filter)))
34 this.el.innerHTML = `
35 <div id='entries'>
36 <ul>
37 ${entries.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', '')}
38 </ul>
39 <div id='pagination'>
40 ${[...Array(Math.ceil(relevantEntries.length / 20)).keys()].reduce((acc, num) => `${acc}<span class='${Number(hallway.finder.page) === num + 1 ? 'selected' : ''}' onclick='filter("${this.finder.filter}^${num + 1}")'>${num + 1}</span>`, '')}
41 </div>
42 </div>
43 <a id='showbar' onclick="toggleVisibility('sidebar');"></a>
44 <div id='sidebar'>
45 <a id='hidebar' onclick="toggleVisibility('sidebar');"></a>
46 <ul id='channels'>
47 <li onclick='filter("")' class='${hallway.finder.filter === '' ? 'selected' : ''}'><a href='#'>hallway <span class='right'>${entries.length}</span></a></li>
48 ${Object.keys(channels).slice(0, 15).reduce((acc, val) => acc + `<li onclick='filter("${val}")' class='${hallway.finder.filter === val ? 'selected' : ''}'><a href='#${val}'>${val} <span class='right'>${channels[val]}</span></a></li>\n`, '')}
49 </ul>
50 <ul id='users'>
51 ${Object.keys(users).slice(0, 15).reduce((acc, val) => acc + `<li onclick='filter("${val}")' class='${hallway.finder.filter === val ? 'selected' : ''}' href='#${val}'>${val} <span class='right'>${users[val]}</span></li>\n`, '')}
52 </ul>
53 <ul id='tags'>
54 ${Object.keys(tags).slice(0, 15).reduce((acc, val) => acc + `<li onclick='filter("${val}")' class='${hallway.finder.filter === val ? 'selected' : ''}' href='#${val}'>#${val} <span class='right'>${tags[val]}</span></li>\n`, '')}
55 </ul>
56 </div>
57 <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>`
59 if (feeds) {
60 this.cache = feeds
61 }
62 }
64 // Entries
66 this.find = function (entries, p) {
67 const h = {}
68 for (const id in entries) {
69 if (entries[id] && entries[id][p]) { h[entries[id][p]] = h[entries[id][p]] ? h[entries[id][p]] + 1 : 1 }
70 }
71 return h
72 }
74 this.findTags = function (entries) {
75 const tags = {}
76 for (const id in entries) {
77 const entry = entries[id]
78 entry.tags.map(tag => {
79 tags[tag] = tags[tag] ? tags[tag] + 1 : 1
80 })
81 }
82 return tags
83 }
85 this.findEntries = function (feeds) {
86 const a = []
87 for (const id in feeds) {
88 for (const i in feeds[id].content) {
89 a.push(feeds[id].content[i])
90 }
91 }
92 return a.sort((a, b) => a.offset - b.offset)
93 }
95 this.findMention = function (found) {
96 const mention = Object.keys(feeds).filter(user => found.indexOf(feeds[user].path) > -1)
97 return mention.length === 1 ? ` <span class='user'>@${mention[0]}</span>` : ` <span class='user'>${found}</span>`
98 }
100 this.templateEntry = function (entry) {
101 entry.html = entry.body
102 .replace(re_channel, `$1<span class='channel'>/$2</span>`)
103 .replace(re_user, this.findMention)
104 .replace(re_tag, `$1<span class='tag'>#$2</span>`)
105 .replace(re_url, `<a target="_blank" href='$1'>$1</a>`)
107 const filter = window.location.hash.substr(1).replace(/\+/g, ' ').toLowerCase()
108 const highlight = filter === entry.author.toLowerCase()
109 const origin = feeds[entry.author].path
111 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>`
112 }
114 // Feeds
116 this.findFeeds = function () {
117 console.log('Finding feeds..')
118 for (const site of sites) {
119 if (site.feed && site.author) {
120 feeds[site.author] = { path: site.feed }
121 }
122 }
123 console.log(`Found ${Object.keys(feeds).length} feeds.`)
124 }
126 this.fetchFeeds = function () {
127 console.log(`Fetching ${Object.keys(feeds).length} feeds..`)
128 for (const id in feeds) {
129 this.fetchFeed(id, feeds[id])
130 }
131 }
133 this.fetchFeed = function (id, feed) {
134 console.log(`Fetching ${id}(${feed.path})..`)
135 fetch(feed.path, { cache: 'no-store' }).then(x => x.text()).then((content) => {
136 feeds[id].content = parseFeed(id, content)
137 this.refresh(feeds)
138 }).catch((err) => {
139 console.warn(`${id}`, err)
140 })
141 }
143 // Utils
145 function parseFeed (author, feed) {
146 const lines = feed.split('\n').filter((line) => line.substr(0, 1) !== '#')
147 const entries = []
148 for (const id in lines) {
149 const line = lines[id].trim()
150 if (line === '') { continue }
151 const parts = line.replace(' ', '\t').split('\t')
152 const date = parts[0].trim()
153 const body = escapeHtml(parts[1].trim()).trim()
154 const channel = body.substr(0, 1) === '/' ? body.split(' ')[0].substr(1).toLowerCase() : body.substr(0, 1) === '@' ? 'veranda' : 'lobby'
155 const tags = (body.match(re_tag) || []).map(a => a.substr(a.indexOf('#') + 1).toLowerCase())
156 const offset = new Date() - new Date(date)
157 entries.push({ date, body, author, offset, channel, tags })
158 }
159 return entries
160 }
162 function timeAgo (dateParam) {
163 const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam)
164 const today = new Date()
165 const yesterday = new Date(today - 86400000)
166 const seconds = Math.round((today - date) / 1000)
167 const minutes = Math.round(seconds / 60)
168 const isToday = today.toDateString() === date.toDateString()
169 const isYesterday = yesterday.toDateString() === date.toDateString()
171 if (seconds < 5) {
172 return 'just now'
173 } else if (seconds < 60) {
174 return `${seconds} seconds ago`
175 } else if (seconds < 90) {
176 return 'a minute ago'
177 } else if (minutes < 60) {
178 return `${minutes} minutes ago`
179 } else if (isToday) {
180 return `${Math.floor(minutes / 60)} hours ago`
181 } else if (isYesterday) {
182 return 'yesterday'
183 }
185 return `${Math.floor(minutes / 1440)} days ago`
186 }
189function toggleVisibility (id) {
190 var e = document.getElementById(id)
191 if (e.style.display == 'block') { e.style.display = 'none' } else { e.style.display = 'block' }
194function escapeHtml (unsafe) {
195 return unsafe.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;')
198function filter (name) {
199 window.location.hash = name
200 const filterAndPage = window.location.hash.split('^')
201 hallway.finder = { filter: filterAndPage[0].trim().replace('#', ''), page: filterAndPage[1] || 1 }
202 hallway.refresh()