git ssb

10+

Matt McKegg / patchwork



Commit 2150c96ae49e67fdcb638a88455af2c3ae8c9c53

Merge branch 'new-rollup'

# Conflicts:
#	package.json
Matt McKegg committed on 6/20/2017, 4:43:36 PM
Parent: c4b41ff3f5a9235cc76395794ae0d413ab7c9eca
Parent: 0900401aa912baeaca5a68feb70f4847503c6c70

Files changed

lib/pull-scroll.jschanged
lib/flumeview-channels.jsadded
lib/next-stepper.jsadded
main-window.jschanged
modules/feed/html/rollup.jschanged
modules/feed/pull/summary.jsdeleted
modules/page/html/render/all.jschanged
modules/page/html/render/mentions.jschanged
modules/page/html/render/private.jschanged
modules/page/html/render/profile.jschanged
modules/page/html/render/public.jschanged
package.jsonchanged
plugs/message/html/render/channel.jschanged
plugs/message/html/render/following.jsadded
plugs/channel/obs/recent.jsadded
server-process.jschanged
styles/gathering-card.mcsschanged
lib/pull-scroll.jsView
@@ -2,9 +2,9 @@
22 // should really PR this, but Dominic might not accept it :D
33
44 var pull = require('pull-stream')
55 var Pause = require('pull-pause')
6-var Obv = require('obv')
6 +var Value = require('mutant/value')
77
88 var next = 'undefined' === typeof setImmediate ? setTimeout : setImmediate
99 var buffer = Math.max(window.innerHeight * 2, 1000)
1010
@@ -17,9 +17,9 @@
1717
1818
1919 function Scroller(scroller, content, render, isPrepend, isSticky, cb) {
2020 assertScrollable(scroller)
21- var obv = Obv()
21 + var obs = Value(0)
2222
2323 //if second argument is a function,
2424 //it means the scroller and content elements are the same.
2525 if('function' === typeof content) {
@@ -42,9 +42,9 @@
4242 if(queue.length) {
4343 var m = queue.shift()
4444 var r = render(m)
4545 append(scroller, content, r, isPrepend, isSticky)
46- obv.set(queue.length)
46 + obs.set(queue.length)
4747 }
4848 }
4949
5050 function scroll (ev) {
@@ -64,9 +64,9 @@
6464 var stream = pull(
6565 pause,
6666 pull.drain(function (e) {
6767 queue.push(e)
68- obv.set(queue.length)
68 + obs.set(queue.length)
6969
7070 if(content.clientHeight < window.innerHeight)
7171 add()
7272
@@ -74,19 +74,20 @@
7474 if (isEnd(scroller, buffer, isPrepend))
7575 add()
7676 }
7777
78- if(queue.length > 5)
78 + if(queue.length > 5) {
7979 pause.pause()
80 + }
8081
8182 }, function (err) {
8283 if(err) console.error(err)
8384 cb ? cb(err) : console.error(err)
8485 })
8586 )
8687
8788 stream.visible = add
88- stream.observ = obv
89 + stream.queue = obs
8990 return stream
9091 }
9192
9293
lib/flumeview-channels.jsView
@@ -1,0 +1,39 @@
1 +var FlumeReduce = require('flumeview-reduce')
2 +
3 +exports.name = 'channels'
4 +exports.version = require('../package.json').version
5 +exports.manifest = {
6 + stream: 'source',
7 + get: 'async'
8 +}
9 +
10 +exports.init = function (ssb, config) {
11 + return ssb._flumeUse('channels', FlumeReduce(1, reduce, map))
12 +}
13 +
14 +function reduce (result, item) {
15 + if (!result) result = {}
16 + if (item) {
17 + var value = result[item.channel]
18 + if (!value) {
19 + value = result[item.channel] = {count: 0, timestamp: 0}
20 + }
21 + value.count += 1
22 + if (item.timestamp > value.timestamp) {
23 + value.timestamp = item.timestamp
24 + }
25 + }
26 + return result
27 +}
28 +
29 +function map (msg) {
30 + if (msg.value.content && typeof msg.value.content.channel === 'string') {
31 + var channel = msg.value.content.channel
32 + if (channel.length > 0 && channel.length < 30) {
33 + return {
34 + channel: channel.replace(/\s/g, ''),
35 + timestamp: msg.timestamp
36 + }
37 + }
38 + }
39 +}
lib/next-stepper.jsView
@@ -1,0 +1,56 @@
1 +const pull = require('pull-stream')
2 +const Next = require('pull-next')
3 +
4 +module.exports = nextStepper
5 +
6 +// TODO - this should be another module?
7 +
8 +function nextStepper (createStream, opts, range) {
9 + range = range || (opts.reverse ? 'lt' : 'gt')
10 +
11 + var last = null
12 + var count = -1
13 +
14 + return Next(function () {
15 + if (last) {
16 + if (count === 0) return
17 + var value = opts[range] = last
18 + if (value == null) return
19 + last = null
20 + }
21 + return pull(
22 + createStream(clone(opts)),
23 + pull.through(function (msg) {
24 + count++
25 + if (!msg.sync) {
26 + last = msg
27 + }
28 + }, function (err) {
29 + // retry on errors...
30 + if (err) {
31 + count = -1
32 + return count
33 + }
34 + // end stream if there were no results
35 + if (last == null) last = {}
36 + })
37 + )
38 + })
39 +}
40 +
41 +function get (obj, path) {
42 + if (!obj) return undefined
43 + if (typeof path === 'string') return obj[path]
44 + if (Array.isArray(path)) {
45 + for (var i = 0; obj && i < path.length; i++) {
46 + obj = obj[path[i]]
47 + }
48 + return obj
49 + }
50 +}
51 +
52 +function clone (obj) {
53 + var _obj = {}
54 + for (var k in obj) _obj[k] = obj[k]
55 + return _obj
56 +}
main-window.jsView
@@ -68,9 +68,8 @@
6868 watch(pendingCount, count => {
6969 electron.remote.app.setBadgeCount(count)
7070 })
7171
72-
7372 insertCss(require('./styles'))
7473
7574 var container = h(`MainWindow -${process.platform}`, [
7675 h('div.top', [
modules/feed/html/rollup.jsView
@@ -1,306 +1,223 @@
1-var Value = require('mutant/value')
2-var Proxy = require('mutant/proxy')
3-var when = require('mutant/when')
4-var computed = require('mutant/computed')
5-var h = require('mutant/h')
6-var MutantArray = require('mutant/array')
1 +var nest = require('depnest')
2 +var {Value, Proxy, Array: MutantArray, h, computed, map, when, onceTrue, throttle} = require('mutant')
3 +var pull = require('pull-stream')
74 var Abortable = require('pull-abortable')
8-var map = require('mutant/map')
9-var pull = require('pull-stream')
10-var nest = require('depnest')
11-
12-var onceTrue = require('mutant/once-true')
135 var Scroller = require('../../../lib/pull-scroll')
6 +var nextStepper = require('../../../lib/next-stepper')
7 +var extend = require('xtend')
8 +var paramap = require('pull-paramap')
149
10 +var bumpMessages = {
11 + 'vote': 'liked this message',
12 + 'post': 'replied to this message',
13 + 'about': 'added changes',
14 + 'mention': 'mentioned you'
15 +}
16 +
1517 exports.needs = nest({
16- 'message.html': {
17- render: 'first',
18- link: 'first'
19- },
18 + 'about.obs.name': 'first',
2019 'app.sync.externalHandler': 'first',
20 + 'message.html.render': 'first',
21 + 'profile.html.person': 'first',
22 + 'message.html.link': 'first',
23 + 'message.sync.root': 'first',
24 + 'feed.pull.rollup': 'first',
2125 '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- }
26 + 'keys.sync.id': 'first'
3127 })
3228
3329 exports.gives = nest({
34- 'feed.html': ['rollup']
30 + 'feed.html.rollup': true
3531 })
3632
3733 exports.create = function (api) {
38- return nest({
39- 'feed.html': { rollup }
40- })
41- function rollup (getStream, opts) {
42- var loading = Proxy(true)
34 + return nest('feed.html.rollup', function (getStream, {
35 + prepend,
36 + rootFilter = returnTrue,
37 + bumpFilter = returnTrue,
38 + displayFilter = returnTrue,
39 + waitFor = true
40 + }) {
4341 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 autoRefresh = opts && opts.autoRefresh
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')
42 + var yourId = api.keys.sync.id()
43 + var throttledUpdates = throttle(updates, 200)
44 + var updateLoader = h('a Notifier -loader', { href: '#', 'ev-click': refresh }, [
45 + 'Show ', h('strong', [throttledUpdates]), ' ', plural(throttledUpdates, 'update', 'updates')
6246 ])
6347
48 + var abortLastFeed = null
6449 var content = Value()
50 + var loading = Proxy(true)
51 + var newSinceRefresh = new Set()
52 + var highlightItems = new Set()
6553
6654 var container = h('Scroller', {
6755 style: { overflow: 'auto' }
6856 }, [
6957 h('div.wrapper', [
70- h('section.prepend', opts.prepend),
58 + h('section.prepend', prepend),
7159 content,
7260 when(loading, h('Loading -large'))
7361 ])
7462 ])
7563
7664 onceTrue(waitFor, () => {
7765 refresh()
66 +
67 + // display pending updates
7868 pull(
7969 getStream({old: false}),
80- pull.drain((item) => {
81- var type = item && item.value && item.value.content.type
70 + LookupRoot(),
71 + pull.filter((msg) => {
72 + return rootFilter(msg.root || msg) && bumpFilter(msg)
73 + }),
74 + pull.drain((msg) => {
75 + if (msg.value.content.type === 'vote') return
76 + if (api.app.sync.externalHandler(msg)) return
77 + newSinceRefresh.add(msg.key)
8278
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 (autoRefresh && 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- }
79 + if (updates() === 0 && msg.value.author === yourId && container.scrollTop < 20) {
80 + refresh()
81 + } else {
82 + updates.set(updates() + 1)
11883 }
11984 })
12085 )
12186 })
12287
123- var abortLastFeed = null
124-
12588 var result = MutantArray([
12689 when(updates, updateLoader),
12790 container
12891 ])
12992
93 + result.pendingUpdates = throttledUpdates
13094 result.reload = refresh
131- result.pendingUpdates = updates
13295
13396 return result
13497
135- // scoped
136-
13798 function refresh () {
138- if (abortLastFeed) {
139- abortLastFeed()
140- }
99 + if (abortLastFeed) abortLastFeed()
141100 updates.set(0)
101 + content.set(h('section.content'))
142102
143- content.set(
144- h('section.content')
145- )
146-
147103 var abortable = Abortable()
148104 abortLastFeed = abortable.abort
149105
150- prioritized = {}
151- newSinceRefresh.forEach(x => {
152- prioritized[x] = 2
153- })
106 + highlightItems = newSinceRefresh
107 + newSinceRefresh = new Set()
154108
155- var stream = api.feed.pull.summary(getStream, {windowSize, bumpFilter, prioritized})
156- loading.set(stream.loading)
109 + var done = Value(false)
110 + var stream = nextStepper(getStream, {reverse: true, limit: 50})
111 + var scroller = Scroller(container, content(), renderItem, false, false, () => done.set(true))
157112
113 + // track loading state
114 + loading.set(computed([done, scroller.queue], (done, queue) => {
115 + return !done && queue < 5
116 + }))
117 +
158118 pull(
159119 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- }),
120 + pull.filter(bumpFilter),
170121 abortable,
171- Scroller(container, content(), renderItem, false, false)
122 + api.feed.pull.rollup(rootFilter),
123 + scroller
172124 )
173-
174- // clear high prioritized items
175- newSinceRefresh.clear()
176125 }
177126
178- function renderItem (item) {
179- var classList = []
180- if (item.priority >= 2) {
181- classList.push('-new')
182- }
127 + function renderItem (item, opts) {
128 + var partial = opts && opts.partial
129 + var meta = null
130 + var previousId = item.key
183131
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
132 + var groupedBumps = {}
133 + var lastBumpType = null
134 +
135 + item.replies.forEach(msg => {
136 + var value = bumpFilter(msg)
137 + if (value) {
138 + var type = typeof value === 'string' ? value : getType(msg)
139 + ;(groupedBumps[type] = groupedBumps[type] || []).push(msg)
140 + lastBumpType = type
141 + }
142 + })
143 +
144 + var replies = item.replies.filter(isReply)
145 + var replyElements = replies.filter(displayFilter).sort(byAssertedTime).slice(-3).map((msg) => {
146 + var result = api.message.html.render(msg, {
147 + inContext: true,
148 + inSummary: true,
149 + previousId,
150 + priority: highlightItems.has(msg.key) ? 2 : 0
196151 })
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- }
152 + previousId = msg.key
153 + return result
154 + })
212155
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- }
156 + var renderedMessage = api.message.html.render(item, {inContext: true})
157 + if (!renderedMessage) return h('div')
158 + if (lastBumpType) {
159 + var bumps = lastBumpType === 'vote'
160 + ? getLikeAuthors(groupedBumps[lastBumpType])
161 + : getAuthors(groupedBumps[lastBumpType])
239162
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- ])
163 + var description = bumpMessages[lastBumpType] || 'added changes'
164 + meta = h('div.meta', { title: names(bumps) }, [
165 + many(bumps, api.profile.html.person), ' ', description
254166 ])
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- ])
265167 }
266168
267- return h('div')
169 + return h('FeedEvent -post', {
170 + attributes: {
171 + 'data-root-id': item.key
172 + }
173 + }, [
174 + meta,
175 + renderedMessage,
176 + when(replyElements.length, [
177 + when(replies.length > replyElements.length || partial,
178 + h('a.full', {href: item.key}, ['View full thread (', replies.length, ')'])
179 + ),
180 + h('div.replies', replyElements)
181 + ])
182 + ])
268183 }
184 + })
185 +
186 + function names (ids) {
187 + var items = map(Array.from(ids), api.about.obs.name)
188 + return computed([items], (names) => names.map((n) => `- ${n}`).join('\n'))
269189 }
270190
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)
191 + function LookupRoot () {
192 + return paramap((msg, cb) => {
193 + var rootId = api.message.sync.root(msg)
194 + if (rootId) {
195 + api.sbot.async.get(rootId, (_, value) => {
196 + cb(null, extend(msg, {
197 + root: {key: rootId, value}
198 + }))
199 + })
276200 } 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- })
201 + cb(null, msg)
284202 }
285- } else {
286- cb(null, item)
287- }
203 + })
288204 }
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- }
294205 }
295206
296-function twoDaysAgo () {
297- return Date.now() - (2 * 24 * 60 * 60 * 1000)
207 +function plural (value, single, many) {
208 + return computed(value, (value) => {
209 + if (value === 1) {
210 + return single
211 + } else {
212 + return many
213 + }
214 + })
298215 }
299216
300217 function many (ids, fn) {
301218 ids = Array.from(ids)
302- var featuredIds = ids.slice(-4).reverse()
219 + var featuredIds = ids.slice(0, 4)
303220
304221 if (ids.length) {
305222 if (ids.length > 4) {
306223 return [
@@ -331,4 +248,50 @@
331248 return fn(featuredIds[0])
332249 }
333250 }
334251 }
252 +
253 +function getAuthors (items) {
254 + return items.reduce((result, msg) => {
255 + result.add(msg.value.author)
256 + return result
257 + }, new Set())
258 +}
259 +
260 +function getLikeAuthors (items) {
261 + return items.reduce((result, msg) => {
262 + if (msg.value.content.type === 'vote') {
263 + if (msg.value.content && msg.value.content.vote && msg.value.content.vote.value === 1) {
264 + result.add(msg.value.author)
265 + } else {
266 + result.delete(msg.value.author)
267 + }
268 + }
269 + return result
270 + }, new Set())
271 +}
272 +
273 +function isUpdate (msg) {
274 + if (msg.value && msg.value.content) {
275 + var type = msg.value.content.type
276 + return type === 'about'
277 + }
278 +}
279 +
280 +function isReply (msg) {
281 + if (msg.value && msg.value.content) {
282 + var type = msg.value.content.type
283 + return type === 'post' || (type === 'about' && msg.value.content.attendee)
284 + }
285 +}
286 +
287 +function getType (msg) {
288 + return msg && msg.value && msg.value.content && msg.value.content.type
289 +}
290 +
291 +function returnTrue () {
292 + return true
293 +}
294 +
295 +function byAssertedTime (a, b) {
296 + return a.value.timestamp - b.value.timestamp
297 +}
modules/feed/pull/summary.jsView
@@ -1,288 +1,0 @@
1-var pull = require('pull-stream')
2-var pullDefer = require('pull-defer')
3-var pullNext = require('pull-next')
4-var SortedArray = require('sorted-array-functions')
5-var nest = require('depnest')
6-var ref = require('ssb-ref')
7-var sustained = require('../../../lib/sustained')
8-var Value = require('mutant/value')
9-
10-exports.gives = nest({
11- 'feed.pull': [ 'summary' ]
12-})
13-
14-exports.create = function () {
15- return nest({
16- 'feed.pull': { summary }
17- })
18-}
19-
20-function summary (source, opts, cb) {
21- var bumpFilter = opts && opts.bumpFilter
22- var windowSize = opts && opts.windowSize || 1000
23- var prioritized = opts && opts.prioritized || {}
24-
25- var loading = Value(true)
26-
27- var last = null
28- var returned = false
29- var done = false
30-
31- var result = pullNext(() => {
32- if (!done) {
33- loading.set(true)
34- var next = {reverse: true, limit: windowSize, live: false}
35- if (last) {
36- next.lt = last
37- }
38- var deferred = pullDefer.source()
39- pull(
40- source(next),
41- pull.collect((err, values) => {
42- loading.set(false)
43- if (err) throw err
44- if (!values.length) {
45- done = true
46- deferred.resolve(pull.values([]))
47- if (!returned) cb && cb()
48- returned = true
49- } else {
50- var fromTime = last && last.timestamp || Date.now()
51- last = values[values.length - 1]
52- groupMessages(values, fromTime, {bumpFilter, prioritized}, (err, result) => {
53- if (err) throw err
54- deferred.resolve(
55- pull.values(result)
56- )
57- if (!returned) cb && cb()
58- returned = true
59- })
60- }
61- })
62- )
63- }
64- return deferred
65- })
66-
67- // switch to loading state immediately, only revert after no loading for > 200 ms
68- result.loading = sustained(loading, 500, x => x)
69-
70- return result
71-}
72-
73-function groupMessages (messages, fromTime, opts, cb) {
74- var subscribes = {}
75- var follows = {}
76- var messageUpdates = {}
77- reverseForEach(messages, function (msg) {
78- if (!msg.value) return
79- var c = msg.value.content
80- if (c.type === 'contact') {
81- updateContact(msg, follows, opts)
82- } else if (c.type === 'channel') {
83- updateChannel(msg, subscribes, opts)
84- } else if (c.type === 'vote') {
85- if (c.vote && c.vote.link) {
86- // only show likes of posts added in the current window
87- // and only for the main post
88- const group = messageUpdates[c.vote.link]
89- if (group) {
90- if (c.vote.value > 0) {
91- group.likes.add(msg.value.author)
92- group.relatedMessages.push(msg)
93- } else {
94- group.likes.delete(msg.value.author)
95- group.relatedMessages.push(msg)
96- }
97- }
98- }
99- } else {
100- if (c.root) {
101- const group = ensureMessage(c.root, messageUpdates)
102- group.fromTime = fromTime
103- group.repliesFrom.add(msg.value.author)
104- SortedArray.add(group.replies, msg, compareUserTimestamp)
105- group.channel = group.channel || msg.value.content.channel
106- group.relatedMessages.push(msg)
107- } else {
108- const group = ensureMessage(msg.key, messageUpdates)
109- group.fromTime = fromTime
110- group.lastUpdateType = 'post'
111- group.updated = msg.timestamp || msg.value.sequence
112- group.author = msg.value.author
113- group.channel = msg.value.content.channel
114- group.message = msg
115- group.boxed = typeof msg.value.content === 'string'
116- }
117- }
118- }, () => {
119- var result = []
120- Object.keys(follows).forEach((key) => {
121- bumpIfNeeded(follows[key], opts)
122- if (follows[key].updated) {
123- SortedArray.add(result, follows[key], compareUpdated)
124- }
125- })
126- Object.keys(subscribes).forEach((key) => {
127- bumpIfNeeded(subscribes[key], opts)
128- if (subscribes[key].updated) {
129- SortedArray.add(result, subscribes[key], compareUpdated)
130- }
131- })
132- Object.keys(messageUpdates).forEach((key) => {
133- bumpIfNeeded(messageUpdates[key], opts)
134- if (messageUpdates[key].updated) {
135- SortedArray.add(result, messageUpdates[key], compareUpdated)
136- }
137- })
138- cb(null, result)
139- })
140-}
141-
142-function bumpIfNeeded (group, {bumpFilter, prioritized}) {
143- group.relatedMessages.forEach(msg => {
144- if (prioritized[msg.key] && group.priority < prioritized[msg.key]) {
145- group.priority = prioritized[msg.key]
146- }
147-
148- var shouldBump = !bumpFilter || bumpFilter(msg, group)
149-
150- // only bump when filter passes
151- var newUpdated = msg.timestamp || msg.value.sequence
152- if (!group.updated || (shouldBump && newUpdated > group.updated)) {
153- group.updated = newUpdated
154- if (msg.value.content.type === 'vote') {
155- if (group.likes.size) {
156- group.lastUpdateType = 'like'
157- } else if (group.repliesFrom.size) {
158- group.lastUpdateType = 'reply'
159- } else if (group.message) {
160- group.lastUpdateType = 'post'
161- }
162- }
163-
164- if (msg.value.content.type === 'post') {
165- if (msg.value.content.root) {
166- group.lastUpdateType = 'reply'
167- } else {
168- group.lastUpdateType = 'post'
169- }
170- }
171- }
172- })
173-}
174-
175-function compareUpdated (a, b) {
176- // highest priority first
177- // then most recent date
178- return b.priority - a.priority || b.updated - a.updated
179-}
180-
181-function reverseForEach (items, fn, cb) {
182- var i = items.length - 1
183- nextBatch()
184-
185- function nextBatch () {
186- var start = Date.now()
187- while (i >= 0) {
188- fn(items[i], i)
189- i -= 1
190- if (Date.now() - start > 10) break
191- }
192-
193- if (i > 0) {
194- setImmediate(nextBatch)
195- } else {
196- cb && cb()
197- }
198- }
199-}
200-
201-function updateContact (msg, groups, opts) {
202- var c = msg.value.content
203- var id = msg.value.author
204- var group = groups[id]
205- if (ref.isFeed(c.contact)) {
206- if (c.following) {
207- if (!group) {
208- group = groups[id] = {
209- type: 'follow',
210- priority: 0,
211- relatedMessages: [],
212- lastUpdateType: null,
213- contacts: new Set(),
214- updated: 0,
215- author: id,
216- id: id
217- }
218- }
219- group.contacts.add(c.contact)
220- group.relatedMessages.push(msg)
221- } else {
222- if (group) {
223- group.contacts.delete(c.contact)
224- if (!group.contacts.size) {
225- delete groups[id]
226- }
227- }
228- }
229- }
230-}
231-
232-function updateChannel (msg, groups, opts) {
233- var c = msg.value.content
234- var channel = c.channel
235- var group = groups[channel]
236- if (typeof channel === 'string') {
237- if (c.subscribed) {
238- if (!group) {
239- group = groups[channel] = {
240- type: 'subscribe',
241- priority: 0,
242- relatedMessages: [],
243- lastUpdateType: null,
244- subscribers: new Set(),
245- updated: 0,
246- channel
247- }
248- }
249- group.subscribers.add(msg.value.author)
250- group.relatedMessages.push(msg)
251- } else {
252- if (group) {
253- group.subscribers.delete(msg.value.author)
254- if (!group.subscribers.size) {
255- delete groups[channel]
256- }
257- }
258- }
259- }
260-}
261-
262-function ensureMessage (id, groups) {
263- var group = groups[id]
264- if (!group) {
265- group = groups[id] = {
266- type: 'message',
267- priority: 0,
268- repliesFrom: new Set(),
269- relatedMessages: [],
270- replies: [],
271- message: null,
272- messageId: id,
273- likes: new Set(),
274- updated: 0
275- }
276- }
277- return group
278-}
279-
280-function compareUserTimestamp (a, b) {
281- var isClose = !a.timestamp || !b.timestamp || Math.abs(a.timestamp - b.timestamp) < (10 * 60e3)
282- if (isClose) {
283- // recieved close together, use provided timestamps
284- return a.value.timestamp - b.value.timestamp
285- } else {
286- return a.timestamp - b.timestamp
287- }
288-}
modules/page/html/render/all.jsView
@@ -28,10 +28,9 @@
2828 api.message.html.compose({ meta: { type: 'post' }, placeholder: 'Write a public message' })
2929 ]
3030
3131 var feedView = api.feed.html.rollup(api.feed.pull.public, {
32- prepend,
33- windowSize: 1000
32 + prepend
3433 })
3534
3635 var result = h('div.SplitView', [
3736 h('div.main', feedView)
modules/page/html/render/mentions.jsView
@@ -13,9 +13,20 @@
1313 return nest('page.html.render', function mentions (path) {
1414 if (path !== '/mentions') return
1515 var id = api.keys.sync.id()
1616 return api.feed.html.rollup(api.feed.pull.mentions(id), {
17- windowSize: 20,
18- partial: true
17 + bumpFilter: mentionFilter,
18 + displayFilter: mentionFilter
1919 })
20 +
21 + // scoped
22 + function mentionFilter (msg) {
23 + if (Array.isArray(msg.value.content.mentions)) {
24 + if (msg.value.content.mentions.some(mention => {
25 + return mention && mention.link === id
26 + })) {
27 + return 'mention'
28 + }
29 + }
30 + }
2031 })
2132 }
modules/page/html/render/private.jsView
@@ -27,7 +27,7 @@
2727 placeholder: `Write a private message \n\n\n\nThis can only be read by yourself and people you have @mentioned.`
2828 })
2929 ]
3030
31- return api.feed.html.rollup(api.feed.pull.private, { prepend, windowSize: 200 })
31 + return api.feed.html.rollup(api.feed.pull.private, { prepend })
3232 })
3333 }
modules/page/html/render/profile.jsView
@@ -164,9 +164,13 @@
164164 h('section', [ namePicker, imagePicker ])
165165 ])
166166 ])
167167
168- var feedView = api.feed.html.rollup(api.feed.pull.profile(id), { prepend, autoRefresh: true })
168 + var feedView = api.feed.html.rollup(api.feed.pull.profile(id), {
169 + prepend,
170 + displayFilter: (msg) => msg.value.author === id,
171 + bumpFilter: (msg) => msg.value.author === id,
172 + })
169173
170174 var container = h('div', {className: 'SplitView'}, [
171175 h('div.main', [
172176 feedView
@@ -208,8 +212,9 @@
208212 h('div.name', [ api.about.obs.name(id) ])
209213 ])
210214 ])
211215 }, {
216 + maxTime: 5,
212217 idle: true
213218 })
214219 ])
215220 ]
modules/page/html/render/public.jsView
@@ -56,48 +56,28 @@
5656 waitFor: computed([
5757 following.sync,
5858 subscribedChannels.sync
5959 ], (...x) => x.every(Boolean)),
60- windowSize: 1000,
61- filter: (item) => {
62- return !item.boxed && (item.lastUpdateType !== 'post' || item.message) && (
63- id === item.author ||
64- (item.author && following().has(item.author)) ||
65- (item.type === 'message' && subscribedChannels().has(item.channel)) ||
66- (item.type === 'subscribe' && item.subscribers.size) ||
67- (item.repliesFrom && item.repliesFrom.has(id)) ||
68- item.likes && item.likes.has(id)
69- )
70- },
71- bumpFilter: (msg, group) => {
72- if (group.type === 'subscribe') {
73- removeStrangers(group.subscribers)
74- }
7560
76- if (group.type === 'message') {
77- removeStrangers(group.likes)
78- removeStrangers(group.repliesFrom)
61 + rootFilter: function (msg) {
62 + if (msg.value && msg.value.content && typeof msg.value.content === 'object') {
63 + var author = msg.value.author
64 + var type = msg.value.content.type
65 + var channel = msg.value.content.channel
7966
80- if (!group.message) {
81- // if message is old, only show replies from friends
82- group.replies = group.replies.filter(x => {
83- return (x.value.author === id || following().has(x.value.author))
84- })
85- }
86- }
87-
88- if (!group.message) {
8967 return (
90- isMentioned(id, msg.value.content.mentions) ||
91- msg.value.author === id || (
92- fromDay(msg, group.fromTime) && (
93- following().has(msg.value.author) ||
94- group.repliesFrom.has(id)
95- )
96- )
68 + id === author ||
69 + following().has(author) ||
70 + (type === 'message' && subscribedChannels().has(channel))
9771 )
9872 }
99- return true
73 + },
74 +
75 + bumpFilter: function (msg) {
76 + if (msg.value && msg.value.content && typeof msg.value.content === 'object') {
77 + var author = msg.value.author
78 + return id === author || following().has(author)
79 + }
10080 }
10181 })
10282
10383 var result = h('div.SplitView', [
@@ -111,52 +91,43 @@
11191 result.reload = feedView.reload
11292
11393 return result
11494
115- function removeStrangers (set) {
116- if (set) {
117- Array.from(set).forEach(key => {
118- if (!following().has(key) && key !== id) {
119- set.delete(key)
120- }
121- })
122- }
123- }
124-
12595 function getSidebar () {
12696 var whoToFollow = computed([following, api.profile.obs.recentlyUpdated(), localPeers], (following, recent, peers) => {
12797 return Array.from(recent).filter(x => x !== id && !following.has(x) && !peers.includes(x)).slice(0, 10)
12898 })
12999 return [
130100 h('button -pub -full', {
131101 'ev-click': api.invite.sheet
132102 }, '+ Join Pub'),
133- when(computed(channels, x => x.length), h('h2', 'Active Channels')),
134- when(loading, [ h('Loading') ]),
135- h('div', {
136- classList: 'ChannelList',
137- hidden: loading
138- }, [
139- map(channels, (channel) => {
140- var subscribed = subscribedChannels.has(channel)
141- return h('a.channel', {
142- href: `#${channel}`,
143- classList: [
144- when(subscribed, '-subscribed')
145- ]
146- }, [
147- h('span.name', '#' + channel),
148- when(subscribed,
149- h('a.-unsubscribe', {
150- 'ev-click': send(unsubscribe, channel)
151- }, 'Unsubscribe'),
152- h('a.-subscribe', {
153- 'ev-click': send(subscribe, channel)
154- }, 'Subscribe')
155- )
156- ])
157- }, {maxTime: 5}),
158- h('a.channel -more', {href: '/channels'}, 'More Channels...')
103 + when(loading, [ h('Loading') ], [
104 + when(computed(channels, x => x.length), h('h2', 'Active Channels')),
105 + h('div', {
106 + classList: 'ChannelList',
107 + hidden: loading
108 + }, [
109 + map(channels, (channel) => {
110 + var subscribed = subscribedChannels.has(channel)
111 + return h('a.channel', {
112 + href: `#${channel}`,
113 + classList: [
114 + when(subscribed, '-subscribed')
115 + ]
116 + }, [
117 + h('span.name', '#' + channel),
118 + when(subscribed,
119 + h('a.-unsubscribe', {
120 + 'ev-click': send(unsubscribe, channel)
121 + }, 'Unsubscribe'),
122 + h('a.-subscribe', {
123 + 'ev-click': send(subscribe, channel)
124 + }, 'Subscribe')
125 + )
126 + ])
127 + }, {maxTime: 5}),
128 + h('a.channel -more', {href: '/channels'}, 'More Channels...')
129 + ])
159130 ]),
160131
161132 PeerList(localPeers, 'Local'),
162133 PeerList(connectedPubs, 'Connected Pubs'),
@@ -222,20 +193,8 @@
222193 }
223194 }
224195 }
225196
226-function isMentioned (id, list) {
227- if (Array.isArray(list)) {
228- return list.includes(id)
229- } else {
230- return false
231- }
232-}
233-
234-function fromDay (msg, fromTime) {
235- return (fromTime - msg.timestamp) < (24 * 60 * 60e3)
236-}
237-
238197 function arrayEq (a, b) {
239198 if (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a !== b) {
240199 return a.every((value, i) => value === b[i])
241200 }
package.jsonView
@@ -28,12 +28,12 @@
2828 "level": "~1.7.0",
2929 "micro-css": "^2.0.0",
3030 "mime-types": "^2.1.15",
3131 "moment": "^2.18.1",
32- "mutant": "^3.20.2",
32 + "mutant": "^3.21.0",
3333 "mutant-pull-reduce": "^1.1.0",
3434 "obv": "0.0.1",
35- "patchcore": "~1.4.3",
35 + "patchcore": "~1.5.0",
3636 "pull-abortable": "^4.1.0",
3737 "pull-defer": "^0.2.2",
3838 "pull-file": "~1.0.0",
3939 "pull-identify-filetype": "^1.1.0",
plugs/message/html/render/channel.jsView
@@ -14,9 +14,9 @@
1414 exports.create = function (api) {
1515 return nest('message.html.render', function renderMessage (msg, opts) {
1616 if (msg.value.content.type !== 'channel') return
1717 var element = api.message.html.layout(msg, extend({
18- content: messageContent(msg),
18 + miniContent: messageContent(msg),
1919 layout: 'mini'
2020 }, opts))
2121
2222 return api.message.html.decorate(element, { msg })
plugs/message/html/render/following.jsView
@@ -1,0 +1,37 @@
1 +var h = require('mutant/h')
2 +var nest = require('depnest')
3 +var extend = require('xtend')
4 +var ref = require('ssb-ref')
5 +
6 +exports.needs = nest({
7 + 'message.html': {
8 + decorate: 'reduce',
9 + layout: 'first'
10 + },
11 + 'profile.html.person': 'first'
12 +})
13 +
14 +exports.gives = nest('message.html.render')
15 +
16 +exports.create = function (api) {
17 + return nest('message.html.render', function renderMessage (msg, opts) {
18 + if (msg.value.content.type !== 'contact') return
19 + if (!ref.isFeed(msg.value.content.contact)) return
20 + if (typeof msg.value.content.following !== 'boolean') return
21 +
22 + var element = api.message.html.layout(msg, extend({
23 + miniContent: messageContent(msg),
24 + layout: 'mini'
25 + }, opts))
26 +
27 + return api.message.html.decorate(element, { msg })
28 + })
29 +
30 + function messageContent (msg) {
31 + var following = msg.value.content.following
32 + return [
33 + following ? 'followed ' : 'unfollowed ',
34 + api.profile.html.person(msg.value.content.contact)
35 + ]
36 + }
37 +}
plugs/channel/obs/recent.jsView
@@ -1,0 +1,78 @@
1 +// uses lib/flumeview-channels
2 +
3 +var nest = require('depnest')
4 +var pull = require('pull-stream')
5 +
6 +var { Value, Dict, Struct, computed, resolve, throttle } = require('mutant')
7 +
8 +exports.needs = nest({
9 + 'sbot.pull.stream': 'first'
10 +})
11 +
12 +exports.gives = nest({
13 + 'channel.obs.recent': true
14 +})
15 +
16 +exports.create = function (api) {
17 + var recentChannels = null
18 + var channelsLookup = null
19 +
20 + return nest({
21 + 'channel.obs.recent': function () {
22 + load()
23 + return recentChannels
24 + }
25 + })
26 +
27 + function load () {
28 + if (!recentChannels) {
29 + var sync = Value(false)
30 + channelsLookup = Dict()
31 +
32 + pull(
33 + api.sbot.pull.stream(sbot => sbot.channels.stream({live: true})),
34 + pull.drain(msg => {
35 + if (!sync()) {
36 + channelsLookup.transaction(() => {
37 + for (var channel in msg) {
38 + var obs = ChannelRef(channel)
39 + obs.set({
40 + id: channel,
41 + updatedAt: msg[channel].timestamp,
42 + count: msg[channel].count
43 + })
44 + channelsLookup.put(channel, obs)
45 + }
46 + sync.set(true)
47 + })
48 + } else {
49 + var obs = channelsLookup.get(msg.channel)
50 + if (!obs) {
51 + obs = ChannelRef(msg.dest)
52 + channelsLookup.put(msg.dest, obs)
53 + }
54 + obs.set({
55 + id: msg.channel,
56 + updatedAt: Math.max(resolve(obs.updatedAt), msg.timestamp),
57 + count: resolve(obs.count) + 1
58 + })
59 + }
60 + })
61 + )
62 +
63 + recentChannels = computed(throttle(channelsLookup, 1000), (lookup) => {
64 + var values = Object.keys(lookup).map(x => lookup[x]).sort((a, b) => b.updatedAt - a.updatedAt).map(x => x.id)
65 + return values
66 + })
67 + recentChannels.sync = sync
68 + }
69 + }
70 +}
71 +
72 +function ChannelRef (id) {
73 + return Struct({
74 + id,
75 + updatedAt: Value(0),
76 + count: Value(0)
77 + }, {merge: true})
78 +}
server-process.jsView
@@ -17,8 +17,9 @@
1717 .use(require('scuttlebot/plugins/logging'))
1818 .use(require('ssb-query'))
1919 .use(require('ssb-about'))
2020 .use(require('ssb-contacts'))
21 + .use(require('./lib/flumeview-channels'))
2122 .use(require('./lib/progress-stream'))
2223
2324 module.exports = function (ssbConfig) {
2425 var context = {
styles/gathering-card.mcssView
@@ -25,8 +25,12 @@
2525 div.attendees {
2626 margin: 0 5px
2727 a {
2828 margin-right: 4px
29 + img {
30 + height: 45px
31 + width: 45px
32 + }
2933 }
3034 }
3135 div.actions {
3236 margin-top: 10px

Built with git-ssb-web