Commit 8e4e9c4e33e4475499a439dc81219dc9da8b19a2
Merge branch 'master' into new_icon
mixmix committed on 7/16/2019, 6:26:52 AMParent: 070487b0c7985fcdf84bdae5f2ee2f3f7f0e1e83
Parent: d119c36e794cfcba29c54f4f2f6da4f6e4d0fb77
Files changed
app/html/modal.js | ||
---|---|---|
@@ -1,6 +1,6 @@ | ||
1 | 1 … | const nest = require('depnest') |
2 | -const { h, computed, Value } = require('mutant') | |
2 … | +const { h, Value } = require('mutant') | |
3 | 3 … | |
4 | 4 … | exports.gives = nest('app.html.modal') |
5 | 5 … | |
6 | 6 … | exports.create = (api) => { |
@@ -14,29 +14,33 @@ | ||
14 | 14 … | isOpen.set(false) |
15 | 15 … | if (typeof onClose === 'function') onClose() |
16 | 16 … | } |
17 | 17 … | |
18 | - const lb = computed(isOpen, _isOpen => { | |
19 | - if (!_isOpen) return h('Modal -close') | |
18 … | + const lb = h('Modal -closed', | |
19 … | + { | |
20 … | + className, | |
21 … | + 'ev-click': closeMe, | |
22 … | + 'ev-keydown': ev => { | |
23 … | + if (ev.keyCode === 27) closeMe() // Escape | |
24 … | + } | |
25 … | + }, | |
26 … | + [ | |
27 … | + h('div.content', { 'ev-click': (ev) => ev.stopPropagation() }, [ | |
28 … | + content | |
29 … | + // I think content must be in the DOM for any downstream mutant Observers to be updating | |
30 … | + ]) | |
31 … | + ] | |
32 … | + ) | |
20 | 33 … | |
21 | - return h('Modal -open', | |
22 | - { | |
23 | - className, | |
24 | - 'ev-click': closeMe, | |
25 | - 'ev-keydown': ev => { | |
26 | - if (ev.keyCode === 27) closeMe() // Escape | |
27 | - } | |
28 | - }, | |
29 | - [ | |
30 | - h('div.content', { 'ev-click': (ev) => ev.stopPropagation() }, [ | |
31 | - content | |
32 | - ]) | |
33 | - ] | |
34 | - ) | |
35 | - }) | |
36 | - | |
37 | 34 … | isOpen(state => { |
38 | - if (state !== true) return | |
35 … | + if (state === true) { | |
36 … | + lb.classList.remove('-closed') | |
37 … | + lb.classList.add('-open') | |
38 … | + } else { | |
39 … | + lb.classList.remove('-open') | |
40 … | + lb.classList.add('-closed') | |
41 … | + return | |
42 … | + } | |
39 | 43 … | |
40 | 44 … | focus() |
41 | 45 … | function focus () { |
42 | 46 … | if (!lb.isConnected) setTimeout(focus, 200) |
app/html/modal.mcss | ||
---|---|---|
@@ -27,9 +27,9 @@ | ||
27 | 27 … | -open { |
28 | 28 … | display: grid |
29 | 29 … | } |
30 | 30 … | |
31 | - -close { | |
31 … | + -closed { | |
32 | 32 … | display: none |
33 | 33 … | } |
34 | 34 … | |
35 | 35 … | -dark { |
app/html/settings/friend-hops.js | ||
---|---|---|
@@ -29,9 +29,9 @@ | ||
29 | 29 … | updateConfig(hops) |
30 | 30 … | }) |
31 | 31 … | |
32 | 32 … | return { |
33 | - group: 'gossip', | |
33 … | + group: 'replication', | |
34 | 34 … | title: 'Friend Hops', |
35 | 35 … | body: h('FriendHops', [ |
36 | 36 … | h('div.description', [ |
37 | 37 … | 'What you replicate (store a local copy of) is based on how many "hops" you replicate. If you replicate out to 1 hop, you are replicating the people you follow, at 2 hops, it is your follows and people they follow. Play with the slider to see this visualised in the graphic below!' |
app/html/settings/friend-pub.js | ||
---|---|---|
@@ -32,13 +32,13 @@ | ||
32 | 32 … | updatePubs(hops) |
33 | 33 … | }) |
34 | 34 … | |
35 | 35 … | return { |
36 | - group: 'gossip', | |
37 | - title: 'Pub gossip', | |
36 … | + group: 'replication', | |
37 … | + title: 'Pub replication', | |
38 | 38 … | body: h('FriendPub', [ |
39 | 39 … | h('div.description', [ |
40 | - 'Limit gossip with pubs based on who owns the pub' | |
40 … | + 'Limit gossip with pubs (always online peers who help replication) based on who owns the pub' | |
41 | 41 … | ]), |
42 | 42 … | h('div.slider', [ |
43 | 43 … | h('datalist', { id: 'pub-gossip-datalist' }, [ |
44 | 44 … | h('option', { value: 0, label: 'My pub' }), |
app/html/settings/tor-only.js | ||
---|---|---|
@@ -26,9 +26,9 @@ | ||
26 | 26 … | updateConfig(torOnly) |
27 | 27 … | }) |
28 | 28 … | |
29 | 29 … | return { |
30 | - group: 'gossip', | |
30 … | + group: 'replication', | |
31 | 31 … | title: 'Tor only connections', |
32 | 32 … | body: h('TorOnly', [ |
33 | 33 … | h('p', [ |
34 | 34 … | 'Preserve your ip privacy by only connecting to other nodes using tor', |
app/page/network.js | ||
---|---|---|
@@ -1,364 +1,0 @@ | ||
1 | -const nest = require('depnest') | |
2 | -const { h, Value, Dict, dictToCollection, onceTrue, computed, watch, watchAll, throttle, resolve } = require('mutant') | |
3 | -const Chart = require('chart.js') | |
4 | -const pull = require('pull-stream') | |
5 | -const { isInvite } = require('ssb-ref') | |
6 | - | |
7 | -const MINUTE = 60 * 1000 | |
8 | -const DAY = 24 * 60 * MINUTE | |
9 | - | |
10 | -const GRAPH_Y_STEP = 50 | |
11 | -const GRAPH_Y_MIN = 100 | |
12 | - | |
13 | -exports.gives = nest({ | |
14 | - 'app.html.menuItem': true, | |
15 | - 'app.page.network': true | |
16 | -}) | |
17 | - | |
18 | -exports.needs = nest({ | |
19 | - 'about.html.avatar': 'first', | |
20 | - 'app.html.scroller': 'first', | |
21 | - 'app.sync.goTo': 'first', | |
22 | - 'sbot.obs.connection': 'first', | |
23 | - 'sbot.obs.localPeers': 'first', | |
24 | - 'sbot.obs.connectedPeers': 'first' | |
25 | -}) | |
26 | - | |
27 | -exports.create = function (api) { | |
28 | - return nest({ | |
29 | - 'app.html.menuItem': menuItem, | |
30 | - 'app.page.network': networkPage | |
31 | - }) | |
32 | - | |
33 | - function menuItem () { | |
34 | - return h('a', { | |
35 | - 'ev-click': () => api.app.sync.goTo({ page: 'network' }) | |
36 | - }, '/network') | |
37 | - } | |
38 | - | |
39 | - function networkPage (location) { | |
40 | - const minsPerStep = 10 | |
41 | - const scale = 1 * DAY | |
42 | - | |
43 | - const state = buildState({ api, minsPerStep, scale }) | |
44 | - const canvas = h('canvas', { height: 500, width: 1200, style: { height: '500px', width: '1200px' } }) | |
45 | - | |
46 | - const inputInvite = ev => { | |
47 | - state.inviteResult.set(null) | |
48 | - const invite = ev.target.value.replace(/^\s*"?/, '').replace(/"?\s*$/, '') | |
49 | - if (!isInvite(invite)) { | |
50 | - state.invite.set() | |
51 | - return | |
52 | - } | |
53 | - | |
54 | - ev.target.value = invite | |
55 | - state.invite.set(invite) | |
56 | - } | |
57 | - const useInvite = () => { | |
58 | - state.inviteProcessing.set(true) | |
59 | - | |
60 | - onceTrue(api.sbot.obs.connection, server => { | |
61 | - server.invite.accept(resolve(state.invite), (err, data) => { | |
62 | - state.inviteProcessing.set(false) | |
63 | - state.invite.set() | |
64 | - | |
65 | - if (err) { | |
66 | - state.inviteResult.set(false) | |
67 | - console.error(err) | |
68 | - return | |
69 | - } | |
70 | - state.inviteResult.set(true) | |
71 | - console.log(data) | |
72 | - }) | |
73 | - }) | |
74 | - } | |
75 | - | |
76 | - const page = h('NetworkPage', [ | |
77 | - h('div.container', [ | |
78 | - h('h1', 'Network'), | |
79 | - h('section', [ | |
80 | - h('h2', [ | |
81 | - 'Local Peers', | |
82 | - h('i.fa.fa-question-circle-o', { title: 'these are people on the same WiFi/ LAN as you right now. You might not know some of them yet, but you can click through to find out more about them and follow them if you like.' }) | |
83 | - ]), | |
84 | - computed(state.localPeers, peers => { | |
85 | - if (!peers.length) return h('p', 'No local peers (on same wifi/ LAN)') | |
86 | - | |
87 | - return peers.map(peer => api.about.html.avatar(peer)) | |
88 | - }) | |
89 | - ]), | |
90 | - h('section', [ | |
91 | - h('h2', [ | |
92 | - 'Remote Peers', | |
93 | - h('i.fa.fa-question-circle-o', { title: 'these are peers which have fixed addresses, and are likely friends of friends (a.k.a. pubs)' }) | |
94 | - ]), | |
95 | - computed(state.remotePeers, peers => { | |
96 | - if (!peers.length) return h('p', 'No remote peers connected') | |
97 | - | |
98 | - return peers.map(peer => api.about.html.avatar(peer)) | |
99 | - }), | |
100 | - h('div.invite', [ | |
101 | - h('input', { | |
102 | - 'placeholder': 'invite code for a remote peer (pub)', | |
103 | - 'ev-input': inputInvite | |
104 | - }), | |
105 | - computed([state.invite, state.inviteProcessing], (invite, processing) => { | |
106 | - if (processing) return h('i.fa.fa-spinner.fa-pulse') | |
107 | - if (invite) return h('button -primary', { 'ev-click': useInvite }, 'use invite') | |
108 | - | |
109 | - return h('button', { disabled: 'disabled', title: 'not a valid invite code' }, 'use invite') | |
110 | - }), | |
111 | - computed(state.inviteResult, result => { | |
112 | - if (result === null) return | |
113 | - | |
114 | - return result | |
115 | - ? h('i.fa.fa-check') | |
116 | - : h('i.fa.fa-times') | |
117 | - }) | |
118 | - ]) | |
119 | - ]), | |
120 | - h('section', [ | |
121 | - h('h2', 'My state'), | |
122 | - // mix: hello friend, this area is a total Work In Progress. It's a mess but useful diagnostics. | |
123 | - // Let's redesign and revisit it aye! | |
124 | - h('div', ['My sequence: ', state.seq]), | |
125 | - h('div', [ | |
126 | - 'Replicated:', | |
127 | - h('div', computed([state.seq, dictToCollection(state.replication)], (seq, replication) => { | |
128 | - return replication.map(r => { | |
129 | - if (!r.value.replicating) { | |
130 | - return h('div', [ | |
131 | - h('code', r.key), | |
132 | - ' no ebt data' | |
133 | - ]) | |
134 | - } | |
135 | - | |
136 | - const { requested, sent } = r.value.replicating | |
137 | - // TODO report that r.value.seq is NOT the current local value of the seq (well it's ok, just just gets out of sync) | |
138 | - // const reqDiff = requested - r.value.seq | |
139 | - const reqDiff = requested - seq | |
140 | - const sentDiff = sent - seq | |
141 | - | |
142 | - return h('div', [ | |
143 | - h('code', r.key), | |
144 | - ` - requested: ${requested} `, | |
145 | - reqDiff === 0 ? h('i.fa.fa-check-circle-o') : `(${reqDiff})`, | |
146 | - `, sent: ${sent} `, | |
147 | - sentDiff === 0 ? h('i.fa.fa-check-circle-o') : `(${sentDiff})` | |
148 | - ]) | |
149 | - }) | |
150 | - })) | |
151 | - ]) | |
152 | - ]), | |
153 | - h('section', [ | |
154 | - h('h2', [ | |
155 | - 'Received Messages', | |
156 | - h('i.fa.fa-question-circle-o', { | |
157 | - title: `Messages received per ${minsPerStep}-minute block over the last ${scale / DAY} days` | |
158 | - }) | |
159 | - ]), | |
160 | - canvas | |
161 | - ]) | |
162 | - ]) | |
163 | - ]) | |
164 | - | |
165 | - initialiseChart({ canvas, state }) | |
166 | - | |
167 | - var { container } = api.app.html.scroller({ prepend: page }) | |
168 | - container.title = '/network' | |
169 | - return container | |
170 | - } | |
171 | -} | |
172 | - | |
173 | -function initialiseChart ({ canvas, state: { data, range } }) { | |
174 | - var chart = new Chart(canvas.getContext('2d'), chartConfig(range)) | |
175 | - | |
176 | - watch(range, ({ lower, upper }) => { | |
177 | - // set horizontal scale | |
178 | - chart.options.scales.xAxes[0].time.min = lower | |
179 | - chart.options.scales.xAxes[0].time.max = upper | |
180 | - chart.update() | |
181 | - }) | |
182 | - | |
183 | - watchAll([throttle(data, 300), range], (data, { lower, upper }) => { | |
184 | - const _data = Object.keys(data) | |
185 | - .sort((a, b) => a < b ? -1 : +1) | |
186 | - .map(ts => { | |
187 | - return { | |
188 | - t: Number(ts), // NOTE - might need to offset by a half-step ? | |
189 | - y: data[ts] | |
190 | - } | |
191 | - }) | |
192 | - | |
193 | - // update chard data | |
194 | - chart.data.datasets[0].data = _data | |
195 | - | |
196 | - // scales the height of the graph (to the visible data)! | |
197 | - const slice = _data | |
198 | - .filter(d => d.t >= lower && d.t < upper) | |
199 | - .map(d => d.y) | |
200 | - .sort((a, b) => a > b ? -1 : +1) | |
201 | - | |
202 | - var h = slice[0] | |
203 | - if (!h || h < GRAPH_Y_MIN) h = GRAPH_Y_MIN // min-height | |
204 | - else h = h + (GRAPH_Y_STEP - h % GRAPH_Y_STEP) // round height to multiples of GRAPH_Y_STEP | |
205 | - chart.options.scales.yAxes[0].ticks.max = h | |
206 | - | |
207 | - chart.update() | |
208 | - }) | |
209 | -} | |
210 | - | |
211 | -// ///// HELPERS ///// | |
212 | - | |
213 | -function buildState ({ api, minsPerStep, scale }) { | |
214 | - // build localPeers, remotePeers | |
215 | - const localPeers = throttle(api.sbot.obs.localPeers(), 1000) | |
216 | - const remotePeers = computed([localPeers, throttle(api.sbot.obs.connectedPeers(), 1000)], (local, connected) => { | |
217 | - return connected.filter(peer => !local.includes(peer)) | |
218 | - }) | |
219 | - | |
220 | - // build seq, replication (my current state, and replicated state) | |
221 | - const seq = Value() | |
222 | - const replication = Dict({}) | |
223 | - onceTrue(api.sbot.obs.connection, server => { | |
224 | - setInterval(() => { | |
225 | - // TODO check ebt docs if this is best method | |
226 | - server.ebt.peerStatus(server.id, (err, data) => { | |
227 | - if (err) return console.error(err) | |
228 | - | |
229 | - seq.set(data.seq) | |
230 | - for (var peer in data.peers) { | |
231 | - replication.put(peer, data.peers[peer]) | |
232 | - } | |
233 | - }) | |
234 | - }, 5e3) | |
235 | - }) | |
236 | - | |
237 | - // build data, range | |
238 | - const data = Dict({ | |
239 | - [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE]: 0, | |
240 | - [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE - scale]: 0 | |
241 | - }) | |
242 | - onceTrue(api.sbot.obs.connection, server => { | |
243 | - getData({ data, server, minsPerStep, scale }) | |
244 | - }) | |
245 | - | |
246 | - const latest = Value(toTimeBlock(Date.now(), minsPerStep)) | |
247 | - // start of the most recent bar | |
248 | - setInterval(() => { | |
249 | - latest.set(toTimeBlock(Date.now(), minsPerStep)) | |
250 | - }, minsPerStep / 4 * MINUTE) | |
251 | - | |
252 | - const range = computed([latest], (latest) => { | |
253 | - return { | |
254 | - upper: latest + minsPerStep * MINUTE, | |
255 | - lower: latest + minsPerStep * MINUTE - scale | |
256 | - } | |
257 | - }) | |
258 | - return { | |
259 | - localPeers, | |
260 | - remotePeers, | |
261 | - seq, | |
262 | - replication, | |
263 | - data, // TODO rename this !! | |
264 | - range, | |
265 | - invite: Value(), | |
266 | - inviteProcessing: Value(false), | |
267 | - inviteResult: Value(null) | |
268 | - } | |
269 | -} | |
270 | - | |
271 | -function getData ({ data, server, minsPerStep, scale }) { | |
272 | - const upperEnd = toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE | |
273 | - const lowerBound = upperEnd - scale | |
274 | - | |
275 | - const query = [ | |
276 | - { | |
277 | - $filter: { | |
278 | - timestamp: { $gte: lowerBound } | |
279 | - } | |
280 | - }, { | |
281 | - $filter: { | |
282 | - value: { | |
283 | - author: { $ne: server.id } | |
284 | - } | |
285 | - } | |
286 | - }, { | |
287 | - $map: { | |
288 | - ts: ['timestamp'] | |
289 | - } | |
290 | - } | |
291 | - ] | |
292 | - | |
293 | - pull( | |
294 | - server.query.read({ query, live: true }), | |
295 | - pull.filter(m => !m.sync), | |
296 | - pull.map(m => toTimeBlock(m.ts, minsPerStep)), | |
297 | - pull.drain(ts => { | |
298 | - if (data.has(ts)) data.put(ts, data.get(ts) + 1) | |
299 | - else data.put(ts, 1) | |
300 | - }) | |
301 | - ) | |
302 | -} | |
303 | - | |
304 | -function toTimeBlock (ts, minsPerStep) { | |
305 | - return Math.floor(ts / (minsPerStep * MINUTE)) * (minsPerStep * MINUTE) | |
306 | -} | |
307 | - | |
308 | -function chartConfig ({ lower, upper }) { | |
309 | - const barColor = 'hsla(215, 57%, 60%, 1)' | |
310 | - | |
311 | - return { | |
312 | - type: 'bar', | |
313 | - data: { | |
314 | - datasets: [{ | |
315 | - backgroundColor: barColor, | |
316 | - borderColor: barColor, | |
317 | - data: [] | |
318 | - }] | |
319 | - }, | |
320 | - options: { | |
321 | - legend: { | |
322 | - display: false | |
323 | - }, | |
324 | - scales: { | |
325 | - xAxes: [{ | |
326 | - type: 'time', | |
327 | - distribution: 'linear', | |
328 | - time: { | |
329 | - // unit: 'day', | |
330 | - // min: lower, | |
331 | - // max: upper, | |
332 | - // tooltipFormat: 'MMMM D', | |
333 | - // stepSize: 7 | |
334 | - unit: 'minute', | |
335 | - min: lower, | |
336 | - max: upper, | |
337 | - tooltipFormat: 'HH:mm', | |
338 | - stepSize: 4 * 60 | |
339 | - // stepSize: 240 | |
340 | - }, | |
341 | - bounds: 'ticks', | |
342 | - ticks: { | |
343 | - // maxTicksLimit: 4 // already disabled | |
344 | - }, | |
345 | - gridLines: { | |
346 | - display: false | |
347 | - } | |
348 | - // maxBarThickness: 2 | |
349 | - }], | |
350 | - | |
351 | - yAxes: [{ | |
352 | - ticks: { | |
353 | - min: 0, | |
354 | - stepSize: GRAPH_Y_STEP, | |
355 | - suggestedMax: GRAPH_Y_MIN | |
356 | - } | |
357 | - }] | |
358 | - }, | |
359 | - animation: { | |
360 | - // duration: 300 | |
361 | - } | |
362 | - } | |
363 | - } | |
364 | -} |
app/page/network/connections.js | ||
---|---|---|
@@ -1,0 +1,50 @@ | ||
1 … | +const { h, computed, throttle } = require('mutant') | |
2 … | + | |
3 … | +module.exports = function Connections ({ localPeers, connectedPeers, avatar }) { | |
4 … | + const state = buildState({ localPeers, connectedPeers }) | |
5 … | + | |
6 … | + return [ | |
7 … | + { | |
8 … | + title: 'local peers', | |
9 … | + body: h('LocalPeers', [ | |
10 … | + h('div.peers', computed(state.localPeers, peers => { | |
11 … | + if (!peers.length) return h('p', 'No local peers (on same wifi/ LAN)') | |
12 … | + | |
13 … | + return peers.map(peer => avatar(peer)) | |
14 … | + })), | |
15 … | + h('p', [ | |
16 … | + h('i.fa.fa-info-circle'), | |
17 … | + 'these are peers on the same WiFi/LAN as you right now. You might not know some of them yet, but you can click through to find out more about them and follow them if you like.' | |
18 … | + ]) | |
19 … | + ]) | |
20 … | + }, | |
21 … | + { | |
22 … | + title: 'remote peers', | |
23 … | + body: h('RemotePeers', [ | |
24 … | + h('div.peers', computed(state.remotePeers, peers => { | |
25 … | + if (!peers.length) return h('p', 'No remote peers connected') | |
26 … | + | |
27 … | + return peers.map(peer => avatar(peer)) | |
28 … | + })), | |
29 … | + h('p', [ | |
30 … | + h('i.fa.fa-info-circle'), | |
31 … | + 'these are peers your\'re connecting to over the internet. You might be connected peers who you haven\'t followed (likely pubs) - this is because friends of yours might have gossiped about them, and you\'re just checking in to see if they have any news about any of your friends. If you don\'t like this, you can change it in ', | |
32 … | + h('a', { href: '/settings' }, '/settings'), | |
33 … | + ' under "replication"' | |
34 … | + ]) | |
35 … | + ]) | |
36 … | + } | |
37 … | + ] | |
38 … | +} | |
39 … | + | |
40 … | +function buildState ({ localPeers, connectedPeers }) { | |
41 … | + const local = throttle(localPeers(), 1000) | |
42 … | + const remote = computed([local, throttle(connectedPeers(), 1000)], (local, connected) => { | |
43 … | + return connected.filter(peer => !local.includes(peer)) | |
44 … | + }) | |
45 … | + | |
46 … | + return { | |
47 … | + localPeers: local, | |
48 … | + remotePeers: remote | |
49 … | + } | |
50 … | +} |
app/page/network/connections.mcss | ||
---|---|---|
@@ -1,0 +1,25 @@ | ||
1 … | +LocalPeers { | |
2 … | + min-height: 9rem | |
3 … | + | |
4 … | + div.peers { | |
5 … | + min-height: 4rem | |
6 … | + } | |
7 … | + p { | |
8 … | + i.fa { | |
9 … | + margin-right: .5rem | |
10 … | + } | |
11 … | + } | |
12 … | +} | |
13 … | + | |
14 … | +RemotePeers { | |
15 … | + min-height: 9rem | |
16 … | + | |
17 … | + div.peers { | |
18 … | + min-height: 4rem | |
19 … | + } | |
20 … | + p { | |
21 … | + i.fa { | |
22 … | + margin-right: .5rem | |
23 … | + } | |
24 … | + } | |
25 … | +} |
app/page/network/index.js | ||
---|---|---|
@@ -1,0 +1,94 @@ | ||
1 … | +const nest = require('depnest') | |
2 … | +const { h, Value, computed } = require('mutant') | |
3 … | + | |
4 … | +const Connections = require('./connections') | |
5 … | +const ReplicationIn = require('./replication-in') | |
6 … | +const ReplicationOut = require('./replication-out') | |
7 … | +const InvitePub = require('./invite-pub') | |
8 … | +const InvitePeer = require('./invite-peer') | |
9 … | + | |
10 … | +exports.gives = nest({ | |
11 … | + 'app.html.menuItem': true, | |
12 … | + 'app.page.network': true | |
13 … | +}) | |
14 … | + | |
15 … | +exports.needs = nest({ | |
16 … | + 'about.html.avatar': 'first', | |
17 … | + 'app.html.scroller': 'first', | |
18 … | + 'app.sync.goTo': 'first', | |
19 … | + 'sbot.obs.connection': 'first', | |
20 … | + 'sbot.obs.localPeers': 'first', | |
21 … | + 'sbot.obs.connectedPeers': 'first' | |
22 … | +}) | |
23 … | + | |
24 … | +exports.create = function (api) { | |
25 … | + return nest({ | |
26 … | + 'app.html.menuItem': menuItem, | |
27 … | + 'app.page.network': networkPage | |
28 … | + }) | |
29 … | + | |
30 … | + function menuItem () { | |
31 … | + return h('a', { | |
32 … | + 'ev-click': () => api.app.sync.goTo({ page: 'network' }) | |
33 … | + }, '/network') | |
34 … | + } | |
35 … | + | |
36 … | + function networkPage (location) { | |
37 … | + const { connection, localPeers, connectedPeers } = api.sbot.obs | |
38 … | + const { avatar } = api.about.html | |
39 … | + | |
40 … | + const state = { | |
41 … | + groups: [ | |
42 … | + { | |
43 … | + name: 'connections', | |
44 … | + subgroups: [ | |
45 … | + ...Connections({ localPeers, connectedPeers, avatar }) | |
46 … | + ] | |
47 … | + }, | |
48 … | + { | |
49 … | + name: 'replication', | |
50 … | + subgroups: [ | |
51 … | + ReplicationIn({ connection }), | |
52 … | + ReplicationOut({ connection }) | |
53 … | + ] | |
54 … | + }, | |
55 … | + { | |
56 … | + name: 'invites', | |
57 … | + subgroups: [ | |
58 … | + InvitePub({ connection }), | |
59 … | + InvitePeer({ connection }) | |
60 … | + ] | |
61 … | + } | |
62 … | + ], | |
63 … | + activeGroup: Value(0) | |
64 … | + } | |
65 … | + | |
66 … | + const page = h('NetworkPage', [ | |
67 … | + computed(state.activeGroup, index => { | |
68 … | + return h('div.container', [ | |
69 … | + h('section.groups', state.groups.map((group, i) => { | |
70 … | + return h('div.group', | |
71 … | + { | |
72 … | + 'className': i === index ? '-active' : '', | |
73 … | + 'ev-click': () => state.activeGroup.set(i) | |
74 … | + }, | |
75 … | + group.name | |
76 … | + ) | |
77 … | + })), | |
78 … | + h('section.subgroups', state.groups[index].subgroups.map(Subgroup)) | |
79 … | + ]) | |
80 … | + }) | |
81 … | + ]) | |
82 … | + | |
83 … | + function Subgroup (subgroup) { | |
84 … | + return h('div.subgroup', [ | |
85 … | + h('h2', subgroup.title), | |
86 … | + subgroup.body | |
87 … | + ]) | |
88 … | + } | |
89 … | + | |
90 … | + var { container } = api.app.html.scroller({ prepend: page }) | |
91 … | + container.title = '/network' | |
92 … | + return container | |
93 … | + } | |
94 … | +} |
app/page/network/index.mcss | ||
---|---|---|
@@ -1,0 +1,49 @@ | ||
1 … | +NetworkPage { | |
2 … | + div.container { | |
3 … | + background-color: hsla(0, 0%, 100%, .3) | |
4 … | + margin: 1rem 0 | |
5 … | + | |
6 … | + display: grid | |
7 … | + grid-template-columns: 8rem 1fr | |
8 … | + grid-gap: 2rem | |
9 … | + | |
10 … | + section.groups { | |
11 … | + display: grid | |
12 … | + align-content: start | |
13 … | + grid-gap: 1px | |
14 … | + | |
15 … | + div { | |
16 … | + background: #fff | |
17 … | + cursor: pointer | |
18 … | + | |
19 … | + padding: .3rem .5rem | |
20 … | + | |
21 … | + -active { | |
22 … | + background: #000 | |
23 … | + color: #fff | |
24 … | + } | |
25 … | + | |
26 … | + :hover { | |
27 … | + background: #000 | |
28 … | + color: #fff | |
29 … | + } | |
30 … | + } | |
31 … | + } | |
32 … | + | |
33 … | + section.subgroups { | |
34 … | + display: grid | |
35 … | + grid-gap: 4rem | |
36 … | + | |
37 … | + div.subgroup { | |
38 … | + h2 { | |
39 … | + font-size: 1rem | |
40 … | + text-transform: uppercase | |
41 … | + letter-spacing: 2px | |
42 … | + | |
43 … | + border-bottom: 2px solid #3e3e3e | |
44 … | + margin: 0 0 1rem 0 | |
45 … | + } | |
46 … | + } | |
47 … | + } | |
48 … | + } | |
49 … | +} |
app/page/network/invite-peer.js | ||
---|---|---|
@@ -1,0 +1,191 @@ | ||
1 … | +const { h, Value, resolve, onceTrue, when, computed } = require('mutant') | |
2 … | +// const { isInvite } = require('ssb-ref') | |
3 … | + | |
4 … | +function isInvite (code) { | |
5 … | + return typeof code === 'string' && | |
6 … | + code.length > 32 && | |
7 … | + code.startsWith('inv:') && | |
8 … | + code.endsWith('=') | |
9 … | + // TODO find actual peer-invite validator! | |
10 … | +} | |
11 … | + | |
12 … | +module.exports = function InvitePeer ({ connection }) { | |
13 … | + const state = { | |
14 … | + use: { | |
15 … | + invite: Value(), | |
16 … | + opening: Value(false), | |
17 … | + message: Value(null), | |
18 … | + accepting: Value(false), | |
19 … | + result: Value(null) | |
20 … | + }, | |
21 … | + create: { | |
22 … | + input: { | |
23 … | + private: Value(), | |
24 … | + reveal: Value() | |
25 … | + }, | |
26 … | + processing: Value(false), | |
27 … | + time: Value(), | |
28 … | + result: Value() | |
29 … | + } | |
30 … | + } | |
31 … | + | |
32 … | + const body = h('InvitePeer', [ | |
33 … | + h('p', [ | |
34 … | + h('i.fa.fa-warning'), | |
35 … | + ' BETA - peer invites are still being rolled out to pubs and tested.' | |
36 … | + ]), | |
37 … | + h('div.use', [ | |
38 … | + h('textarea', { | |
39 … | + 'placeholder': 'peer invite code', | |
40 … | + 'ev-input': handleInput | |
41 … | + }), | |
42 … | + // MIX : I hate this, it's a mess. There's a state machine emerging, but I don't have time to build it right now | |
43 … | + computed( | |
44 … | + [state.use.invite, state.use.opening, state.use.message, state.use.accepting], | |
45 … | + (invite, opening, message, accepting) => { | |
46 … | + if (opening || accepting) { | |
47 … | + return [ | |
48 … | + h('button', { disabled: 'disabled' }, [ | |
49 … | + h('i.fa.fa-spinner.fa-pulse') | |
50 … | + ]) | |
51 … | + ] | |
52 … | + } | |
53 … | + | |
54 … | + return [ | |
55 … | + message && message.private ? h('div.private', message.private) : '', | |
56 … | + message && message.reveal ? h('div.private', message.reveal) : '', | |
57 … | + message | |
58 … | + ? h('button -primary', { 'ev-click': acceptInvite }, 'accept invitation') | |
59 … | + : invite | |
60 … | + ? h('button -primary', { 'ev-click': openInvite }, 'use invite') | |
61 … | + : h('button', { disabled: 'disabled', title: 'not a valid invite code' }, 'use invite') | |
62 … | + ] | |
63 … | + } | |
64 … | + ), | |
65 … | + computed(state.use.result, result => { | |
66 … | + if (result === null) return | |
67 … | + | |
68 … | + return result | |
69 … | + ? h('i.fa.fa-check') | |
70 … | + : h('i.fa.fa-times') | |
71 … | + }) | |
72 … | + ]), | |
73 … | + h('div.create', [ | |
74 … | + h('p', 'make a new peer invite code:'), | |
75 … | + h('div.form', [ | |
76 … | + h('div.inputs', [ | |
77 … | + h('textarea.private', { | |
78 … | + placeholder: 'private message your friend will see when they open this invite', | |
79 … | + 'ev-input': (ev) => state.create.input.private.set(ev.target.value) | |
80 … | + }), | |
81 … | + h('textarea.reveal', { | |
82 … | + placeholder: 'an introduction message which the community will be able to read when this invite is accepted', | |
83 … | + 'ev-input': (ev) => state.create.input.reveal.set(ev.target.value) | |
84 … | + }) | |
85 … | + ]), | |
86 … | + h('button', { | |
87 … | + 'ev-click': createInvite, | |
88 … | + disabled: state.create.processing | |
89 … | + }, 'create peer-invite') | |
90 … | + ]), | |
91 … | + h('div.result', [ | |
92 … | + when(state.create.processing, [ | |
93 … | + 'creating an peer invite takes some time.', | |
94 … | + h('br'), | |
95 … | + 'time so far: ', | |
96 … | + state.create.time | |
97 … | + ]), | |
98 … | + when(state.create.result, h('div.code', [ | |
99 … | + h('code', state.create.result) | |
100 … | + ])) | |
101 … | + ]) | |
102 … | + ]) | |
103 … | + ]) | |
104 … | + | |
105 … | + function createInvite () { | |
106 … | + state.create.processing.set(true) | |
107 … | + var start = Date.now() | |
108 … | + var SECOND = 1e3 | |
109 … | + var MINUTE = 60 * SECOND | |
110 … | + | |
111 … | + onceTrue(connection, sbot => { | |
112 … | + var interval = setInterval( | |
113 … | + () => { | |
114 … | + const dt = Date.now() - start | |
115 … | + const mins = Math.floor(dt / MINUTE) | |
116 … | + const secs = Math.floor((dt - mins * MINUTE) / SECOND) | |
117 … | + state.create.time.set(`${mins} mins, ${secs} seconds`) | |
118 … | + }, | |
119 … | + SECOND | |
120 … | + ) | |
121 … | + | |
122 … | + const opts = { | |
123 … | + private: resolve(state.create.input.private), | |
124 … | + reveal: resolve(state.create.input.reveal) | |
125 … | + } | |
126 … | + | |
127 … | + sbot.peerInvites.create(opts, (err, invite) => { | |
128 … | + if (err) return console.error(err) | |
129 … | + | |
130 … | + clearInterval(interval) | |
131 … | + state.create.result.set(invite) | |
132 … | + state.create.processing.set(false) | |
133 … | + }) | |
134 … | + }) | |
135 … | + } | |
136 … | + | |
137 … | + function handleInput (ev) { | |
138 … | + state.use.result.set(null) | |
139 … | + const invite = ev.target.value.replace(/^\s*"?/, '').replace(/"?\s*$/, '') | |
140 … | + | |
141 … | + if (!isInvite(invite)) return | |
142 … | + | |
143 … | + ev.target.value = invite | |
144 … | + state.use.invite.set(invite) | |
145 … | + } | |
146 … | + | |
147 … | + function openInvite () { | |
148 … | + state.use.opening.set(true) | |
149 … | + | |
150 … | + onceTrue(connection, server => { | |
151 … | + server.peerInvites.openInvite(resolve(state.use.invite), (err, data) => { | |
152 … | + state.use.opening.set(false) | |
153 … | + if (err) { | |
154 … | + state.use.result.set(false) | |
155 … | + console.error(err) | |
156 … | + return | |
157 … | + } | |
158 … | + | |
159 … | + console.log(err, data) // NOTE no opened arriving ... | |
160 … | + // HACK | |
161 … | + const m = data.opened || { | |
162 … | + private: 'kiaora gorgeous, welcome' | |
163 … | + } | |
164 … | + state.use.message.set(m) | |
165 … | + }) | |
166 … | + }) | |
167 … | + } | |
168 … | + | |
169 … | + function acceptInvite () { | |
170 … | + state.use.accepting.set(true) | |
171 … | + | |
172 … | + onceTrue(connection, server => { | |
173 … | + server.peerInvites.acceptInvite(resolve(state.use.invite), (err, confirm) => { | |
174 … | + state.use.accepting.set(false) | |
175 … | + if (err) { | |
176 … | + state.use.result.set(false) | |
177 … | + console.error(err) | |
178 … | + return | |
179 … | + } | |
180 … | + | |
181 … | + console.log('peerInvites.acceptInvite worked:', confirm) | |
182 … | + state.use.result.set(true) | |
183 … | + }) | |
184 … | + }) | |
185 … | + } | |
186 … | + | |
187 … | + return { | |
188 … | + title: 'peer invites', | |
189 … | + body | |
190 … | + } | |
191 … | +} |
app/page/network/invite-peer.mcss | ||
---|---|---|
@@ -1,0 +1,60 @@ | ||
1 … | +InvitePeer { | |
2 … | + display: grid | |
3 … | + | |
4 … | + div.use { | |
5 … | + display: grid | |
6 … | + grid-template-columns: minmax(500px, 4fr) 1fr 3rem | |
7 … | + align-items: start | |
8 … | + grid-gap: 1rem | |
9 … | + | |
10 … | + textarea { | |
11 … | + font-size: 1rem | |
12 … | + min-height: 5rem | |
13 … | + | |
14 … | + padding: .5rem | |
15 … | + } | |
16 … | + | |
17 … | + margin-bottom: 2rem | |
18 … | + } | |
19 … | + | |
20 … | + div.create { | |
21 … | + p {} | |
22 … | + | |
23 … | + div.form { | |
24 … | + display: grid | |
25 … | + grid-template-columns: minmax(500px, 4fr) 1fr 3rem | |
26 … | + align-items: start | |
27 … | + grid-gap: 1rem | |
28 … | + | |
29 … | + div.inputs { | |
30 … | + display: grid | |
31 … | + grid-gap: 1rem | |
32 … | + | |
33 … | + textarea { | |
34 … | + font-size: 1rem | |
35 … | + min-height: 3.5rem | |
36 … | + | |
37 … | + padding: .5rem | |
38 … | + } | |
39 … | + } | |
40 … | + | |
41 … | + margin-bottom: 1rem | |
42 … | + } | |
43 … | + | |
44 … | + div.result { | |
45 … | + display: grid | |
46 … | + grid-template-columns: minmax(500px, 4fr) 1fr 3rem | |
47 … | + | |
48 … | + div.code { | |
49 … | + padding: .5rem | |
50 … | + user-select: all | |
51 … | + background: deeppink | |
52 … | + | |
53 … | + code { | |
54 … | + color: white | |
55 … | + font-weight: 600 | |
56 … | + } | |
57 … | + } | |
58 … | + } | |
59 … | + } | |
60 … | +} |
app/page/network/invite-pub.js | ||
---|---|---|
@@ -1,0 +1,66 @@ | ||
1 … | +const { h, Value, resolve, onceTrue, computed } = require('mutant') | |
2 … | +const { isInvite } = require('ssb-ref') | |
3 … | + | |
4 … | +module.exports = function InvitePub ({ connection }) { | |
5 … | + const state = { | |
6 … | + invite: Value(), | |
7 … | + inviteProcessing: Value(false), | |
8 … | + inviteResult: Value(null) | |
9 … | + } | |
10 … | + | |
11 … | + const body = h('InvitePub', [ | |
12 … | + h('input', { | |
13 … | + 'placeholder': 'invite code for a remote peer (pub)', | |
14 … | + 'ev-input': handleInviteInput | |
15 … | + }), | |
16 … | + computed([state.invite, state.inviteProcessing], (invite, processing) => { | |
17 … | + if (processing) return h('i.fa.fa-spinner.fa-pulse') | |
18 … | + if (invite) return h('button -primary', { 'ev-click': useInvite }, 'use invite') | |
19 … | + | |
20 … | + return h('button', { disabled: 'disabled', title: 'not a valid invite code' }, 'use invite') | |
21 … | + }), | |
22 … | + computed(state.inviteResult, result => { | |
23 … | + if (result === null) return | |
24 … | + | |
25 … | + return result | |
26 … | + ? h('i.fa.fa-check') | |
27 … | + : h('i.fa.fa-times') | |
28 … | + }) | |
29 … | + ]) | |
30 … | + | |
31 … | + function handleInviteInput (ev) { | |
32 … | + state.inviteResult.set(null) | |
33 … | + const invite = ev.target.value.replace(/^\s*"?/, '').replace(/"?\s*$/, '') | |
34 … | + if (!isInvite(invite)) { | |
35 … | + state.invite.set() | |
36 … | + return | |
37 … | + } | |
38 … | + | |
39 … | + ev.target.value = invite | |
40 … | + state.invite.set(invite) | |
41 … | + } | |
42 … | + | |
43 … | + function useInvite () { | |
44 … | + state.inviteProcessing.set(true) | |
45 … | + | |
46 … | + onceTrue(connection, server => { | |
47 … | + server.invite.accept(resolve(state.invite), (err, data) => { | |
48 … | + state.inviteProcessing.set(false) | |
49 … | + state.invite.set() | |
50 … | + | |
51 … | + if (err) { | |
52 … | + state.inviteResult.set(false) | |
53 … | + console.error(err) | |
54 … | + return | |
55 … | + } | |
56 … | + state.inviteResult.set(true) | |
57 … | + console.log(data) | |
58 … | + }) | |
59 … | + }) | |
60 … | + } | |
61 … | + | |
62 … | + return { | |
63 … | + title: 'pub invites (classic)', | |
64 … | + body | |
65 … | + } | |
66 … | +} |
app/page/network/invite-pub.mcss | ||
---|---|---|
@@ -1,0 +1,12 @@ | ||
1 … | +InvitePub { | |
2 … | + display: grid | |
3 … | + grid-template-columns: minmax(500px, 4fr) 1fr 3rem | |
4 … | + grid-gap: 1rem | |
5 … | + | |
6 … | + input { | |
7 … | + font-size: 1rem | |
8 … | + | |
9 … | + padding: .5rem | |
10 … | + border: 1px solid gainsboro | |
11 … | + } | |
12 … | +} |
app/page/network/replication-in.js | ||
---|---|---|
@@ -1,0 +1,202 @@ | ||
1 … | +const { h, Value, Dict, onceTrue, computed, watch, watchAll, throttle } = require('mutant') | |
2 … | +const Chart = require('chart.js') | |
3 … | +const pull = require('pull-stream') | |
4 … | + | |
5 … | +const MINUTE = 60 * 1000 | |
6 … | +const DAY = 24 * 60 * MINUTE | |
7 … | + | |
8 … | +const GRAPH_Y_MIN_STEP = 50 | |
9 … | +const GRAPH_Y_MIN = 100 | |
10 … | + | |
11 … | +module.exports = function ReplicationIn ({ connection }) { | |
12 … | + const minsPerStep = 10 | |
13 … | + const scale = 1 * DAY | |
14 … | + const height = 300 | |
15 … | + const width = 800 | |
16 … | + | |
17 … | + const state = buildState({ connection, minsPerStep, scale }) | |
18 … | + const canvas = h('canvas', { height, width, style: { height: `${height}px`, width: `${width}px` } }) | |
19 … | + | |
20 … | + const body = h('ReplicationIn', [ | |
21 … | + h('p', `Messages received per ${minsPerStep}-minute block over the last ${scale / DAY} days`), | |
22 … | + canvas | |
23 … | + ]) | |
24 … | + // TODO hook to abort streams | |
25 … | + | |
26 … | + initialiseChart({ state, canvas }) | |
27 … | + | |
28 … | + return { | |
29 … | + title: 'Incoming Traffic', | |
30 … | + body | |
31 … | + } | |
32 … | +} | |
33 … | + | |
34 … | +function buildState ({ connection, minsPerStep, scale }) { | |
35 … | + // build data, range | |
36 … | + const data = Dict({ | |
37 … | + [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE]: 0, | |
38 … | + [toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE - scale]: 0 | |
39 … | + }) | |
40 … | + onceTrue(connection, server => { | |
41 … | + getData({ data, server, minsPerStep, scale }) | |
42 … | + }) | |
43 … | + | |
44 … | + const latest = Value(toTimeBlock(Date.now(), minsPerStep)) | |
45 … | + // start of the most recent bar | |
46 … | + setInterval(() => { | |
47 … | + latest.set(toTimeBlock(Date.now(), minsPerStep)) | |
48 … | + }, minsPerStep / 4 * MINUTE) | |
49 … | + | |
50 … | + const range = computed([latest], (latest) => { | |
51 … | + return { | |
52 … | + upper: latest + minsPerStep * MINUTE, | |
53 … | + lower: latest + minsPerStep * MINUTE - scale | |
54 … | + } | |
55 … | + }) | |
56 … | + | |
57 … | + return { | |
58 … | + data, // TODO rename this !! | |
59 … | + range | |
60 … | + } | |
61 … | +} | |
62 … | + | |
63 … | +function getData ({ data, server, minsPerStep, scale }) { | |
64 … | + const upperEnd = toTimeBlock(Date.now(), minsPerStep) + minsPerStep * MINUTE | |
65 … | + const lowerBound = upperEnd - scale | |
66 … | + | |
67 … | + const query = [ | |
68 … | + { | |
69 … | + $filter: { | |
70 … | + timestamp: { $gte: lowerBound } | |
71 … | + } | |
72 … | + }, { | |
73 … | + $filter: { | |
74 … | + value: { | |
75 … | + author: { $ne: server.id } | |
76 … | + } | |
77 … | + } | |
78 … | + }, { | |
79 … | + $map: { | |
80 … | + ts: ['timestamp'] | |
81 … | + } | |
82 … | + } | |
83 … | + ] | |
84 … | + | |
85 … | + pull( | |
86 … | + server.query.read({ query, live: true }), | |
87 … | + pull.filter(m => !m.sync), | |
88 … | + pull.map(m => toTimeBlock(m.ts, minsPerStep)), | |
89 … | + pull.drain(ts => { | |
90 … | + if (data.has(ts)) data.put(ts, data.get(ts) + 1) | |
91 … | + else data.put(ts, 1) | |
92 … | + }) | |
93 … | + ) | |
94 … | +} | |
95 … | + | |
96 … | +function toTimeBlock (ts, minsPerStep) { | |
97 … | + return Math.floor(ts / (minsPerStep * MINUTE)) * (minsPerStep * MINUTE) | |
98 … | +} | |
99 … | + | |
100 … | +function initialiseChart ({ canvas, state: { data, range } }) { | |
101 … | + var chart = new Chart(canvas.getContext('2d'), chartConfig(range)) | |
102 … | + | |
103 … | + watch(range, ({ lower, upper }) => { | |
104 … | + // set horizontal scale | |
105 … | + chart.options.scales.xAxes[0].time.min = lower | |
106 … | + chart.options.scales.xAxes[0].time.max = upper | |
107 … | + chart.update() | |
108 … | + }) | |
109 … | + | |
110 … | + watchAll([throttle(data, 300), range], (data, { lower, upper }) => { | |
111 … | + const _data = Object.keys(data) | |
112 … | + .sort((a, b) => a < b ? -1 : +1) | |
113 … | + .map(ts => { | |
114 … | + return { | |
115 … | + t: Number(ts), // NOTE - might need to offset by a half-step ? | |
116 … | + y: data[ts] | |
117 … | + } | |
118 … | + }) | |
119 … | + | |
120 … | + // update chard data | |
121 … | + chart.data.datasets[0].data = _data | |
122 … | + | |
123 … | + // scales the height of the graph (to the visible data)! | |
124 … | + const slice = _data | |
125 … | + .filter(d => d.t >= lower && d.t < upper) | |
126 … | + .map(d => d.y) | |
127 … | + .sort((a, b) => a > b ? -1 : +1) | |
128 … | + | |
129 … | + var max = slice[0] | |
130 … | + var stepSize = GRAPH_Y_MIN_STEP | |
131 … | + if (!max || max < GRAPH_Y_MIN) max = GRAPH_Y_MIN // min-height | |
132 … | + else { | |
133 … | + while ((max / stepSize) > 7) stepSize = stepSize * 2 | |
134 … | + max = Math.ceil(max / stepSize) * stepSize // round height to multiples of stepSize | |
135 … | + // max = max + (stepSize - max % stepSize) | |
136 … | + } | |
137 … | + | |
138 … | + chart.options.scales.yAxes[0].ticks.max = max | |
139 … | + chart.options.scales.yAxes[0].ticks.stepSize = stepSize // not sure this works | |
140 … | + | |
141 … | + chart.update() | |
142 … | + }) | |
143 … | +} | |
144 … | + | |
145 … | +function chartConfig ({ lower, upper }) { | |
146 … | + const barColor = 'hsla(215, 57%, 60%, 1)' | |
147 … | + | |
148 … | + return { | |
149 … | + type: 'bar', | |
150 … | + data: { | |
151 … | + datasets: [{ | |
152 … | + backgroundColor: barColor, | |
153 … | + borderColor: barColor, | |
154 … | + data: [] | |
155 … | + }] | |
156 … | + }, | |
157 … | + options: { | |
158 … | + legend: { | |
159 … | + display: false | |
160 … | + }, | |
161 … | + scales: { | |
162 … | + xAxes: [{ | |
163 … | + type: 'time', | |
164 … | + distribution: 'linear', | |
165 … | + time: { | |
166 … | + // unit: 'day', | |
167 … | + // min: lower, | |
168 … | + // max: upper, | |
169 … | + // tooltipFormat: 'MMMM D', | |
170 … | + // stepSize: 7 | |
171 … | + unit: 'minute', | |
172 … | + min: lower, | |
173 … | + max: upper, | |
174 … | + tooltipFormat: 'HH:mm', | |
175 … | + stepSize: 4 * 60 | |
176 … | + // stepSize: 240 | |
177 … | + }, | |
178 … | + bounds: 'ticks', | |
179 … | + ticks: { | |
180 … | + // maxTicksLimit: 4 // already disabled | |
181 … | + }, | |
182 … | + gridLines: { | |
183 … | + display: false | |
184 … | + }, | |
185 … | + maxBarThickness: 4 | |
186 … | + }], | |
187 … | + | |
188 … | + yAxes: [{ | |
189 … | + ticks: { | |
190 … | + min: 0, | |
191 … | + maxTicksLimit: 7, | |
192 … | + stepSize: GRAPH_Y_MIN_STEP, | |
193 … | + suggestedMax: GRAPH_Y_MIN | |
194 … | + } | |
195 … | + }] | |
196 … | + }, | |
197 … | + animation: { | |
198 … | + // duration: 300 | |
199 … | + } | |
200 … | + } | |
201 … | + } | |
202 … | +} |
app/page/network/replication-out.js | ||
---|---|---|
@@ -1,0 +1,67 @@ | ||
1 … | +const { h, Value, Dict, dictToCollection, onceTrue, computed } = require('mutant') | |
2 … | + | |
3 … | +module.exports = function ReplicationOut ({ connection }) { | |
4 … | + const state = buildState(connection) | |
5 … | + | |
6 … | + const body = h('ReplicationOut', [ | |
7 … | + // mix: hello friend, this area is a total Work In Progress. It's a mess but useful diagnostics. | |
8 … | + // Let's redesign and revisit it aye! | |
9 … | + h('div', ['My sequence: ', state.seq]), | |
10 … | + h('div', [ | |
11 … | + 'Replicated:', | |
12 … | + h('div', computed([state.seq, dictToCollection(state.replication)], (seq, replication) => { | |
13 … | + return replication.map(r => { | |
14 … | + if (!r.value.replicating) { | |
15 … | + return h('div', [ | |
16 … | + h('code', r.key), | |
17 … | + ' no ebt data' | |
18 … | + ]) | |
19 … | + } | |
20 … | + | |
21 … | + const { requested, sent } = r.value.replicating | |
22 … | + // TODO report that r.value.seq is NOT the current local value of the seq (well it's ok, just just gets out of sync) | |
23 … | + // const reqDiff = requested - r.value.seq | |
24 … | + const reqDiff = requested - seq | |
25 … | + const sentDiff = sent - seq | |
26 … | + | |
27 … | + return h('div', [ | |
28 … | + h('code', r.key), | |
29 … | + ` - requested: ${requested} `, | |
30 … | + reqDiff === 0 ? h('i.fa.fa-check-circle-o') : `(${reqDiff})`, | |
31 … | + `, sent: ${sent} `, | |
32 … | + sentDiff === 0 ? h('i.fa.fa-check-circle-o') : `(${sentDiff})` | |
33 … | + ]) | |
34 … | + }) | |
35 … | + })) | |
36 … | + ]) | |
37 … | + ]) | |
38 … | + | |
39 … | + return { | |
40 … | + title: 'Outgoing Traffic', | |
41 … | + body | |
42 … | + } | |
43 … | +} | |
44 … | + | |
45 … | +function buildState (connection) { | |
46 … | + // build seq, replication (my current state, and replicated state) | |
47 … | + const seq = Value() | |
48 … | + const replication = Dict({}) | |
49 … | + onceTrue(connection, server => { | |
50 … | + setInterval(() => { | |
51 … | + // TODO check ebt docs if this is best method | |
52 … | + server.ebt.peerStatus(server.id, (err, data) => { | |
53 … | + if (err) return console.error(err) | |
54 … | + | |
55 … | + seq.set(data.seq) | |
56 … | + for (var peer in data.peers) { | |
57 … | + replication.put(peer, data.peers[peer]) | |
58 … | + } | |
59 … | + }) | |
60 … | + }, 5e3) | |
61 … | + }) | |
62 … | + | |
63 … | + return { | |
64 … | + seq, | |
65 … | + replication | |
66 … | + } | |
67 … | +} |
app/page/network.mcss | ||
---|---|---|
@@ -1,40 +1,0 @@ | ||
1 | -NetworkPage { | |
2 | - width: 100% | |
3 | - | |
4 | - display: grid | |
5 | - justify-content: center | |
6 | - | |
7 | - div.container { | |
8 | - section { | |
9 | - margin-bottom: 2rem | |
10 | - h2 { | |
11 | - font-size: .8rem | |
12 | - text-transform: uppercase | |
13 | - letter-spacing: 2px | |
14 | - | |
15 | - i { | |
16 | - color: #666 | |
17 | - margin-left: .5rem | |
18 | - } | |
19 | - } | |
20 | - | |
21 | - div.invite { | |
22 | - margin-top: 1rem | |
23 | - | |
24 | - display: grid | |
25 | - grid-template-columns: 55rem 8rem 2rem | |
26 | - grid-gap: 1rem | |
27 | - | |
28 | - input { | |
29 | - padding-left: .5rem | |
30 | - } | |
31 | - | |
32 | - i.fa { | |
33 | - color: fuchsia | |
34 | - justify-self: center | |
35 | - align-self: center | |
36 | - } | |
37 | - } | |
38 | - } | |
39 | - } | |
40 | -} |
exports.js | ||
---|---|---|
@@ -6,17 +6,17 @@ | ||
6 | 6 … | const configModule = require('./config') |
7 | 7 … | |
8 | 8 … | const patchbay = { |
9 | 9 … | patchbay: { |
10 | - about: bulk(__dirname, [ 'about/**/*.js' ]), | |
11 | - app: bulk(__dirname, [ 'app/**/*.js' ]), | |
12 | - blob: bulk(__dirname, [ 'blob/**/*.js' ]), | |
13 | - channel: bulk(__dirname, [ 'channel/**/*.js' ]), | |
14 | - contact: bulk(__dirname, [ 'contact/**/*.js' ]), | |
15 | - message: bulk(__dirname, [ 'message/**/*.js' ]), | |
16 | - router: bulk(__dirname, [ 'router/**/*.js' ]), | |
17 | - styles: bulk(__dirname, [ 'styles/**/*.js' ]), | |
18 | - sbot: bulk(__dirname, [ 'sbot/**/*.js' ]), | |
10 … | + about: getModules('about/**/*.js'), | |
11 … | + app: getModules('app/**/*.js'), | |
12 … | + blob: getModules('blob/**/*.js'), | |
13 … | + channel: getModules('channel/**/*.js'), | |
14 … | + contact: getModules('contact/**/*.js'), | |
15 … | + message: getModules('message/**/*.js'), | |
16 … | + router: getModules('router/**/*.js'), | |
17 … | + styles: getModules('styles/**/*.js'), | |
18 … | + sbot: getModules('sbot/**/*.js'), | |
19 | 19 … | |
20 | 20 … | suggestions: require('patch-suggest'), |
21 | 21 … | settings: require('patch-settings'), |
22 | 22 … | drafts: require('patch-drafts'), |
@@ -39,4 +39,8 @@ | ||
39 | 39 … | plugins, |
40 | 40 … | patchbay, |
41 | 41 … | patchcore |
42 | 42 … | } |
43 … | + | |
44 … | +function getModules (path) { | |
45 … | + return bulk(__dirname, [path]) | |
46 … | +} |
index.js | ||
---|---|---|
@@ -11,9 +11,8 @@ | ||
11 | 11 … | |
12 | 12 … | 'ssb-legacy-conn', |
13 | 13 … | 'ssb-replicate', |
14 | 14 … | 'ssb-friends', |
15 | - 'ssb-invite', | |
16 | 15 … | |
17 | 16 … | 'ssb-blobs', |
18 | 17 … | 'ssb-ws', |
19 | 18 … | |
@@ -27,9 +26,15 @@ | ||
27 | 26 … | 'ssb-query', |
28 | 27 … | 'ssb-search', |
29 | 28 … | 'ssb-suggest', |
30 | 29 … | 'ssb-tangle', |
31 | - 'ssb-unread' | |
30 … | + 'ssb-unread', | |
31 … | + | |
32 … | + 'ssb-invite', | |
33 … | + | |
34 … | + 'ssb-device-address', // for peer-invites | |
35 … | + 'ssb-identities', // for peer invites | |
36 … | + 'ssb-peer-invites' | |
32 | 37 … | ] |
33 | 38 … | |
34 | 39 … | ahoy( |
35 | 40 … | { |
package-lock.json | ||
---|---|---|
The diff is too large to show. Use a local git client to view these changes. Old file size: 796953 bytes New file size: 799402 bytes |
package.json | ||
---|---|---|
@@ -66,9 +66,9 @@ | ||
66 | 66 … | "libnested": "^1.3.2", |
67 | 67 … | "lodash": "^4.17.10", |
68 | 68 … | "marama": "^1.2.2", |
69 | 69 … | "micro-css": "^2.0.1", |
70 | - "mutant": "^3.22.3", | |
70 … | + "mutant": "^3.23.0", | |
71 | 71 … | "mutant-scroll": "^1.0.2", |
72 | 72 … | "noto-color-emoji": "^1.0.0", |
73 | 73 … | "open-dyslexic": "^1.0.3", |
74 | 74 … | "open-external": "^0.1.1", |
@@ -78,9 +78,9 @@ | ||
78 | 78 … | "patch-settings": "^1.1.2", |
79 | 79 … | "patch-suggest": "^3.0.1", |
80 | 80 … | "patchbay-book": "^2.0.4", |
81 | 81 … | "patchbay-dark-crystal": "^2.0.0", |
82 | - "patchbay-gatherings": "^3.2.9", | |
82 … | + "patchbay-gatherings": "^3.2.14", | |
83 | 83 … | "patchbay-hacky-art": "^1.0.0", |
84 | 84 … | "patchbay-poll": "^1.1.4", |
85 | 85 … | "patchbay-scry": "^1.5.0", |
86 | 86 … | "patchcore": "^2.1.6", |
@@ -97,24 +97,27 @@ | ||
97 | 97 … | "scuttle-thread": "^1.0.2", |
98 | 98 … | "setimmediate": "^1.0.5", |
99 | 99 … | "sharp": "^0.22.1", |
100 | 100 … | "ssb-about": "^2.0.1", |
101 | - "ssb-ahoy": "^1.0.0", | |
101 … | + "ssb-ahoy": "^1.0.1", | |
102 | 102 … | "ssb-backlinks": "^0.7.3", |
103 | 103 … | "ssb-blob-files": "^1.1.6", |
104 | 104 … | "ssb-blobs": "^1.1.12", |
105 | 105 … | "ssb-chess-db": "^1.0.6", |
106 | 106 … | "ssb-chess-mithril": "1.0.10", |
107 | 107 … | "ssb-config": "^3.3.0", |
108 | - "ssb-ebt": "^5.6.4", | |
108 … | + "ssb-device-address": "^1.1.6", | |
109 … | + "ssb-ebt": "^5.6.6", | |
109 | 110 … | "ssb-friend-pub": "^1.0.7", |
110 | - "ssb-friends": "^4.1.3", | |
111 … | + "ssb-friends": "^4.1.4", | |
112 … | + "ssb-identities": "^2.1.0", | |
111 | 113 … | "ssb-invite": "~2.0.4", |
112 | 114 … | "ssb-legacy-conn": "^1.0.25", |
113 | 115 … | "ssb-meme": "^1.0.4", |
114 | 116 … | "ssb-mentions": "^0.5.0", |
115 | 117 … | "ssb-mutual": "^0.1.0", |
116 | 118 … | "ssb-ooo": "^1.1.1", |
119 … | + "ssb-peer-invites": "^2.0.0", | |
117 | 120 … | "ssb-private": "^0.2.3", |
118 | 121 … | "ssb-query": "^2.1.0", |
119 | 122 … | "ssb-ref": "^2.13.6", |
120 | 123 … | "ssb-replicate": "^1.3.0", |
Built with git-ssb-web