git ssb

10+

Matt McKegg / patchwork



Tree: 9bd39239444aeffd84194ae531a8fa96d2fcba13

Files: 9bd39239444aeffd84194ae531a8fa96d2fcba13 / modules / feed / html / rollup.js

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

Built with git-ssb-web