git ssb

10+

Matt McKegg / patchwork



Commit 6b5fb10bffe7df13233ba65f8a3631d6121ed3cd

add feed sync and index progress to user interface

Matt McKegg committed on 2/24/2017, 5:34:43 PM
Parent: 1e7b8b22b00038209334ed25b4c8b3e151780691

Files changed

lib/friends-with-sync.jsadded
lib/mutant-to-pull.jsadded
lib/query-with-progress.jsadded
lib/replicate-with-progress.jsadded
main-window.jschanged
modules/app/html/progress-notifier.jsadded
modules/feed/html/rollup.jschanged
modules/page/html/render/public.jschanged
modules/progress/html/peer.jsadded
modules/progress/html/render.jsadded
modules/progress/obs.jsadded
package.jsonchanged
server-process.jschanged
styles/loading.mcsschanged
styles/main-window.mcsschanged
styles/profile-list.mcsschanged
lib/friends-with-sync.jsView
@@ -1,0 +1,184 @@
1 +var Graphmitter = require('graphmitter')
2 +var pull = require('pull-stream')
3 +var mlib = require('ssb-msgs')
4 +var memview = require('level-memview')
5 +var pushable = require('pull-pushable')
6 +var mdm = require('mdmanifest')
7 +var valid = require('scuttlebot/lib/validators')
8 +var apidoc = require('scuttlebot/lib/apidocs').friends
9 +
10 +// friends plugin
11 +// methods to analyze the social graph
12 +// maintains a 'follow' and 'flag' graph
13 +
14 +function isFunction (f) {
15 + return 'function' === typeof f
16 +}
17 +
18 +function isString (s) {
19 + return 'string' === typeof s
20 +}
21 +
22 +function isFriend (friends, a, b) {
23 + return friends[a] && friends[b] && friends[a][b] && friends[b][a]
24 +}
25 +
26 +exports.name = 'friends'
27 +exports.version = '1.0.0'
28 +exports.manifest = mdm.manifest(apidoc)
29 +
30 +exports.init = function (sbot, config) {
31 +
32 + var graphs = {
33 + follow: new Graphmitter(),
34 + flag: new Graphmitter()
35 + }
36 +
37 + // view processor
38 + var syncCbs = []
39 + function awaitSync (cb) {
40 + if (syncCbs) syncCbs.push(cb)
41 + else cb()
42 + }
43 +
44 + // read/watch the log for changes to the social graph
45 + pull(sbot.createLogStream({ live: true }), pull.drain(function (msg) {
46 +
47 + if (msg.sync) {
48 + syncCbs.forEach(function (cb) { cb() })
49 + syncCbs = null
50 +
51 + if (sbot.gossip) {
52 + // prioritize friends
53 + var friends = graphs['follow'].toJSON()
54 + sbot.gossip.peers().forEach(function(peer) {
55 + if (isFriend(friends, sbot.id, peer.key)) {
56 + sbot.gossip.add(peer, 'friends')
57 + }
58 + })
59 + }
60 +
61 + return
62 + }
63 +
64 + var c = msg.value.content
65 + if (c.type == 'contact') {
66 + mlib.asLinks(c.contact, 'feed').forEach(function (link) {
67 + if ('following' in c) {
68 + if (c.following)
69 + graphs.follow.edge(msg.value.author, link.link, true)
70 + else
71 + graphs.follow.del(msg.value.author, link.link)
72 +
73 + }
74 + if ('flagged' in c) {
75 + if (c.flagged)
76 + graphs.flag.edge(msg.value.author, link.link, c.flagged)
77 + else
78 + graphs.flag.del(msg.value.author, link.link)
79 + }
80 + })
81 + }
82 + }))
83 +
84 + return {
85 +
86 + get: valid.sync(function (opts) {
87 + var g = graphs[opts.graph || 'follow']
88 + if(!g) throw new Error('opts.graph must be provided')
89 + return g.get(opts.source, opts.dest)
90 + }, 'object?'),
91 +
92 + all: valid.async(function (graph, cb) {
93 + if (typeof graph == 'function') {
94 + cb = graph
95 + graph = null
96 + }
97 + if (!graph)
98 + graph = 'follow'
99 + awaitSync(function () {
100 + cb(null, graphs[graph] ? graphs[graph].toJSON() : null)
101 + })
102 + }, 'string?'),
103 +
104 + path: valid.sync(function (opts) {
105 + if(isString(opts))
106 + opts = {source: sbot.id, dest: opts}
107 + return graphs.follow.path(opts)
108 +
109 + }, 'string|object'),
110 +
111 + createFriendStream: valid.source(function (opts) {
112 + opts = opts || {}
113 + var live = opts.live === true
114 + var meta = opts.meta === true
115 + var start = opts.start || sbot.id
116 + var graph = graphs[opts.graph || 'follow']
117 + if(!graph)
118 + return pull.error(new Error('unknown graph:' + opts.graph))
119 + var cancel, ps = pushable(function () {
120 + cancel && cancel()
121 + })
122 +
123 + function push (to, hops) {
124 + return ps.push(meta ? {id: to, hops: hops} : to)
125 + }
126 +
127 + if (live) {
128 + awaitSync(function () {
129 + ps.push({sync: true})
130 + })
131 + }
132 +
133 + //by default, also emit your own key.
134 + if(opts.self !== false)
135 + push(start, 0)
136 +
137 + var conf = config.friends || {}
138 + cancel = graph.traverse({
139 + start: start,
140 + hops: opts.hops || conf.hops || 3,
141 + max: opts.dunbar || conf.dunbar || 150,
142 + each: function (_, to, hops) {
143 + if(to !== start) push(to, hops)
144 + }
145 + })
146 +
147 + if(!live) { cancel(); ps.end() }
148 +
149 + return ps
150 + }, 'createFriendStreamOpts?'),
151 +
152 + hops: valid.async(function (start, graph, opts, cb) {
153 + if (typeof opts == 'function') { // (start|opts, graph, cb)
154 + cb = opts
155 + opts = null
156 + } else if (typeof graph == 'function') { // (start|opts, cb)
157 + cb = graph
158 + opts = graph = null
159 + }
160 + opts = opts || {}
161 + if(isString(start)) { // (start, ...)
162 + // first arg is id string
163 + opts.start = start
164 + } else if (start && typeof start == 'object') { // (opts, ...)
165 + // first arg is opts
166 + for (var k in start)
167 + opts[k] = start[k]
168 + }
169 +
170 + var conf = config.friends || {}
171 + opts.start = opts.start || sbot.id
172 + opts.dunbar = opts.dunbar || conf.dunbar || 150
173 + opts.hops = opts.hops || conf.hops || 3
174 +
175 + var g = graphs[graph || 'follow']
176 + if (!g)
177 + return cb(new Error('Invalid graph type: '+graph))
178 +
179 + awaitSync(function () {
180 + cb(null, g.traverse(opts))
181 + })
182 + }, ['feedId', 'string?', 'object?'], ['createFriendStreamOpts'])
183 + }
184 +}
lib/mutant-to-pull.jsView
@@ -1,0 +1,20 @@
1 +var watch = require('mutant/watch')
2 +var pushable = require('pull-pushable')
3 +
4 +module.exports = function (obs) {
5 + var releases = []
6 +
7 + // create listener with `onClose` handler
8 + var listener = pushable(function onClose () {
9 + // if listener is found, delete from list
10 + while (releases.length) {
11 + releases.pop()()
12 + }
13 + })
14 +
15 + releases.push(watch(obs, (value) => {
16 + listener.push(value)
17 + }))
18 +
19 + return listener
20 +}
lib/query-with-progress.jsView
@@ -1,0 +1,115 @@
1 +
2 +var pull = require('pull-stream')
3 +var path = require('path')
4 +var Links = require('streamview-links')
5 +var explain = require('explain-error')
6 +var Notify = require('pull-notify')
7 +var Value = require('mutant/value')
8 +var watchThrottle = require('mutant/watch-throttle')
9 +
10 +exports.name = 'query'
11 +exports.version = '0.1.2'
12 +exports.manifest = {
13 + read: 'source', dump: 'source', progress: 'source'
14 +}
15 +
16 +var indexes = [
17 + {key: 'clk', value: [['value', 'author'], ['value', 'sequence'], 'timestamp'] },
18 + {key: 'typ', value: [['value', 'content', 'type'], 'timestamp'] },
19 + {key: 'hsh', value: ['key', 'timestamp']},
20 + {key: 'cha', value: [['value', 'content', 'channel'], 'timestamp'] },
21 +// {key: 'aty', value: [['value', 'author'], ['value', 'content', 'type'], 'ts']}
22 +]
23 +
24 +//createHistoryStream( id, seq )
25 +//[{$filter: {author: <id>, sequence: {$gt: <seq>}}}, {$map: true}]
26 +
27 +//messagesByType (type)
28 +
29 +//[{$filter: {content: {type: <type>}}}, {$map: true}]
30 +
31 +exports.init = function (ssb, config) {
32 +
33 + var dir = path.join(config.path, 'query')
34 +
35 + var version = 13
36 + //it's really nice to tweak a few things
37 + //and then change the version number,
38 + //restart the server and have it regenerate the indexes,
39 + //all consistent again.
40 + function id (e, emit) {
41 + return emit(e)
42 + }
43 +
44 + var links = Links(dir, indexes, id, version)
45 + var notify = Notify()
46 + var pending = Value(0)
47 +
48 + watchThrottle(pending, 200, (value) => {
49 + notify({pending: Math.max(0, value)})
50 + })
51 +
52 + links.init(function (err, since) {
53 + countChanges(since, function (err, changes) {
54 + if (err) throw err
55 + pending.set(changes)
56 + onChange(() => {
57 + pending.set(pending() + 1)
58 + })
59 + pull(
60 + ssb.createLogStream({gt: since || 0, live: true, sync: false}),
61 + pull.through(function () {
62 + pending.set(pending() - 1)
63 + }),
64 + links.write(function (err) {
65 + if(err) throw err
66 + })
67 + )
68 + })
69 + })
70 +
71 + return {
72 + dump: function () {
73 + return links.dump()
74 + },
75 +
76 + read: function (opts) {
77 + if(opts && 'string' == typeof opts)
78 + try { opts = {query: JSON.parse(opts) } } catch (err) {
79 + return pull.error(err)
80 + }
81 + return links.read(opts, function (ts, cb) {
82 + ssb.sublevel('log').get(ts, function (err, key) {
83 + if(err) return cb(explain(err, 'missing timestamp:'+ts))
84 + ssb.get(key, function (err, value) {
85 + if(err) return cb(explain(err, 'missing key:'+key))
86 + cb(null, {key: key, value: value, timestamp: ts})
87 + })
88 + })
89 + })
90 + },
91 +
92 + progress: notify.listen
93 + }
94 +
95 + function countChanges (since, cb) {
96 + var result = 0
97 + pull(
98 + ssb.createLogStream({gt: since || 0, keys: false, values: false}),
99 + pull.drain(function () {
100 + result += 1
101 + }, function (err) {
102 + cb(err, result)
103 + })
104 + )
105 + }
106 +
107 + function onChange (cb) {
108 + pull(
109 + ssb.createLogStream({keys: false, values: false, old: false}),
110 + pull.drain(function () {
111 + cb()
112 + })
113 + )
114 + }
115 +}
lib/replicate-with-progress.jsView
@@ -1,0 +1,278 @@
1 +'use strict'
2 +var pull = require('pull-stream')
3 +var para = require('pull-paramap')
4 +var Notify = require('pull-notify')
5 +var Cat = require('pull-cat')
6 +var Debounce = require('observ-debounce')
7 +var mdm = require('mdmanifest')
8 +var apidoc = require('scuttlebot/lib/apidocs').replicate
9 +var MutantToPull = require('./mutant-to-pull')
10 +var {Struct, Dict} = require('mutant')
11 +
12 +var Pushable = require('pull-pushable')
13 +
14 +// compatibility function for old implementations of `latestSequence`
15 +function toSeq (s) {
16 + return typeof s === 'number' ? s : s.sequence
17 +}
18 +
19 +module.exports = {
20 + name: 'replicate',
21 + version: '2.0.0',
22 + manifest: mdm.manifest(apidoc),
23 + init: function (sbot, config) {
24 + var debounce = Debounce(200)
25 + var listeners = {}
26 + var newPeer = Notify()
27 +
28 + // keep track of sync progress and provide to client
29 +
30 + var start = null
31 + var count = 0
32 + var rate = 0
33 + var toSend = {}
34 + var peerHas = {}
35 + var pendingPeer = {}
36 +
37 + window.pendingPeer = pendingPeer
38 +
39 + var syncStatus = Struct({
40 + type: 'global',
41 + incomplete: 0,
42 + pendingCount: 0,
43 + pendingPeers: Dict({}, {fixedIndexing: true}),
44 + feeds: null,
45 + rate: 0
46 + })
47 +
48 + window.syncStatus = syncStatus
49 +
50 + debounce(function () {
51 + var incomplete = 0
52 + var totalPending = 0
53 + var feeds = Object.keys(toSend).length
54 + var peers = {}
55 +
56 + Object.keys(pendingPeer).forEach(function (peerId) {
57 + if (pendingPeer[peerId]) {
58 + totalPending += 1
59 +
60 + if (Object.keys(toSend).some(function (feedId) {
61 + if (peerHas[peerId] && peerHas[peerId][feedId]) {
62 + return peerHas[peerId][feedId] > toSend[feedId]
63 + }
64 + })) {
65 + incomplete += 1
66 + }
67 +
68 + peers[peerId] = pendingPeer[peerId]
69 + }
70 + })
71 +
72 + syncStatus.set({
73 + incomplete: incomplete,
74 + feeds: syncStatus.loadedFriends ? feeds : null,
75 + pendingPeers: peers,
76 + pending: totalPending,
77 + rate: rate
78 + }, {merge: true})
79 + })
80 +
81 + pull(
82 + sbot.createLogStream({old: false, live: true, sync: false, keys: false}),
83 + pull.drain(function (e) {
84 + // track writes per second, mainly used for developing initial sync.
85 + if (!start) start = Date.now()
86 + var time = (Date.now() - start) / 1000
87 + if (time >= 1) {
88 + rate = count / time
89 + start = Date.now()
90 + count = 0
91 + }
92 + var pushable = listeners[e.author]
93 +
94 + if (pushable && pushable.sequence === e.sequence) {
95 + pushable.sequence++
96 + pushable.forEach(function (p) {
97 + p.push(e)
98 + })
99 + }
100 + count++
101 + addPeer({id: e.author, sequence: e.sequence})
102 + })
103 + )
104 +
105 + sbot.createHistoryStream.hook(function (fn, args) {
106 + var upto = args[0] || {}
107 + var seq = upto.sequence || upto.seq
108 +
109 + if (this._emit) this._emit('call:createHistoryStream', args[0])
110 +
111 + // if we are calling this locally, skip cleverness
112 + if (this === sbot) return fn.call(this, upto)
113 +
114 + // keep track of each requested value, per feed / per peer.
115 + peerHas[this.id] = peerHas[this.id] || {}
116 + peerHas[this.id][upto.id] = seq - 1
117 +
118 + debounce.set()
119 +
120 + // handle creating lots of history streams efficiently.
121 + // maybe this could be optimized in map-filter-reduce queries instead?
122 + if (toSend[upto.id] == null || (seq > toSend[upto.id])) {
123 + upto.old = false
124 + if (!upto.live) return pull.empty()
125 + var pushable = listeners[upto.id] = listeners[upto.id] || []
126 + var p = Pushable(function () {
127 + var i = pushable.indexOf(p)
128 + pushable.splice(i, 1)
129 + })
130 + pushable.push(p)
131 + pushable.sequence = upto.sequence
132 + return p
133 + }
134 + return fn.call(this, upto)
135 + })
136 +
137 + // collect the IDs of feeds we want to request
138 + var opts = config.replication || {}
139 + opts.hops = opts.hops || 3
140 + opts.dunbar = opts.dunbar || 150
141 + opts.live = true
142 + opts.meta = true
143 +
144 + function localPeers () {
145 + if (!sbot.gossip) return
146 + sbot.gossip.peers().forEach(function (e) {
147 + if (toSend[e.key] == null) {
148 + addPeer({id: e.key, sequence: 0})
149 + }
150 + })
151 + }
152 +
153 + // also request local peers.
154 + if (sbot.gossip) {
155 + // if we have the gossip plugin active, then include new local peers
156 + // so that you can put a name to someone on your local network.
157 + var int = setInterval(localPeers, 1000)
158 + if (int.unref) int.unref()
159 + localPeers()
160 + }
161 +
162 + function loadedFriends () {
163 + console.log('>>>> loaded friends')
164 + syncStatus.loadedFriends = true
165 + debounce.set()
166 + }
167 +
168 + function addPeer (upto) {
169 + if (upto.sync) return loadedFriends()
170 + if (!upto.id) return console.log('invalid', upto)
171 +
172 + if (toSend[upto.id] == null) {
173 + toSend[upto.id] = Math.max(toSend[upto.id] || 0, upto.sequence || upto.seq || 0)
174 + newPeer({ id: upto.id, sequence: toSend[upto.id], type: 'new' })
175 + debounce.set()
176 + } else {
177 + toSend[upto.id] = Math.max(toSend[upto.id] || 0, upto.sequence || upto.seq || 0)
178 + }
179 +
180 + debounce.set()
181 + }
182 +
183 + // create read-streams for the desired feeds
184 + pull(
185 + sbot.friends.createFriendStream(opts),
186 + // filter out duplicates, and also keep track of what we expect to receive
187 + // lookup the latest sequence from each user
188 + para(function (data, cb) {
189 + if (data.sync) return cb(null, data)
190 + var id = data.id || data
191 + sbot.latestSequence(id, function (err, seq) {
192 + cb(null, {
193 + id: id, sequence: err ? 0 : toSeq(seq)
194 + })
195 + })
196 + }, 32),
197 + pull.drain(addPeer, loadedFriends)
198 + )
199 +
200 + function upto (opts) {
201 + opts = opts || {}
202 + var ary = Object.keys(toSend).map(function (k) {
203 + return { id: k, sequence: toSend[k] }
204 + })
205 + if (opts.live) {
206 + return Cat([pull.values(ary), pull.once({sync: true}), newPeer.listen()])
207 + }
208 +
209 + return pull.values(ary)
210 + }
211 +
212 + sbot.on('rpc:connect', function (rpc) {
213 + // this is the cli client, just ignore.
214 + if (rpc.id === sbot.id) return
215 + // check for local peers, or manual connections.
216 + localPeers()
217 + sbot.emit('replicate:start', rpc)
218 + rpc.on('closed', function () {
219 + sbot.emit('replicate:finish', toSend)
220 + })
221 + pull(
222 + upto({live: opts.live}),
223 + pull.drain(function (upto) {
224 + if (upto.sync) return
225 + var last = (upto.sequence || upto.seq || 0)
226 + pendingPeer[rpc.id] = (pendingPeer[rpc.id] || 0) + 1
227 + debounce.set()
228 +
229 + pull(
230 + rpc.createHistoryStream({
231 + id: upto.id,
232 + seq: last + 1,
233 + live: false,
234 + keys: false
235 + }),
236 + pull.through((msg) => {
237 + start = Math.max(start, msg.sequence)
238 + }),
239 + sbot.createWriteStream(function () {
240 + // TODO: do something with the error
241 + // this seems to be thrown fairly regularly whenever something weird happens to the stream
242 +
243 + pendingPeer[rpc.id] -= 1
244 + debounce.set()
245 +
246 + // all synched, now lets keep watching for live changes
247 + // need to handle this separately because there is no {sync: true} event with HistoryStream
248 + // and we want to notify the client that sync has completed
249 +
250 + pull(
251 + rpc.createHistoryStream({
252 + id: upto.id,
253 + seq: last + 1,
254 + live: true,
255 + keys: false
256 + }),
257 + sbot.createWriteStream(function () {
258 + // TODO: handle error
259 + })
260 + )
261 + })
262 + )
263 + }, function (err) {
264 + if (err) {
265 + sbot.emit('log:error', ['replication', rpc.id, 'error', err])
266 + }
267 + })
268 + )
269 + })
270 +
271 + return {
272 + changes: function () {
273 + return MutantToPull(syncStatus)
274 + },
275 + upto: upto
276 + }
277 + }
278 +}
main-window.jsView
@@ -24,9 +24,10 @@
2424 'keys.sync.id': 'first',
2525 'blob.sync.url': 'first',
2626 'page.html.render': 'first',
2727 'app.html.search': 'first',
28- 'app.views': 'first'
28 + 'app.views': 'first',
29 + 'app.html.progressNotifier': 'first'
2930 }))
3031
3132 var id = api.keys.sync.id()
3233 var latestUpdate = LatestUpdate()
@@ -66,8 +67,9 @@
6667 h('strong', ['Patchwork ', latestUpdate, ' has been released.']), ' Click here for more info!'
6768 ])
6869 ])
6970 ),
71 + api.app.html.progressNotifier(),
7072 views.html
7173 ])
7274
7375 catchLinks(container, (href, external) => {
modules/app/html/progress-notifier.jsView
@@ -1,0 +1,65 @@
1 +var {computed, when, h, throttle} = require('mutant')
2 +var nest = require('depnest')
3 +
4 +exports.gives = nest('app.html.progressNotifier')
5 +
6 +exports.needs = nest({
7 + 'progress.html.render': 'first',
8 + 'progress.obs.global': 'first',
9 + 'progress.obs.query': 'first'
10 +})
11 +
12 +exports.create = function (api) {
13 + return nest('app.html.progressNotifier', function (id) {
14 + var progress = api.progress.obs.global()
15 + var queryProgress = api.progress.obs.query()
16 +
17 + var maxQueryPending = 0
18 +
19 + var indexProgress = computed([queryProgress.pending], (pending) => {
20 + if (pending === 0 || pending > maxQueryPending) {
21 + maxQueryPending = pending
22 + }
23 + if (pending === 0) {
24 + return 1
25 + } else {
26 + return (maxQueryPending - pending) / maxQueryPending
27 + }
28 + })
29 +
30 + var downloadProgress = computed([progress.feeds, progress.incomplete], (feeds, incomplete) => {
31 + if (feeds) {
32 + return clamp((incomplete - feeds) / feeds)
33 + } else {
34 + return 1
35 + }
36 + })
37 +
38 + var hidden = computed([progress.incomplete, progress.feeds, queryProgress.pending], (incomplete, feeds, indexing) => {
39 + return incomplete <= 5 && !indexing && feeds
40 + })
41 +
42 + var hasDownloadProgress = computed([progress.feeds, progress.incomplete], (feeds, incomplete) => {
43 + if (feeds) {
44 + return incomplete > 5
45 + }
46 + })
47 +
48 + return h('div.info', { hidden: throttle(hidden, 5000) }, [
49 + h('div.status', [
50 + h('Loading -small', [
51 + when(hasDownloadProgress,
52 + ['Downloading new messages', h('progress', { style: {'margin-left': '10px'}, min: 0, max: 1, value: downloadProgress })],
53 + when(queryProgress.pending, [
54 + ['Indexing database', h('progress', { style: {'margin-left': '10px'}, min: 0, max: 1, value: indexProgress })]
55 + ], 'Checking for changes...')
56 + )
57 + ])
58 + ])
59 + ])
60 + })
61 +}
62 +
63 +function clamp (value) {
64 + return Math.min(1, Math.max(0, value)) || 0
65 +}
modules/feed/html/rollup.jsView
@@ -75,9 +75,9 @@
7575 pull(
7676 getStream({old: false}),
7777 pull.drain((item) => {
7878 var type = item && item.value && item.value.content.type
79- if (type && type !== 'vote') {
79 + if (type && type !== 'vote' && typeof item.value.content === 'object') {
8080 if (item.value && item.value.author === api.keys.sync.id() && !updates() && type !== 'git-update') {
8181 return refresh()
8282 }
8383 if (filter) {
modules/page/html/render/public.jsView
@@ -22,8 +22,9 @@
2222 'about.obs.name': 'first',
2323 'invite.sheet': 'first',
2424
2525 'message.html.compose': 'first',
26 + 'progress.html.peer': 'first',
2627
2728 'feed.html.rollup': 'first',
2829 'profile.obs.recentlyUpdated': 'first',
2930 'contact.obs.following': 'first',
@@ -194,9 +195,12 @@
194195 href: id
195196 }, [
196197 h('div.avatar', [api.about.html.image(id)]),
197198 h('div.main', [
198- h('div.name', [ '@', api.about.obs.name(id) ])
199 + h('div.name', [ api.about.obs.name(id) ])
200 + ]),
201 + h('div.progress', [
202 + api.progress.html.peer(id)
199203 ])
200204 ])
201205 })
202206 ])
modules/progress/html/peer.jsView
@@ -1,0 +1,23 @@
1 +var {computed, when} = require('mutant')
2 +var nest = require('depnest')
3 +
4 +exports.gives = nest('progress.html.peer')
5 +
6 +exports.needs = nest({
7 + 'progress.html.render': 'first',
8 + 'progress.obs.peer': 'first',
9 + 'progress.obs.global': 'first'
10 +})
11 +
12 +exports.create = function (api) {
13 + return nest('progress.html.peer', function (id) {
14 + var progress = api.progress.obs.peer(id)
15 + var feeds = api.progress.obs.global().feeds
16 +
17 + var value = computed([progress, feeds], (pending, feeds) => {
18 + return (feeds - pending) / feeds
19 + })
20 +
21 + return when(progress, api.progress.html.render(value))
22 + })
23 +}
modules/progress/html/render.jsView
@@ -1,0 +1,30 @@
1 +var svg = require('mutant/svg-element')
2 +var computed = require('mutant/computed')
3 +var nest = require('depnest')
4 +
5 +exports.gives = nest('progress.html.render')
6 +
7 +exports.create = function (api) {
8 + return nest('progress.html.render', function (pos) {
9 + return svg('svg RadialProgress', {
10 + viewBox: '-20 -20 240 240'
11 + }, [
12 + svg('path', {
13 + d: 'M100,0 a100,100 0 0 1 0,200 a100,100 0 0 1 0,-200,0',
14 + 'stroke': '#DADADA',
15 + 'fill': 'none'
16 + }),
17 + svg('path', {
18 + d: 'M100,0 a100,100 0 0 1 0,200 a100,100 0 0 1 0,-200,0',
19 + 'stroke-dashoffset': computed(pos, (pos) => {
20 + pos = Math.min(Math.max(pos, 0), 1)
21 + return (1 - pos) * 629
22 + }),
23 + 'stroke-width': 40,
24 + 'stroke-dasharray': 629,
25 + 'stroke': '#33DA33',
26 + 'fill': 'none'
27 + })
28 + ])
29 + })
30 +}
modules/progress/obs.jsView
@@ -1,0 +1,88 @@
1 +var nest = require('depnest')
2 +var pull = require('pull-stream')
3 +var {Struct, Dict, computed} = require('mutant')
4 +
5 +exports.gives = nest({
6 + 'progress.obs': ['global', 'peer', 'query']
7 +})
8 +
9 +exports.needs = nest({
10 + 'sbot.pull.replicateProgress': 'first',
11 + 'sbot.pull.queryProgress': 'first'
12 +})
13 +
14 +exports.create = function (api) {
15 + var syncStatus = null
16 + var queryProgress = null
17 +
18 + return nest({
19 + 'progress.obs': {global, peer, query}
20 + })
21 +
22 + function global () {
23 + load()
24 + return syncStatus
25 + }
26 +
27 + function peer (id) {
28 + load()
29 + var result = computed(syncStatus, (status) => {
30 + return status.pendingPeers[id] || 0
31 + })
32 + return result
33 + }
34 +
35 + function query () {
36 + if (!queryProgress) {
37 + queryProgress = Struct({
38 + pending: 0
39 + })
40 +
41 + pull(
42 + api.sbot.pull.queryProgress(),
43 + pull.drain((event) => {
44 + queryProgress.set(event)
45 + })
46 + )
47 + }
48 + return queryProgress
49 + }
50 +
51 + function load () {
52 + if (!syncStatus) {
53 + syncStatus = Struct({
54 + incomplete: 0,
55 + pendingCount: 0,
56 + pendingPeers: Dict({}, {fixedIndexing: true}),
57 + feeds: null,
58 + rate: 0
59 + })
60 +
61 + pull(
62 + api.sbot.pull.replicateProgress(),
63 + pull.drain((event) => {
64 + if (!event.sync) {
65 + syncStatus.set(event)
66 + }
67 + })
68 + )
69 + }
70 + }
71 +}
72 +
73 +function Peer (id) {
74 + return Struct({
75 + type: 'peer',
76 + id: id,
77 + pending: 0
78 + })
79 +}
80 +
81 +function Feed (id) {
82 + return Struct({
83 + type: 'feed',
84 + id: id,
85 + available: 0,
86 + local: 0
87 + })
88 +}
package.jsonView
@@ -29,14 +29,14 @@
2929 "is-visible": "^2.1.1",
3030 "level": "~1.4.0",
3131 "level-memview": "0.0.0",
3232 "micro-css": "^2.0.0",
33- "mutant": "^3.15.2",
33 + "mutant": "^3.17.0",
3434 "mutant-pull-reduce": "^1.0.1",
3535 "non-private-ip": "^1.4.1",
3636 "on-change-network": "0.0.2",
3737 "on-wakeup": "^1.0.1",
38- "patchcore": "github:ssbc/patchcore",
38 + "patchcore": "~0.3.1",
3939 "prebuild": "github:mmckegg/prebuild#use-npm-conf",
4040 "pull-abortable": "^4.1.0",
4141 "pull-file": "~1.0.0",
4242 "pull-identify-filetype": "^1.1.0",
@@ -50,11 +50,9 @@
5050 "scuttlebot": "^9.4.4",
5151 "sorted-array-functions": "~1.0.0",
5252 "ssb-avatar": "^0.2.0",
5353 "ssb-blobs": "~0.1.7",
54- "ssb-fulltext": "^1.0.1",
5554 "ssb-keys": "~7.0.0",
56- "ssb-links": "~2.0.0",
5755 "ssb-mentions": "^0.1.1",
5856 "ssb-msgs": "^5.2.0",
5957 "ssb-query": "~0.1.1",
6058 "ssb-ref": "~2.6.2",
server-process.jsView
@@ -5,18 +5,17 @@
55
66 var createSbot = require('scuttlebot')
77 .use(require('scuttlebot/plugins/master'))
88 .use(require('scuttlebot/plugins/gossip'))
9- .use(require('scuttlebot/plugins/friends'))
10- .use(require('scuttlebot/plugins/replicate'))
9 + .use(require('./lib/friends-with-sync'))
10 + .use(require('./lib/replicate-with-progress'))
1111 .use(require('ssb-blobs'))
1212 .use(require('scuttlebot/plugins/invite'))
1313 .use(require('scuttlebot/plugins/block'))
1414 .use(require('scuttlebot/plugins/local'))
1515 .use(require('scuttlebot/plugins/logging'))
1616 .use(require('scuttlebot/plugins/private'))
17- .use(require('ssb-links'))
18- .use(require('ssb-query'))
17 + .use(require('./lib/query-with-progress'))
1918 //.use(require('ssb-fulltext')) // disabled for now
2019
2120 module.exports = function (ssbConfig) {
2221 var context = {
styles/loading.mcssView
@@ -16,8 +16,15 @@
1616 width: 16px
1717 }
1818 }
1919
20 + -small {
21 + ::before {
22 + height: 30px
23 + width: 30px
24 + }
25 + }
26 +
2027 -large {
2128 ::before {
2229 height: 100px
2330 width: 100px
styles/main-window.mcssView
@@ -109,10 +109,29 @@
109109 text-decoration: none
110110 background: #c0ffae
111111 }
112112 }
113- box-shadow: 0 0 3px #618b63
113 +
114 + div.status {
115 + padding: 5px
116 + background: #7c7c7c
117 + color: white
118 + (svg) {
119 + width: 20px
120 + height: 20px
121 + }
122 + }
123 +
124 + [hidden] {
125 + display: block
126 + max-height: 0
127 + animation: none
128 + }
129 +
130 + max-height: 100px
131 + box-shadow: 0 0 3px #616161
114132 overflow: hidden
133 + transition: 0.5s max-height
115134 animation: 0.5s slide-in
116135 position: relative
117136 z-index: 1
118137 }
styles/profile-list.mcssView
@@ -47,6 +47,15 @@
4747 color: #636363
4848 -webkit-mask-image: linear-gradient(90deg, rgba(0,0,0,1) 90%, rgba(0,0,0,0))
4949 }
5050 }
51 +
52 + div.progress {
53 + display: flex
54 + flex-direction: column
55 + svg {
56 + width: 20px
57 + flex: 1
58 + }
59 + }
5160 }
5261 }

Built with git-ssb-web