git ssb

1+

Daan Patchwork / patchwork



Tree: 06ded70b47c2b7b6dbd436a59f532f6caff0e67c

Files: 06ded70b47c2b7b6dbd436a59f532f6caff0e67c / lib / depject / feed / html / rollup.js

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

Built with git-ssb-web