Files: 01729a41ce366a9c658652fdbc3a6793b0c89de5 / views / public-feed.js
8011 bytesRaw
1 | var SortedArray = require('sorted-array-functions') |
2 | var Value = require('@mmckegg/mutant/value') |
3 | var h = require('@mmckegg/mutant/html-element') |
4 | var when = require('@mmckegg/mutant/when') |
5 | var computed = require('@mmckegg/mutant/computed') |
6 | var MutantArray = require('@mmckegg/mutant/array') |
7 | var pullPushable = require('pull-pushable') |
8 | var pullNext = require('pull-next') |
9 | var Scroller = require('../lib/pull-scroll') |
10 | var Abortable = require('pull-abortable') |
11 | |
12 | var m = require('../lib/h') |
13 | |
14 | var pull = require('pull-stream') |
15 | |
16 | var plugs = require('patchbay/plugs') |
17 | var message_render = plugs.first(exports.message_render = []) |
18 | var message_compose = plugs.first(exports.message_compose = []) |
19 | var sbot_log = plugs.first(exports.sbot_log = []) |
20 | var avatar_name = plugs.first(exports.avatar_name = []) |
21 | var avatar_link = plugs.first(exports.avatar_link = []) |
22 | var message_link = plugs.first(exports.message_link = []) |
23 | |
24 | exports.screen_view = function (path, sbot) { |
25 | if (path === '/public') { |
26 | var sync = Value(false) |
27 | var updates = Value(0) |
28 | |
29 | var updateLoader = m('a.loader', { |
30 | href: '#', |
31 | 'ev-click': refresh |
32 | }, [ |
33 | 'Show ', |
34 | h('strong', [updates]), ' ', |
35 | when(computed(updates, a => a === 1), 'update', 'updates') |
36 | ]) |
37 | |
38 | var content = h('div.column.scroller__content') |
39 | |
40 | var scrollElement = h('div.column.scroller', { |
41 | style: { |
42 | 'overflow': 'auto' |
43 | } |
44 | }, [ |
45 | h('div.scroller__wrapper', [ |
46 | message_compose({type: 'post'}, {placeholder: 'Write a public message'}), |
47 | content |
48 | ]) |
49 | ]) |
50 | |
51 | setTimeout(refresh, 10) |
52 | |
53 | pull( |
54 | sbot_log({old: false}), |
55 | pull.drain((item) => { |
56 | if (item.value.content.type !== 'vote') { |
57 | updates.set(updates() + 1) |
58 | } |
59 | }) |
60 | ) |
61 | |
62 | var abortLastFeed = null |
63 | |
64 | return MutantArray([ |
65 | when(updates, updateLoader), |
66 | when(sync, scrollElement, m('Loading -large')) |
67 | ]) |
68 | } |
69 | |
70 | // scoped |
71 | function refresh () { |
72 | if (abortLastFeed) { |
73 | abortLastFeed() |
74 | } |
75 | updates.set(0) |
76 | sync.set(false) |
77 | content.innerHTML = '' |
78 | |
79 | var abortable = Abortable() |
80 | abortLastFeed = abortable.abort |
81 | |
82 | pull( |
83 | FeedSummary(sbot_log, 100, () => { |
84 | sync.set(true) |
85 | }), |
86 | abortable, |
87 | Scroller(scrollElement, content, renderItem, false, false) |
88 | ) |
89 | } |
90 | } |
91 | |
92 | function FeedSummary (stream, windowSize, cb) { |
93 | var last = null |
94 | var returned = false |
95 | return pullNext(() => { |
96 | var next = {reverse: true, limit: windowSize, live: false} |
97 | if (last) { |
98 | next.lt = last.timestamp |
99 | } |
100 | var pushable = pullPushable() |
101 | pull( |
102 | sbot_log(next), |
103 | pull.collect((err, values) => { |
104 | if (err) throw err |
105 | groupMessages(values).forEach(v => pushable.push(v)) |
106 | last = values[values.length - 1] |
107 | pushable.end() |
108 | if (!returned) cb && cb() |
109 | returned = true |
110 | }) |
111 | ) |
112 | return pushable |
113 | }) |
114 | } |
115 | |
116 | function renderItem (item) { |
117 | if (item.type === 'message') { |
118 | var meta = null |
119 | var replies = item.replies.slice(-3).map(message_render) |
120 | var renderedMessage = item.message ? message_render(item.message) : null |
121 | if (renderedMessage) { |
122 | if (item.lastUpdateType === 'reply' && item.repliesFrom.size) { |
123 | meta = m('div.meta', [ |
124 | manyPeople(item.repliesFrom), ' replied' |
125 | ]) |
126 | } else if (item.lastUpdateType === 'dig' && item.digs.size) { |
127 | meta = m('div.meta', [ |
128 | manyPeople(item.digs), ' dug this message' |
129 | ]) |
130 | } |
131 | |
132 | return m('FeedEvent', [ |
133 | meta, |
134 | renderedMessage, |
135 | when(replies.length, [ |
136 | when(item.replies.length > replies.length, |
137 | m('a.full', {href: `#${item.messageId}`}, ['View full thread']) |
138 | ), |
139 | m('div.replies', replies) |
140 | ]) |
141 | ]) |
142 | } else { |
143 | if (item.lastUpdateType === 'reply' && item.repliesFrom.size) { |
144 | meta = m('div.meta', [ |
145 | manyPeople(item.repliesFrom), ' replied to ', message_link(item.messageId) |
146 | ]) |
147 | } else if (item.lastUpdateType === 'dig' && item.digs.size) { |
148 | meta = m('div.meta', [ |
149 | manyPeople(item.digs), ' dug ', message_link(item.messageId) |
150 | ]) |
151 | } |
152 | |
153 | if (meta || replies.length) { |
154 | return m('FeedEvent', [ |
155 | meta, m('div.replies', replies) |
156 | ]) |
157 | } |
158 | } |
159 | } else if (item.type === 'follow') { |
160 | return m('FeedEvent -follow', [ |
161 | m('div.meta', [ |
162 | person(item.id), ' followed ', manyPeople(item.contacts) |
163 | ]) |
164 | ]) |
165 | } |
166 | |
167 | return h('div') |
168 | } |
169 | |
170 | function person (id) { |
171 | return avatar_link(id, avatar_name(id), '') |
172 | } |
173 | |
174 | function manyPeople (ids) { |
175 | ids = Array.from(ids) |
176 | var featuredIds = ids.slice(-3).reverse() |
177 | |
178 | if (ids.length) { |
179 | if (ids.length > 3) { |
180 | return [ |
181 | person(featuredIds[0]), ', ', |
182 | person(featuredIds[1]), |
183 | ' and ', ids.length - 2, ' others' |
184 | ] |
185 | } else if (ids.length === 3) { |
186 | return [ |
187 | person(featuredIds[0]), ', ', |
188 | person(featuredIds[1]), ' and ', |
189 | person(featuredIds[2]) |
190 | ] |
191 | } else if (ids.length === 2) { |
192 | return [ |
193 | person(featuredIds[0]), ' and ', |
194 | person(featuredIds[1]) |
195 | ] |
196 | } else { |
197 | return person(featuredIds[0]) |
198 | } |
199 | } |
200 | } |
201 | |
202 | function groupMessages (messages) { |
203 | var follows = {} |
204 | var messageUpdates = {} |
205 | reverseForEach(messages, function (msg) { |
206 | var c = msg.value.content |
207 | if (c.type === 'contact') { |
208 | updateContact(msg, follows) |
209 | } else if (c.type === 'vote') { |
210 | if (c.vote && c.vote.link) { |
211 | // only show digs of posts added in the current window |
212 | // and only for the main post |
213 | const group = messageUpdates[c.vote.link] |
214 | if (group) { |
215 | if (c.vote.value > 0) { |
216 | group.lastUpdateType = 'dig' |
217 | group.digs.add(msg.value.author) |
218 | group.updated = msg.timestamp |
219 | } else { |
220 | group.digs.delete(msg.value.author) |
221 | if (group.lastUpdateType === 'dig' && !group.digs.size && !group.replies.length) { |
222 | group.lastUpdateType = 'reply' |
223 | } |
224 | } |
225 | } |
226 | } |
227 | } else { |
228 | if (c.root) { |
229 | const group = ensureMessage(c.root, messageUpdates) |
230 | group.lastUpdateType = 'reply' |
231 | group.repliesFrom.add(msg.value.author) |
232 | group.replies.push(msg) |
233 | group.updated = msg.timestamp |
234 | } else { |
235 | const group = ensureMessage(msg.key, messageUpdates) |
236 | group.lastUpdateType = 'post' |
237 | group.updated = msg.timestamp |
238 | group.message = msg |
239 | } |
240 | } |
241 | }) |
242 | |
243 | var result = [] |
244 | Object.keys(follows).forEach((key) => { |
245 | SortedArray.add(result, follows[key], compareUpdated) |
246 | }) |
247 | Object.keys(messageUpdates).forEach((key) => { |
248 | SortedArray.add(result, messageUpdates[key], compareUpdated) |
249 | }) |
250 | return result |
251 | } |
252 | |
253 | function compareUpdated (a, b) { |
254 | return b.updated - a.updated |
255 | } |
256 | |
257 | function reverseForEach (items, fn) { |
258 | var i = items.length - 1 |
259 | var start = Date.now() |
260 | nextBatch() |
261 | |
262 | function nextBatch () { |
263 | while (i >= 0 && Date.now() - start < 10) { |
264 | fn(items[i], i) |
265 | i -= 1 |
266 | } |
267 | |
268 | if (i > 0) { |
269 | setImmediate(nextBatch) |
270 | } |
271 | } |
272 | } |
273 | |
274 | function updateContact (msg, groups) { |
275 | var c = msg.value.content |
276 | var id = msg.value.author |
277 | var group = groups[id] |
278 | if (c.following) { |
279 | if (!group) { |
280 | group = groups[id] = { |
281 | type: 'follow', |
282 | lastUpdateType: null, |
283 | contacts: new Set(), |
284 | updated: 0, |
285 | id: id |
286 | } |
287 | } |
288 | group.contacts.add(c.contact) |
289 | group.updated = msg.timestamp |
290 | } else { |
291 | if (group) { |
292 | group.contacts.delete(c.contact) |
293 | if (!group.contacts.size) { |
294 | delete groups[id] |
295 | } |
296 | } |
297 | } |
298 | } |
299 | |
300 | function ensureMessage (id, groups) { |
301 | var group = groups[id] |
302 | if (!group) { |
303 | group = groups[id] = { |
304 | type: 'message', |
305 | repliesFrom: new Set(), |
306 | replies: [], |
307 | message: null, |
308 | messageId: id, |
309 | digs: new Set(), |
310 | updated: 0 |
311 | } |
312 | } |
313 | return group |
314 | } |
315 |
Built with git-ssb-web