Files: 2150c96ae49e67fdcb638a88455af2c3ae8c9c53 / modules / feed / html / rollup.js
7770 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/pull-scroll') |
6 | var nextStepper = require('../../../lib/next-stepper') |
7 | var extend = require('xtend') |
8 | var paramap = require('pull-paramap') |
9 | |
10 | var bumpMessages = { |
11 | 'vote': 'liked this message', |
12 | 'post': 'replied to this message', |
13 | 'about': 'added changes', |
14 | 'mention': 'mentioned you' |
15 | } |
16 | |
17 | exports.needs = nest({ |
18 | 'about.obs.name': 'first', |
19 | 'app.sync.externalHandler': 'first', |
20 | 'message.html.render': 'first', |
21 | 'profile.html.person': 'first', |
22 | 'message.html.link': 'first', |
23 | 'message.sync.root': 'first', |
24 | 'feed.pull.rollup': 'first', |
25 | 'sbot.async.get': 'first', |
26 | 'keys.sync.id': 'first' |
27 | }) |
28 | |
29 | exports.gives = nest({ |
30 | 'feed.html.rollup': true |
31 | }) |
32 | |
33 | exports.create = function (api) { |
34 | return nest('feed.html.rollup', function (getStream, { |
35 | prepend, |
36 | rootFilter = returnTrue, |
37 | bumpFilter = returnTrue, |
38 | displayFilter = returnTrue, |
39 | waitFor = true |
40 | }) { |
41 | var updates = Value(0) |
42 | var yourId = api.keys.sync.id() |
43 | var throttledUpdates = throttle(updates, 200) |
44 | var updateLoader = h('a Notifier -loader', { href: '#', 'ev-click': refresh }, [ |
45 | 'Show ', h('strong', [throttledUpdates]), ' ', plural(throttledUpdates, 'update', 'updates') |
46 | ]) |
47 | |
48 | var abortLastFeed = null |
49 | var content = Value() |
50 | var loading = Proxy(true) |
51 | var newSinceRefresh = new Set() |
52 | var highlightItems = new Set() |
53 | |
54 | var container = h('Scroller', { |
55 | style: { overflow: 'auto' } |
56 | }, [ |
57 | h('div.wrapper', [ |
58 | h('section.prepend', prepend), |
59 | content, |
60 | when(loading, h('Loading -large')) |
61 | ]) |
62 | ]) |
63 | |
64 | onceTrue(waitFor, () => { |
65 | refresh() |
66 | |
67 | // display pending updates |
68 | pull( |
69 | getStream({old: false}), |
70 | LookupRoot(), |
71 | pull.filter((msg) => { |
72 | return rootFilter(msg.root || msg) && bumpFilter(msg) |
73 | }), |
74 | pull.drain((msg) => { |
75 | if (msg.value.content.type === 'vote') return |
76 | if (api.app.sync.externalHandler(msg)) return |
77 | newSinceRefresh.add(msg.key) |
78 | |
79 | if (updates() === 0 && msg.value.author === yourId && container.scrollTop < 20) { |
80 | refresh() |
81 | } else { |
82 | updates.set(updates() + 1) |
83 | } |
84 | }) |
85 | ) |
86 | }) |
87 | |
88 | var result = MutantArray([ |
89 | when(updates, updateLoader), |
90 | container |
91 | ]) |
92 | |
93 | result.pendingUpdates = throttledUpdates |
94 | result.reload = refresh |
95 | |
96 | return result |
97 | |
98 | function refresh () { |
99 | if (abortLastFeed) abortLastFeed() |
100 | updates.set(0) |
101 | content.set(h('section.content')) |
102 | |
103 | var abortable = Abortable() |
104 | abortLastFeed = abortable.abort |
105 | |
106 | highlightItems = newSinceRefresh |
107 | newSinceRefresh = new Set() |
108 | |
109 | var done = Value(false) |
110 | var stream = nextStepper(getStream, {reverse: true, limit: 50}) |
111 | var scroller = Scroller(container, content(), renderItem, false, false, () => done.set(true)) |
112 | |
113 | // track loading state |
114 | loading.set(computed([done, scroller.queue], (done, queue) => { |
115 | return !done && queue < 5 |
116 | })) |
117 | |
118 | pull( |
119 | stream, |
120 | pull.filter(bumpFilter), |
121 | abortable, |
122 | api.feed.pull.rollup(rootFilter), |
123 | scroller |
124 | ) |
125 | } |
126 | |
127 | function renderItem (item, opts) { |
128 | var partial = opts && opts.partial |
129 | var meta = null |
130 | var previousId = item.key |
131 | |
132 | var groupedBumps = {} |
133 | var lastBumpType = null |
134 | |
135 | item.replies.forEach(msg => { |
136 | var value = bumpFilter(msg) |
137 | if (value) { |
138 | var type = typeof value === 'string' ? value : getType(msg) |
139 | ;(groupedBumps[type] = groupedBumps[type] || []).push(msg) |
140 | lastBumpType = type |
141 | } |
142 | }) |
143 | |
144 | var replies = item.replies.filter(isReply) |
145 | var replyElements = replies.filter(displayFilter).sort(byAssertedTime).slice(-3).map((msg) => { |
146 | var result = api.message.html.render(msg, { |
147 | inContext: true, |
148 | inSummary: true, |
149 | previousId, |
150 | priority: highlightItems.has(msg.key) ? 2 : 0 |
151 | }) |
152 | previousId = msg.key |
153 | return result |
154 | }) |
155 | |
156 | var renderedMessage = api.message.html.render(item, {inContext: true}) |
157 | if (!renderedMessage) return h('div') |
158 | if (lastBumpType) { |
159 | var bumps = lastBumpType === 'vote' |
160 | ? getLikeAuthors(groupedBumps[lastBumpType]) |
161 | : getAuthors(groupedBumps[lastBumpType]) |
162 | |
163 | var description = bumpMessages[lastBumpType] || 'added changes' |
164 | meta = h('div.meta', { title: names(bumps) }, [ |
165 | many(bumps, api.profile.html.person), ' ', description |
166 | ]) |
167 | } |
168 | |
169 | return h('FeedEvent -post', { |
170 | attributes: { |
171 | 'data-root-id': item.key |
172 | } |
173 | }, [ |
174 | meta, |
175 | renderedMessage, |
176 | when(replyElements.length, [ |
177 | when(replies.length > replyElements.length || partial, |
178 | h('a.full', {href: item.key}, ['View full thread (', replies.length, ')']) |
179 | ), |
180 | h('div.replies', replyElements) |
181 | ]) |
182 | ]) |
183 | } |
184 | }) |
185 | |
186 | function names (ids) { |
187 | var items = map(Array.from(ids), api.about.obs.name) |
188 | return computed([items], (names) => names.map((n) => `- ${n}`).join('\n')) |
189 | } |
190 | |
191 | function LookupRoot () { |
192 | return paramap((msg, cb) => { |
193 | var rootId = api.message.sync.root(msg) |
194 | if (rootId) { |
195 | api.sbot.async.get(rootId, (_, value) => { |
196 | cb(null, extend(msg, { |
197 | root: {key: rootId, value} |
198 | })) |
199 | }) |
200 | } else { |
201 | cb(null, msg) |
202 | } |
203 | }) |
204 | } |
205 | } |
206 | |
207 | function plural (value, single, many) { |
208 | return computed(value, (value) => { |
209 | if (value === 1) { |
210 | return single |
211 | } else { |
212 | return many |
213 | } |
214 | }) |
215 | } |
216 | |
217 | function many (ids, fn) { |
218 | ids = Array.from(ids) |
219 | var featuredIds = ids.slice(0, 4) |
220 | |
221 | if (ids.length) { |
222 | if (ids.length > 4) { |
223 | return [ |
224 | fn(featuredIds[0]), ', ', |
225 | fn(featuredIds[1]), ', ', |
226 | fn(featuredIds[2]), ' and ', |
227 | ids.length - 3, ' others' |
228 | ] |
229 | } else if (ids.length === 4) { |
230 | return [ |
231 | fn(featuredIds[0]), ', ', |
232 | fn(featuredIds[1]), ', ', |
233 | fn(featuredIds[2]), ' and ', |
234 | fn(featuredIds[3]) |
235 | ] |
236 | } else if (ids.length === 3) { |
237 | return [ |
238 | fn(featuredIds[0]), ', ', |
239 | fn(featuredIds[1]), ' and ', |
240 | fn(featuredIds[2]) |
241 | ] |
242 | } else if (ids.length === 2) { |
243 | return [ |
244 | fn(featuredIds[0]), ' and ', |
245 | fn(featuredIds[1]) |
246 | ] |
247 | } else { |
248 | return fn(featuredIds[0]) |
249 | } |
250 | } |
251 | } |
252 | |
253 | function getAuthors (items) { |
254 | return items.reduce((result, msg) => { |
255 | result.add(msg.value.author) |
256 | return result |
257 | }, new Set()) |
258 | } |
259 | |
260 | function getLikeAuthors (items) { |
261 | return items.reduce((result, msg) => { |
262 | if (msg.value.content.type === 'vote') { |
263 | if (msg.value.content && msg.value.content.vote && msg.value.content.vote.value === 1) { |
264 | result.add(msg.value.author) |
265 | } else { |
266 | result.delete(msg.value.author) |
267 | } |
268 | } |
269 | return result |
270 | }, new Set()) |
271 | } |
272 | |
273 | function isUpdate (msg) { |
274 | if (msg.value && msg.value.content) { |
275 | var type = msg.value.content.type |
276 | return type === 'about' |
277 | } |
278 | } |
279 | |
280 | function isReply (msg) { |
281 | if (msg.value && msg.value.content) { |
282 | var type = msg.value.content.type |
283 | return type === 'post' || (type === 'about' && msg.value.content.attendee) |
284 | } |
285 | } |
286 | |
287 | function getType (msg) { |
288 | return msg && msg.value && msg.value.content && msg.value.content.type |
289 | } |
290 | |
291 | function returnTrue () { |
292 | return true |
293 | } |
294 | |
295 | function byAssertedTime (a, b) { |
296 | return a.value.timestamp - b.value.timestamp |
297 | } |
298 |
Built with git-ssb-web