Files: e5ad753b8414d998113f9305e0826f4cf1035fe4 / modules / feed / html / rollup.js
8827 bytesRaw
1 | var nest = require('depnest') |
2 | var {Value, Proxy, Array: MutantArray, h, computed, map, when, onceTrue, throttle} = require('mutant') |
3 | var pull = require('pull-stream') |
4 | var Abortable = require('pull-abortable') |
5 | var Scroller = require('../../../lib/scroller') |
6 | var nextStepper = require('../../../lib/next-stepper') |
7 | var extend = require('xtend') |
8 | var paramap = require('pull-paramap') |
9 | |
10 | var appRoot = require('app-root-path'); |
11 | var i18n = require(appRoot + '/lib/i18n').i18n |
12 | |
13 | var bumpMessages = { |
14 | 'vote': i18n.__('liked this message'), |
15 | 'post': i18n.__('replied to this message'), |
16 | 'about': i18n.__('added changes'), |
17 | 'mention': i18n.__('mentioned you'), |
18 | 'channel-mention': i18n.__('mentioned this channel') |
19 | } |
20 | |
21 | // bump even for first message |
22 | var rootBumpTypes = ['mention', 'channel-mention'] |
23 | |
24 | |
25 | exports.needs = nest({ |
26 | 'about.obs.name': 'first', |
27 | 'app.sync.externalHandler': 'first', |
28 | 'message.html.render': 'first', |
29 | 'profile.html.person': 'first', |
30 | 'message.html.link': 'first', |
31 | 'message.sync.root': 'first', |
32 | 'feed.pull.rollup': 'first', |
33 | 'sbot.async.get': 'first', |
34 | 'keys.sync.id': 'first' |
35 | }) |
36 | |
37 | exports.gives = nest({ |
38 | 'feed.html.rollup': true |
39 | }) |
40 | |
41 | exports.create = function (api) { |
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 | newSinceRefresh.add(msg.key) |
101 | |
102 | if (updates() === 0 && msg.value.author === yourId && container.scrollTop < 20) { |
103 | refresh() |
104 | } else { |
105 | updates.set(newSinceRefresh.size) |
106 | } |
107 | }) |
108 | ) |
109 | }) |
110 | |
111 | var result = MutantArray([ |
112 | when(updates, updateLoader), |
113 | container |
114 | ]) |
115 | |
116 | result.pendingUpdates = throttledUpdates |
117 | result.reload = refresh |
118 | |
119 | return result |
120 | |
121 | function refresh () { |
122 | onceTrue(waitFor, () => { |
123 | if (abortLastFeed) abortLastFeed() |
124 | updates.set(0) |
125 | content.set(h('section.content')) |
126 | |
127 | var abortable = Abortable() |
128 | abortLastFeed = abortable.abort |
129 | |
130 | highlightItems = newSinceRefresh |
131 | newSinceRefresh = new Set() |
132 | |
133 | var done = Value(false) |
134 | var stream = nextStepper(getStream, {reverse: true, limit: 50}) |
135 | var scroller = Scroller(container, content(), renderItem, () => done.set(true)) |
136 | |
137 | // track loading state |
138 | loading.set(computed([done, scroller.queue], (done, queue) => { |
139 | return !done && queue < 5 |
140 | })) |
141 | |
142 | pull( |
143 | stream, |
144 | pull.filter(bumpFilter), |
145 | abortable, |
146 | api.feed.pull.rollup(rootFilter), |
147 | scroller |
148 | ) |
149 | }) |
150 | } |
151 | |
152 | function renderItem (item, opts) { |
153 | var partial = opts && opts.partial |
154 | var meta = null |
155 | var previousId = item.key |
156 | |
157 | var groupedBumps = {} |
158 | var lastBumpType = null |
159 | |
160 | var rootBumpType = bumpFilter(item) |
161 | if (rootBumpTypes.includes(rootBumpType)) { |
162 | lastBumpType = rootBumpType |
163 | groupedBumps[lastBumpType] = [item] |
164 | } |
165 | |
166 | item.replies.forEach(msg => { |
167 | var value = bumpFilter(msg) |
168 | if (value) { |
169 | var type = typeof value === 'string' ? value : getType(msg) |
170 | ;(groupedBumps[type] = groupedBumps[type] || []).unshift(msg) |
171 | lastBumpType = type |
172 | } |
173 | }) |
174 | |
175 | var replies = item.replies.filter(isReply) |
176 | var replyElements = replies.filter(displayFilter).sort(byAssertedTime).slice(-3).map((msg) => { |
177 | var result = api.message.html.render(msg, { |
178 | inContext: true, |
179 | inSummary: true, |
180 | previousId, |
181 | priority: highlightItems.has(msg.key) ? 2 : 0 |
182 | }) |
183 | previousId = msg.key |
184 | return result |
185 | }) |
186 | |
187 | var renderedMessage = api.message.html.render(item, { |
188 | inContext: true, |
189 | priority: highlightItems.has(item.key) ? 2 : 0 |
190 | }) |
191 | |
192 | if (!renderedMessage) return h('div') |
193 | if (lastBumpType) { |
194 | var bumps = lastBumpType === 'vote' |
195 | ? getLikeAuthors(groupedBumps[lastBumpType]) |
196 | : getAuthors(groupedBumps[lastBumpType]) |
197 | |
198 | var description = bumpMessages[lastBumpType] || 'added changes' |
199 | meta = h('div.meta', { title: names(bumps) }, [ |
200 | many(bumps, api.profile.html.person), ' ', description |
201 | ]) |
202 | } |
203 | |
204 | return h('FeedEvent -post', { |
205 | attributes: { |
206 | 'data-root-id': item.key |
207 | } |
208 | }, [ |
209 | meta, |
210 | renderedMessage, |
211 | when(replyElements.length, [ |
212 | when(replies.length > replyElements.length || partial, |
213 | h('a.full', {href: item.key}, [i18n.__('View full thread') +' (', replies.length, ')']) |
214 | ), |
215 | h('div.replies', replyElements) |
216 | ]) |
217 | ]) |
218 | } |
219 | }) |
220 | |
221 | function names (ids) { |
222 | var items = map(Array.from(ids), api.about.obs.name) |
223 | return computed([items], (names) => names.map((n) => `- ${n}`).join('\n')) |
224 | } |
225 | |
226 | function LookupRoot () { |
227 | return paramap((msg, cb) => { |
228 | var rootId = api.message.sync.root(msg) |
229 | if (rootId) { |
230 | api.sbot.async.get(rootId, (_, value) => { |
231 | cb(null, extend(msg, { |
232 | root: {key: rootId, value} |
233 | })) |
234 | }) |
235 | } else { |
236 | cb(null, msg) |
237 | } |
238 | }) |
239 | } |
240 | } |
241 | |
242 | function plural (value, single, many) { |
243 | return computed(value, (value) => { |
244 | if (value === 1) { |
245 | return single |
246 | } else { |
247 | return many |
248 | } |
249 | }) |
250 | } |
251 | |
252 | function many (ids, fn) { |
253 | ids = Array.from(ids) |
254 | var featuredIds = ids.slice(0, 4) |
255 | |
256 | if (ids.length) { |
257 | if (ids.length > 4) { |
258 | return [ |
259 | fn(featuredIds[0]), ', ', |
260 | fn(featuredIds[1]), ', ', |
261 | fn(featuredIds[2]), i18n.__(' and '), |
262 | ids.length - 3, i18n.__(' others') |
263 | ] |
264 | } else if (ids.length === 4) { |
265 | return [ |
266 | fn(featuredIds[0]), ', ', |
267 | fn(featuredIds[1]), ', ', |
268 | fn(featuredIds[2]), i18n.__(' and '), |
269 | fn(featuredIds[3]) |
270 | ] |
271 | } else if (ids.length === 3) { |
272 | return [ |
273 | fn(featuredIds[0]), ', ', |
274 | fn(featuredIds[1]), i18n.__(' and '), |
275 | fn(featuredIds[2]) |
276 | ] |
277 | } else if (ids.length === 2) { |
278 | return [ |
279 | fn(featuredIds[0]), i18n.__(' and '), |
280 | fn(featuredIds[1]) |
281 | ] |
282 | } else { |
283 | return fn(featuredIds[0]) |
284 | } |
285 | } |
286 | } |
287 | |
288 | function getAuthors (items) { |
289 | return items.reduce((result, msg) => { |
290 | result.add(msg.value.author) |
291 | return result |
292 | }, new Set()) |
293 | } |
294 | |
295 | function getLikeAuthors (items) { |
296 | return items.reduce((result, msg) => { |
297 | if (msg.value.content.type === 'vote') { |
298 | if (msg.value.content && msg.value.content.vote && msg.value.content.vote.value === 1) { |
299 | result.add(msg.value.author) |
300 | } else { |
301 | result.delete(msg.value.author) |
302 | } |
303 | } |
304 | return result |
305 | }, new Set()) |
306 | } |
307 | |
308 | function isReply (msg) { |
309 | if (msg.value && msg.value.content) { |
310 | var type = msg.value.content.type |
311 | return type === 'post' || (type === 'about' && msg.value.content.attendee) |
312 | } |
313 | } |
314 | |
315 | function getType (msg) { |
316 | return msg && msg.value && msg.value.content && msg.value.content.type |
317 | } |
318 | |
319 | function returnTrue () { |
320 | return true |
321 | } |
322 | |
323 | function byAssertedTime (a, b) { |
324 | return a.value.timestamp - b.value.timestamp |
325 | } |
326 |
Built with git-ssb-web