git ssb

10+

Matt McKegg / patchwork



Tree: 3e9111306b522a46fa42a83d9e640509260fbf04

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

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

Built with git-ssb-web