on d769069 help sign in
webring/scripts/hallway.js
192 lines 7.0 kB history raw
1'use strict'
3function Hallway (sites) {
4 const feeds = {}
5 this.sites = sites
6 this.el = document.createElement('div')
7 this.el.id = 'hallway'
8 this.filter = window.location.hash.replace('#', '').trim()
9 this.cache = null
11 this.install = function (host) {
12 host.appendChild(this.el)
13 this.findFeeds()
14 }
16 this.start = function () {
17 this.el.innerHTML = 'hello.'
18 this.fetchFeeds()
19 }
21 this.refresh = function (feeds = this.cache) {
22 const entries = this.findEntries(feeds)
23 const channels = this.findChannels(entries)
24 const users = this.findUsers(entries)
26 this.el.innerHTML = `
27 <ul id='entries'>
28 ${entries.filter((val, id) => { return !(this.filter && val.author !== this.filter && val.channel !== this.filter) }).filter((val, id) => { return id < 50 }).reduce((acc, val, id) => { return acc + this.templateEntry(val) + '\n' }, '')}
29 </ul>
30 <div id='sidebar'>
31 <ul id='channels'>
32 <li onclick='filter("")' class='${hallway.filter === '' ? 'selected' : ''}'><a href='#'>hallway <span class='right'>${entries.length}</span></a></li>
33 ${Object.keys(channels).reduce((acc, val, id) => { return acc + `<li onclick='filter("${val}")' class='${hallway.filter === val ? 'selected' : ''}'><a href='#${val}'>${val} <span class='right'>${channels[val]}</span></a></li>\n` }, '')}
34 </ul>
35 <ul id='users'>
36 ${Object.keys(users).reduce((acc, val, id) => { return acc + `<li onclick='filter("${val}")' class='${hallway.filter === val ? 'selected' : ''}' href='#${val}'>${val} <span class='right'>${users[val]}</span></li>\n` }, '')}
37 </ul>
38 </div>
39 <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>`
41 if (feeds) {
42 this.cache = feeds
43 }
44 }
46 // Entries
48 this.findChannels = function (entries) {
49 const channels = {}
50 for (const id in entries) {
51 const entry = entries[id]
52 if (!entry.channel) { continue }
53 channels[entry.channel] = channels[entry.channel] ? channels[entry.channel] + 1 : 1
54 }
55 return channels
56 }
58 this.findUsers = function (entries) {
59 const users = {}
60 for (const id in entries) {
61 const entry = entries[id]
62 users[entry.author] = users[entry.author] ? users[entry.author] + 1 : 1
63 }
64 return users
65 }
67 this.findEntries = function (feeds) {
68 const a = []
69 for (const id in feeds) {
70 for (const i in feeds[id].content) {
71 a.push(feeds[id].content[i])
72 }
73 }
74 return a.sort(compare)
75 }
77 this.templateEntry = function (entry) {
78 const words = entry.body.split(' ')
79 const users = []
80 const channels = []
81 const tags = []
82 const links = []
84 // Channels
85 for (const id in words) {
86 if (words[id].substr(0, 1) === '@') { users.push(words[id]) }
87 if (words[id].substr(0, 1) === '/') { channels.push(words[id]) }
88 if (words[id].substr(0, 1) === '#') { tags.push(words[id]) }
89 if (words[id].substr(0, 4) === 'http') { links.push(words[id]) }
90 }
92 for (const id in users) { entry.body = entry.body.replace(users[id], `<span class='user'>${users[id]}</span>`) }
93 for (const id in channels) { entry.body = entry.body.replace(channels[id], `<span class='channel'>${channels[id]}</span>`) }
94 for (const id in tags) { entry.body = entry.body.replace(tags[id], `<span class='tag'>${tags[id]}</span>`) }
95 for (const id in links) { entry.body = entry.body.replace(links[id], `<a href='${links[id]}'>${links[id].replace('https://', '').replace('http://', '')}</a>`) }
97 const filter = window.location.hash.substr(1).replace(/\+/g, ' ').toLowerCase()
98 const highlight = filter === entry.author.toLowerCase()
99 const origin = feeds[entry.author].path
101 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.body}</span></li>`
102 }
104 // Feeds
106 this.findFeeds = function () {
107 console.log('Finding feeds..')
108 for (const id in sites) {
109 const site = sites[id]
110 if (site.feed && site.author) {
111 feeds[site.author] = { path: site.feed }
112 }
113 }
114 console.log(`Found ${Object.keys(feeds).length} feeds.`)
115 }
117 this.fetchFeeds = function () {
118 console.log(`Fetching ${Object.keys(feeds).length} feeds..`)
119 for (const id in feeds) {
120 this.fetchFeed(id, feeds[id])
121 }
122 }
124 this.fetchFeed = function (id, feed) {
125 console.log(`Fetching ${id}(${feed.path})..`)
126 fetch(feed.path, { cache: 'no-store' }).then(x => x.text()).then((content) => {
127 feeds[id].content = parseFeed(id, content)
128 this.refresh(feeds)
129 }).catch((err) => {
130 console.warn(`${id}`, err)
131 })
132 }
134 // Utils
136 function compare (a, b) {
137 return a.offset < b.offset ? -1 : a.offset > b.offset ? 1 : 0
138 }
140 function parseFeed (author, feed) {
141 const lines = feed.split('\n')
142 const entries = []
143 for (const id in lines) {
144 const line = lines[id].trim()
145 if (line === '') { continue }
146 const parts = line.replace(' ', '\t').split('\t')
147 const date = parts[0].trim()
148 const body = escapeHtml(parts[1].trim()).trim()
149 const channel = body.substr(0, 1) === '/' ? body.split(' ')[0].substr(1).toLowerCase() : body.substr(0, 1) === '@' ? 'veranda' : 'lobby'
150 const offset = new Date() - new Date(date)
151 entries.push({ date, body, author, offset, channel })
152 }
153 return entries
154 }
156 function timeAgo (dateParam) {
157 const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam)
158 const DAY_IN_MS = 86400000 // 24 * 60 * 60 * 1000
159 const today = new Date()
160 const yesterday = new Date(today - DAY_IN_MS)
161 const seconds = Math.round((today - date) / 1000)
162 const minutes = Math.round(seconds / 60)
163 const isToday = today.toDateString() === date.toDateString()
164 const isYesterday = yesterday.toDateString() === date.toDateString()
166 if (seconds < 5) {
167 return 'just now'
168 } else if (seconds < 60) {
169 return `${seconds} seconds ago`
170 } else if (seconds < 90) {
171 return 'a minute ago'
172 } else if (minutes < 60) {
173 return `${minutes} minutes ago`
174 } else if (isToday) {
175 return `${Math.floor(minutes / 60)} hours ago`
176 } else if (isYesterday) {
177 return 'yesterday'
178 }
180 return `${Math.floor(minutes / 1440)} days ago`
181 }
183 function escapeHtml (unsafe) {
184 return unsafe.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;')
185 }
188function filter (name) {
189 window.location.hash = name
190 hallway.filter = window.location.hash.replace('#', '').trim()
191 hallway.refresh()