git ssb

10+

Matt McKegg / patchwork



Tree: cff270be8e13b929bbb91731f12bf29dfa001197

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

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

Built with git-ssb-web