git ssb

10+

Matt McKegg / patchwork



Tree: 2150c96ae49e67fdcb638a88455af2c3ae8c9c53

Files: 2150c96ae49e67fdcb638a88455af2c3ae8c9c53 / modules / feed / html / rollup.js

7770 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}
16
17exports.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
29exports.gives = nest({
30 'feed.html.rollup': true
31})
32
33exports.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
207function 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
217function 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
253function getAuthors (items) {
254 return items.reduce((result, msg) => {
255 result.add(msg.value.author)
256 return result
257 }, new Set())
258}
259
260function 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
273function isUpdate (msg) {
274 if (msg.value && msg.value.content) {
275 var type = msg.value.content.type
276 return type === 'about'
277 }
278}
279
280function 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
287function getType (msg) {
288 return msg && msg.value && msg.value.content && msg.value.content.type
289}
290
291function returnTrue () {
292 return true
293}
294
295function byAssertedTime (a, b) {
296 return a.value.timestamp - b.value.timestamp
297}
298

Built with git-ssb-web