git ssb

10+

Matt McKegg / patchwork



Tree: f439db1134f706506d4370ff10b10a671b15769a

Files: f439db1134f706506d4370ff10b10a671b15769a / modules / feed / html / rollup.js

12965 bytesRaw
1var nest = require('depnest')
2var {Value, Proxy, Array: MutantArray, h, computed, when, onceTrue, throttle} = require('mutant')
3var pull = require('pull-stream')
4var Abortable = require('pull-abortable')
5var Scroller = require('../../../lib/scroller')
6var nextStepper = require('../../../lib/next-stepper')
7var extend = require('xtend')
8var paramap = require('pull-paramap')
9
10var bumpMessages = {
11 'vote': 'liked this message',
12 'post': 'replied to this message',
13 'about': 'added changes',
14 'mention': 'mentioned you',
15 'channel-mention': 'mentioned this channel',
16 'attending': 'can attend'
17}
18
19// bump even for first message
20var rootBumpTypes = ['mention', 'channel-mention']
21
22exports.needs = nest({
23 'about.obs.name': 'first',
24 'app.sync.externalHandler': 'first',
25 'message.html.canRender': 'first',
26 'message.html.render': 'first',
27 'message.sync.isBlocked': 'first',
28 'message.sync.unbox': 'first',
29 'message.sync.timestamp': 'first',
30 'profile.html.person': 'first',
31 'channel.html.link': 'first',
32 'message.html.link': 'first',
33 'message.sync.root': 'first',
34 'feed.pull.rollup': 'first',
35 'feed.pull.withReplies': 'first',
36 'feed.pull.unique': 'first',
37 'sbot.async.get': 'first',
38 'keys.sync.id': 'first',
39 'intl.sync.i18n': 'first',
40 'intl.sync.i18n_n': 'first',
41 'message.html.missing': 'first'
42})
43
44exports.gives = nest({
45 'feed.html.rollup': true
46})
47
48exports.create = function (api) {
49 const i18n = api.intl.sync.i18n
50 const i18nPlural = api.intl.sync.i18n_n
51 return nest('feed.html.rollup', function (getStream, {
52 prepend,
53 rootFilter = returnTrue,
54 bumpFilter = returnTrue,
55 resultFilter = returnTrue, // filter after replies have been resolved (just before append to scroll)
56 compactFilter = returnFalse,
57 prefiltered = false,
58 displayFilter = returnTrue,
59 updateStream, // override the stream used for realtime updates
60 waitFor = true
61 }) {
62 var updates = Value(0)
63 var yourId = api.keys.sync.id()
64 var throttledUpdates = throttle(updates, 200)
65 var updateLoader = h('a Notifier -loader', { href: '#', 'ev-click': refresh }, [
66 'Show ', h('strong', [throttledUpdates]), ' ', plural(throttledUpdates, i18n('update'), i18n('updates'))
67 ])
68
69 var abortLastFeed = null
70 var content = Value()
71 var loading = Proxy(true)
72 var unreadIds = new Set()
73 var newSinceRefresh = new Set()
74 var highlightItems = new Set()
75
76 var container = h('Scroller', {
77 style: { overflow: 'auto' },
78 hooks: [(element) => {
79 // don't activate until added to DOM
80 refresh()
81
82 // deactivate when removed from DOM
83 return () => {
84 if (abortLastFeed) {
85 abortLastFeed()
86 abortLastFeed = null
87 }
88 }
89 }]
90 }, [
91 h('div.wrapper', [
92 h('section.prepend', prepend),
93 content,
94 when(loading, h('Loading -large'))
95 ])
96 ])
97
98 onceTrue(waitFor, () => {
99 // display pending updates
100 pull(
101 updateStream || pull(
102 getStream({old: false}),
103 LookupRoot()
104 ),
105 pull.filter((msg) => {
106 // only render posts that have a root message
107 var root = msg.root || msg
108 return root && root.value && root.value.content && rootFilter(root) && bumpFilter(msg, root) && displayFilter(msg)
109 }),
110 pull.drain((msg) => {
111 if (msg.value.content.type === 'vote') return
112 if (api.app.sync.externalHandler(msg)) return
113
114 // Only increment the 'new since' for items that we render on
115 // the feed as otherwise the 'show <n> updates message' will be
116 // shown on new messages that patchwork cannot render
117 if (canRenderMessage(msg) && msg.value.author !== yourId && (!msg.root || canRenderMessage(msg.root))) {
118 newSinceRefresh.add(msg.key)
119 unreadIds.add(msg.key)
120 }
121
122 if (updates() === 0 && msg.value.author === yourId && container.scrollTop < 500) {
123 refresh()
124 } else if (msg.value.author === yourId && content()) {
125 // dynamically insert this post into the feed! (manually so that it doesn't get slow with mutant)
126 var existingContainer = content().querySelector(`[data-root-id="${msg.value.content.root}"]`)
127 if (existingContainer) {
128 var replies = existingContainer.querySelector('div.replies')
129 var lastReply = existingContainer.querySelector('div.replies > .Message:last-child')
130 var previousId = lastReply ? lastReply.getAttribute('data-id') : existingContainer.getAttribute('data-root-id')
131 replies.appendChild(api.message.html.render(msg, {
132 previousId,
133 compact: false,
134 priority: 2
135 }))
136 }
137 }
138
139 updates.set(newSinceRefresh.size)
140 })
141 )
142 })
143
144 var result = MutantArray([
145 when(updates, updateLoader),
146 container
147 ])
148
149 result.pendingUpdates = throttledUpdates
150 result.reload = refresh
151
152 return result
153
154 function canRenderMessage (msg) {
155 return api.message.html.canRender(msg)
156 }
157
158 function refresh () {
159 onceTrue(waitFor, () => {
160 if (abortLastFeed) abortLastFeed()
161 updates.set(0)
162 content.set(h('section.content'))
163
164 var abortable = Abortable()
165 abortLastFeed = abortable.abort
166
167 highlightItems = newSinceRefresh
168 newSinceRefresh = new Set()
169
170 var done = Value(false)
171 var stream = nextStepper(getStream, {reverse: true, limit: 50})
172 var scroller = Scroller(container, content(), renderItem, {
173 onDone: () => done.set(true),
174 onItemVisible: (item) => {
175 if (Array.isArray(item.msgIds)) {
176 item.msgIds.forEach(id => {
177 unreadIds.delete(id)
178 })
179 }
180 }
181 })
182
183 // track loading state
184 loading.set(computed([done, scroller.queue], (done, queue) => {
185 return !done && queue < 5
186 }))
187
188 pull(
189 stream,
190 abortable,
191 pull.filter(msg => msg && msg.value && msg.value.content),
192 prefiltered ? pull(
193 pull.filter(msg => !api.message.sync.isBlocked(msg)),
194 pull.filter(rootFilter),
195 api.feed.pull.unique(),
196 api.feed.pull.withReplies()
197 ) : pull(
198 pull.filter(bumpFilter),
199 api.feed.pull.rollup(rootFilter)
200 ),
201 pull.filter(resultFilter),
202 scroller
203 )
204 })
205 }
206
207 function renderItem (item, opts) {
208 var partial = opts && opts.partial
209 var meta = null
210 var previousId = item.key
211
212 var groupedBumps = {}
213 var lastBumpType = null
214
215 var rootBumpType = bumpFilter(item)
216 if (rootBumpTypes.includes(rootBumpType)) {
217 lastBumpType = rootBumpType
218 groupedBumps[lastBumpType] = [item]
219 }
220
221 item.replies.forEach(msg => {
222 var value = bumpFilter(msg)
223 if (value) {
224 var type = typeof value === 'string' ? value : getType(msg)
225 ;(groupedBumps[type] = groupedBumps[type] || []).unshift(msg)
226 lastBumpType = type
227 }
228 })
229
230 var replies = item.replies.filter(isReply).sort(byAssertedTime)
231 var highlightedReplies = replies.filter(getPriority)
232 var replyElements = replies.filter(displayFilter).slice(-3).map((msg) => {
233 var result = api.message.html.render(msg, {
234 previousId,
235 compact: compactFilter(msg, item),
236 priority: getPriority(msg)
237 })
238 previousId = msg.key
239
240 return [
241 // insert missing message marker (if can't be found)
242 api.message.html.missing(last(msg.value.content.branch), msg),
243 result
244 ]
245 })
246
247 var renderedMessage = api.message.html.render(item, {
248 compact: compactFilter(item),
249 includeForks: false, // this is a root message, so forks are already displayed as replies
250 priority: getPriority(item)
251 })
252
253 unreadIds.delete(item.key)
254
255 if (!renderedMessage) return h('div')
256
257 if (rootBumpType === 'matches-channel') {
258 var channels = new Set()
259 if (item.filterResult) {
260 if (item.filterResult.matchesChannel) channels.add(item.value.content.channel)
261 if (Array.isArray(item.filterResult.matchingTags)) item.filterResult.matchingTags.forEach(x => channels.add(x))
262 }
263 meta = h('div.meta', [
264 many(channels, api.channel.html.link, i18n), ' ', i18n('mentioned in your network')
265 ])
266 } else if (lastBumpType) {
267 var bumps = lastBumpType === 'vote'
268 ? getLikeAuthors(groupedBumps[lastBumpType])
269 : getAuthors(groupedBumps[lastBumpType])
270
271 if (lastBumpType === 'matches-channel' && item.value.content.channel) {
272 var channel = api.channel.html.link(item.value.content.channel)
273 meta = h('div.meta', [
274 i18nPlural('%s people from your network replied to this message on ', groupedBumps[lastBumpType].length), channel
275 ])
276 } else {
277 var description = i18n(bumpMessages[lastBumpType] || 'added changes')
278 meta = h('div.meta', [
279 many(bumps, api.profile.html.person, i18n), ' ', description
280 ])
281 }
282 }
283
284 // if there are new messages, view full thread goes to the top of those, otherwise to very first reply
285 var anchorReply = highlightedReplies.length >= 3 ? highlightedReplies[0] : replies[0]
286
287 var result = h('FeedEvent -post', {
288 attributes: {
289 'data-root-id': item.key
290 }
291 }, [
292 meta,
293 renderedMessage,
294 when(replies.length > replyElements.length || partial,
295 h('a.full', {href: item.key, anchor: anchorReply && anchorReply.key}, [i18n('View full thread') + ' (', replies.length, ')'])
296 ),
297 h('div.replies', replyElements)
298 ])
299
300 result.msgIds = [item.key].concat(item.replies.map(x => x.key))
301
302 return result
303 }
304
305 function getPriority (msg) {
306 if (highlightItems.has(msg.key)) {
307 return 2
308 } else if (unreadIds.has(msg.key)) {
309 return 1
310 } else {
311 return 0
312 }
313 }
314 })
315
316 function LookupRoot () {
317 return paramap((msg, cb) => {
318 var rootId = api.message.sync.root(msg)
319 if (rootId) {
320 api.sbot.async.get(rootId, (_, value) => {
321 if (value && typeof value.content === 'string') {
322 // unbox private message
323 value = api.message.sync.unbox(value)
324 }
325 cb(null, extend(msg, {
326 root: {key: rootId, value}
327 }))
328 })
329 } else {
330 cb(null, msg)
331 }
332 })
333 }
334
335 function byAssertedTime (a, b) {
336 return api.message.sync.timestamp(a) - api.message.sync.timestamp(b)
337 }
338}
339
340function plural (value, single, many) {
341 return computed(value, (value) => {
342 if (value === 1) {
343 return single
344 } else {
345 return many
346 }
347 })
348}
349
350function many (ids, fn, intl) {
351 ids = Array.from(ids)
352 var featuredIds = ids.slice(0, 4)
353
354 if (ids.length) {
355 if (ids.length > 4) {
356 return [
357 fn(featuredIds[0]), ', ',
358 fn(featuredIds[1]), ', ',
359 fn(featuredIds[2]), intl(' and '),
360 ids.length - 3, intl(' others')
361 ]
362 } else if (ids.length === 4) {
363 return [
364 fn(featuredIds[0]), ', ',
365 fn(featuredIds[1]), ', ',
366 fn(featuredIds[2]), intl(' and '),
367 fn(featuredIds[3])
368 ]
369 } else if (ids.length === 3) {
370 return [
371 fn(featuredIds[0]), ', ',
372 fn(featuredIds[1]), intl(' and '),
373 fn(featuredIds[2])
374 ]
375 } else if (ids.length === 2) {
376 return [
377 fn(featuredIds[0]), intl(' and '),
378 fn(featuredIds[1])
379 ]
380 } else {
381 return fn(featuredIds[0])
382 }
383 }
384}
385
386function getAuthors (items) {
387 return items.reduce((result, msg) => {
388 result.add(msg.value.author)
389 return result
390 }, new Set())
391}
392
393function getLikeAuthors (items) {
394 return items.reduce((result, msg) => {
395 if (msg.value.content.type === 'vote') {
396 if (msg.value.content && msg.value.content.vote && msg.value.content.vote.value === 1) {
397 result.add(msg.value.author)
398 } else {
399 result.delete(msg.value.author)
400 }
401 }
402 return result
403 }, new Set())
404}
405
406function isReply (msg) {
407 if (msg.value && msg.value.content) {
408 var type = msg.value.content.type
409 return type === 'post' || (type === 'about' && msg.value.content.attendee)
410 }
411}
412
413function getType (msg) {
414 return msg && msg.value && msg.value.content && msg.value.content.type
415}
416
417function returnTrue () {
418 return true
419}
420
421function returnFalse () {
422 return false
423}
424
425function last (array) {
426 if (Array.isArray(array)) {
427 return array[array.length - 1]
428 } else {
429 return array
430 }
431}
432

Built with git-ssb-web