git ssb

10+

Matt McKegg / patchwork



Tree: 4fab101f4eca1ce3107af419d5980a34e67fe185

Files: 4fab101f4eca1ce3107af419d5980a34e67fe185 / modules / feed / html / rollup.js

9385 bytesRaw
1var Value = require('mutant/value')
2var Proxy = require('mutant/proxy')
3var when = require('mutant/when')
4var computed = require('mutant/computed')
5var h = require('mutant/h')
6var MutantArray = require('mutant/array')
7var Abortable = require('pull-abortable')
8var map = require('mutant/map')
9var pull = require('pull-stream')
10var nest = require('depnest')
11
12var onceTrue = require('mutant/once-true')
13var Scroller = require('pull-scroll')
14
15exports.needs = nest({
16 'message.html': {
17 render: 'first',
18 link: 'first'
19 },
20 'app.sync.externalHandler': 'first',
21 'sbot.async.get': 'first',
22 'keys.sync.id': 'first',
23 'about.obs.name': 'first',
24 feed: {
25 'html.rollup': 'first',
26 'pull.summary': 'first'
27 },
28 profile: {
29 'html.person': 'first'
30 }
31})
32
33exports.gives = nest({
34 'feed.html': ['rollup']
35})
36
37exports.create = function (api) {
38 return nest({
39 'feed.html': { rollup }
40 })
41 function rollup (getStream, opts) {
42 var loading = Proxy(true)
43 var updates = Value(0)
44
45 var filter = opts && opts.filter
46 var bumpFilter = opts && opts.bumpFilter
47 var windowSize = opts && opts.windowSize
48 var waitFor = opts && opts.waitFor || true
49 var getSequence = opts && opts.getSequence
50
51 var newSinceRefresh = new Set()
52 var newInSession = new Set()
53 var prioritized = {}
54
55 var updateLoader = h('a Notifier -loader', {
56 href: '#',
57 'ev-click': refresh
58 }, [
59 'Show ',
60 h('strong', [updates]), ' ',
61 when(computed(updates, a => a === 1), 'update', 'updates')
62 ])
63
64 var content = Value()
65
66 var container = h('Scroller', {
67 style: { overflow: 'auto' }
68 }, [
69 h('div.wrapper', [
70 h('section.prepend', opts.prepend),
71 content,
72 when(loading, h('Loading -large'))
73 ])
74 ])
75
76 onceTrue(waitFor, () => {
77 refresh()
78 pull(
79 getStream({old: false}),
80 pull.drain((item) => {
81 var type = item && item.value && item.value.content.type
82
83 // prioritize new messages on next refresh
84 newInSession.add(item.key)
85 newSinceRefresh.add(item.key)
86
87 // ignore message handled by another app
88 if (api.app.sync.externalHandler(item)) return
89
90 if (type && type !== 'vote' && typeof item.value.content === 'object' && item.value.timestamp > twoDaysAgo()) {
91 if (item.value && item.value.author === api.keys.sync.id() && !updates()) {
92 return refresh()
93 }
94 if (filter) {
95 if (item.value.content.type === 'post') {
96 var update = (item.value.content.root) ? {
97 type: 'message',
98 messageId: item.value.content.root,
99 channel: item.value.content.channel
100 } : {
101 type: 'message',
102 author: item.value.author,
103 channel: item.value.content.channel,
104 messageId: item.key
105 }
106
107 ensureMessageAndAuthor(update, (err, update) => {
108 if (!err) {
109 if (filter(update)) {
110 updates.set(updates() + 1)
111 }
112 }
113 })
114 }
115 } else {
116 updates.set(updates() + 1)
117 }
118 }
119 })
120 )
121 })
122
123 var abortLastFeed = null
124
125 var result = MutantArray([
126 when(updates, updateLoader),
127 container
128 ])
129
130 result.reload = refresh
131 result.pendingUpdates = updates
132
133 return result
134
135 // scoped
136
137 function refresh () {
138 if (abortLastFeed) {
139 abortLastFeed()
140 }
141 updates.set(0)
142
143 content.set(
144 h('section.content')
145 )
146
147 var abortable = Abortable()
148 abortLastFeed = abortable.abort
149
150 prioritized = {}
151 newSinceRefresh.forEach(x => {
152 prioritized[x] = 2
153 })
154
155 var stream = api.feed.pull.summary(getStream, {windowSize, bumpFilter, prioritized, getSequence})
156 loading.set(stream.loading)
157
158 pull(
159 stream,
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.rootMessage) {
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