git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: f6b15ef0c66ef8142dc6e63a6065df61a37573a2

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

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

Built with git-ssb-web