git ssb

10+

Matt McKegg / patchwork



Tree: 0481ba494e6d6b6e34cdfb26477c94602fc663ea

Files: 0481ba494e6d6b6e34cdfb26477c94602fc663ea / modules / feed / html / rollup.js

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

Built with git-ssb-web