git ssb

1+

Daan Patchwork / patchwork



Tree: f50400a95d8c5a977f0f39ee8b857c5ee9cdd630

Files: f50400a95d8c5a977f0f39ee8b857c5ee9cdd630 / lib / depject / feed / html / rollup.js

11397 bytesRaw
1const nest = require('depnest')
2const { Value, Proxy, Array: MutantArray, h, computed, when, throttle, isObservable, resolve } = require('mutant')
3const pull = require('pull-stream')
4const Abortable = require('pull-abortable')
5const Scroller = require('../../../scroller')
6const extend = require('xtend')
7const GroupSummaries = require('../../../group-summaries')
8const rootBumpTypes = ['mention', 'channel-mention', 'invite']
9const getBump = require('../../../get-bump')
10const many = require('../../../many')
11
12const bumpMessages = {
13 reaction: 'liked this message',
14 invite: 'invited you to this gathering',
15 reply: 'replied to this message',
16 updated: 'added changes',
17 mention: 'mentioned you',
18 'channel-mention': 'mentioned this channel',
19 attending: 'can attend'
20}
21
22exports.needs = nest({
23 'app.sync.externalHandler': 'first',
24 'message.html.canRender': 'first',
25 'message.html.render': 'first',
26 'message.sync.unbox': 'first',
27 'message.sync.timestamp': 'first',
28 'profile.html.person': 'first',
29 'channel.html.link': 'first',
30 'message.html.link': 'first',
31 'message.sync.root': 'first',
32 'sbot.async.get': 'first',
33 'keys.sync.id': 'first',
34 'intl.sync.i18n': 'first',
35 'intl.sync.i18n_n': 'first',
36 'message.html.missing': 'first',
37 'feed.html.metaSummary': 'first'
38})
39
40exports.gives = nest({
41 'feed.html.rollup': true
42})
43
44exports.create = function (api) {
45 const i18n = api.intl.sync.i18n
46 const i18nPlural = api.intl.sync.i18n_n
47 return nest('feed.html.rollup', function (getStream, {
48 prepend,
49 hidden = false,
50 groupSummaries = true,
51 compactFilter = returnFalse,
52 ungroupFilter = returnFalse,
53 searchSpinner = false,
54 updateStream
55 }) {
56 const updates = Value(0)
57 const yourId = api.keys.sync.id()
58 const throttledUpdates = throttle(updates, 200)
59 const updateLoader = h('a Notifier -loader', { href: '#', 'ev-click': refresh }, [
60 'Show ', h('strong', [throttledUpdates]), ' ', plural(throttledUpdates, i18n('update'), i18n('updates'))
61 ])
62
63 let abortLastFeed = null
64 let unwatchHidden = null
65 const content = Value()
66 const loading = Proxy(true)
67 const unreadIds = new Set()
68 let newSinceRefresh = new Set()
69 let highlightItems = new Set()
70
71 const container = h('Scroller', {
72 // only bind elements that are visible in scroller
73 intersectionBindingViewport: { rootMargin: '1000px' },
74
75 style: { overflow: 'auto' },
76 hooks: [() => {
77 // don't activate until added to DOM
78 refresh()
79
80 if (isObservable(hidden)) {
81 unwatchHidden = hidden(() => {
82 refresh()
83 })
84 }
85
86 // deactivate when removed from DOM
87 return () => {
88 if (abortLastFeed) {
89 abortLastFeed()
90 abortLastFeed = null
91 }
92
93 if (unwatchHidden) {
94 unwatchHidden()
95 unwatchHidden = null
96 }
97 }
98 }]
99 }, [
100 h('div.wrapper', [
101 h('section.prepend', prepend),
102 content,
103 when(loading, searchSpinner ? h('Loading -large -search') : h('Loading -large'))
104 ])
105 ])
106
107 if (updateStream) {
108 pull(
109 updateStream,
110 pull.drain((msg) => {
111 if (!(msg && msg.value && msg.value.content && msg.value.content.type)) return
112 if (!canRenderMessage(msg)) return
113
114 // Only increment the 'new since' for items that we render on
115 // the feed as otherwise the 'show <n> updates message' will be
116 // shown on new messages that patchwork cannot render
117 if (msg.value.author !== yourId && (!msg.root || canRenderMessage(msg.root))) {
118 newSinceRefresh.add(msg.key)
119 unreadIds.add(msg.key)
120 }
121
122 if (msg.value.author === yourId && content()) {
123 // dynamically insert this post into the feed! (manually so that it doesn't get slow with mutant)
124 if (api.message.sync.root(msg)) {
125 const existingContainer = content().querySelector(`[data-root-id="${api.message.sync.root(msg)}"]`)
126 if (existingContainer) {
127 const replies = existingContainer.querySelector('div.replies')
128 replies.appendChild(api.message.html.render(msg, {
129 compact: false,
130 priority: 2
131 }))
132 }
133 } else {
134 highlightItems.add(msg.key)
135 content().prepend(
136 renderItem(extend(msg, {
137 latestReplies: [],
138 totalReplies: 0,
139 rootBump: { type: 'post' }
140 }))
141 )
142 }
143 }
144
145 updates.set(newSinceRefresh.size)
146 })
147 )
148 }
149
150 const result = MutantArray([
151 when(hidden,
152 null,
153 when(updates, updateLoader)
154 ),
155 container
156 ])
157
158 result.pendingUpdates = throttledUpdates
159 result.reload = refresh
160
161 return result
162
163 function canRenderMessage (msg) {
164 if (msg && msg.value && msg.value.content) {
165 return api.message.html.canRender(msg)
166 }
167 }
168
169 function refresh () {
170 if (resolve(hidden)) {
171 content.set(null)
172 loading.set(false)
173 return
174 }
175 if (abortLastFeed) abortLastFeed()
176 updates.set(0)
177 content.set(h('section.content'))
178
179 const abortable = Abortable()
180 abortLastFeed = abortable.abort
181
182 highlightItems = newSinceRefresh
183 newSinceRefresh = new Set()
184
185 const firstItemVisible = Value(false)
186
187 const done = Value(false)
188 const scroller = Scroller(container, content(), renderItem, {
189 onDone: () => done.set(true),
190 onItemVisible: (item) => {
191 if (!firstItemVisible()) {
192 firstItemVisible.set(true)
193 }
194 if (Array.isArray(item.msgIds)) {
195 item.msgIds.forEach(id => {
196 unreadIds.delete(id)
197 })
198 }
199 }
200 })
201
202 // track loading state
203 loading.set(scroller.waiting)
204
205 const seen = new Set()
206
207 pull(
208 getStream(),
209 abortable,
210 pull.filter(item => {
211 // don't show the same threads again (repeats due to pull-resume)
212 if (seen.has(item.key)) {
213 return false
214 }
215 seen.add(item.key)
216 return true
217 }),
218 pull.filter(canRenderMessage),
219
220 // group related items (follows, subscribes, abouts)
221 groupSummaries ? GroupSummaries({ windowSize: 15, getPriority, ungroupFilter }) : pull.through(),
222
223 scroller
224 )
225 }
226
227 function renderItem (item, opts) {
228 if (item.group) {
229 return api.feed.html.metaSummary(item, renderItem, getPriority, opts)
230 }
231
232 let mostRecentBumpType = (item.bumps && item.bumps[0] && item.bumps[0].type)
233
234 const rootBump = getBump(item, item.rootBump)
235 if (!mostRecentBumpType) {
236 if (rootBump && rootBumpTypes.includes(rootBump.type)) {
237 mostRecentBumpType = rootBump.type
238 } else {
239 mostRecentBumpType = 'reply'
240 }
241 }
242
243 const bumps = getBumps(item)[mostRecentBumpType]
244
245 const renderedMessage = api.message.html.render(item, {
246 compact: compactFilter(item),
247 includeForks: false,
248 pageId: item.key,
249 forkedFrom: item.value.content.root,
250 priority: getPriority(item)
251 })
252
253 const unreadBumps = []
254 if (!highlightItems.has(item.key) && item.bumps) {
255 item.bumps.forEach(bump => {
256 if (highlightItems.has(bump.id)) {
257 unreadBumps.push(bump.id)
258 }
259 })
260 }
261
262 unreadIds.delete(item.key)
263
264 let meta = null
265
266 // explain why this message is in your feed
267 if (mostRecentBumpType !== 'matches-channel' && item.rootBump && item.rootBump.type === 'matches-channel') {
268 // the root post was in a channel that you subscribe to
269 meta = h('div.meta', [
270 many(item.rootBump.channels, api.channel.html.link, i18n), ' ', i18n('mentioned in your network')
271 ])
272 } else if (bumps && bumps.length) {
273 const authors = getAuthors(bumps)
274 if (mostRecentBumpType === 'matches-channel') {
275 // a reply to this post matches a channel you subscribe to
276 const channels = new Set()
277 bumps.forEach(bump => bump.channels && bump.channels.forEach(c => channels.add(c)))
278 meta = h('div.meta', [
279 i18nPlural('%s people from your network replied to this message on ', authors.length),
280 many(channels, api.channel.html.link, i18n)
281 ])
282 } else {
283 // someone you follow replied to this message
284 const description = i18n(bumpMessages[mostRecentBumpType] || 'added changes')
285 meta = h('div.meta', [
286 many(authors, api.profile.html.person, i18n), ' ', description
287 ])
288 }
289 }
290
291 const latestReplyIds = item.latestReplies.map(msg => msg.key)
292 const hasMoreNewReplies = unreadBumps.some(id => !latestReplyIds.includes(id))
293
294 return h('FeedEvent -post', {
295 msgIds: [item.key].concat(item.latestReplies.map(x => x.key)),
296 attributes: {
297 'data-root-id': item.key
298 }
299 }, [
300 meta,
301 renderedMessage,
302 item.totalReplies > item.latestReplies.length
303 ? h('a.full',
304 {
305 href: item.key,
306 anchor: {
307 // highlight unread messages in thread view
308 unread: unreadBumps,
309 // only drop to latest messages if there are more new messages not visible in thread summary
310 anchor: hasMoreNewReplies ? unreadBumps[unreadBumps.length - 1] : null
311 }
312 }, [i18n('View full thread') + ' (', item.totalReplies, ')'])
313 : null,
314 h('div.replies', [
315 item.latestReplies.map(msg => {
316 const result = api.message.html.render(msg, {
317 compact: compactFilter(msg, item),
318 priority: getPriority(msg)
319 })
320
321 return [
322 // insert missing message marker (if can't be found)
323 api.message.html.missing(last(msg.value.content.branch), msg, item),
324 result
325 ]
326 })
327 ])
328 ])
329 }
330
331 function getPriority (msg) {
332 if (highlightItems.has(msg.key)) {
333 return 2
334 } else if (unreadIds.has(msg.key)) {
335 return 1
336 } else {
337 return 0
338 }
339 }
340 })
341}
342
343function plural (value, single, many) {
344 return computed(value, (value) => {
345 if (value === 1) {
346 return single
347 } else {
348 return many
349 }
350 })
351}
352
353function getAuthors (items) {
354 const authors = {}
355 items.forEach(item => {
356 authors[item.author] = true
357 })
358 return Object.keys(authors)
359}
360
361function returnFalse () {
362 return false
363}
364
365function last (array) {
366 if (Array.isArray(array)) {
367 return array[array.length - 1]
368 } else {
369 return array
370 }
371}
372
373function getBumps (msg) {
374 const bumps = {}
375 const rootBump = getBump(msg, msg.rootBump)
376
377 if (rootBump) {
378 bumps[rootBump.type] = [rootBump]
379 }
380
381 if (Array.isArray(msg.bumps)) {
382 msg.bumps.forEach(bump => {
383 const type = bump.type || 'reply'
384 bumps[type] = bumps[type] || []
385 bumps[type].push(bump)
386 })
387 }
388 return bumps
389}
390

Built with git-ssb-web