git ssb

10+

Matt McKegg / patchwork



Tree: 3d32dd2cc4892720337acdff65bf871556608561

Files: 3d32dd2cc4892720337acdff65bf871556608561 / modules / feed / html / rollup.js

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

Built with git-ssb-web