git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: 40388d6b54ff2c0d8c314271ce81dcbd0d3797d2

Files: 40388d6b54ff2c0d8c314271ce81dcbd0d3797d2 / modules / feed / html / rollup.js

8536 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 updateLoader = h('a Notifier -loader', {
50 href: '#',
51 'ev-click': refresh
52 }, [
53 'Show ',
54 h('strong', [updates]), ' ',
55 when(computed(updates, a => a === 1), 'update', 'updates')
56 ])
57
58 var content = Value()
59
60 var container = h('Scroller', {
61 style: { overflow: 'auto' }
62 }, [
63 h('div.wrapper', [
64 h('section.prepend', opts.prepend),
65 when(sync, null, h('Loading -large')),
66 content
67 ])
68 ])
69
70 onceTrue(waitFor, () => {
71 refresh()
72 pull(
73 getStream({old: false}),
74 pull.drain((item) => {
75 var type = item && item.value && item.value.content.type
76
77 // ignore message handled by another app
78 if (api.app.sync.externalHandler(item)) return
79
80 if (type && type !== 'vote' && typeof item.value.content === 'object' && item.value.timestamp > twoDaysAgo()) {
81 if (item.value && item.value.author === api.keys.sync.id() && !updates()) {
82 return refresh()
83 }
84 if (filter) {
85 var update = (item.value.content.type === 'post' && item.value.content.root) ? {
86 type: 'message',
87 messageId: item.value.content.root,
88 channel: item.value.content.channel
89 } : {
90 type: 'message',
91 author: item.value.author,
92 channel: item.value.content.channel,
93 messageId: item.key
94 }
95
96 ensureAuthor(update, (err, update) => {
97 if (!err) {
98 if (filter(update)) {
99 updates.set(updates() + 1)
100 }
101 }
102 })
103 } else {
104 updates.set(updates() + 1)
105 }
106 }
107 })
108 )
109 })
110
111 var abortLastFeed = null
112
113 var result = MutantArray([
114 when(updates, updateLoader),
115 container
116 ])
117
118 result.reload = refresh
119 result.pendingUpdates = updates
120
121 return result
122
123 // scoped
124
125 function refresh () {
126 if (abortLastFeed) {
127 abortLastFeed()
128 }
129 updates.set(0)
130 sync.set(false)
131
132 content.set(
133 h('section.content', {
134 hidden: computed(sync, s => !s)
135 })
136 )
137
138 var abortable = Abortable()
139 abortLastFeed = abortable.abort
140
141 pull(
142 api.feed.pull.summary(getStream, {windowSize, bumpFilter}, () => {
143 sync.set(true)
144 }),
145 pull.asyncMap(ensureMessageAndAuthor),
146 pull.filter((item) => {
147 // ignore messages that are handled by other apps
148 if (item.rootMessage && api.app.sync.externalHandler(item.rootMessage)) return
149 if (filter) {
150 return filter(item)
151 } else {
152 return true
153 }
154 }),
155 abortable,
156 Scroller(container, content(), renderItem, false, false)
157 )
158 }
159
160 function renderItem (item) {
161 if (item.type === 'message') {
162 var meta = null
163 var previousId = item.messageId
164 var replies = item.replies.slice(-4).map((msg) => {
165 var result = api.message.html.render(msg, {inContext: true, inSummary: true, previousId})
166 previousId = msg.key
167 return result
168 })
169 var renderedMessage = item.message ? api.message.html.render(item.message, {inContext: true}) : null
170 if (renderedMessage) {
171 if (item.lastUpdateType === 'reply' && item.repliesFrom.size) {
172 meta = h('div.meta', {
173 title: names(item.repliesFrom)
174 }, [
175 many(item.repliesFrom, api.profile.html.person), ' replied'
176 ])
177 } else if (item.lastUpdateType === 'like' && item.likes.size) {
178 meta = h('div.meta', {
179 title: names(item.likes)
180 }, [
181 many(item.likes, api.profile.html.person), ' liked this message'
182 ])
183 }
184
185 return h('FeedEvent', [
186 meta,
187 renderedMessage,
188 when(replies.length, [
189 when(item.replies.length > replies.length || opts.partial,
190 h('a.full', {href: item.messageId}, ['View full thread'])
191 ),
192 h('div.replies', replies)
193 ])
194 ])
195 } else {
196 if (item.lastUpdateType === 'reply' && item.repliesFrom.size) {
197 meta = h('div.meta', {
198 title: names(item.repliesFrom)
199 }, [
200 many(item.repliesFrom, api.profile.html.person), ' replied to ', api.message.html.link(item.messageId)
201 ])
202 } else if (item.lastUpdateType === 'like' && item.likes.size) {
203 meta = h('div.meta', {
204 title: names(item.likes)
205 }, [
206 many(item.likes, api.profile.html.person), ' liked ', api.message.html.link(item.messageId)
207 ])
208 }
209
210 if (meta || replies.length) {
211 return h('FeedEvent', [
212 meta, h('div.replies', replies)
213 ])
214 }
215 }
216 } else if (item.type === 'follow') {
217 return h('FeedEvent -follow', [
218 h('div.meta', {
219 title: names(item.contacts)
220 }, [
221 api.profile.html.person(item.id), ' followed ', many(item.contacts, api.profile.html.person)
222 ])
223 ])
224 } else if (item.type === 'subscribe') {
225 return h('FeedEvent -subscribe', [
226 h('div.meta', {
227 title: Array.from(item.channels).map(c => `#${c}`).join('\n')
228 }, [
229 api.profile.html.person(item.id), ' subscribed to ', many(item.channels, (channel) => {
230 return h('a', {href: `#${channel}`}, `#${channel}`)
231 })
232 ])
233 ])
234 }
235
236 return h('div')
237 }
238 }
239
240 function ensureMessageAndAuthor (item, cb) {
241 if (item.type === 'message' && !item.message) {
242 if (item.message) {
243 item.rootMessage = item.message
244 cb(null, item)
245 } else {
246 api.sbot.async.get(item.messageId, (_, value) => {
247 if (value) {
248 item.author = value.author
249 item.rootMessage = {key: item.messageId, value}
250 }
251 cb(null, item)
252 })
253 }
254 } else {
255 cb(null, item)
256 }
257 }
258
259 function names (ids) {
260 var items = map(Array.from(ids), api.about.obs.name)
261 return computed([items], (names) => names.map((n) => `- ${n}`).join('\n'))
262 }
263}
264
265function twoDaysAgo () {
266 return Date.now() - (2 * 24 * 60 * 60 * 1000)
267}
268
269function many (ids, fn) {
270 ids = Array.from(ids)
271 var featuredIds = ids.slice(-4).reverse()
272
273 if (ids.length) {
274 if (ids.length > 4) {
275 return [
276 fn(featuredIds[0]), ', ',
277 fn(featuredIds[1]), ', ',
278 fn(featuredIds[2]), ' and ',
279 ids.length - 3, ' others'
280 ]
281 } else if (ids.length === 4) {
282 return [
283 fn(featuredIds[0]), ', ',
284 fn(featuredIds[1]), ', ',
285 fn(featuredIds[2]), ' and ',
286 fn(featuredIds[3])
287 ]
288 } else if (ids.length === 3) {
289 return [
290 fn(featuredIds[0]), ', ',
291 fn(featuredIds[1]), ' and ',
292 fn(featuredIds[2])
293 ]
294 } else if (ids.length === 2) {
295 return [
296 fn(featuredIds[0]), ' and ',
297 fn(featuredIds[1])
298 ]
299 } else {
300 return fn(featuredIds[0])
301 }
302 }
303}
304

Built with git-ssb-web