git ssb

10+

Matt McKegg / patchwork



Tree: 3d5a912d4f26780685c60d2ccc930cd6dd9567cc

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

8769 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
21var appRoot = require('app-root-path');
22var i18n = require(appRoot + '/lib/i18n').i18n
23
24
25exports.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
37exports.gives = nest({
38 'feed.html.rollup': true
39})
40
41exports.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}, ['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
242function 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
252function 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
288function getAuthors (items) {
289 return items.reduce((result, msg) => {
290 result.add(msg.value.author)
291 return result
292 }, new Set())
293}
294
295function 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
308function 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
315function getType (msg) {
316 return msg && msg.value && msg.value.content && msg.value.content.type
317}
318
319function returnTrue () {
320 return true
321}
322
323function byAssertedTime (a, b) {
324 return a.value.timestamp - b.value.timestamp
325}
326

Built with git-ssb-web