git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: c0e5334e0811d15dbcbc0b86a35ba498c3e9640d

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

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

Built with git-ssb-web