git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: b37ae77198a3ac258726bdf306d783f31965a1e1

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

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

Built with git-ssb-web