git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: f06d537e3a5dcdce8c60eb9c129923924e3d7326

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

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

Built with git-ssb-web