git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: 769b943fffb01b5b2627c5b9a6cfcadd2bf8bf14

Files: 769b943fffb01b5b2627c5b9a6cfcadd2bf8bf14 / modules / feed / html / rollup.js

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

Built with git-ssb-web