git ssb

10+

Matt McKegg / patchwork



Tree: 452d3838ab4f6ffb18b71fff39793437a4d88c4e

Files: 452d3838ab4f6ffb18b71fff39793437a4d88c4e / modules / feed / html / rollup.js

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

Built with git-ssb-web