git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: 3a3732c95a5d34d4a8f506224643609f35c55a5d

Files: 3a3732c95a5d34d4a8f506224643609f35c55a5d / modules / feed / html / rollup.js

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

Built with git-ssb-web