git ssb

10+

Matt McKegg / patchwork



Tree: 5e06ff6caacaf2d29ccced2d9c39800bbf1923de

Files: 5e06ff6caacaf2d29ccced2d9c39800bbf1923de / modules / feed / html / rollup.js

9223 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 if (item.type === 'message') {
180 var meta = null
181 var previousId = item.messageId
182 var replies = item.replies.slice(-4).map((msg) => {
183 var result = api.message.html.render(msg, {
184 inContext: true,
185 inSummary: true,
186 previousId,
187 priority: prioritized[msg.key]
188 })
189 previousId = msg.key
190 return result
191 })
192 var renderedMessage = item.message ? api.message.html.render(item.message, {inContext: true}) : null
193 if (renderedMessage) {
194 if (item.lastUpdateType === 'reply' && item.repliesFrom.size) {
195 meta = h('div.meta', {
196 title: names(item.repliesFrom)
197 }, [
198 many(item.repliesFrom, api.profile.html.person), ' replied'
199 ])
200 } else if (item.lastUpdateType === 'like' && item.likes.size) {
201 meta = h('div.meta', {
202 title: names(item.likes)
203 }, [
204 many(item.likes, api.profile.html.person), ' liked this message'
205 ])
206 }
207
208 return h('FeedEvent', [
209 meta,
210 renderedMessage,
211 when(replies.length, [
212 when(item.replies.length > replies.length || opts.partial,
213 h('a.full', {href: item.messageId}, ['View full thread'])
214 ),
215 h('div.replies', replies)
216 ])
217 ])
218 } else {
219 // when there is no root message in this window,
220 // try and show reply message, only show like message if we have nothing else to give
221 if (item.repliesFrom.size) {
222 meta = h('div.meta', {
223 title: names(item.repliesFrom)
224 }, [
225 many(item.repliesFrom, api.profile.html.person), ' replied to ', api.message.html.link(item.messageId)
226 ])
227 } else if (item.lastUpdateType === 'like' && item.likes.size) {
228 meta = h('div.meta', {
229 title: names(item.likes)
230 }, [
231 many(item.likes, api.profile.html.person), ' liked ', api.message.html.link(item.messageId)
232 ])
233 }
234
235 // only show this event if it has a meta description
236 if (meta) {
237 return h('FeedEvent', [
238 meta, h('div.replies', replies)
239 ])
240 }
241 }
242 } else if (item.type === 'follow') {
243 return h('FeedEvent -follow', [
244 h('div.meta', {
245 title: names(item.contacts)
246 }, [
247 api.profile.html.person(item.id), ' followed ', many(item.contacts, api.profile.html.person)
248 ])
249 ])
250 } else if (item.type === 'subscribe') {
251 return h('FeedEvent -subscribe', [
252 h('div.meta', {
253 title: names(item.subscribers)
254 }, [
255 many(item.subscribers, api.profile.html.person),
256 ' subscribed to ',
257 h('a', {href: `#${item.channel}`}, `#${item.channel}`)
258 ])
259 ])
260 }
261
262 return h('div')
263 }
264 }
265
266 function ensureMessageAndAuthor (item, cb) {
267 if (item.type === 'message' && !item.message) {
268 if (item.message) {
269 item.rootMessage = item.message
270 cb(null, item)
271 } else {
272 api.sbot.async.get(item.messageId, (_, value) => {
273 if (value) {
274 item.author = value.author
275 item.rootMessage = {key: item.messageId, value}
276 }
277 cb(null, item)
278 })
279 }
280 } else {
281 cb(null, item)
282 }
283 }
284
285 function names (ids) {
286 var items = map(Array.from(ids), api.about.obs.name)
287 return computed([items], (names) => names.map((n) => `- ${n}`).join('\n'))
288 }
289}
290
291function twoDaysAgo () {
292 return Date.now() - (2 * 24 * 60 * 60 * 1000)
293}
294
295function many (ids, fn) {
296 ids = Array.from(ids)
297 var featuredIds = ids.slice(-4).reverse()
298
299 if (ids.length) {
300 if (ids.length > 4) {
301 return [
302 fn(featuredIds[0]), ', ',
303 fn(featuredIds[1]), ', ',
304 fn(featuredIds[2]), ' and ',
305 ids.length - 3, ' others'
306 ]
307 } else if (ids.length === 4) {
308 return [
309 fn(featuredIds[0]), ', ',
310 fn(featuredIds[1]), ', ',
311 fn(featuredIds[2]), ' and ',
312 fn(featuredIds[3])
313 ]
314 } else if (ids.length === 3) {
315 return [
316 fn(featuredIds[0]), ', ',
317 fn(featuredIds[1]), ' and ',
318 fn(featuredIds[2])
319 ]
320 } else if (ids.length === 2) {
321 return [
322 fn(featuredIds[0]), ' and ',
323 fn(featuredIds[1])
324 ]
325 } else {
326 return fn(featuredIds[0])
327 }
328 }
329}
330

Built with git-ssb-web