git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: 7b05904de35826ff191a15f884a7b89328afa6b9

Files: 7b05904de35826ff191a15f884a7b89328afa6b9 / modules / feed / html / rollup.js

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

Built with git-ssb-web