git ssb

16+

Dominic / patchbay



Commit 8e4e9c4e33e4475499a439dc81219dc9da8b19a2

Merge branch 'master' into new_icon

mixmix committed on 7/16/2019, 6:26:52 AM
Parent: 070487b0c7985fcdf84bdae5f2ee2f3f7f0e1e83
Parent: d119c36e794cfcba29c54f4f2f6da4f6e4d0fb77

Files changed

app/html/modal.jschanged
app/html/modal.mcsschanged
app/html/settings/friend-hops.jschanged
app/html/settings/friend-pub.jschanged
app/html/settings/tor-only.jschanged
app/page/network.jsdeleted
app/page/network/connections.jsadded
app/page/network/connections.mcssadded
app/page/network/index.jsadded
app/page/network/index.mcssadded
app/page/network/invite-peer.jsadded
app/page/network/invite-peer.mcssadded
app/page/network/invite-pub.jsadded
app/page/network/invite-pub.mcssadded
app/page/network/replication-in.jsadded
app/page/network/replication-out.jsadded
app/page/network.mcssdeleted
exports.jschanged
index.jschanged
package-lock.jsonchanged
package.jsonchanged
app/html/modal.jsView
@@ -1,6 +1,6 @@
11 const nest = require('depnest')
2-const { h, computed, Value } = require('mutant')
2 +const { h, Value } = require('mutant')
33
44 exports.gives = nest('app.html.modal')
55
66 exports.create = (api) => {
@@ -14,29 +14,33 @@
1414 isOpen.set(false)
1515 if (typeof onClose === 'function') onClose()
1616 }
1717
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 + )
2033
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-
3734 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 + }
3943
4044 focus()
4145 function focus () {
4246 if (!lb.isConnected) setTimeout(focus, 200)
app/html/modal.mcssView
@@ -27,9 +27,9 @@
2727 -open {
2828 display: grid
2929 }
3030
31- -close {
31 + -closed {
3232 display: none
3333 }
3434
3535 -dark {
app/html/settings/friend-hops.jsView
@@ -29,9 +29,9 @@
2929 updateConfig(hops)
3030 })
3131
3232 return {
33- group: 'gossip',
33 + group: 'replication',
3434 title: 'Friend Hops',
3535 body: h('FriendHops', [
3636 h('div.description', [
3737 '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.jsView
@@ -32,13 +32,13 @@
3232 updatePubs(hops)
3333 })
3434
3535 return {
36- group: 'gossip',
37- title: 'Pub gossip',
36 + group: 'replication',
37 + title: 'Pub replication',
3838 body: h('FriendPub', [
3939 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'
4141 ]),
4242 h('div.slider', [
4343 h('datalist', { id: 'pub-gossip-datalist' }, [
4444 h('option', { value: 0, label: 'My pub' }),
app/html/settings/tor-only.jsView
@@ -26,9 +26,9 @@
2626 updateConfig(torOnly)
2727 })
2828
2929 return {
30- group: 'gossip',
30 + group: 'replication',
3131 title: 'Tor only connections',
3232 body: h('TorOnly', [
3333 h('p', [
3434 'Preserve your ip privacy by only connecting to other nodes using tor',
app/page/network.jsView
@@ -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.jsView
@@ -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.mcssView
@@ -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.jsView
@@ -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.mcssView
@@ -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.jsView
@@ -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.mcssView
@@ -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.jsView
@@ -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.mcssView
@@ -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.jsView
@@ -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.jsView
@@ -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.mcssView
@@ -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.jsView
@@ -6,17 +6,17 @@
66 const configModule = require('./config')
77
88 const patchbay = {
99 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'),
1919
2020 suggestions: require('patch-suggest'),
2121 settings: require('patch-settings'),
2222 drafts: require('patch-drafts'),
@@ -39,4 +39,8 @@
3939 plugins,
4040 patchbay,
4141 patchcore
4242 }
43 +
44 +function getModules (path) {
45 + return bulk(__dirname, [path])
46 +}
index.jsView
@@ -11,9 +11,8 @@
1111
1212 'ssb-legacy-conn',
1313 'ssb-replicate',
1414 'ssb-friends',
15- 'ssb-invite',
1615
1716 'ssb-blobs',
1817 'ssb-ws',
1918
@@ -27,9 +26,15 @@
2726 'ssb-query',
2827 'ssb-search',
2928 'ssb-suggest',
3029 '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'
3237 ]
3338
3439 ahoy(
3540 {
package-lock.jsonView
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.jsonView
@@ -66,9 +66,9 @@
6666 "libnested": "^1.3.2",
6767 "lodash": "^4.17.10",
6868 "marama": "^1.2.2",
6969 "micro-css": "^2.0.1",
70- "mutant": "^3.22.3",
70 + "mutant": "^3.23.0",
7171 "mutant-scroll": "^1.0.2",
7272 "noto-color-emoji": "^1.0.0",
7373 "open-dyslexic": "^1.0.3",
7474 "open-external": "^0.1.1",
@@ -78,9 +78,9 @@
7878 "patch-settings": "^1.1.2",
7979 "patch-suggest": "^3.0.1",
8080 "patchbay-book": "^2.0.4",
8181 "patchbay-dark-crystal": "^2.0.0",
82- "patchbay-gatherings": "^3.2.9",
82 + "patchbay-gatherings": "^3.2.14",
8383 "patchbay-hacky-art": "^1.0.0",
8484 "patchbay-poll": "^1.1.4",
8585 "patchbay-scry": "^1.5.0",
8686 "patchcore": "^2.1.6",
@@ -97,24 +97,27 @@
9797 "scuttle-thread": "^1.0.2",
9898 "setimmediate": "^1.0.5",
9999 "sharp": "^0.22.1",
100100 "ssb-about": "^2.0.1",
101- "ssb-ahoy": "^1.0.0",
101 + "ssb-ahoy": "^1.0.1",
102102 "ssb-backlinks": "^0.7.3",
103103 "ssb-blob-files": "^1.1.6",
104104 "ssb-blobs": "^1.1.12",
105105 "ssb-chess-db": "^1.0.6",
106106 "ssb-chess-mithril": "1.0.10",
107107 "ssb-config": "^3.3.0",
108- "ssb-ebt": "^5.6.4",
108 + "ssb-device-address": "^1.1.6",
109 + "ssb-ebt": "^5.6.6",
109110 "ssb-friend-pub": "^1.0.7",
110- "ssb-friends": "^4.1.3",
111 + "ssb-friends": "^4.1.4",
112 + "ssb-identities": "^2.1.0",
111113 "ssb-invite": "~2.0.4",
112114 "ssb-legacy-conn": "^1.0.25",
113115 "ssb-meme": "^1.0.4",
114116 "ssb-mentions": "^0.5.0",
115117 "ssb-mutual": "^0.1.0",
116118 "ssb-ooo": "^1.1.1",
119 + "ssb-peer-invites": "^2.0.0",
117120 "ssb-private": "^0.2.3",
118121 "ssb-query": "^2.1.0",
119122 "ssb-ref": "^2.13.6",
120123 "ssb-replicate": "^1.3.0",

Built with git-ssb-web