git ssb

10+

Matt McKegg / patchwork



Tree: ffb7bedc3c585728df51f065d1d5d2f2d826319e

Files: ffb7bedc3c585728df51f065d1d5d2f2d826319e / modules / feed / html / rollup.js

9346 bytesRaw
1var Value = require('mutant/value')
2var when = require('mutant/when')
3var computed = require('mutant/computed')
4var h = require('mutant/h')
5var MutantArray = require('mutant/array')
6var Abortable = require('pull-abortable')
7var map = require('mutant/map')
8var pull = require('pull-stream')
9var nest = require('depnest')
10
11var onceTrue = require('mutant/once-true')
12var Scroller = require('pull-scroll')
13
14exports.needs = nest({
15 'message.html': {
16 render: 'first',
17 link: 'first'
18 },
19 'app.sync.externalHandler': 'first',
20 'sbot.async.get': 'first',
21 'keys.sync.id': 'first',
22 'about.obs.name': 'first',
23 feed: {
24 'html.rollup': 'first',
25 'pull.summary': 'first'
26 },
27 profile: {
28 'html.person': 'first'
29 }
30})
31
32exports.gives = nest({
33 'feed.html': ['rollup']
34})
35
36exports.create = function (api) {
37 return nest({
38 'feed.html': { rollup }
39 })
40 function rollup (getStream, opts) {
41 var sync = Value(false)
42 var updates = Value(0)
43
44 var filter = opts && opts.filter
45 var bumpFilter = opts && opts.bumpFilter
46 var windowSize = opts && opts.windowSize
47 var waitFor = opts && opts.waitFor || true
48
49 var newSinceRefresh = new Set()
50 var newInSession = new Set()
51 var prioritized = {}
52
53 var updateLoader = h('a Notifier -loader', {
54 href: '#',
55 'ev-click': refresh
56 }, [
57 'Show ',
58 h('strong', [updates]), ' ',
59 when(computed(updates, a => a === 1), 'update', 'updates')
60 ])
61
62 var content = Value()
63
64 var container = h('Scroller', {
65 style: { overflow: 'auto' }
66 }, [
67 h('div.wrapper', [
68 h('section.prepend', opts.prepend),
69 when(sync, null, h('Loading -large')),
70 content
71 ])
72 ])
73
74 onceTrue(waitFor, () => {
75 refresh()
76 pull(
77 getStream({old: false}),
78 pull.drain((item) => {
79 var type = item && item.value && item.value.content.type
80
81 // prioritize new messages on next refresh
82 newInSession.add(item.key)
83 newSinceRefresh.add(item.key)
84
85 // ignore message handled by another app
86 if (api.app.sync.externalHandler(item)) return
87
88 if (type && type !== 'vote' && typeof item.value.content === 'object' && item.value.timestamp > twoDaysAgo()) {
89 if (item.value && item.value.author === api.keys.sync.id() && !updates()) {
90 return refresh()
91 }
92 if (filter) {
93 if (item.value.content.type === 'post') {
94 var update = (item.value.content.root) ? {
95 type: 'message',
96 messageId: item.value.content.root,
97 channel: item.value.content.channel
98 } : {
99 type: 'message',
100 author: item.value.author,
101 channel: item.value.content.channel,
102 messageId: item.key
103 }
104
105 ensureMessageAndAuthor(update, (err, update) => {
106 if (!err) {
107 if (filter(update)) {
108 updates.set(updates() + 1)
109 }
110 }
111 })
112 }
113 } else {
114 updates.set(updates() + 1)
115 }
116 }
117 })
118 )
119 })
120
121 var abortLastFeed = null
122
123 var result = MutantArray([
124 when(updates, updateLoader),
125 container
126 ])
127
128 result.reload = refresh
129 result.pendingUpdates = updates
130
131 return result
132
133 // scoped
134
135 function refresh () {
136 if (abortLastFeed) {
137 abortLastFeed()
138 }
139 updates.set(0)
140 sync.set(false)
141
142 content.set(
143 h('section.content', {
144 hidden: computed(sync, s => !s)
145 })
146 )
147
148 var abortable = Abortable()
149 abortLastFeed = abortable.abort
150
151 prioritized = {}
152 newSinceRefresh.forEach(x => {
153 prioritized[x] = 2
154 })
155
156 pull(
157 api.feed.pull.summary(getStream, {windowSize, bumpFilter, prioritized}, () => {
158 sync.set(true)
159 }),
160 pull.asyncMap(ensureMessageAndAuthor),
161 pull.filter((item) => {
162 // ignore messages that are handled by other apps
163 if (item.rootMessage && api.app.sync.externalHandler(item.rootMessage)) return
164 if (filter) {
165 return filter(item)
166 } else {
167 return true
168 }
169 }),
170 abortable,
171 Scroller(container, content(), renderItem, false, false)
172 )
173
174 // clear high prioritized items
175 newSinceRefresh.clear()
176 }
177
178 function renderItem (item) {
179 var classList = []
180 if (item.priority >= 2) {
181 classList.push('-new')
182 }
183
184 if (item.type === 'message') {
185 var meta = null
186 var previousId = item.messageId
187 var replies = item.replies.slice(-4).map((msg) => {
188 var result = api.message.html.render(msg, {
189 inContext: true,
190 inSummary: true,
191 previousId,
192 priority: prioritized[msg.key]
193 })
194 previousId = msg.key
195 return result
196 })
197 var renderedMessage = item.message ? api.message.html.render(item.message, {inContext: true}) : null
198 if (renderedMessage) {
199 if (item.lastUpdateType === 'reply' && item.repliesFrom.size) {
200 meta = h('div.meta', {
201 title: names(item.repliesFrom)
202 }, [
203 many(item.repliesFrom, api.profile.html.person), ' replied'
204 ])
205 } else if (item.lastUpdateType === 'like' && item.likes.size) {
206 meta = h('div.meta', {
207 title: names(item.likes)
208 }, [
209 many(item.likes, api.profile.html.person), ' liked this message'
210 ])
211 }
212
213 return h('FeedEvent', [
214 meta,
215 renderedMessage,
216 when(replies.length, [
217 when(item.replies.length > replies.length || opts.partial,
218 h('a.full', {href: item.messageId}, ['View full thread'])
219 ),
220 h('div.replies', replies)
221 ])
222 ])
223 } else {
224 // when there is no root message in this window,
225 // try and show reply message, only show like message if we have nothing else to give
226 if (item.repliesFrom.size) {
227 meta = h('div.meta', {
228 title: names(item.repliesFrom)
229 }, [
230 many(item.repliesFrom, api.profile.html.person), ' replied to ', api.message.html.link(item.messageId)
231 ])
232 } else if (item.lastUpdateType === 'like' && item.likes.size) {
233 meta = h('div.meta', {
234 title: names(item.likes)
235 }, [
236 many(item.likes, api.profile.html.person), ' liked ', api.message.html.link(item.messageId)
237 ])
238 }
239
240 // only show this event if it has a meta description
241 if (meta) {
242 return h('FeedEvent', [
243 meta, h('div.replies', replies)
244 ])
245 }
246 }
247 } else if (item.type === 'follow') {
248 return h('FeedEvent -follow', {classList}, [
249 h('div.meta', {
250 title: names(item.contacts)
251 }, [
252 api.profile.html.person(item.id), ' followed ', many(item.contacts, api.profile.html.person)
253 ])
254 ])
255 } else if (item.type === 'subscribe') {
256 return h('FeedEvent -subscribe', {classList}, [
257 h('div.meta', {
258 title: names(item.subscribers)
259 }, [
260 many(item.subscribers, api.profile.html.person),
261 ' subscribed to ',
262 h('a', {href: `#${item.channel}`}, `#${item.channel}`)
263 ])
264 ])
265 }
266
267 return h('div')
268 }
269 }
270
271 function ensureMessageAndAuthor (item, cb) {
272 if (item.type === 'message' && !item.message) {
273 if (item.message) {
274 item.rootMessage = item.message
275 cb(null, item)
276 } else {
277 api.sbot.async.get(item.messageId, (_, value) => {
278 if (value) {
279 item.author = value.author
280 item.rootMessage = {key: item.messageId, value}
281 }
282 cb(null, item)
283 })
284 }
285 } else {
286 cb(null, item)
287 }
288 }
289
290 function names (ids) {
291 var items = map(Array.from(ids), api.about.obs.name)
292 return computed([items], (names) => names.map((n) => `- ${n}`).join('\n'))
293 }
294}
295
296function twoDaysAgo () {
297 return Date.now() - (2 * 24 * 60 * 60 * 1000)
298}
299
300function many (ids, fn) {
301 ids = Array.from(ids)
302 var featuredIds = ids.slice(-4).reverse()
303
304 if (ids.length) {
305 if (ids.length > 4) {
306 return [
307 fn(featuredIds[0]), ', ',
308 fn(featuredIds[1]), ', ',
309 fn(featuredIds[2]), ' and ',
310 ids.length - 3, ' others'
311 ]
312 } else if (ids.length === 4) {
313 return [
314 fn(featuredIds[0]), ', ',
315 fn(featuredIds[1]), ', ',
316 fn(featuredIds[2]), ' and ',
317 fn(featuredIds[3])
318 ]
319 } else if (ids.length === 3) {
320 return [
321 fn(featuredIds[0]), ', ',
322 fn(featuredIds[1]), ' and ',
323 fn(featuredIds[2])
324 ]
325 } else if (ids.length === 2) {
326 return [
327 fn(featuredIds[0]), ' and ',
328 fn(featuredIds[1])
329 ]
330 } else {
331 return fn(featuredIds[0])
332 }
333 }
334}
335

Built with git-ssb-web