git ssb

10+

Matt McKegg / patchwork



Tree: 01729a41ce366a9c658652fdbc3a6793b0c89de5

Files: 01729a41ce366a9c658652fdbc3a6793b0c89de5 / views / public-feed.js

8011 bytesRaw
1var SortedArray = require('sorted-array-functions')
2var Value = require('@mmckegg/mutant/value')
3var h = require('@mmckegg/mutant/html-element')
4var when = require('@mmckegg/mutant/when')
5var computed = require('@mmckegg/mutant/computed')
6var MutantArray = require('@mmckegg/mutant/array')
7var pullPushable = require('pull-pushable')
8var pullNext = require('pull-next')
9var Scroller = require('../lib/pull-scroll')
10var Abortable = require('pull-abortable')
11
12var m = require('../lib/h')
13
14var pull = require('pull-stream')
15
16var plugs = require('patchbay/plugs')
17var message_render = plugs.first(exports.message_render = [])
18var message_compose = plugs.first(exports.message_compose = [])
19var sbot_log = plugs.first(exports.sbot_log = [])
20var avatar_name = plugs.first(exports.avatar_name = [])
21var avatar_link = plugs.first(exports.avatar_link = [])
22var message_link = plugs.first(exports.message_link = [])
23
24exports.screen_view = function (path, sbot) {
25 if (path === '/public') {
26 var sync = Value(false)
27 var updates = Value(0)
28
29 var updateLoader = m('a.loader', {
30 href: '#',
31 'ev-click': refresh
32 }, [
33 'Show ',
34 h('strong', [updates]), ' ',
35 when(computed(updates, a => a === 1), 'update', 'updates')
36 ])
37
38 var content = h('div.column.scroller__content')
39
40 var scrollElement = h('div.column.scroller', {
41 style: {
42 'overflow': 'auto'
43 }
44 }, [
45 h('div.scroller__wrapper', [
46 message_compose({type: 'post'}, {placeholder: 'Write a public message'}),
47 content
48 ])
49 ])
50
51 setTimeout(refresh, 10)
52
53 pull(
54 sbot_log({old: false}),
55 pull.drain((item) => {
56 if (item.value.content.type !== 'vote') {
57 updates.set(updates() + 1)
58 }
59 })
60 )
61
62 var abortLastFeed = null
63
64 return MutantArray([
65 when(updates, updateLoader),
66 when(sync, scrollElement, m('Loading -large'))
67 ])
68 }
69
70 // scoped
71 function refresh () {
72 if (abortLastFeed) {
73 abortLastFeed()
74 }
75 updates.set(0)
76 sync.set(false)
77 content.innerHTML = ''
78
79 var abortable = Abortable()
80 abortLastFeed = abortable.abort
81
82 pull(
83 FeedSummary(sbot_log, 100, () => {
84 sync.set(true)
85 }),
86 abortable,
87 Scroller(scrollElement, content, renderItem, false, false)
88 )
89 }
90}
91
92function FeedSummary (stream, windowSize, cb) {
93 var last = null
94 var returned = false
95 return pullNext(() => {
96 var next = {reverse: true, limit: windowSize, live: false}
97 if (last) {
98 next.lt = last.timestamp
99 }
100 var pushable = pullPushable()
101 pull(
102 sbot_log(next),
103 pull.collect((err, values) => {
104 if (err) throw err
105 groupMessages(values).forEach(v => pushable.push(v))
106 last = values[values.length - 1]
107 pushable.end()
108 if (!returned) cb && cb()
109 returned = true
110 })
111 )
112 return pushable
113 })
114}
115
116function renderItem (item) {
117 if (item.type === 'message') {
118 var meta = null
119 var replies = item.replies.slice(-3).map(message_render)
120 var renderedMessage = item.message ? message_render(item.message) : null
121 if (renderedMessage) {
122 if (item.lastUpdateType === 'reply' && item.repliesFrom.size) {
123 meta = m('div.meta', [
124 manyPeople(item.repliesFrom), ' replied'
125 ])
126 } else if (item.lastUpdateType === 'dig' && item.digs.size) {
127 meta = m('div.meta', [
128 manyPeople(item.digs), ' dug this message'
129 ])
130 }
131
132 return m('FeedEvent', [
133 meta,
134 renderedMessage,
135 when(replies.length, [
136 when(item.replies.length > replies.length,
137 m('a.full', {href: `#${item.messageId}`}, ['View full thread'])
138 ),
139 m('div.replies', replies)
140 ])
141 ])
142 } else {
143 if (item.lastUpdateType === 'reply' && item.repliesFrom.size) {
144 meta = m('div.meta', [
145 manyPeople(item.repliesFrom), ' replied to ', message_link(item.messageId)
146 ])
147 } else if (item.lastUpdateType === 'dig' && item.digs.size) {
148 meta = m('div.meta', [
149 manyPeople(item.digs), ' dug ', message_link(item.messageId)
150 ])
151 }
152
153 if (meta || replies.length) {
154 return m('FeedEvent', [
155 meta, m('div.replies', replies)
156 ])
157 }
158 }
159 } else if (item.type === 'follow') {
160 return m('FeedEvent -follow', [
161 m('div.meta', [
162 person(item.id), ' followed ', manyPeople(item.contacts)
163 ])
164 ])
165 }
166
167 return h('div')
168}
169
170function person (id) {
171 return avatar_link(id, avatar_name(id), '')
172}
173
174function manyPeople (ids) {
175 ids = Array.from(ids)
176 var featuredIds = ids.slice(-3).reverse()
177
178 if (ids.length) {
179 if (ids.length > 3) {
180 return [
181 person(featuredIds[0]), ', ',
182 person(featuredIds[1]),
183 ' and ', ids.length - 2, ' others'
184 ]
185 } else if (ids.length === 3) {
186 return [
187 person(featuredIds[0]), ', ',
188 person(featuredIds[1]), ' and ',
189 person(featuredIds[2])
190 ]
191 } else if (ids.length === 2) {
192 return [
193 person(featuredIds[0]), ' and ',
194 person(featuredIds[1])
195 ]
196 } else {
197 return person(featuredIds[0])
198 }
199 }
200}
201
202function groupMessages (messages) {
203 var follows = {}
204 var messageUpdates = {}
205 reverseForEach(messages, function (msg) {
206 var c = msg.value.content
207 if (c.type === 'contact') {
208 updateContact(msg, follows)
209 } else if (c.type === 'vote') {
210 if (c.vote && c.vote.link) {
211 // only show digs of posts added in the current window
212 // and only for the main post
213 const group = messageUpdates[c.vote.link]
214 if (group) {
215 if (c.vote.value > 0) {
216 group.lastUpdateType = 'dig'
217 group.digs.add(msg.value.author)
218 group.updated = msg.timestamp
219 } else {
220 group.digs.delete(msg.value.author)
221 if (group.lastUpdateType === 'dig' && !group.digs.size && !group.replies.length) {
222 group.lastUpdateType = 'reply'
223 }
224 }
225 }
226 }
227 } else {
228 if (c.root) {
229 const group = ensureMessage(c.root, messageUpdates)
230 group.lastUpdateType = 'reply'
231 group.repliesFrom.add(msg.value.author)
232 group.replies.push(msg)
233 group.updated = msg.timestamp
234 } else {
235 const group = ensureMessage(msg.key, messageUpdates)
236 group.lastUpdateType = 'post'
237 group.updated = msg.timestamp
238 group.message = msg
239 }
240 }
241 })
242
243 var result = []
244 Object.keys(follows).forEach((key) => {
245 SortedArray.add(result, follows[key], compareUpdated)
246 })
247 Object.keys(messageUpdates).forEach((key) => {
248 SortedArray.add(result, messageUpdates[key], compareUpdated)
249 })
250 return result
251}
252
253function compareUpdated (a, b) {
254 return b.updated - a.updated
255}
256
257function reverseForEach (items, fn) {
258 var i = items.length - 1
259 var start = Date.now()
260 nextBatch()
261
262 function nextBatch () {
263 while (i >= 0 && Date.now() - start < 10) {
264 fn(items[i], i)
265 i -= 1
266 }
267
268 if (i > 0) {
269 setImmediate(nextBatch)
270 }
271 }
272}
273
274function updateContact (msg, groups) {
275 var c = msg.value.content
276 var id = msg.value.author
277 var group = groups[id]
278 if (c.following) {
279 if (!group) {
280 group = groups[id] = {
281 type: 'follow',
282 lastUpdateType: null,
283 contacts: new Set(),
284 updated: 0,
285 id: id
286 }
287 }
288 group.contacts.add(c.contact)
289 group.updated = msg.timestamp
290 } else {
291 if (group) {
292 group.contacts.delete(c.contact)
293 if (!group.contacts.size) {
294 delete groups[id]
295 }
296 }
297 }
298}
299
300function ensureMessage (id, groups) {
301 var group = groups[id]
302 if (!group) {
303 group = groups[id] = {
304 type: 'message',
305 repliesFrom: new Set(),
306 replies: [],
307 message: null,
308 messageId: id,
309 digs: new Set(),
310 updated: 0
311 }
312 }
313 return group
314}
315

Built with git-ssb-web