git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Commit 3779d0d650174a19772e8ccad74d892554310b54

initial work upgrading to use latest patchbay (currently only displaying patchbay views)

still need to port all of the patchwork-next views to the new api
Matt McKegg committed on 2/10/2017, 8:21:26 AM
Parent: 02bc77c77c6801ad0cfcffd1a585dfed52a2f5f8

Files changed

lib/friends-with-gossip-priority.jsdeleted
lib/persistent-gossip/index.jsdeleted
lib/persistent-gossip/init.jsdeleted
lib/persistent-gossip/schedule.jsdeleted
main-window.jschanged
modules/index.jschanged
modules/about.jsdeleted
modules/app.jsadded
modules/channel.jsdeleted
modules/helpers/blob-url.jsadded
modules/helpers/emoji.jsadded
modules/data-feed.jsdeleted
modules/feed-summary.jsdeleted
modules/sbot.jsadded
modules/feed.jsdeleted
modules/git-mini-messages.jsdeleted
modules/like.jsdeleted
modules/many-people.jsdeleted
modules/message-confirm.jsdeleted
modules/message-name.jsdeleted
modules/message.jsdeleted
modules/notifications.jsdeleted
modules/obs-connected.jsdeleted
modules/obs-following.jsdeleted
modules/obs-local.jsdeleted
modules/obs-recently-updated-feeds.jsdeleted
modules/obs-subscribed-channels.jsdeleted
modules/people-names.jsdeleted
modules/person.jsdeleted
modules/post.jsdeleted
modules/private.jsdeleted
modules/public.jsdeleted
modules/raw.jsdeleted
modules/thread.jsdeleted
modules/timestamp.jsdeleted
package.jsonchanged
server-process.jschanged
styles/index.jschanged
styles/channel-list.mcssdeleted
styles/emoji.cssadded
styles/feed-event.mcssdeleted
styles/loading.mcssdeleted
styles/message-confirm.mcssdeleted
styles/message.mcssdeleted
styles/notifier.mcssdeleted
styles/page-heading.mcssdeleted
styles/patchbay-tweaks.cssdeleted
styles/profile-list.mcssdeleted
styles/split-view.mcssdeleted
old_modules/about.jsadded
old_modules/app.jsadded
old_modules/channel.jsadded
old_modules/data-feed.jsadded
old_modules/feed-summary.jsadded
old_modules/feed.jsadded
old_modules/git-mini-messages.jsadded
old_modules/index.jsadded
old_modules/like.jsadded
old_modules/many-people.jsadded
old_modules/message-confirm.jsadded
old_modules/message-name.jsadded
old_modules/message.jsadded
old_modules/notifications.jsadded
old_modules/obs-connected.jsadded
old_modules/obs-following.jsadded
old_modules/obs-local.jsadded
old_modules/obs-recently-updated-feeds.jsadded
old_modules/obs-subscribed-channels.jsadded
old_modules/people-names.jsadded
old_modules/person.jsadded
old_modules/post.jsadded
old_modules/private.jsadded
old_modules/public.jsadded
old_modules/raw.jsadded
old_modules/thread.jsadded
old_modules/timestamp.jsadded
old_styles/channel-list.mcssadded
old_styles/feed-event.mcssadded
old_styles/loading.mcssadded
old_styles/message-confirm.mcssadded
old_styles/message.mcssadded
old_styles/notifier.mcssadded
old_styles/page-heading.mcssadded
old_styles/patchbay-tweaks.cssadded
old_styles/profile-list.mcssadded
old_styles/split-view.mcssadded
lib/friends-with-gossip-priority.jsView
@@ -1,178 +1,0 @@
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- //by default, also emit your own key.
128- if(opts.self !== false)
129- push(start, 0)
130-
131- var conf = config.friends || {}
132- cancel = graph.traverse({
133- start: start,
134- hops: opts.hops || conf.hops || 3,
135- max: opts.dunbar || conf.dunbar || 150,
136- each: function (_, to, hops) {
137- if(to !== start) push(to, hops)
138- }
139- })
140-
141- if(!live) { cancel(); ps.end() }
142-
143- return ps
144- }, 'createFriendStreamOpts?'),
145-
146- hops: valid.async(function (start, graph, opts, cb) {
147- if (typeof opts == 'function') { // (start|opts, graph, cb)
148- cb = opts
149- opts = null
150- } else if (typeof graph == 'function') { // (start|opts, cb)
151- cb = graph
152- opts = graph = null
153- }
154- opts = opts || {}
155- if(isString(start)) { // (start, ...)
156- // first arg is id string
157- opts.start = start
158- } else if (start && typeof start == 'object') { // (opts, ...)
159- // first arg is opts
160- for (var k in start)
161- opts[k] = start[k]
162- }
163-
164- var conf = config.friends || {}
165- opts.start = opts.start || sbot.id
166- opts.dunbar = opts.dunbar || conf.dunbar || 150
167- opts.hops = opts.hops || conf.hops || 3
168-
169- var g = graphs[graph || 'follow']
170- if (!g)
171- return cb(new Error('Invalid graph type: '+graph))
172-
173- awaitSync(function () {
174- cb(null, g.traverse(opts))
175- })
176- }, ['feedId', 'string?', 'object?'], ['createFriendStreamOpts'])
177- }
178-}
lib/persistent-gossip/index.jsView
@@ -1,255 +1,0 @@
1-'use strict'
2-var pull = require('pull-stream')
3-var Notify = require('pull-notify')
4-var mdm = require('mdmanifest')
5-var valid = require('scuttlebot/lib/validators')
6-var apidoc = require('scuttlebot/lib/apidocs').gossip
7-var u = require('scuttlebot/lib/util')
8-var ref = require('ssb-ref')
9-var ping = require('pull-ping')
10-var stats = require('statistics')
11-var Schedule = require('./schedule')
12-var Init = require('./init')
13-var AtomicFile = require('atomic-file')
14-var path = require('path')
15-var deepEqual = require('deep-equal')
16-
17-function isFunction (f) {
18- return 'function' === typeof f
19-}
20-
21-function stringify(peer) {
22- return [peer.host, peer.port, peer.key].join(':')
23-}
24-
25-/*
26-Peers : [{
27- key: id,
28- host: ip,
29- port: int,
30- //to be backwards compatible with patchwork...
31- announcers: {length: int}
32- source: 'pub'|'manual'|'local'
33-}]
34-*/
35-
36-
37-module.exports = {
38- name: 'gossip',
39- version: '1.0.0',
40- manifest: mdm.manifest(apidoc),
41- permissions: {
42- anonymous: {allow: ['ping']}
43- },
44- init: function (server, config) {
45- var notify = Notify()
46- var conf = config.gossip || {}
47- var home = ref.parseAddress(server.getAddress())
48-
49- var stateFile = AtomicFile(path.join(config.path, 'gossip.json'))
50-
51- //Known Peers
52- var peers = []
53-
54- function getPeer(id) {
55- return u.find(peers, function (e) {
56- return e && e.key === id
57- })
58- }
59-
60- var timer_ping = 5*6e4
61-
62- var gossip = {
63- wakeup: 0,
64- peers: function () {
65- return peers
66- },
67- get: function (addr) {
68- addr = ref.parseAddress(addr)
69- return u.find(peers, function (a) {
70- return (
71- addr.port === a.port
72- && addr.host === a.host
73- && addr.key === a.key
74- )
75- })
76- },
77- connect: valid.async(function (addr, cb) {
78- addr = ref.parseAddress(addr)
79- if (!addr || typeof addr != 'object')
80- return cb(new Error('first param must be an address'))
81-
82- if(!addr.key) return cb(new Error('address must have ed25519 key'))
83- // add peer to the table, incase it isn't already.
84- gossip.add(addr, 'manual')
85- var p = gossip.get(addr)
86- if(!p) return cb()
87-
88- p.stateChange = Date.now()
89- p.state = 'connecting'
90- server.connect(p, function (err, rpc) {
91- if (err) {
92- p.state = undefined
93- p.failure = (p.failure || 0) + 1
94- p.stateChange = Date.now()
95- notify({ type: 'connect-failure', peer: p })
96- server.emit('log:info', ['SBOT', p.host+':'+p.port+p.key, 'connection failed', err.message || err])
97- p.duration = stats(p.duration, 0)
98- return (cb && cb(err))
99- }
100- else {
101- p.state = 'connected'
102- p.failure = 0
103- }
104- cb && cb(null, rpc)
105- })
106-
107- }, 'string|object'),
108-
109- disconnect: valid.async(function (addr, cb) {
110- var peer = this.get(addr)
111-
112- peer.state = 'disconnecting'
113- peer.stateChange = Date.now()
114- if(!peer || !peer.disconnect) cb && cb()
115- else peer.disconnect(true, function (err) {
116- peer.stateChange = Date.now()
117- })
118-
119- }, 'string|object'),
120-
121- changes: function () {
122- return notify.listen()
123- },
124- //add an address to the peer table.
125- add: valid.sync(function (addr, source) {
126- addr = ref.parseAddress(addr)
127- if(!ref.isAddress(addr))
128- throw new Error('not a valid address:' + JSON.stringify(addr))
129- // check that this is a valid address, and not pointing at self.
130-
131- if(addr.key === home.key) return
132- if(addr.host === home.host && addr.port === home.port) return
133-
134- var f = gossip.get(addr)
135-
136- if(!f) {
137- // new peer
138- addr.source = source
139- addr.announcers = 1
140- addr.duration = addr.duration || null
141- peers.push(addr)
142- notify({ type: 'discover', peer: addr, source: source || 'manual' })
143- return addr
144- } else if (source === 'friends' || source === 'local') {
145- // this peer is a friend or local, override old source to prioritize gossip
146- f.source = source
147- }
148- //don't count local over and over
149- else if(f.source != 'local')
150- f.announcers ++
151-
152- return f
153- }, 'string|object', 'string?'),
154- delete: function (addr) {
155- var peer = gossip.get(addr)
156- var index = peers.indexOf(peer)
157- if (~index) {
158- peers.splice(index, 1)
159- }
160- },
161- ping: function (opts) {
162- var timeout = config.timers && config.timers.ping || 5*60e3
163- //between 10 seconds and 30 minutes, default 5 min
164- timeout = Math.max(10e3, Math.min(timeout, 30*60e3))
165- return ping({timeout: timeout})
166- },
167- reconnect: function () {
168- for(var id in server.peers)
169- if(id !== server.id) //don't disconnect local client
170- server.peers[id].forEach(function (peer) {
171- peer.close(true)
172- })
173- return gossip.wakeup = Date.now()
174- }
175- }
176-
177- Schedule (gossip, config, server)
178- Init (gossip, config, server)
179- //get current state
180-
181- server.on('rpc:connect', function (rpc, isClient) {
182- var peer = getPeer(rpc.id)
183- //don't track clients that connect, but arn't considered peers.
184- //maybe we should though?
185- if(!peer) return
186- console.log('Connected', stringify(peer))
187- //means that we have created this connection, not received it.
188- peer.client = !!isClient
189- peer.state = 'connected'
190- peer.stateChange = Date.now()
191- peer.disconnect = function (err, cb) {
192- if(isFunction(err)) cb = err, err = null
193- rpc.close(err, cb)
194- }
195-
196- if(isClient) {
197- //default ping is 5 minutes...
198- var pp = ping({serve: true, timeout: timer_ping}, function (_) {})
199- peer.ping = {rtt: pp.rtt, skew: pp.skew}
200- pull(
201- pp,
202- rpc.gossip.ping({timeout: timer_ping}, function (err) {
203- if(err.name === 'TypeError') peer.ping.fail = true
204- }),
205- pp
206- )
207- }
208-
209- rpc.on('closed', function () {
210- console.log('Disconnected', stringify(peer))
211- //track whether we have successfully connected.
212- //or how many failures there have been.
213- var since = peer.stateChange
214- peer.stateChange = Date.now()
215-// if(peer.state === 'connected') //may be "disconnecting"
216- peer.duration = stats(peer.duration, peer.stateChange - since)
217-// console.log(peer.duration)
218- peer.state = undefined
219- notify({ type: 'disconnect', peer: peer })
220- server.emit('log:info', ['SBOT', rpc.id, 'disconnect'])
221- })
222-
223- notify({ type: 'connect', peer: peer })
224- })
225-
226- var last
227- stateFile.get(function (err, ary) {
228- last = ary || []
229- if(Array.isArray(ary))
230- ary.forEach(function (v) {
231- delete v.state
232- // don't add local peers (wait to rediscover)
233- if(v.source !== 'local') {
234- gossip.add(v, 'stored')
235- }
236- })
237- })
238-
239- var int = setInterval(function () {
240- var copy = JSON.parse(JSON.stringify(peers))
241- copy.forEach(function (e) {
242- delete e.state
243- })
244- if(deepEqual(copy, last)) return
245- last = copy
246- stateFile.set(copy, function(err) {
247- if (err) console.log(err)
248- })
249- }, 10*1000)
250-
251- if(int.unref) int.unref()
252-
253- return gossip
254- }
255-}
lib/persistent-gossip/init.jsView
@@ -1,31 +1,0 @@
1-var isArray = Array.isArray
2-var pull = require('pull-stream')
3-var ref = require('ssb-ref')
4-
5-module.exports = function (gossip, config, server) {
6-
7- // populate peertable with configured seeds (mainly used in testing)
8- var seeds = config.seeds
9-
10- ;(isArray(seeds) ? seeds : [seeds]).filter(Boolean)
11- .forEach(function (addr) { gossip.add(addr, 'seed') })
12-
13- // populate peertable with pub announcements on the feed
14- pull(
15- server.messagesByType({
16- type: 'pub', live: true, keys: false
17- }),
18- pull.drain(function (msg) {
19- if(msg.sync) return
20- if(!msg.content.address) return
21- if(ref.isAddress(msg.content.address))
22- gossip.add(msg.content.address, 'pub')
23- })
24- )
25-
26- // populate peertable with announcements on the LAN multicast
27- server.on('local', function (_peer) {
28- gossip.add(_peer, 'local')
29- })
30-
31-}
lib/persistent-gossip/schedule.jsView
@@ -1,233 +1,0 @@
1-var ip = require('ip')
2-var onWakeup = require('on-wakeup')
3-var onNetwork = require('on-change-network')
4-var hasNetwork = require('has-network')
5-
6-var pull = require('pull-stream')
7-
8-function not (fn) {
9- return function (e) { return !fn(e) }
10-}
11-
12-function and () {
13- var args = [].slice.call(arguments)
14- return function (value) {
15- return args.every(function (fn) { return fn.call(null, value) })
16- }
17-}
18-
19-//min delay (delay since last disconnect of most recent peer in unconnected set)
20-//unconnected filter delay peer < min delay
21-function delay (failures, factor, max) {
22- return Math.min(Math.pow(2, failures)*factor, max || Infinity)
23-}
24-
25-function maxStateChange (M, e) {
26- return Math.max(M, e.stateChange || 0)
27-}
28-
29-function peerNext(peer, opts) {
30- return (peer.stateChange|0) + delay(peer.failure|0, opts.factor, opts.max)
31-}
32-
33-
34-//detect if not connected to wifi or other network
35-//(i.e. if there is only localhost)
36-
37-function isOffline (e) {
38- if(ip.isLoopback(e.host)) return false
39- return !hasNetwork()
40-}
41-
42-var isOnline = not(isOffline)
43-
44-function isLocal (e) {
45- // don't rely on private ip address, because
46- // cjdns creates fake private ip addresses.
47- return ip.isPrivate(e.host) && e.source === 'local'
48-}
49-
50-function isFriend (e) {
51- return e.source === 'friends'
52-}
53-
54-function isUnattempted (e) {
55- return !e.stateChange
56-}
57-
58-//select peers which have never been successfully connected to yet,
59-//but have been tried.
60-function isInactive (e) {
61- return e.stateChange && (!e.duration || e.duration.mean == 0)
62-}
63-
64-function isLongterm (e) {
65- return e.ping && e.ping.rtt && e.ping.rtt.mean > 0
66-}
67-
68-//peers which we can connect to, but are not upgraded.
69-//select peers which we can connect to, but are not upgraded to LT.
70-//assume any peer is legacy, until we know otherwise...
71-function isLegacy (peer) {
72- return peer.duration && peer.duration.mean > 0 && !exports.isLongterm(peer)
73-}
74-
75-function isConnect (e) {
76- return 'connected' === e.state || 'connecting' === e.state
77-}
78-
79-//sort oldest to newest then take first n
80-function earliest(peers, n) {
81- return peers.sort(function (a, b) {
82- return a.stateChange - b.stateChange
83- }).slice(0, Math.max(n, 0))
84-}
85-
86-function select(peers, ts, filter, opts) {
87- if(opts.disable) return []
88- //opts: { quota, groupMin, min, factor, max }
89- var type = peers.filter(filter)
90- var unconnect = type.filter(not(isConnect))
91- var count = Math.max(opts.quota - type.filter(isConnect).length, 0)
92- var min = unconnect.reduce(maxStateChange, 0) + opts.groupMin
93- if(ts < min) return []
94-
95- return earliest(unconnect.filter(function (peer) {
96- return peerNext(peer, opts) < ts
97- }), count)
98-}
99-
100-var schedule = exports = module.exports =
101-function (gossip, config, server) {
102-// return
103- var min = 60e3, hour = 60*60e3
104-
105- //trigger hard reconnect after suspend or local network changes
106- onWakeup(gossip.reconnect)
107- onNetwork(gossip.reconnect)
108-
109- function conf(name, def) {
110- if(!config.gossip) return def
111- var value = config.gossip[name]
112- return (value === undefined || value === '') ? def : value
113- }
114-
115- function connect (peers, ts, name, filter, opts) {
116- opts.group = name
117- var connected = peers.filter(isConnect).filter(filter)
118-
119- //disconnect if over quota
120- if(connected.length > opts.quota) {
121- return earliest(connected, connected.length - opts.quota)
122- .forEach(function (peer) {
123- gossip.disconnect(peer)
124- })
125- }
126-
127- //will return [] if the quota is full
128- var selected = select(peers, ts, and(filter, isOnline), opts)
129- selected
130- .forEach(function (peer) {
131- gossip.connect(peer)
132- })
133- }
134-
135-
136- var connecting = false
137- function connections () {
138- if(connecting) return
139- connecting = true
140- setTimeout(function () {
141- connecting = false
142- var ts = Date.now()
143- var peers = gossip.peers()
144-
145- var connected = peers.filter(and(isConnect, not(isLocal), not(isFriend))).length
146- var connectedFriends = peers.filter(and(isConnect, isFriend)).length
147-
148- connect(peers, ts, 'local', exports.isLocal, {
149- quota: 3, factor: 2e3, max: 10*min, groupMin: 1e3,
150- disable: !conf('local', true)
151- })
152-
153- // prioritize friends
154- connect(peers, ts, 'friends', and(exports.isFriend, exports.isLongterm), {
155- quota: 2, factor: 10e3, max: 10*min, groupMin: 5e3,
156- disable: !conf('local', true)
157- })
158-
159- if (connectedFriends < 2)
160- connect(peers, ts, 'attemptFriend', and(exports.isFriend, exports.isUnattempted), {
161- min: 0, quota: 1, factor: 0, max: 0, groupMin: 0,
162- disable: !conf('global', true)
163- })
164-
165- connect(peers, ts, 'retryFriends', and(exports.isFriend, exports.isInactive), {
166- min: 0,
167- quota: 3, factor: 60e3, max: 3*60*60e3, groupMin: 5*60e3
168- })
169-
170- // standard longterm peers
171- connect(peers, ts, 'longterm', and(
172- exports.isLongterm,
173- not(exports.isFriend),
174- not(exports.isLocal)
175- ), {
176- quota: 2, factor: 10e3, max: 10*min, groupMin: 5e3,
177- disable: !conf('global', true)
178- })
179-
180- if(!connected)
181- connect(peers, ts, 'attempt', exports.isUnattempted, {
182- min: 0, quota: 1, factor: 0, max: 0, groupMin: 0,
183- disable: !conf('global', true)
184- })
185-
186- //quota, groupMin, min, factor, max
187- connect(peers, ts, 'retry', exports.isInactive, {
188- min: 0,
189- quota: 3, factor: 5*60e3, max: 3*60*60e3, groupMin: 5*50e3
190- })
191-
192- var longterm = peers.filter(isConnect).filter(exports.isLongterm).length
193-
194- connect(peers, ts, 'legacy', exports.isLegacy, {
195- quota: 3 - longterm,
196- factor: 5*min, max: 3*hour, groupMin: 5*min,
197- disable: !conf('global', true)
198- })
199-
200- peers.filter(isConnect).forEach(function (e) {
201- var permanent = exports.isLongterm(e) || exports.isLocal(e)
202- if((!permanent || e.state === 'connecting') && e.stateChange + 10e3 < ts) {
203- gossip.disconnect(e)
204- }
205- })
206-
207- }, 100*Math.random())
208-
209- }
210-
211- pull(
212- gossip.changes(),
213- pull.drain(function (ev) {
214- if(ev.type == 'disconnect')
215- connections()
216- })
217- )
218-
219- var int = setInterval(connections, 2e3)
220- if(int.unref) int.unref()
221-
222- connections()
223-
224-}
225-
226-exports.isUnattempted = isUnattempted
227-exports.isInactive = isInactive
228-exports.isLongterm = isLongterm
229-exports.isLegacy = isLegacy
230-exports.isLocal = isLocal
231-exports.isFriend = isFriend
232-exports.isConnectedOrConnecting = isConnect
233-exports.select = select
main-window.jsView
@@ -1,198 +1,26 @@
1-var Modules = require('./modules')
2-var h = require('./lib/h')
3-var Value = require('@mmckegg/mutant/value')
4-var when = require('@mmckegg/mutant/when')
5-var computed = require('@mmckegg/mutant/computed')
6-var toCollection = require('@mmckegg/mutant/dict-to-collection')
7-var MutantDict = require('@mmckegg/mutant/dict')
8-var MutantMap = require('@mmckegg/mutant/map')
9-var watch = require('@mmckegg/mutant/watch')
1+module.exports = function (config) {
2+ var modules = require('depject')(
3+ overrideConfig(config),
4+ require('patchbay/modules_extra'),
5+ require('patchbay/modules_basic'),
6+ require('patchbay/modules_core'),
7+ require('./modules')
8+ )
109
11-var plugs = require('patchbay/plugs')
10+ return modules.app[0]()
11+}
1212
13-module.exports = function (config, ssbClient) {
14- var modules = Modules(config, ssbClient)
15-
16- var screenView = plugs.first(modules.plugs.screen_view)
17-
18- var searchTimer = null
19- var searchBox = h('input.search', {
20- type: 'search',
21- placeholder: 'word, @key, #channel'
22- })
23-
24- searchBox.oninput = function () {
25- clearTimeout(searchTimer)
26- searchTimer = setTimeout(doSearch, 500)
27- }
28-
29- searchBox.onfocus = function () {
30- if (searchBox.value) {
31- doSearch()
32- }
33- }
34-
35- var forwardHistory = []
36- var backHistory = []
37-
38- var views = MutantDict({
39- // preload tabs (and subscribe to update notifications)
40- '/public': screenView('/public'),
41- '/private': screenView('/private'),
42- [ssbClient.id]: screenView(ssbClient.id),
43- '/notifications': screenView('/notifications')
44- })
45-
46- var lastViewed = {}
47-
48- // delete cached view after 30 mins of last seeing
49- setInterval(() => {
50- views.keys().forEach((view) => {
51- if (lastViewed[view] !== true && Date.now() - lastViewed[view] > (30 * 60e3) && view !== currentView()) {
52- views.delete(view)
53- }
54- })
55- }, 60e3)
56-
57- var canGoForward = Value(false)
58- var canGoBack = Value(false)
59- var currentView = Value('/public')
60-
61- watch(currentView, (view) => {
62- window.location.hash = `#${view}`
63- })
64-
65- window.onhashchange = function (ev) {
66- var path = window.location.hash.substring(1)
67- if (path) {
68- setView(path)
69- }
70- }
71-
72- var mainElement = h('div.main', MutantMap(toCollection(views), (item) => {
73- return h('div.view', {
74- hidden: computed([item.key, currentView], (a, b) => a !== b)
75- }, [ item.value ])
76- }))
77-
78- return h('MainWindow', {
79- classList: [ '-' + process.platform ]
80- }, [
81- h('div.top', [
82- h('span.history', [
83- h('a', {
84- 'ev-click': goBack,
85- classList: [ when(canGoBack, '-active') ]
86- }, '<'),
87- h('a', {
88- 'ev-click': goForward,
89- classList: [ when(canGoForward, '-active') ]
90- }, '>')
91- ]),
92- h('span.nav', [
93- tab('Public', '/public'),
94- tab('Private', '/private')
95- ]),
96- h('span.appTitle', ['Patchwork']),
97- h('span', [ searchBox ]),
98- h('span.nav', [
99- tab('Profile', ssbClient.id),
100- tab('Mentions', '/notifications')
101- ])
102- ]),
103- mainElement
104- ])
105-
106- // scoped
107-
108- function tab (name, view) {
109- var instance = views.get(view)
110- lastViewed[view] = true
111- return h('a', {
112- 'ev-click': function (ev) {
113- if (instance.pendingUpdates && instance.pendingUpdates() && instance.reload) {
114- instance.reload()
13+function overrideConfig (config) {
14+ return {
15+ config: {
16+ gives: {'config': true},
17+ create: function (api) {
18+ return {
19+ config () {
20+ return config
21+ }
11522 }
116- },
117- href: `#${view}`,
118- classList: [
119- when(selected(view), '-selected')
120- ]
121- }, [
122- name,
123- when(instance.pendingUpdates, [
124- ' (', instance.pendingUpdates, ')'
125- ])
126- ])
127- }
128-
129- function goBack () {
130- if (backHistory.length) {
131- canGoForward.set(true)
132- forwardHistory.push(currentView())
133- currentView.set(backHistory.pop())
134- canGoBack.set(backHistory.length > 0)
135- }
136- }
137-
138- function goForward () {
139- if (forwardHistory.length) {
140- backHistory.push(currentView())
141- currentView.set(forwardHistory.pop())
142- canGoForward.set(forwardHistory.length > 0)
143- canGoBack.set(true)
144- }
145- }
146-
147- function setView (view) {
148- if (!views.has(view)) {
149- views.put(view, screenView(view))
150- }
151-
152- if (lastViewed[view] !== true) {
153- lastViewed[view] = Date.now()
154- }
155-
156- if (currentView() && lastViewed[currentView()] !== true) {
157- lastViewed[currentView()] = Date.now()
158- }
159-
160- if (view !== currentView()) {
161- canGoForward.set(false)
162- canGoBack.set(true)
163- forwardHistory.length = 0
164- backHistory.push(currentView())
165- currentView.set(view)
166- }
167- }
168-
169- function doSearch () {
170- var value = searchBox.value.trim()
171- if (value.startsWith('/') || value.startsWith('?') || value.startsWith('@') || value.startsWith('#') || value.startsWith('%')) {
172- setView(value)
173- } else if (value.trim()) {
174- setView(`?${value.trim()}`)
175- } else {
176- setView('/public')
177- }
178- }
179-
180- function selected (view) {
181- return computed([currentView, view], (currentView, view) => {
182- return currentView === view
183- })
184- }
185-}
186-
187-function isSame (a, b) {
188- if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
189- for (var i = 0; i < a.length; i++) {
190- if (a[i] !== b[i]) {
191- return false
19223 }
19324 }
194- return true
195- } else if (a === b) {
196- return true
19725 }
19826 }
modules/index.jsView
@@ -1,31 +1,1 @@
1-var SbotApi = require('../api')
2-var extend = require('xtend')
3-var combine = require('depject')
4-var fs = require('fs')
5-var patchbayModules = require('patchbay/modules')
6-
7-module.exports = function (config, ssbClient, overrides) {
8- var api = SbotApi(ssbClient, config)
9- var localModules = getLocalModules()
10- return combine(extend(patchbayModules, localModules, {
11- 'sbot-api.js': api,
12- 'blob-url.js': {
13- blob_url: function (link) {
14- var prefix = config.blobsPrefix != null ? config.blobsPrefix : `http://localhost:${config.blobsPort}`
15- if (typeof link.link === 'string') {
16- link = link.link
17- }
18- return `${prefix}/${encodeURIComponent(link)}`
19- }
20- }
21- }, overrides))
22-}
23-
24-function getLocalModules () {
25- return fs.readdirSync(__dirname).reduce(function (result, e) {
26- if (e !== 'index.js' && /\js$/.test(e)) {
27- result[e] = require('./' + e)
28- }
29- return result
30- }, {})
31-}
1+module.exports = require('bulk-require')(__dirname, ['**/!(index).js'])
modules/about.jsView
@@ -1,37 +1,0 @@
1-var h = require('hyperscript')
2-
3-function idLink (id) {
4- return h('a', {href:'#'+id}, id.slice(0, 10))
5-}
6-
7-function asLink (ln) {
8- return 'string' === typeof ln ? ln : ln.link
9-}
10-
11-var plugs = require('patchbay/plugs')
12-var blob_url = plugs.first(exports.blob_url = [])
13-var avatar_name = plugs.first(exports.avatar_name = [])
14-var avatar_link = plugs.first(exports.avatar_link = [])
15-
16-exports.message_content = function (msg) {
17- if(msg.value.content.type !== 'about' || !msg.value.content.about) return
18-
19- if(!msg.value.content.image && !msg.value.content.name)
20- return
21-
22- var about = msg.value.content
23- var id = msg.value.content.about
24- return h('p',
25- about.about === msg.value.author
26- ? h('span', 'self-identifies ')
27- : h('span', 'identifies ', about.name ? idLink(id) : avatar_link(id, avatar_name(id))),
28- ' as ',
29- h('a', {href:"#"+about.about},
30- about.name || null,
31- about.image
32- ? h('img.avatar--fullsize', {src: blob_url(about.image)})
33- : null
34- )
35- )
36-
37-}
modules/app.jsView
@@ -1,0 +1,194 @@
1+var h = require('../lib/h')
2+var Value = require('mutant/value')
3+var when = require('mutant/when')
4+var computed = require('mutant/computed')
5+var toCollection = require('mutant/dict-to-collection')
6+var MutantDict = require('mutant/dict')
7+var MutantMap = require('mutant/map')
8+var watch = require('mutant/watch')
9+
10+exports.needs = {
11+ page: 'first',
12+ sbot: {
13+ get_id: 'first'
14+ }
15+}
16+
17+exports.gives = {
18+ app: true
19+}
20+
21+exports.create = function (api) {
22+ return {
23+ app: function () {
24+ var key = api.sbot.get_id()
25+ var searchTimer = null
26+ var searchBox = h('input.search', {
27+ type: 'search',
28+ placeholder: 'word, @key, #channel'
29+ })
30+
31+ searchBox.oninput = function () {
32+ clearTimeout(searchTimer)
33+ searchTimer = setTimeout(doSearch, 500)
34+ }
35+
36+ searchBox.onfocus = function () {
37+ if (searchBox.value) {
38+ doSearch()
39+ }
40+ }
41+
42+ var forwardHistory = []
43+ var backHistory = []
44+
45+ var views = MutantDict({
46+ // preload tabs (and subscribe to update notifications)
47+ '/public': api.page('/public'),
48+ '/private': api.page('/private'),
49+ [key]: api.page(key),
50+ '/notifications': api.page('/notifications')
51+ })
52+
53+ var lastViewed = {}
54+
55+ // delete cached view after 30 mins of last seeing
56+ setInterval(() => {
57+ views.keys().forEach((view) => {
58+ if (lastViewed[view] !== true && Date.now() - lastViewed[view] > (30 * 60e3) && view !== currentView()) {
59+ views.delete(view)
60+ }
61+ })
62+ }, 60e3)
63+
64+ var canGoForward = Value(false)
65+ var canGoBack = Value(false)
66+ var currentView = Value('/public')
67+
68+ watch(currentView, (view) => {
69+ window.location.hash = `#${view}`
70+ })
71+
72+ window.onhashchange = function (ev) {
73+ var path = window.location.hash.substring(1)
74+ if (path) {
75+ setView(path)
76+ }
77+ }
78+
79+ var mainElement = h('div.main', MutantMap(toCollection(views), (item) => {
80+ return h('div.view', {
81+ hidden: computed([item.key, currentView], (a, b) => a !== b)
82+ }, [ item.value ])
83+ }))
84+
85+ return h('MainWindow', {
86+ classList: [ '-' + process.platform ]
87+ }, [
88+ h('div.top', [
89+ h('span.history', [
90+ h('a', {
91+ 'ev-click': goBack,
92+ classList: [ when(canGoBack, '-active') ]
93+ }, '<'),
94+ h('a', {
95+ 'ev-click': goForward,
96+ classList: [ when(canGoForward, '-active') ]
97+ }, '>')
98+ ]),
99+ h('span.nav', [
100+ tab('Public', '/public'),
101+ tab('Private', '/private')
102+ ]),
103+ h('span.appTitle', ['Patchwork']),
104+ h('span', [ searchBox ]),
105+ h('span.nav', [
106+ tab('Profile', key),
107+ tab('Mentions', '/notifications')
108+ ])
109+ ]),
110+ mainElement
111+ ])
112+
113+ // scoped
114+
115+ function tab (name, view) {
116+ var instance = views.get(view)
117+ lastViewed[view] = true
118+ return h('a', {
119+ 'ev-click': function (ev) {
120+ if (instance.pendingUpdates && instance.pendingUpdates() && instance.reload) {
121+ instance.reload()
122+ }
123+ },
124+ href: `#${view}`,
125+ classList: [
126+ when(selected(view), '-selected')
127+ ]
128+ }, [
129+ name,
130+ when(instance.pendingUpdates, [
131+ ' (', instance.pendingUpdates, ')'
132+ ])
133+ ])
134+ }
135+
136+ function goBack () {
137+ if (backHistory.length) {
138+ canGoForward.set(true)
139+ forwardHistory.push(currentView())
140+ currentView.set(backHistory.pop())
141+ canGoBack.set(backHistory.length > 0)
142+ }
143+ }
144+
145+ function goForward () {
146+ if (forwardHistory.length) {
147+ backHistory.push(currentView())
148+ currentView.set(forwardHistory.pop())
149+ canGoForward.set(forwardHistory.length > 0)
150+ canGoBack.set(true)
151+ }
152+ }
153+
154+ function setView (view) {
155+ if (!views.has(view)) {
156+ views.put(view, api.page(view))
157+ }
158+
159+ if (lastViewed[view] !== true) {
160+ lastViewed[view] = Date.now()
161+ }
162+
163+ if (currentView() && lastViewed[currentView()] !== true) {
164+ lastViewed[currentView()] = Date.now()
165+ }
166+
167+ if (view !== currentView()) {
168+ canGoForward.set(false)
169+ canGoBack.set(true)
170+ forwardHistory.length = 0
171+ backHistory.push(currentView())
172+ currentView.set(view)
173+ }
174+ }
175+
176+ function doSearch () {
177+ var value = searchBox.value.trim()
178+ if (value.startsWith('/') || value.startsWith('?') || value.startsWith('@') || value.startsWith('#') || value.startsWith('%')) {
179+ setView(value)
180+ } else if (value.trim()) {
181+ setView(`?${value.trim()}`)
182+ } else {
183+ setView('/public')
184+ }
185+ }
186+
187+ function selected (view) {
188+ return computed([currentView, view], (currentView, view) => {
189+ return currentView === view
190+ })
191+ }
192+ }
193+ }
194+}
modules/channel.jsView
@@ -1,78 +1,0 @@
1-var when = require('@mmckegg/mutant/when')
2-var send = require('@mmckegg/mutant/send')
3-var plugs = require('patchbay/plugs')
4-var message_compose = plugs.first(exports.message_compose = [])
5-var sbot_log = plugs.first(exports.sbot_log = [])
6-var feed_summary = plugs.first(exports.feed_summary = [])
7-var h = require('../lib/h')
8-var pull = require('pull-stream')
9-var obs_subscribed_channels = plugs.first(exports.obs_subscribed_channels = [])
10-var get_id = plugs.first(exports.get_id = [])
11-var publish = plugs.first(exports.sbot_publish = [])
12-
13-exports.screen_view = function (path, sbot) {
14- if (path[0] === '#') {
15- var channel = path.substr(1)
16- var subscribedChannels = obs_subscribed_channels(get_id())
17-
18- return feed_summary((opts) => {
19- return pull(
20- sbot_log(opts),
21- pull.map(matchesChannel)
22- )
23- }, [
24- h('PageHeading', [
25- h('h1', `#${channel}`),
26- h('div.meta', [
27- when(subscribedChannels.has(channel),
28- h('a -unsubscribe', {
29- 'href': '#',
30- 'title': 'Click to unsubscribe',
31- 'ev-click': send(unsubscribe, channel)
32- }, 'Subscribed'),
33- h('a -subscribe', {
34- 'href': '#',
35- 'ev-click': send(subscribe, channel)
36- }, 'Subscribe')
37- )
38- ])
39- ]),
40- message_compose({type: 'post', channel: channel}, {placeholder: 'Write a message in this channel\n\n\n\nPeople who follow you or subscribe to this channel will also see this message in their main feed.\n\nTo create a new channel, type the channel name (preceded by a #) into the search box above. e.g #cat-pics'})
41- ])
42- }
43-
44- // scoped
45-
46- function matchesChannel (msg) {
47- if (msg.sync) console.error('SYNC', msg)
48- var c = msg && msg.value && msg.value.content
49- if (c && c.channel === channel) {
50- return msg
51- } else {
52- return {timestamp: msg.timestamp}
53- }
54- }
55-}
56-
57-exports.message_meta = function (msg) {
58- var chan = msg.value.content.channel
59- if (chan) {
60- return h('a.channel', {href: '##' + chan}, '#' + chan)
61- }
62-}
63-
64-function subscribe (id) {
65- publish({
66- type: 'channel',
67- channel: id,
68- subscribed: true
69- })
70-}
71-
72-function unsubscribe (id) {
73- publish({
74- type: 'channel',
75- channel: id,
76- subscribed: false
77- })
78-}
modules/helpers/blob-url.jsView
@@ -1,0 +1,22 @@
1+exports.needs = {
2+ config: 'first'
3+}
4+
5+exports.gives = {
6+ helpers: { blob_url: true }
7+}
8+
9+exports.create = function (api) {
10+ return {
11+ helpers: {
12+ blob_url (link) {
13+ var config = api.config()
14+ var prefix = config.blobsPrefix != null ? config.blobsPrefix : `http://localhost:${config.blobsPort}`
15+ if (typeof link.link === 'string') {
16+ link = link.link
17+ }
18+ return `${prefix}/${encodeURIComponent(link)}`
19+ }
20+ }
21+ }
22+}
modules/helpers/emoji.jsView
@@ -1,0 +1,30 @@
1+var emojis = require('emoji-named-characters')
2+var emojiNames = Object.keys(emojis)
3+
4+exports.needs = {
5+ helpers: { blob_url: 'first' }
6+}
7+
8+exports.gives = {
9+ helpers: {
10+ emoji_names: true,
11+ emoji_url: true
12+ }
13+}
14+
15+exports.create = function (api) {
16+ return {
17+ helpers: {
18+ emoji_names,
19+ emoji_url
20+ }
21+ }
22+
23+ function emoji_names () {
24+ return emojiNames
25+ }
26+
27+ function emoji_url (emoji) {
28+ return emoji in emojis && `img/emoji/${emoji}.png`
29+ }
30+}
modules/data-feed.jsView
@@ -1,32 +1,0 @@
1-var h = require('hyperscript')
2-var u = require('patchbay/util')
3-var pull = require('pull-stream')
4-var Scroller = require('pull-scroll')
5-
6-var plugs = require('patchbay/plugs')
7-var sbot_log = plugs.first(exports.sbot_log = [])
8-var data_render = plugs.first(exports.data_render = [])
9-
10-exports.screen_view = function (path, sbot) {
11- if(path === '/data-feed' || path === '/data') {
12- var content = h('div.column.scroller__content')
13- var div = h('div.column.scroller',
14- {style: {'overflow':'auto'}},
15- h('div.scroller__wrapper',
16- content
17- )
18- )
19-
20- pull(
21- u.next(sbot_log, {old: false, limit: 100}),
22- Scroller(div, content, data_render, true, false)
23- )
24-
25- pull(
26- u.next(sbot_log, {reverse: true, limit: 100, live: false}),
27- Scroller(div, content, data_render, false, false)
28- )
29-
30- return div
31- }
32-}
modules/feed-summary.jsView
@@ -1,215 +1,0 @@
1-var Value = require('@mmckegg/mutant/value')
2-var h = require('@mmckegg/mutant/html-element')
3-var when = require('@mmckegg/mutant/when')
4-var computed = require('@mmckegg/mutant/computed')
5-var MutantArray = require('@mmckegg/mutant/array')
6-var Abortable = require('pull-abortable')
7-var Scroller = require('../lib/pull-scroll')
8-var FeedSummary = require('../lib/feed-summary')
9-var onceTrue = require('../lib/once-true')
10-
11-var m = require('../lib/h')
12-
13-var pull = require('pull-stream')
14-
15-var plugs = require('patchbay/plugs')
16-var message_render = plugs.first(exports.message_render = [])
17-var message_link = plugs.first(exports.message_link = [])
18-var person = plugs.first(exports.person = [])
19-var many_people = plugs.first(exports.many_people = [])
20-var people_names = plugs.first(exports.people_names = [])
21-var sbot_get = plugs.first(exports.sbot_get = [])
22-var get_id = plugs.first(exports.get_id = [])
23-
24-exports.feed_summary = function (getStream, prefix, opts) {
25- var sync = Value(false)
26- var updates = Value(0)
27-
28- var filter = opts && opts.filter
29- var bumpFilter = opts && opts.bumpFilter
30- var windowSize = opts && opts.windowSize
31- var waitFor = opts && opts.waitFor || true
32-
33- var updateLoader = m('a Notifier -loader', {
34- href: '#',
35- 'ev-click': refresh
36- }, [
37- 'Show ',
38- h('strong', [updates]), ' ',
39- when(computed(updates, a => a === 1), 'update', 'updates')
40- ])
41-
42- var content = h('div.column.scroller__content')
43-
44- var scrollElement = h('div.column.scroller', {
45- style: {
46- 'overflow': 'auto'
47- }
48- }, [
49- h('div.scroller__wrapper', [
50- prefix, content
51- ])
52- ])
53-
54- setTimeout(refresh, 10)
55-
56- onceTrue(waitFor, () => {
57- pull(
58- getStream({old: false}),
59- pull.drain((item) => {
60- var type = item && item.value && item.value.content.type
61- if (type && type !== 'vote') {
62- if (item.value && item.value.author === get_id() && !updates()) {
63- return refresh()
64- }
65- if (filter) {
66- var update = (item.value.content.type === 'post' && item.value.content.root) ? {
67- type: 'message',
68- messageId: item.value.content.root,
69- channel: item.value.content.channel
70- } : {
71- type: 'message',
72- author: item.value.author,
73- channel: item.value.content.channel,
74- messageId: item.key
75- }
76-
77- ensureAuthor(update, (err, update) => {
78- if (!err) {
79- if (filter(update)) {
80- updates.set(updates() + 1)
81- }
82- }
83- })
84- } else {
85- updates.set(updates() + 1)
86- }
87- }
88- })
89- )
90- })
91-
92- var abortLastFeed = null
93-
94- var result = MutantArray([
95- when(updates, updateLoader),
96- when(sync, scrollElement, m('Loading -large'))
97- ])
98-
99- result.reload = refresh
100- result.pendingUpdates = updates
101-
102- return result
103-
104- // scoped
105-
106- function refresh () {
107- if (abortLastFeed) {
108- abortLastFeed()
109- }
110- updates.set(0)
111- sync.set(false)
112- content.innerHTML = ''
113-
114- var abortable = Abortable()
115- abortLastFeed = abortable.abort
116-
117- pull(
118- FeedSummary(getStream, {windowSize, bumpFilter}, () => {
119- sync.set(true)
120- }),
121- pull.asyncMap(ensureAuthor),
122- pull.filter((item) => {
123- if (filter) {
124- return filter(item)
125- } else {
126- return true
127- }
128- }),
129- abortable,
130- Scroller(scrollElement, content, renderItem, false, false)
131- )
132- }
133-}
134-
135-function ensureAuthor (item, cb) {
136- if (item.type === 'message' && !item.message) {
137- sbot_get(item.messageId, (_, value) => {
138- if (value) {
139- item.author = value.author
140- }
141- cb(null, item)
142- })
143- } else {
144- cb(null, item)
145- }
146-}
147-
148-function renderItem (item) {
149- if (item.type === 'message') {
150- var meta = null
151- var previousId = item.messageId
152- var replies = item.replies.slice(-4).map((msg) => {
153- var result = message_render(msg, {inContext: true, inSummary: true, previousId})
154- previousId = msg.key
155- return result
156- })
157- var renderedMessage = item.message ? message_render(item.message, {inContext: true}) : null
158- if (renderedMessage) {
159- if (item.lastUpdateType === 'reply' && item.repliesFrom.size) {
160- meta = m('div.meta', {
161- title: people_names(item.repliesFrom)
162- }, [
163- many_people(item.repliesFrom), ' replied'
164- ])
165- } else if (item.lastUpdateType === 'dig' && item.digs.size) {
166- meta = m('div.meta', {
167- title: people_names(item.digs)
168- }, [
169- many_people(item.digs), ' dug this message'
170- ])
171- }
172-
173- return m('FeedEvent', [
174- meta,
175- renderedMessage,
176- when(replies.length, [
177- when(item.replies.length > replies.length,
178- m('a.full', {href: `#${item.messageId}`}, ['View full thread'])
179- ),
180- m('div.replies', replies)
181- ])
182- ])
183- } else {
184- if (item.lastUpdateType === 'reply' && item.repliesFrom.size) {
185- meta = m('div.meta', {
186- title: people_names(item.repliesFrom)
187- }, [
188- many_people(item.repliesFrom), ' replied to ', message_link(item.messageId)
189- ])
190- } else if (item.lastUpdateType === 'dig' && item.digs.size) {
191- meta = m('div.meta', {
192- title: people_names(item.digs)
193- }, [
194- many_people(item.digs), ' dug ', message_link(item.messageId)
195- ])
196- }
197-
198- if (meta || replies.length) {
199- return m('FeedEvent', [
200- meta, m('div.replies', replies)
201- ])
202- }
203- }
204- } else if (item.type === 'follow') {
205- return m('FeedEvent -follow', [
206- m('div.meta', {
207- title: people_names(item.contacts)
208- }, [
209- person(item.id), ' followed ', many_people(item.contacts)
210- ])
211- ])
212- }
213-
214- return h('div')
215-}
modules/sbot.jsView
@@ -1,0 +1,187 @@
1+var pull = require('pull-stream')
2+var ssbKeys = require('ssb-keys')
3+var ref = require('ssb-ref')
4+var Reconnect = require('pull-reconnect')
5+
6+function Hash (onHash) {
7+ var buffers = []
8+ return pull.through(function (data) {
9+ buffers.push('string' === typeof data
10+ ? new Buffer(data, 'utf8')
11+ : data
12+ )
13+ }, function (err) {
14+ if(err && !onHash) throw err
15+ var b = buffers.length > 1 ? Buffer.concat(buffers) : buffers[0]
16+ var h = '&'+ssbKeys.hash(b)
17+ onHash && onHash(err, h)
18+ })
19+}
20+//uncomment this to use from browser...
21+//also depends on having ssb-ws installed.
22+//var createClient = require('ssb-lite')
23+var createClient = require('ssb-client')
24+
25+var createFeed = require('ssb-feed')
26+var keys = require('patchbay/keys')
27+
28+var cache = CACHE = {}
29+
30+exports.needs = {
31+ connection_status: 'map',
32+ config: 'first'
33+}
34+
35+exports.gives = {
36+// connection_status: true,
37+ sbot: {
38+ blobs_add: true,
39+ links: true,
40+ links2: true,
41+ query: true,
42+ fulltext_search: true,
43+ get: true,
44+ log: true,
45+ user_feed: true,
46+ gossip_peers: true,
47+ gossip_connect: true,
48+ progress: true,
49+ publish: true,
50+ whoami: true,
51+
52+ // additional
53+ get_id: true
54+ }
55+}
56+
57+exports.create = function (api) {
58+
59+ var sbot = null
60+ var config = api.config()
61+
62+ var rec = Reconnect(function (isConn) {
63+ function notify (value) {
64+ isConn(value); api.connection_status(value)
65+ }
66+
67+ createClient(config.keys, config, function (err, _sbot) {
68+ if(err)
69+ return notify(err)
70+
71+ sbot = _sbot
72+ sbot.on('closed', function () {
73+ sbot = null
74+ notify(new Error('closed'))
75+ })
76+
77+ notify()
78+ })
79+ })
80+
81+ var internal = {
82+ getLatest: rec.async(function (id, cb) {
83+ sbot.getLatest(id, cb)
84+ }),
85+ add: rec.async(function (msg, cb) {
86+ sbot.add(msg, cb)
87+ })
88+ }
89+
90+ var feed = createFeed(internal, keys, {remote: true})
91+
92+ return {
93+ // connection_status,
94+ sbot: {
95+ blobs_add: rec.sink(function (cb) {
96+ return pull(
97+ Hash(function (err, id) {
98+ if(err) return cb(err)
99+ //completely UGLY hack to tell when the blob has been sucessfully written...
100+ var start = Date.now(), n = 5
101+ ;(function next () {
102+ setTimeout(function () {
103+ sbot.blobs.has(id, function (err, has) {
104+ if(has) return cb(null, id)
105+ if(n--) next()
106+ else cb(new Error('write failed'))
107+ })
108+ }, Date.now() - start)
109+ })()
110+ }),
111+ sbot.blobs.add()
112+ )
113+ }),
114+ links: rec.source(function (query) {
115+ return sbot.links(query)
116+ }),
117+ links2: rec.source(function (query) {
118+ return sbot.links2.read(query)
119+ }),
120+ query: rec.source(function (query) {
121+ return sbot.query.read(query)
122+ }),
123+ log: rec.source(function (opts) {
124+ return pull(
125+ sbot.createLogStream(opts),
126+ pull.through(function (e) {
127+ CACHE[e.key] = CACHE[e.key] || e.value
128+ })
129+ )
130+ }),
131+ user_feed: rec.source(function (opts) {
132+ return sbot.createUserStream(opts)
133+ }),
134+ fulltext_search: rec.source(function (opts) {
135+ return sbot.fulltext.search(opts)
136+ }),
137+ get: rec.async(function (key, cb) {
138+ if('function' !== typeof cb)
139+ throw new Error('cb must be function')
140+ if(CACHE[key]) cb(null, CACHE[key])
141+ else sbot.get(key, function (err, value) {
142+ if(err) return cb(err)
143+ cb(null, CACHE[key] = value)
144+ })
145+ }),
146+ gossip_peers: rec.async(function (cb) {
147+ sbot.gossip.peers(cb)
148+ }),
149+ //liteclient won't have permissions for this
150+ gossip_connect: rec.async(function (opts, cb) {
151+ sbot.gossip.connect(opts, cb)
152+ }),
153+ progress: rec.source(function () {
154+ return sbot.replicate.changes()
155+ }),
156+ publish: rec.async(function (content, cb) {
157+ if(content.recps)
158+ content = ssbKeys.box(content, content.recps.map(function (e) {
159+ return ref.isFeed(e) ? e : e.link
160+ }))
161+ else if(content.mentions)
162+ content.mentions.forEach(function (mention) {
163+ if(ref.isBlob(mention.link)) {
164+ sbot.blobs.push(mention.link, function (err) {
165+ if(err) console.error(err)
166+ })
167+ }
168+ })
169+
170+ feed.add(content, function (err, msg) {
171+ if(err) console.error(err)
172+ else if(!cb) console.log(msg)
173+ cb && cb(err, msg)
174+ })
175+ }),
176+ whoami: rec.async(function (cb) {
177+ sbot.whoami(cb)
178+ }),
179+
180+ // ADDITIONAL:
181+
182+ get_id: function () {
183+ return keys.id
184+ }
185+ }
186+ }
187+}
modules/feed.jsView
@@ -1,20 +1,0 @@
1-var ref = require('ssb-ref')
2-var h = require('hyperscript')
3-var extend = require('xtend')
4-
5-var plugs = require('patchbay/plugs')
6-var sbot_user_feed = plugs.first(exports.sbot_user_feed = [])
7-var avatar_profile = plugs.first(exports.avatar_profile = [])
8-var feed_summary = plugs.first(exports.feed_summary = [])
9-
10-exports.screen_view = function (id) {
11- if (ref.isFeed(id)) {
12- return feed_summary((opts) => {
13- return sbot_user_feed(extend(opts, {id: id}))
14- }, [
15- h('div', [avatar_profile(id)])
16- ], {
17- windowSize: 50
18- })
19- }
20-}
modules/git-mini-messages.jsView
@@ -1,26 +1,0 @@
1-var h = require('../lib/h')
2-var when = require('@mmckegg/mutant/when')
3-var plugs = require('patchbay/plugs')
4-var message_link = plugs.first(exports.message_link = [])
5-
6-exports.message_content = exports.message_content_mini = function (msg, sbot) {
7- if (msg.value.content.type === 'git-update') {
8- var commits = msg.value.content.commits || []
9- return [
10- h('a', {href: `#${msg.key}`, title: commitSummary(commits)}, [
11- 'pushed',
12- when(commits, [' ', pluralizeCommits(commits)])
13- ]),
14- ' to ',
15- message_link(msg.value.content.repo)
16- ]
17- }
18-}
19-
20-function pluralizeCommits (commits) {
21- return when(commits.length === 1, '1 commit', `${commits.length} commits`)
22-}
23-
24-function commitSummary (commits) {
25- return commits.map(commit => `- ${commit.title}`).join('\n')
26-}
modules/like.jsView
@@ -1,72 +1,0 @@
1-var h = require('../lib/h')
2-var computed = require('@mmckegg/mutant/computed')
3-var when = require('@mmckegg/mutant/when')
4-var plugs = require('patchbay/plugs')
5-var message_link = plugs.first(exports.message_link = [])
6-var get_id = plugs.first(exports.get_id = [])
7-var get_likes = plugs.first(exports.get_likes = [])
8-var publish = plugs.first(exports.sbot_publish = [])
9-var people_names = plugs.first(exports.people_names = [])
10-
11-exports.message_content = exports.message_content_mini = function (msg, sbot) {
12- if (msg.value.content.type !== 'vote') return
13- var link = msg.value.content.vote.link
14- return [
15- msg.value.content.vote.value > 0 ? 'dug' : 'undug',
16- ' ', message_link(link)
17- ]
18-}
19-
20-exports.message_meta = function (msg, sbot) {
21- return computed(get_likes(msg.key), likeCount)
22-}
23-
24-exports.message_action = function (msg, sbot) {
25- var id = get_id()
26- var dug = computed([get_likes(msg.key), id], doesLike)
27- dug(() => {})
28-
29- if (msg.value.content.type !== 'vote') {
30- return h('a.dig', {
31- href: '#',
32- 'ev-click': function () {
33- var dig = dug() ? {
34- type: 'vote',
35- vote: { link: msg.key, value: 0, expression: 'Undig' }
36- } : {
37- type: 'vote',
38- vote: { link: msg.key, value: 1, expression: 'Dig' }
39- }
40- if (msg.value.content.recps) {
41- dig.recps = msg.value.content.recps.map(function (e) {
42- return e && typeof e !== 'string' ? e.link : e
43- })
44- dig.private = true
45- }
46- publish(dig)
47- }
48- }, when(dug, 'Undig', 'Dig'))
49- }
50-}
51-
52-function doesLike (likes, userId) {
53- return likes && likes[userId] && likes[userId][0] || false
54-}
55-
56-function likeCount (data) {
57- var likes = getLikes(data)
58- if (likes.length) {
59- return [' ', h('span.likes', {
60- title: people_names(likes)
61- }, ['+', h('strong', `${likes.length}`)])]
62- }
63-}
64-
65-function getLikes (likes) {
66- return Object.keys(likes).reduce((result, id) => {
67- if (likes[id][0]) {
68- result.push(id)
69- }
70- return result
71- }, [])
72-}
modules/many-people.jsView
@@ -1,31 +1,0 @@
1-var plugs = require('patchbay/plugs')
2-var person = plugs.first(exports.person = [])
3-exports.many_people = manyPeople
4-
5-function manyPeople (ids) {
6- ids = Array.from(ids)
7- var featuredIds = ids.slice(-3).reverse()
8-
9- if (ids.length) {
10- if (ids.length > 3) {
11- return [
12- person(featuredIds[0]), ', ',
13- person(featuredIds[1]),
14- ' and ', ids.length - 2, ' others'
15- ]
16- } else if (ids.length === 3) {
17- return [
18- person(featuredIds[0]), ', ',
19- person(featuredIds[1]), ' and ',
20- person(featuredIds[2])
21- ]
22- } else if (ids.length === 2) {
23- return [
24- person(featuredIds[0]), ' and ',
25- person(featuredIds[1])
26- ]
27- } else {
28- return person(featuredIds[0])
29- }
30- }
31-}
modules/message-confirm.jsView
@@ -1,51 +1,0 @@
1-var lightbox = require('hyperlightbox')
2-var h = require('../lib/h')
3-var plugs = require('patchbay/plugs')
4-var get_id = plugs.first(exports.get_id = [])
5-var publish = plugs.first(exports.sbot_publish = [])
6-var message_render = plugs.first(exports.message_render = [])
7-
8-exports.message_confirm = function (content, cb) {
9- cb = cb || function () {}
10-
11- var lb = lightbox()
12- document.body.appendChild(lb)
13-
14- var msg = {
15- value: {
16- author: get_id(),
17- previous: null,
18- sequence: null,
19- timestamp: Date.now(),
20- content: content
21- }
22- }
23-
24- var okay = h('button', {
25- 'ev-click': function () {
26- lb.remove()
27- publish(content, cb)
28- },
29- 'ev-keydown': function (ev) {
30- if (ev.keyCode === 27) cancel.click() // escape
31- }
32- }, [
33- 'okay'
34- ])
35-
36- var cancel = h('button', {'ev-click': function () {
37- lb.remove()
38- cb(null)
39- }}, [
40- 'Cancel'
41- ])
42-
43- lb.show(h('MessageConfirm', [
44- h('section', [
45- message_render(msg)
46- ]),
47- h('footer', [okay, cancel])
48- ]))
49-
50- okay.focus()
51-}
modules/message-name.jsView
@@ -1,44 +1,0 @@
1-var plugs = require('patchbay/plugs')
2-var sbot_links = plugs.first(exports.sbot_links = [])
3-var get_id = plugs.first(exports.get_id = [])
4-var sbot_get = plugs.first(exports.sbot_get = [])
5-var getAvatar = require('ssb-avatar')
6-
7-exports.message_name = function (id, cb) {
8- sbot_get(id, function (err, value) {
9- if (err && err.name === 'NotFoundError') {
10- return cb(null, id.substring(0, 10) + '...(missing)')
11- } else if (value.content.type === 'post' && typeof value.content.text === 'string') {
12- if (value.content.text.trim()) {
13- return cb(null, titleFromMarkdown(value.content.text, 40))
14- }
15- } else if (value.content.type === 'git-repo') {
16- return getRepoName(id, cb)
17- } else if (typeof value.content.text === 'string') {
18- return cb(null, value.content.type + ': ' + titleFromMarkdown(value.content.text, 30))
19- }
20-
21- return cb(null, id.substring(0, 10) + '...')
22- })
23-}
24-
25-function titleFromMarkdown (text, max) {
26- text = text.trim().split('\n', 2).join('\n')
27- text = text.replace(/_|`|\*|\#|\[.*?\]|\(\S*?\)/g, '').trim()
28- text = text.replace(/\:$/, '')
29- text = text.trim().split('\n', 1)[0].trim()
30- if (text.length > max) {
31- text = text.substring(0, max - 2) + '...'
32- }
33- return text
34-}
35-
36-function getRepoName (id, cb) {
37- getAvatar({
38- links: sbot_links,
39- get: sbot_get
40- }, get_id(), id, function (err, avatar) {
41- if (err) return cb(err)
42- cb(null, avatar.name)
43- })
44-}
modules/message.jsView
@@ -1,119 +1,0 @@
1-var h = require('../lib/h')
2-var when = require('@mmckegg/mutant/when')
3-
4-var plugs = require('patchbay/plugs')
5-var message_content = plugs.first(exports.message_content = [])
6-var message_content_mini = plugs.first(exports.message_content_mini = [])
7-var message_link = plugs.first(exports.message_link = [])
8-var avatar_image = plugs.first(exports.avatar_image = [])
9-var avatar_name = plugs.first(exports.avatar_name = [])
10-var avatar_link = plugs.first(exports.avatar_link = [])
11-var message_meta = plugs.map(exports.message_meta = [])
12-var message_main_meta = plugs.map(exports.message_main_meta = [])
13-var message_action = plugs.map(exports.message_action = [])
14-var contextMenu = require('../lib/context-menu')
15-
16-exports.data_render = function (msg) {
17- var div = h('Message -data', {
18- 'ev-contextmenu': contextMenu.bind(null, msg)
19- }, [
20- messageHeader(msg),
21- h('section', [
22- h('pre', [
23- JSON.stringify(msg, null, 2)
24- ])
25- ])
26- ])
27- return div
28-}
29-
30-exports.message_render = function (msg, opts) {
31- opts = opts || {}
32- var inContext = opts.inContext
33- var previousId = opts.previousId
34- var inSummary = opts.inSummary
35-
36- var elMini = message_content_mini(msg)
37- var el = message_content(msg)
38-
39- if (elMini && (!el || inSummary)) {
40- var div = h('Message', {
41- 'ev-contextmenu': contextMenu.bind(null, msg)
42- }, [
43- h('header', [
44- h('div.mini', [
45- avatar_link(msg.value.author, avatar_name(msg.value.author), ''),
46- ' ', elMini
47- ]),
48- h('div.meta', [message_main_meta(msg)])
49- ])
50- ])
51- div.setAttribute('tabindex', '0')
52- return div
53- }
54-
55- if (!el) return
56-
57- var classList = []
58- var replyInfo = null
59-
60- if (msg.value.content.root) {
61- classList.push('-reply')
62- if (!inContext) {
63- replyInfo = h('span', ['in reply to ', message_link(msg.value.content.root)])
64- } else if (previousId && last(msg.value.content.branch) && previousId !== last(msg.value.content.branch)) {
65- replyInfo = h('span', ['in reply to ', message_link(last(msg.value.content.branch))])
66- }
67- }
68-
69- var element = h('Message', {
70- classList,
71- 'ev-contextmenu': contextMenu.bind(null, msg),
72- 'ev-keydown': function (ev) {
73- // on enter, hit first meta.
74- if (ev.keyCode === 13) {
75- element.querySelector('.enter').click()
76- }
77- }
78- }, [
79- messageHeader(msg, replyInfo),
80- h('section', [el]),
81- when(msg.key, h('footer', [
82- h('div.actions', [
83- message_action(msg),
84- h('a', {href: '#' + msg.key}, 'Reply')
85- ])
86- ]))
87- ])
88-
89- // ); hyperscript does not seem to set attributes correctly.
90- element.setAttribute('tabindex', '0')
91-
92- return element
93-}
94-
95-function messageHeader (msg, replyInfo) {
96- return h('header', [
97- h('div.main', [
98- h('a.avatar', {href: `#${msg.value.author}`}, avatar_image(msg.value.author)),
99- h('div.main', [
100- h('div.name', [
101- h('a', {href: `#${msg.value.author}`}, avatar_name(msg.value.author))
102- ]),
103- h('div.meta', [
104- message_main_meta(msg),
105- ' ', replyInfo
106- ])
107- ])
108- ]),
109- h('div.meta', message_meta(msg))
110- ])
111-}
112-
113-function last (array) {
114- if (Array.isArray(array)) {
115- return array[array.length - 1]
116- } else {
117- return array
118- }
119-}
modules/notifications.jsView
@@ -1,148 +1,0 @@
1-var pull = require('pull-stream')
2-var paramap = require('pull-paramap')
3-var plugs = require('patchbay/plugs')
4-var cont = require('cont')
5-var ref = require('ssb-ref')
6-
7-var sbot_log = plugs.first(exports.sbot_log = [])
8-var sbot_get = plugs.first(exports.sbot_get = [])
9-var sbot_user_feed = plugs.first(exports.sbot_user_feed = [])
10-var message_unbox = plugs.first(exports.message_unbox = [])
11-var get_id = plugs.first(exports.get_id = [])
12-var feed_summary = plugs.first(exports.feed_summary = [])
13-
14-exports.screen_view = function (path) {
15- if (path === '/notifications') {
16- var oldest = null
17- var id = get_id()
18- var ids = {
19- [id]: true
20- }
21-
22- getFirstMessage(id, function (err, msg) {
23- if (err) return console.error(err)
24- if (!oldest || msg.value.timestamp < oldest) {
25- oldest = msg.value.timestamp
26- }
27- })
28-
29- return feed_summary((opts) => {
30- if (opts.old === false) {
31- return pull(
32- sbot_log(opts),
33- unbox(),
34- notifications(ids),
35- pull.filter()
36- )
37- } else {
38- return pull(
39- sbot_log(opts),
40- unbox(),
41- notifications(ids),
42- pull.filter(),
43- pull.take(function (msg) {
44- // abort stream after we pass the oldest messages of our feeds
45- return !oldest || msg.value.timestamp > oldest
46- })
47- )
48- }
49- }, [], {
50- windowSize: 200,
51- filter: (group) => {
52- return (
53- ((group.message || group.type !== 'message') && (group.author !== id || group.digs && group.digs.size)) || (
54- group.repliesFrom && group.repliesFrom.size && (
55- !group.repliesFrom.has(id) || group.repliesFrom.size > 1
56- )
57- )
58- )
59- }
60- })
61- }
62-}
63-
64-function unbox () {
65- return pull(
66- pull.map(function (msg) {
67- return msg.value && typeof msg.value.content === 'string'
68- ? message_unbox(msg)
69- : msg
70- }),
71- pull.filter(Boolean)
72- )
73-}
74-
75-function notifications (ourIds) {
76- function linksToUs (link) {
77- return link && link.link in ourIds
78- }
79-
80- function isOurMsg (id, cb) {
81- if (!id) return cb(null, false)
82- if (typeof id === 'object' && typeof id.link === 'string') id = id.link
83- if (!ref.isMsg(id)) return cb(null, false)
84- sbot_get(id, function (err, msg) {
85- if (err && err.name === 'NotFoundError') cb(null, false)
86- else if (err) cb(err)
87- else if (msg.content.type === 'issue' || msg.content.type === 'pull-request') {
88- isOurMsg(msg.content.repo || msg.content.project, cb)
89- } else {
90- cb(err, msg.author in ourIds)
91- }
92- })
93- }
94-
95- function isAnyOurMessage (msg, ids, cb) {
96- cont.para(ids.map(function (id) {
97- return function (cb) { isOurMsg(id, cb) }
98- }))(function (err, results) {
99- if (err) cb(err)
100- else if (results.some(Boolean)) cb(null, msg)
101- else cb()
102- })
103- }
104-
105- return paramap(function (msg, cb) {
106- var c = msg.value && msg.value.content
107- if (!c || typeof c !== 'object') return cb()
108- if (msg.value.author in ourIds) return cb(null, msg)
109-
110- if (c.mentions && Array.isArray(c.mentions) && c.mentions.some(linksToUs)) {
111- return cb(null, msg)
112- }
113-
114- if (msg.private) {
115- return cb(null, msg)
116- }
117-
118- switch (c.type) {
119- case 'post':
120- if (c.branch || c.root) {
121- return isAnyOurMessage(msg, [].concat(c.branch, c.root), cb)
122- } else {
123- return cb()
124- }
125- case 'contact':
126- return cb(null, c.contact in ourIds ? msg : null)
127- case 'vote':
128- if (c.vote && c.vote.link)
129- return isOurMsg(c.vote.link, function (err, isOurs) {
130- cb(err, isOurs ? msg : null)
131- })
132- else return cb()
133- case 'issue':
134- case 'pull-request':
135- return isOurMsg(c.project || c.repo, function (err, isOurs) {
136- cb(err, isOurs ? msg : null)
137- })
138- case 'issue-edit':
139- return isAnyOurMessage(msg, [c.issue].concat(c.issues), cb)
140- default:
141- cb()
142- }
143- }, 4)
144-}
145-
146-function getFirstMessage (feedId, cb) {
147- sbot_user_feed({id: feedId, gte: 0, limit: 1})(null, cb)
148-}
modules/obs-connected.jsView
@@ -1,29 +1,0 @@
1-var MutantSet = require('@mmckegg/mutant/set')
2-var plugs = require('patchbay/plugs')
3-var sbot_gossip_peers = plugs.first(exports.sbot_gossip_peers = [])
4-
5-var cache = null
6-
7-exports.obs_connected = function () {
8- if (cache) {
9- return cache
10- } else {
11- var result = MutantSet([], {nextTick: true})
12- // todo: make this clean up on unlisten
13-
14- refresh()
15- setInterval(refresh, 10e3)
16-
17- cache = result
18- return result
19- }
20-
21- // scope
22-
23- function refresh () {
24- sbot_gossip_peers((err, peers) => {
25- if (err) throw console.log(err)
26- result.set(peers.filter(x => x.state === 'connected').map(x => x.key))
27- })
28- }
29-}
modules/obs-following.jsView
@@ -1,47 +1,0 @@
1-var pull = require('pull-stream')
2-var computed = require('@mmckegg/mutant/computed')
3-var MutantPullReduce = require('../lib/mutant-pull-reduce')
4-var plugs = require('patchbay/plugs')
5-var sbot_user_feed = plugs.first(exports.sbot_user_feed = [])
6-var cache = {}
7-var throttle = require('@mmckegg/mutant/throttle')
8-
9-exports.obs_following = function (userId) {
10- if (cache[userId]) {
11- return cache[userId]
12- } else {
13- var stream = pull(
14- sbot_user_feed({id: userId, live: true}),
15- pull.filter((msg) => {
16- return !msg.value || msg.value.content.type === 'contact'
17- })
18- )
19-
20- var result = MutantPullReduce(stream, (result, msg) => {
21- var c = msg.value.content
22- if (c.contact) {
23- if (typeof c.following === 'boolean') {
24- if (c.following) {
25- result.add(c.contact)
26- } else {
27- result.delete(c.contact)
28- }
29- }
30- }
31- return result
32- }, {
33- startValue: new Set(),
34- nextTick: true
35- })
36-
37- var instance = throttle(result, 2000)
38- instance.sync = result.sync
39-
40- instance.has = function (value) {
41- return computed(instance, x => x.has(value))
42- }
43-
44- cache[userId] = instance
45- return instance
46- }
47-}
modules/obs-local.jsView
@@ -1,29 +1,0 @@
1-var MutantSet = require('@mmckegg/mutant/set')
2-var plugs = require('patchbay/plugs')
3-var sbot_list_local = plugs.first(exports.sbot_list_local = [])
4-
5-var cache = null
6-
7-exports.obs_local = function () {
8- if (cache) {
9- return cache
10- } else {
11- var result = MutantSet([], {nextTick: true})
12- // todo: make this clean up on unlisten
13-
14- refresh()
15- setInterval(refresh, 10e3)
16-
17- cache = result
18- return result
19- }
20-
21- // scope
22-
23- function refresh () {
24- sbot_list_local((err, keys) => {
25- if (err) throw console.log(err)
26- result.set(keys)
27- })
28- }
29-}
modules/obs-recently-updated-feeds.jsView
@@ -1,36 +1,0 @@
1-var pull = require('pull-stream')
2-var pullCat = require('pull-cat')
3-var computed = require('@mmckegg/mutant/computed')
4-var MutantPullReduce = require('../lib/mutant-pull-reduce')
5-var plugs = require('patchbay/plugs')
6-var sbot_log = plugs.first(exports.sbot_log = [])
7-var throttle = require('@mmckegg/mutant/throttle')
8-var hr = 60 * 60 * 1000
9-
10-exports.obs_recently_updated_feeds = function (limit) {
11- var stream = pull(
12- pullCat([
13- sbot_log({reverse: true, limit: limit || 500}),
14- sbot_log({old: false})
15- ])
16- )
17-
18- var result = MutantPullReduce(stream, (result, msg) => {
19- if (msg.value.timestamp && Date.now() - msg.value.timestamp < 24 * hr) {
20- result.add(msg.value.author)
21- }
22- return result
23- }, {
24- startValue: new Set(),
25- nextTick: true
26- })
27-
28- var instance = throttle(result, 2000)
29- instance.sync = result.sync
30-
31- instance.has = function (value) {
32- return computed(instance, x => x.has(value))
33- }
34-
35- return instance
36-}
modules/obs-subscribed-channels.jsView
@@ -1,50 +1,0 @@
1-var pull = require('pull-stream')
2-var computed = require('@mmckegg/mutant/computed')
3-var MutantPullReduce = require('../lib/mutant-pull-reduce')
4-var plugs = require('patchbay/plugs')
5-var sbot_user_feed = plugs.first(exports.sbot_user_feed = [])
6-var cache = {}
7-var throttle = require('@mmckegg/mutant/throttle')
8-
9-exports.obs_subscribed_channels = function (userId) {
10- if (cache[userId]) {
11- return cache[userId]
12- } else {
13- var stream = pull(
14- sbot_user_feed({id: userId, live: true}),
15- pull.filter((msg) => {
16- return !msg.value || msg.value.content.type === 'channel'
17- })
18- )
19-
20- var result = MutantPullReduce(stream, (result, msg) => {
21- var c = msg.value.content
22- if (typeof c.channel === 'string' && c.channel) {
23- var channel = c.channel.trim()
24- if (channel) {
25- if (typeof c.subscribed === 'boolean') {
26- if (c.subscribed) {
27- result.add(channel)
28- } else {
29- result.delete(channel)
30- }
31- }
32- }
33- }
34- return result
35- }, {
36- startValue: new Set(),
37- nextTick: true
38- })
39-
40- var instance = throttle(result, 2000)
41- instance.sync = result.sync
42-
43- instance.has = function (value) {
44- return computed(instance, x => x.has(value))
45- }
46-
47- cache[userId] = instance
48- return instance
49- }
50-}
modules/people-names.jsView
@@ -1,22 +1,0 @@
1-var Value = require('@mmckegg/mutant/value')
2-var plugs = require('patchbay/plugs')
3-var signifier = plugs.first(exports.signifier = [])
4-var computed = require('@mmckegg/mutant/computed')
5-
6-exports.people_names = function (ids) {
7- return computed(Array.from(ids).map(ObservName), join) || ''
8-}
9-
10-function join (...args) {
11- return args.join('\n')
12-}
13-
14-function ObservName (id) {
15- var obs = Value(id.slice(0, 10))
16- signifier(id, (_, value) => {
17- if (value && value.length) {
18- obs.set(value[0].name)
19- }
20- })
21- return obs
22-}
modules/person.jsView
@@ -1,9 +1,0 @@
1-var plugs = require('patchbay/plugs')
2-var avatar_name = plugs.first(exports.avatar_name = [])
3-var avatar_link = plugs.first(exports.avatar_link = [])
4-
5-exports.person = person
6-
7-function person (id) {
8- return avatar_link(id, avatar_name(id), '')
9-}
modules/post.jsView
@@ -1,13 +1,0 @@
1-var h = require('hyperscript')
2-var plugs = require('patchbay/plugs')
3-var message_link = plugs.first(exports.message_link = [])
4-var markdown = plugs.first(exports.markdown = [])
5-
6-exports.message_content = function (data) {
7- if(!data.value.content || !data.value.content.text) return
8-
9- return h('div',
10- markdown(data.value.content)
11- )
12-
13-}
modules/private.jsView
@@ -1,84 +1,0 @@
1-var pull = require('pull-stream')
2-var ref = require('ssb-ref')
3-var plugs = require('patchbay/plugs')
4-var message_compose = plugs.first(exports.message_compose = [])
5-var sbot_log = plugs.first(exports.sbot_log = [])
6-var feed_summary = plugs.first(exports.feed_summary = [])
7-var message_unbox = plugs.first(exports.message_unbox = [])
8-var get_id = plugs.first(exports.get_id = [])
9-var avatar_image_link = plugs.first(exports.avatar_image_link = [])
10-var update_cache = plugs.first(exports.update_cache = [])
11-var h = require('../lib/h')
12-
13-exports.screen_view = function (path, sbot) {
14- if (path === '/private') {
15- var id = get_id()
16-
17- return feed_summary((opts) => {
18- return pull(
19- sbot_log(opts),
20- loosen(10), // release tight loops if they continue too long (avoid scroll jank)
21- unbox(),
22- pull.through((item) => {
23- if (item.value) {
24- update_cache(item)
25- }
26- })
27- )
28- }, [
29- message_compose({type: 'post', recps: [], private: true}, {
30- prepublish: function (msg) {
31- msg.recps = [id].concat(msg.mentions).filter(function (e) {
32- return ref.isFeed(typeof e === 'string' ? e : e.link)
33- })
34- if (!msg.recps.length) {
35- throw new Error('cannot make private message without recipients - just mention the user in an at reply in the message you send')
36- }
37- return msg
38- },
39- placeholder: `Write a private message \n\n\n\nThis can only be read by yourself and people you have @mentioned.`
40- })
41- ], {
42- windowSize: 1000
43- })
44- }
45-}
46-
47-exports.message_meta = function (msg) {
48- if (msg.value.content.recps || msg.value.private) {
49- return h('span.private', [
50- map(msg.value.content.recps, function (id) {
51- return avatar_image_link(typeof id === 'string' ? id : id.link, 'thumbnail')
52- })
53- ])
54- }
55-}
56-
57-function unbox () {
58- return pull(
59- pull.filter(function (msg) {
60- return typeof msg.value.content === 'string'
61- }),
62- pull.map(function (msg) {
63- return message_unbox(msg) || { timestamp: msg.timestamp }
64- })
65- )
66-}
67-
68-function map (ary, iter) {
69- if (Array.isArray(ary)) return ary.map(iter)
70-}
71-
72-function loosen (max) {
73- var lastRelease = Date.now()
74- return pull.asyncMap(function (item, cb) {
75- if (Date.now() - lastRelease > max) {
76- setImmediate(() => {
77- lastRelease = Date.now()
78- cb(null, item)
79- })
80- } else {
81- cb(null, item)
82- }
83- })
84-}
modules/public.jsView
@@ -1,225 +1,0 @@
1-var MutantMap = require('@mmckegg/mutant/map')
2-var computed = require('@mmckegg/mutant/computed')
3-var when = require('@mmckegg/mutant/when')
4-var send = require('@mmckegg/mutant/send')
5-var pull = require('pull-stream')
6-var extend = require('xtend')
7-
8-var plugs = require('patchbay/plugs')
9-var h = require('../lib/h')
10-var message_compose = plugs.first(exports.message_compose = [])
11-var sbot_log = plugs.first(exports.sbot_log = [])
12-var sbot_feed = plugs.first(exports.sbot_feed = [])
13-var sbot_user_feed = plugs.first(exports.sbot_user_feed = [])
14-
15-var feed_summary = plugs.first(exports.feed_summary = [])
16-var obs_channels = plugs.first(exports.obs_channels = [])
17-var obs_subscribed_channels = plugs.first(exports.obs_subscribed_channels = [])
18-var get_id = plugs.first(exports.get_id = [])
19-var publish = plugs.first(exports.sbot_publish = [])
20-var obs_following = plugs.first(exports.obs_following = [])
21-var obs_recently_updated_feeds = plugs.first(exports.obs_recently_updated_feeds = [])
22-var avatar_image = plugs.first(exports.avatar_image = [])
23-var avatar_name = plugs.first(exports.avatar_name = [])
24-var obs_local = plugs.first(exports.obs_local = [])
25-var obs_connected = plugs.first(exports.obs_connected = [])
26-
27-exports.screen_view = function (path, sbot) {
28- if (path === '/public') {
29- var id = get_id()
30- var channels = computed(obs_channels(), items => items.slice(0, 8), {comparer: arrayEq})
31- var subscribedChannels = obs_subscribed_channels(id)
32- var loading = computed(subscribedChannels.sync, x => !x)
33- var connectedPeers = obs_connected()
34- var localPeers = obs_local()
35- var connectedPubs = computed([connectedPeers, localPeers], (c, l) => c.filter(x => !l.includes(x)))
36- var following = obs_following(id)
37-
38- var oldest = Date.now() - (2 * 24 * 60 * 60e3)
39- getFirstMessage(id, (_, msg) => {
40- if (msg) {
41- // fall back to timestamp stream before this, give 48 hrs for feeds to stabilize
42- if (msg.value.timestamp > oldest) {
43- oldest = Date.now()
44- }
45- }
46- })
47-
48- var whoToFollow = computed([obs_following(id), obs_recently_updated_feeds(200)], (following, recent) => {
49- return Array.from(recent).filter(x => x !== id && !following.has(x)).slice(0, 10)
50- })
51-
52- var feedSummary = feed_summary(getFeed, [
53- message_compose({type: 'post'}, {placeholder: 'Write a public message'})
54- ], {
55- waitUntil: computed([
56- following.sync,
57- subscribedChannels.sync
58- ], x => x.every(Boolean)),
59- windowSize: 500,
60- filter: (item) => {
61- return (
62- id === item.author ||
63- following().has(item.author) ||
64- subscribedChannels().has(item.channel) ||
65- (item.repliesFrom && item.repliesFrom.has(id)) ||
66- item.digs && item.digs.has(id)
67- )
68- },
69- bumpFilter: (msg, group) => {
70- if (!group.message) {
71- return (
72- isMentioned(id, msg.value.content.mentions) ||
73- msg.value.author === id || (
74- fromDay(msg, group.fromTime) && (
75- following().has(msg.value.author) ||
76- group.repliesFrom.has(id)
77- )
78- )
79- )
80- }
81- return true
82- }
83- })
84-
85- var result = h('SplitView', [
86- h('div.side', [
87- h('h2', 'Active Channels'),
88- when(loading, [ h('Loading') ]),
89- h('ChannelList', {
90- hidden: loading
91- }, [
92- MutantMap(channels, (channel) => {
93- var subscribed = subscribedChannels.has(channel.id)
94- return h('a.channel', {
95- href: `##${channel.id}`,
96- classList: [
97- when(subscribed, '-subscribed')
98- ]
99- }, [
100- h('span.name', '#' + channel.id),
101- when(subscribed,
102- h('a -unsubscribe', {
103- 'ev-click': send(unsubscribe, channel.id)
104- }, 'Unsubscribe'),
105- h('a -subscribe', {
106- 'ev-click': send(subscribe, channel.id)
107- }, 'Subscribe')
108- )
109- ])
110- }, {maxTime: 5})
111- ]),
112-
113- when(computed(localPeers, x => x.length), h('h2', 'Local')),
114- h('ProfileList', [
115- MutantMap(localPeers, (id) => {
116- return h('a.profile', {
117- classList: [
118- when(computed([connectedPeers, id], (p, id) => p.includes(id)), '-connected')
119- ],
120- href: `#${id}`
121- }, [
122- h('div.avatar', [avatar_image(id)]),
123- h('div.main', [
124- h('div.name', [ avatar_name(id) ])
125- ])
126- ])
127- })
128- ]),
129-
130- when(computed(whoToFollow, x => x.length), h('h2', 'Who to follow')),
131- h('ProfileList', [
132- MutantMap(whoToFollow, (id) => {
133- return h('a.profile', {
134- href: `#${id}`
135- }, [
136- h('div.avatar', [avatar_image(id)]),
137- h('div.main', [
138- h('div.name', [ avatar_name(id) ])
139- ])
140- ])
141- })
142- ]),
143-
144- when(computed(connectedPubs, x => x.length), h('h2', 'Connected Pubs')),
145- h('ProfileList', [
146- MutantMap(connectedPubs, (id) => {
147- return h('a.profile', {
148- classList: [ '-connected' ],
149- href: `#${id}`
150- }, [
151- h('div.avatar', [avatar_image(id)]),
152- h('div.main', [
153- h('div.name', [ avatar_name(id) ])
154- ])
155- ])
156- })
157- ])
158- ]),
159- h('div.main', [ feedSummary ])
160- ])
161-
162- result.pendingUpdates = feedSummary.pendingUpdates
163- result.reload = feedSummary.reload
164-
165- return result
166- }
167-
168- // scoped
169-
170- function getFeed (opts) {
171- if (opts.lt && opts.lt < oldest) {
172- opts = extend(opts, {lt: parseInt(opts.lt, 10)})
173- return pull(
174- sbot_feed(opts),
175- pull.map((msg) => {
176- if (msg.sync) {
177- return msg
178- } else {
179- return {key: msg.key, value: msg.value, timestamp: msg.value.timestamp}
180- }
181- })
182- )
183- } else {
184- return sbot_log(opts)
185- }
186- }
187-}
188-
189-function fromDay (msg, fromTime) {
190- return (fromTime - msg.timestamp) < (24 * 60 * 60e3)
191-}
192-
193-function isMentioned (id, list) {
194- if (Array.isArray(list)) {
195- return list.includes(id)
196- } else {
197- return false
198- }
199-}
200-
201-function subscribe (id) {
202- publish({
203- type: 'channel',
204- channel: id,
205- subscribed: true
206- })
207-}
208-
209-function unsubscribe (id) {
210- publish({
211- type: 'channel',
212- channel: id,
213- subscribed: false
214- })
215-}
216-
217-function arrayEq (a, b) {
218- if (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a !== b) {
219- return a.every((value, i) => value === b[i])
220- }
221-}
222-
223-function getFirstMessage (feedId, cb) {
224- sbot_user_feed({id: feedId, gte: 0, limit: 1})(null, cb)
225-}
modules/raw.jsView
@@ -1,1 +1,0 @@
1-// disable
modules/thread.jsView
@@ -1,127 +1,0 @@
1-var ui = require('patchbay/ui')
2-var pull = require('pull-stream')
3-var Cat = require('pull-cat')
4-var sort = require('ssb-sort')
5-var ref = require('ssb-ref')
6-var h = require('hyperscript')
7-var u = require('patchbay/util')
8-var Scroller = require('pull-scroll')
9-
10-
11-function once (cont) {
12- var ended = false
13- return function (abort, cb) {
14- if(abort) return cb(abort)
15- else if (ended) return cb(ended)
16- else
17- cont(function (err, data) {
18- if(err) return cb(ended = err)
19- ended = true
20- cb(null, data)
21- })
22- }
23-}
24-
25-var plugs = require('patchbay/plugs')
26-
27-var message_render = plugs.first(exports.message_render = [])
28-var message_name = plugs.first(exports.message_name = [])
29-var message_compose = plugs.first(exports.message_compose = [])
30-var message_unbox = plugs.first(exports.message_unbox = [])
31-
32-var sbot_get = plugs.first(exports.sbot_get = [])
33-var sbot_links = plugs.first(exports.sbot_links = [])
34-var get_id = plugs.first(exports.get_id = [])
35-
36-function getThread (root, cb) {
37- //in this case, it's inconvienent that panel only takes
38- //a stream. maybe it would be better to accept an array?
39-
40- sbot_get(root, function (err, value) {
41- var msg = {key: root, value: value}
42-// if(value.content.root) return getThread(value.content.root, cb)
43-
44- pull(
45- sbot_links({rel: 'root', dest: root, values: true, keys: true}),
46- pull.collect(function (err, ary) {
47- if(err) return cb(err)
48- ary.unshift(msg)
49- cb(null, ary)
50- })
51- )
52- })
53-
54-}
55-
56-exports.screen_view = function (id) {
57- if(ref.isMsg(id)) {
58- var meta = {
59- type: 'post',
60- root: id,
61- branch: id //mutated when thread is loaded.
62- }
63-
64- var previousId = id
65- var content = h('div.column.scroller__content')
66- var div = h('div.column.scroller',
67- {style: {'overflow-y': 'auto'}},
68- h('div.scroller__wrapper',
69- content,
70- message_compose(meta, {shrink: false, placeholder: 'Write a reply'})
71- )
72- )
73-
74- message_name(id, function (err, name) {
75- div.title = name
76- })
77-
78- pull(
79- sbot_links({
80- rel: 'root', dest: id, keys: true, old: false
81- }),
82- pull.drain(function (msg) {
83- loadThread() //redraw thread
84- }, function () {} )
85- )
86-
87-
88- function loadThread () {
89- getThread(id, function (err, thread) {
90- //would probably be better keep an id for each message element
91- //(i.e. message key) and then update it if necessary.
92- //also, it may have moved (say, if you received a missing message)
93- content.innerHTML = ''
94- //decrypt
95- thread = thread.map(function (msg) {
96- return 'string' === typeof msg.value.content ? message_unbox(msg) : msg
97- })
98-
99- if(err) return content.appendChild(h('pre', err.stack))
100- sort(thread).map((msg) => {
101- var result = message_render(msg, {inContext: true, previousId})
102- previousId = msg.key
103- return result
104- }).filter(Boolean).forEach(function (el) {
105- content.appendChild(el)
106- })
107-
108- var branches = sort.heads(thread)
109- meta.branch = branches.length > 1 ? branches : branches[0]
110- meta.root = thread[0].value.content.root || thread[0].key
111- meta.channel = thread[0].value.content.channel
112-
113- var recps = thread[0].value.content.recps
114- var private = thread[0].value.private
115- if(private) {
116- if(recps)
117- meta.recps = recps
118- else
119- meta.recps = [thread[0].value.author, get_id()]
120- }
121- })
122- }
123-
124- loadThread()
125- return div
126- }
127-}
modules/timestamp.jsView
@@ -1,20 +1,0 @@
1-var h = require('hyperscript')
2-var human = require('human-time')
3-
4-function updateTimestampEl(el) {
5- el.firstChild.nodeValue = human(new Date(el.timestamp))
6- return el
7-}
8-
9-setInterval(function () {
10- var els = [].slice.call(document.querySelectorAll('.pw__timestamp'))
11- els.forEach(updateTimestampEl)
12-}, 60e3)
13-
14-exports.message_main_meta = function (msg) {
15- return updateTimestampEl(h('a.enter.pw__timestamp', {
16- href: '#'+msg.key,
17- timestamp: msg.value.timestamp,
18- title: new Date(msg.value.timestamp)
19- }, ''))
20-}
package.jsonView
@@ -27,9 +27,9 @@
2727 "micro-css": "~0.6.2",
2828 "non-private-ip": "^1.4.1",
2929 "on-change-network": "0.0.2",
3030 "on-wakeup": "^1.0.1",
31- "patchbay": "~4.0.1",
31+ "patchbay": "github:ssbc/patchbay#extract-styles-from-depject",
3232 "prebuild": "github:mmckegg/prebuild#use-npm-conf",
3333 "pull-abortable": "^4.1.0",
3434 "pull-file": "~1.0.0",
3535 "pull-identify-filetype": "^1.1.0",
@@ -38,9 +38,9 @@
3838 "pull-pause": "0.0.0",
3939 "pull-ping": "^2.0.2",
4040 "pull-pushable": "^2.0.1",
4141 "pull-stream": "~3.4.5",
42- "scuttlebot": "~9.2.0",
42+ "scuttlebot": "^9.4.3",
4343 "sorted-array-functions": "~1.0.0",
4444 "ssb-avatar": "^0.2.0",
4545 "ssb-blobs": "~0.1.7",
4646 "ssb-keys": "~7.0.0",
server-process.jsView
@@ -4,10 +4,10 @@
44 var electron = require('electron')
55
66 var createSbot = require('scuttlebot')
77 .use(require('scuttlebot/plugins/master'))
8- .use(require('./lib/persistent-gossip')) // override
9- .use(require('./lib/friends-with-gossip-priority'))
8+ .use(require('scuttlebot/plugins/gossip')) // override
9+ .use(require('scuttlebot/plugins/friends'))
1010 .use(require('scuttlebot/plugins/replicate'))
1111 .use(require('ssb-blobs'))
1212 .use(require('scuttlebot/plugins/invite'))
1313 .use(require('scuttlebot/plugins/block'))
styles/index.jsView
@@ -2,11 +2,10 @@
22 var path = require('path')
33 var compile = require('micro-css')
44 var result = ''
55 var additional = ''
6+var baseStyles = require('patchbay/styles')
67
7-additional += fs.readFileSync(require.resolve('patchbay/style.css'), 'utf8')
8-
98 fs.readdirSync(__dirname).forEach(function (file) {
109 if (/\.mcss$/i.test(file)) {
1110 result += fs.readFileSync(path.resolve(__dirname, file), 'utf8') + '\n'
1211 }
@@ -15,5 +14,5 @@
1514 additional += fs.readFileSync(path.resolve(__dirname, file), 'utf8') + '\n'
1615 }
1716 })
1817
19-module.exports = compile(result) + additional
18+module.exports = baseStyles + compile(result) + additional
styles/channel-list.mcssView
@@ -1,73 +1,0 @@
1-ChannelList {
2- a.channel {
3- display: flex;
4- padding: 8px 10px;
5- font-size: 110%;
6- margin: 4px 0;
7- background: rgba(255, 255, 255, 0.22);
8- border-radius: 5px;
9- position: relative
10- transition: background-color 0.2s
11- max-width: 250px;
12-
13- background-repeat: no-repeat
14- background-position: right
15-
16- -subscribed {
17- background-image: svg(subscribed)
18- span.name {
19- font-weight: bold
20- }
21- }
22-
23- @svg subscribed {
24- width: 20px
25- height: 12px
26- content: "<circle cx='6' stroke='#888' fill='none' cy='6' r='5' /> <circle cx='6' cy='6' r='3' fill='#888'/>"
27- }
28-
29- :hover {
30- background: rgba(255, 255, 255, 0.4);
31- text-decoration: none;
32- a {
33- transition: opacity 0.05s
34- opacity: 1
35- }
36- }
37-
38- span.name {
39- flex: 1
40- white-space: nowrap;
41- min-width: 0;
42- }
43-
44- a {
45- display: inline
46- opacity: 0;
47- font-size: 80%;
48- background-color: rgb(112, 112, 112);
49- transition: opacity 0.2s, background-color 0.4s
50- padding: 9px 10px;
51- color: white;
52- border-radius: 4px;
53- font-weight: bold;
54- margin: -8px -10px -8px 4px;
55- border-top-left-radius: 0;
56- border-bottom-left-radius: 0;
57- border-left: 2px solid rgba(255, 255, 255, 0.9);
58- text-decoration: none
59-
60- -subscribe {
61- :hover {
62- background-color: rgb(112, 184, 212);
63- }
64- }
65-
66- -unsubscribe {
67- :hover {
68- background: rgb(212, 112, 112);
69- }
70- }
71- }
72- }
73-}
styles/emoji.cssView
@@ -1,0 +1,6 @@
1+img.emoji {
2+ width: 1.5em;
3+ height: 1.5em;
4+ align-content: center;
5+ margin-top: -0.2em;
6+}
styles/feed-event.mcssView
@@ -1,36 +1,0 @@
1-FeedEvent {
2- display: flex
3- flex: 1
4- flex-direction: column
5- background: white
6- margin-top: 10px
7-
8- div {
9- flex: 1
10- }
11-
12- a.full {
13- display: block;
14- padding: 10px;
15- background: #daecd6;
16- border-top: 1px solid #bbc9d2;
17- border-bottom: 1px solid #bbc9d2;
18- text-align: center;
19- color: #759053;
20- }
21-
22- div.replies {
23- font-size: 100%
24- display: flex
25- flex-direction: column
26- div {
27- flex: 1
28- margin: 0
29- }
30- }
31-
32- div.meta {
33- font-size: 100%
34- padding: 10px
35- }
36-}
styles/loading.mcssView
@@ -1,63 +1,0 @@
1-Loading {
2- height: 25%
3- display: flex
4- align-items: center
5- justify-content: center
6-
7- -inline {
8- height: 16px
9- width: 16px
10- display: inline-block
11- margin: -3px 3px
12-
13- ::before {
14- display: block
15- height: 16px
16- width: 16px
17- }
18- }
19-
20- -large {
21- ::before {
22- height: 100px
23- width: 100px
24- }
25- ::after {
26- color: #CCC;
27- content: 'Loading...'
28- font-size: 200%
29- }
30- }
31-
32- ::before {
33- content: ' '
34- height: 50px
35- width: 50px
36- background-image: svg(waitingIcon)
37- background-repeat: no-repeat
38- background-position: center
39- background-size: contain
40- animation: spin 3s infinite linear
41- }
42-}
43-
44-@svg waitingIcon {
45- width: 30px
46- height: 30px
47- content: "<circle cx='15' cy='15' r='10' /><circle cx='10' cy='10' r='2' /><circle cx='20' cy='20' r='3' />"
48-
49- circle {
50- stroke: #CCC
51- stroke-width: 3px
52- fill: none
53- }
54-}
55-
56-@keyframes spin {
57- 0% {
58- transform: rotate(0deg);
59- }
60- 100% {
61- transform: rotate(360deg);
62- }
63-}
styles/message-confirm.mcssView
@@ -1,18 +1,0 @@
1-MessageConfirm {
2- section {
3- max-height: 80vh;
4- overflow: auto;
5- margin: -20px;
6- padding: 20px;
7- margin-bottom: 0;
8- }
9- footer {
10- text-align: right;
11- background: #e2e2e2;
12- margin: 0px -20px -20px;
13- padding: 5px;
14- border-top: 1px solid #d6d6d6;
15- position: relative;
16- box-shadow: 0 0 6px rgba(51, 51, 51, 0.47);
17- }
18-}
styles/message.mcssView
@@ -1,166 +1,0 @@
1-Message {
2- display: flex
3- flex-direction: column
4- box-shadow: #dadada 1px 2px 8px
5- border: 1px solid #f5f5f5
6- background: white
7- position: relative
8- font-size: 120%
9-
10- :focus {
11- z-index: 1
12- }
13-
14- -data {
15- header {
16- div.main {
17- font-size: 80%
18- a.avatar {
19- img {
20- width: 25px
21- }
22- }
23- }
24- }
25- (pre) {
26- overflow: auto
27- max-height: 200px
28- }
29- }
30-
31- -reply {
32- font-size: 100%
33- header {
34- div.main {
35- a.avatar {
36- img {
37- width: 30px
38- }
39- }
40- }
41- }
42- }
43-
44- header {
45- margin: 15px 15px
46- display: flex
47-
48- div.mini {
49- flex: 1
50- }
51-
52- div.main {
53- display: flex
54- flex: 1
55-
56- a.avatar {
57- img {
58- width: 50px
59- }
60- }
61-
62- div.main {
63- div.name {
64- font-size: 120%
65- a {
66- color: #444
67- font-weight: bold
68- }
69- }
70- div.meta {
71- font-size: 90%
72- }
73- margin-left: 10px
74- }
75-
76- div.meta {
77-
78- }
79- }
80-
81- div.meta {
82-
83- em {
84- display: inline-block
85- padding: 4px
86- }
87-
88- a.channel {
89- display: inline-block
90- padding: 4px
91- }
92-
93- span.likes {
94- color: #ffffff;
95- background: linear-gradient(45deg, #859c88, #87d47d);
96- padding: 5px 8px;
97- border-radius: 10px;
98- display: inline-block;
99- vertical-align: top;
100- }
101-
102- span.private {
103- display: inline-block;
104- margin: -3px -3px -3px 4px;
105- border: 4px solid #525050;
106- position: relative;
107-
108- a {
109- display: inline-block
110-
111- img {
112- margin: 0
113- vertical-align: bottom
114- border: none
115- }
116- }
117-
118- :after {
119- content: 'private';
120- position: absolute;
121- background: #525050;
122- bottom: 0;
123- left: -1px;
124- font-size: 10px;
125- padding: 2px 4px 0 2px;
126- border-top-right-radius: 5px;
127- color: white;
128- font-weight: bold;
129- pointer-events: none;
130- white-space: nowrap
131- }
132- }
133- }
134- }
135-
136- section {
137- margin: 0 15px
138- (img) {
139- max-width: 100%
140- }
141- }
142-
143- footer {
144- background: #fdfdfd
145- padding: 15px 15px
146- text-align: right
147-
148- div.actions {
149- a {
150- margin-left: 5px;
151- color: #5f5f5f;
152- padding: 3px 6px;
153- background: white;
154- border: 2px solid #DDD;
155- border-radius: 4px;
156-
157- :hover {
158- background: #cbeeff;
159- color: #3b7ba2;
160- text-decoration: none;
161- border-color: #8eafc1;
162- }
163- }
164- }
165- }
166-}
styles/notifier.mcssView
@@ -1,24 +1,0 @@
1-Notifier {
2- padding: 10px;
3- display: block;
4- background: rgb(214, 228, 236);
5- border-bottom: 1px solid rgb(187, 201, 210);
6- text-align: center;
7-
8- :hover {
9- background: rgb(220, 242, 255);
10- }
11-
12- animation: 0.5s slide-in
13- position: relative
14-
15- -loader {
16- background: rgb(255, 239, 217);
17- border-bottom: 1px solid rgb(228, 220, 195);
18- color: #10100c !important;
19-
20- :hover {
21- background: rgb(245, 229, 207);
22- }
23- }
24-}
styles/page-heading.mcssView
@@ -1,43 +1,0 @@
1-PageHeading {
2- display: flex
3- h1 {
4- flex: 1
5- }
6- div.meta {
7- a {
8- font-size: 80%;
9- background: rgb(112, 112, 112);
10- border: 2px solid #313131;
11- transition: opacity 0.2s;
12- opacity: 0.6;
13- padding: 6px 12px;
14- color: white;
15- border-radius: 4px;
16- font-weight: bold;
17- text-decoration: none;
18- display: block;
19-
20- -subscribe {
21- background-color: rgb(88, 171, 204);
22- border-color: #20699c;
23- }
24-
25- -unsubscribe {
26- background-repeat: no-repeat
27- background-position: right
28- background-image: svg(subscribed)
29- padding-right: 25px
30- }
31-
32- :hover {
33- opacity: 1
34- }
35- }
36- }
37-
38- @svg subscribed {
39- width: 20px
40- height: 12px
41- content: "<circle cx='6' stroke='#FFF' fill='none' cy='6' r='5' /> <circle cx='6' cy='6' r='3' fill='#FFF'/>"
42- }
43-}
styles/patchbay-tweaks.cssView
@@ -1,59 +1,0 @@
1-.scroller__wrapper {
2- width: 600px;
3- padding: 20px;
4-}
5-
6-.compose__controls > input[type=file] {
7- flex: 1;
8- padding: 5px;
9-}
10-
11-a.avatar {
12- display: inline;
13- font-weight: bold;
14- color: #222
15-}
16-
17-div.avatar > a.avatar {
18- display: flex;
19- font-size: 120%;
20-}
21-
22-img.emoji {
23- width: 1.5em;
24- height: 1.5em;
25- align-content: center;
26- margin-top: -0.2em;
27-}
28-
29-div.compose textarea {
30- transition: height 0.1s
31-}
32-
33-div.lightbox {
34- box-shadow: #5f5f5f 1px 2px 100px;
35- bottom: inherit !important;
36- overflow: hidden !important;
37-}
38-
39-div.suggest-box {
40- background: #333;
41-}
42-
43-div.suggest-box ul {
44- left: 390px;
45- top: 87px;
46- position: fixed;
47- padding: 5px;
48- border-radius: 3px;
49- background: #fdfdfd;
50- border: 1px solid #CCC;
51-}
52-
53-div.suggest-box li {
54- padding: 3px;
55-}
56-
57-div.suggest-box strong {
58- font-weight: normal;
59-}
styles/profile-list.mcssView
@@ -1,51 +1,0 @@
1-ProfileList {
2- a.profile {
3- display: flex;
4- padding: 4px;
5- font-size: 110%;
6- margin: 4px 0;
7- background: rgba(255, 255, 255, 0.22);
8- border-radius: 5px;
9- position: relative
10- text-decoration: none
11- transition: background-color 0.2s
12-
13- background-repeat: no-repeat
14- background-position: right
15-
16- -connected {
17- background-image: svg(connected)
18- }
19-
20- @svg connected {
21- width: 20px
22- height: 12px
23- content: "<circle cx='6' stroke='none' fill='green' cy='6' r='5' />"
24- }
25-
26- :hover {
27- background-color: rgba(255, 255, 255, 0.4);
28- }
29-
30- div.avatar {
31- img {
32- width: 40px
33- height: 40px
34- }
35- }
36-
37- div.main {
38- display: flex
39- flex-direction: column
40- flex: 1
41- margin-left: 10px
42- justify-content: center
43- div.name {
44- font-weight: bold
45- font-size: 110%
46- color: #333
47- -webkit-mask-image: linear-gradient(90deg, rgba(0,0,0,1) 90%, rgba(0,0,0,0))
48- }
49- }
50- }
51-}
styles/split-view.mcssView
@@ -1,28 +1,0 @@
1-SplitView {
2- display: flex
3- flex: 3
4- div.main {
5- display: flex
6- flex-direction: column
7- flex: 1
8- }
9- div.side {
10- min-width: 280px;
11- padding: 20px;
12- background: linear-gradient(100deg, #cee4ef, #efebeb);
13- border-right: 1px solid #dcdcdc;
14- overflow-y: auto;
15-
16- h2 {
17- margin-top: 20px
18- margin-bottom: 8px
19- color: #527b90;
20- text-shadow: 0px 0px 3px #fff;
21- font-weight: normal;
22- span.sub {
23- font-weight: normal
24- font-size: 90%
25- }
26- }
27- }
28-}
old_modules/about.jsView
@@ -1,0 +1,37 @@
1+var h = require('hyperscript')
2+
3+function idLink (id) {
4+ return h('a', {href:'#'+id}, id.slice(0, 10))
5+}
6+
7+function asLink (ln) {
8+ return 'string' === typeof ln ? ln : ln.link
9+}
10+
11+var plugs = require('patchbay/plugs')
12+var blob_url = plugs.first(exports.blob_url = [])
13+var avatar_name = plugs.first(exports.avatar_name = [])
14+var avatar_link = plugs.first(exports.avatar_link = [])
15+
16+exports.message_content = function (msg) {
17+ if(msg.value.content.type !== 'about' || !msg.value.content.about) return
18+
19+ if(!msg.value.content.image && !msg.value.content.name)
20+ return
21+
22+ var about = msg.value.content
23+ var id = msg.value.content.about
24+ return h('p',
25+ about.about === msg.value.author
26+ ? h('span', 'self-identifies ')
27+ : h('span', 'identifies ', about.name ? idLink(id) : avatar_link(id, avatar_name(id))),
28+ ' as ',
29+ h('a', {href:"#"+about.about},
30+ about.name || null,
31+ about.image
32+ ? h('img.avatar--fullsize', {src: blob_url(about.image)})
33+ : null
34+ )
35+ )
36+
37+}
old_modules/app.jsView
@@ -1,0 +1,198 @@
1+var Modules = require('./modules')
2+var h = require('./lib/h')
3+var Value = require('@mmckegg/mutant/value')
4+var when = require('@mmckegg/mutant/when')
5+var computed = require('@mmckegg/mutant/computed')
6+var toCollection = require('@mmckegg/mutant/dict-to-collection')
7+var MutantDict = require('@mmckegg/mutant/dict')
8+var MutantMap = require('@mmckegg/mutant/map')
9+var watch = require('@mmckegg/mutant/watch')
10+
11+var plugs = require('patchbay/plugs')
12+
13+module.exports = function (config, ssbClient) {
14+ var modules = Modules(config, ssbClient)
15+
16+ var screenView = plugs.first(modules.plugs.screen_view)
17+
18+ var searchTimer = null
19+ var searchBox = h('input.search', {
20+ type: 'search',
21+ placeholder: 'word, @key, #channel'
22+ })
23+
24+ searchBox.oninput = function () {
25+ clearTimeout(searchTimer)
26+ searchTimer = setTimeout(doSearch, 500)
27+ }
28+
29+ searchBox.onfocus = function () {
30+ if (searchBox.value) {
31+ doSearch()
32+ }
33+ }
34+
35+ var forwardHistory = []
36+ var backHistory = []
37+
38+ var views = MutantDict({
39+ // preload tabs (and subscribe to update notifications)
40+ '/public': screenView('/public'),
41+ '/private': screenView('/private'),
42+ [ssbClient.id]: screenView(ssbClient.id),
43+ '/notifications': screenView('/notifications')
44+ })
45+
46+ var lastViewed = {}
47+
48+ // delete cached view after 30 mins of last seeing
49+ setInterval(() => {
50+ views.keys().forEach((view) => {
51+ if (lastViewed[view] !== true && Date.now() - lastViewed[view] > (30 * 60e3) && view !== currentView()) {
52+ views.delete(view)
53+ }
54+ })
55+ }, 60e3)
56+
57+ var canGoForward = Value(false)
58+ var canGoBack = Value(false)
59+ var currentView = Value('/public')
60+
61+ watch(currentView, (view) => {
62+ window.location.hash = `#${view}`
63+ })
64+
65+ window.onhashchange = function (ev) {
66+ var path = window.location.hash.substring(1)
67+ if (path) {
68+ setView(path)
69+ }
70+ }
71+
72+ var mainElement = h('div.main', MutantMap(toCollection(views), (item) => {
73+ return h('div.view', {
74+ hidden: computed([item.key, currentView], (a, b) => a !== b)
75+ }, [ item.value ])
76+ }))
77+
78+ return h('MainWindow', {
79+ classList: [ '-' + process.platform ]
80+ }, [
81+ h('div.top', [
82+ h('span.history', [
83+ h('a', {
84+ 'ev-click': goBack,
85+ classList: [ when(canGoBack, '-active') ]
86+ }, '<'),
87+ h('a', {
88+ 'ev-click': goForward,
89+ classList: [ when(canGoForward, '-active') ]
90+ }, '>')
91+ ]),
92+ h('span.nav', [
93+ tab('Public', '/public'),
94+ tab('Private', '/private')
95+ ]),
96+ h('span.appTitle', ['Patchwork']),
97+ h('span', [ searchBox ]),
98+ h('span.nav', [
99+ tab('Profile', ssbClient.id),
100+ tab('Mentions', '/notifications')
101+ ])
102+ ]),
103+ mainElement
104+ ])
105+
106+ // scoped
107+
108+ function tab (name, view) {
109+ var instance = views.get(view)
110+ lastViewed[view] = true
111+ return h('a', {
112+ 'ev-click': function (ev) {
113+ if (instance.pendingUpdates && instance.pendingUpdates() && instance.reload) {
114+ instance.reload()
115+ }
116+ },
117+ href: `#${view}`,
118+ classList: [
119+ when(selected(view), '-selected')
120+ ]
121+ }, [
122+ name,
123+ when(instance.pendingUpdates, [
124+ ' (', instance.pendingUpdates, ')'
125+ ])
126+ ])
127+ }
128+
129+ function goBack () {
130+ if (backHistory.length) {
131+ canGoForward.set(true)
132+ forwardHistory.push(currentView())
133+ currentView.set(backHistory.pop())
134+ canGoBack.set(backHistory.length > 0)
135+ }
136+ }
137+
138+ function goForward () {
139+ if (forwardHistory.length) {
140+ backHistory.push(currentView())
141+ currentView.set(forwardHistory.pop())
142+ canGoForward.set(forwardHistory.length > 0)
143+ canGoBack.set(true)
144+ }
145+ }
146+
147+ function setView (view) {
148+ if (!views.has(view)) {
149+ views.put(view, screenView(view))
150+ }
151+
152+ if (lastViewed[view] !== true) {
153+ lastViewed[view] = Date.now()
154+ }
155+
156+ if (currentView() && lastViewed[currentView()] !== true) {
157+ lastViewed[currentView()] = Date.now()
158+ }
159+
160+ if (view !== currentView()) {
161+ canGoForward.set(false)
162+ canGoBack.set(true)
163+ forwardHistory.length = 0
164+ backHistory.push(currentView())
165+ currentView.set(view)
166+ }
167+ }
168+
169+ function doSearch () {
170+ var value = searchBox.value.trim()
171+ if (value.startsWith('/') || value.startsWith('?') || value.startsWith('@') || value.startsWith('#') || value.startsWith('%')) {
172+ setView(value)
173+ } else if (value.trim()) {
174+ setView(`?${value.trim()}`)
175+ } else {
176+ setView('/public')
177+ }
178+ }
179+
180+ function selected (view) {
181+ return computed([currentView, view], (currentView, view) => {
182+ return currentView === view
183+ })
184+ }
185+}
186+
187+function isSame (a, b) {
188+ if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
189+ for (var i = 0; i < a.length; i++) {
190+ if (a[i] !== b[i]) {
191+ return false
192+ }
193+ }
194+ return true
195+ } else if (a === b) {
196+ return true
197+ }
198+}
old_modules/channel.jsView
@@ -1,0 +1,78 @@
1+var when = require('@mmckegg/mutant/when')
2+var send = require('@mmckegg/mutant/send')
3+var plugs = require('patchbay/plugs')
4+var message_compose = plugs.first(exports.message_compose = [])
5+var sbot_log = plugs.first(exports.sbot_log = [])
6+var feed_summary = plugs.first(exports.feed_summary = [])
7+var h = require('../lib/h')
8+var pull = require('pull-stream')
9+var obs_subscribed_channels = plugs.first(exports.obs_subscribed_channels = [])
10+var get_id = plugs.first(exports.get_id = [])
11+var publish = plugs.first(exports.sbot_publish = [])
12+
13+exports.screen_view = function (path, sbot) {
14+ if (path[0] === '#') {
15+ var channel = path.substr(1)
16+ var subscribedChannels = obs_subscribed_channels(get_id())
17+
18+ return feed_summary((opts) => {
19+ return pull(
20+ sbot_log(opts),
21+ pull.map(matchesChannel)
22+ )
23+ }, [
24+ h('PageHeading', [
25+ h('h1', `#${channel}`),
26+ h('div.meta', [
27+ when(subscribedChannels.has(channel),
28+ h('a -unsubscribe', {
29+ 'href': '#',
30+ 'title': 'Click to unsubscribe',
31+ 'ev-click': send(unsubscribe, channel)
32+ }, 'Subscribed'),
33+ h('a -subscribe', {
34+ 'href': '#',
35+ 'ev-click': send(subscribe, channel)
36+ }, 'Subscribe')
37+ )
38+ ])
39+ ]),
40+ message_compose({type: 'post', channel: channel}, {placeholder: 'Write a message in this channel\n\n\n\nPeople who follow you or subscribe to this channel will also see this message in their main feed.\n\nTo create a new channel, type the channel name (preceded by a #) into the search box above. e.g #cat-pics'})
41+ ])
42+ }
43+
44+ // scoped
45+
46+ function matchesChannel (msg) {
47+ if (msg.sync) console.error('SYNC', msg)
48+ var c = msg && msg.value && msg.value.content
49+ if (c && c.channel === channel) {
50+ return msg
51+ } else {
52+ return {timestamp: msg.timestamp}
53+ }
54+ }
55+}
56+
57+exports.message_meta = function (msg) {
58+ var chan = msg.value.content.channel
59+ if (chan) {
60+ return h('a.channel', {href: '##' + chan}, '#' + chan)
61+ }
62+}
63+
64+function subscribe (id) {
65+ publish({
66+ type: 'channel',
67+ channel: id,
68+ subscribed: true
69+ })
70+}
71+
72+function unsubscribe (id) {
73+ publish({
74+ type: 'channel',
75+ channel: id,
76+ subscribed: false
77+ })
78+}
old_modules/data-feed.jsView
@@ -1,0 +1,32 @@
1+var h = require('hyperscript')
2+var u = require('patchbay/util')
3+var pull = require('pull-stream')
4+var Scroller = require('pull-scroll')
5+
6+var plugs = require('patchbay/plugs')
7+var sbot_log = plugs.first(exports.sbot_log = [])
8+var data_render = plugs.first(exports.data_render = [])
9+
10+exports.screen_view = function (path, sbot) {
11+ if(path === '/data-feed' || path === '/data') {
12+ var content = h('div.column.scroller__content')
13+ var div = h('div.column.scroller',
14+ {style: {'overflow':'auto'}},
15+ h('div.scroller__wrapper',
16+ content
17+ )
18+ )
19+
20+ pull(
21+ u.next(sbot_log, {old: false, limit: 100}),
22+ Scroller(div, content, data_render, true, false)
23+ )
24+
25+ pull(
26+ u.next(sbot_log, {reverse: true, limit: 100, live: false}),
27+ Scroller(div, content, data_render, false, false)
28+ )
29+
30+ return div
31+ }
32+}
old_modules/feed-summary.jsView
@@ -1,0 +1,215 @@
1+var Value = require('@mmckegg/mutant/value')
2+var h = require('@mmckegg/mutant/html-element')
3+var when = require('@mmckegg/mutant/when')
4+var computed = require('@mmckegg/mutant/computed')
5+var MutantArray = require('@mmckegg/mutant/array')
6+var Abortable = require('pull-abortable')
7+var Scroller = require('../lib/pull-scroll')
8+var FeedSummary = require('../lib/feed-summary')
9+var onceTrue = require('../lib/once-true')
10+
11+var m = require('../lib/h')
12+
13+var pull = require('pull-stream')
14+
15+var plugs = require('patchbay/plugs')
16+var message_render = plugs.first(exports.message_render = [])
17+var message_link = plugs.first(exports.message_link = [])
18+var person = plugs.first(exports.person = [])
19+var many_people = plugs.first(exports.many_people = [])
20+var people_names = plugs.first(exports.people_names = [])
21+var sbot_get = plugs.first(exports.sbot_get = [])
22+var get_id = plugs.first(exports.get_id = [])
23+
24+exports.feed_summary = function (getStream, prefix, opts) {
25+ var sync = Value(false)
26+ var updates = Value(0)
27+
28+ var filter = opts && opts.filter
29+ var bumpFilter = opts && opts.bumpFilter
30+ var windowSize = opts && opts.windowSize
31+ var waitFor = opts && opts.waitFor || true
32+
33+ var updateLoader = m('a Notifier -loader', {
34+ href: '#',
35+ 'ev-click': refresh
36+ }, [
37+ 'Show ',
38+ h('strong', [updates]), ' ',
39+ when(computed(updates, a => a === 1), 'update', 'updates')
40+ ])
41+
42+ var content = h('div.column.scroller__content')
43+
44+ var scrollElement = h('div.column.scroller', {
45+ style: {
46+ 'overflow': 'auto'
47+ }
48+ }, [
49+ h('div.scroller__wrapper', [
50+ prefix, content
51+ ])
52+ ])
53+
54+ setTimeout(refresh, 10)
55+
56+ onceTrue(waitFor, () => {
57+ pull(
58+ getStream({old: false}),
59+ pull.drain((item) => {
60+ var type = item && item.value && item.value.content.type
61+ if (type && type !== 'vote') {
62+ if (item.value && item.value.author === get_id() && !updates()) {
63+ return refresh()
64+ }
65+ if (filter) {
66+ var update = (item.value.content.type === 'post' && item.value.content.root) ? {
67+ type: 'message',
68+ messageId: item.value.content.root,
69+ channel: item.value.content.channel
70+ } : {
71+ type: 'message',
72+ author: item.value.author,
73+ channel: item.value.content.channel,
74+ messageId: item.key
75+ }
76+
77+ ensureAuthor(update, (err, update) => {
78+ if (!err) {
79+ if (filter(update)) {
80+ updates.set(updates() + 1)
81+ }
82+ }
83+ })
84+ } else {
85+ updates.set(updates() + 1)
86+ }
87+ }
88+ })
89+ )
90+ })
91+
92+ var abortLastFeed = null
93+
94+ var result = MutantArray([
95+ when(updates, updateLoader),
96+ when(sync, scrollElement, m('Loading -large'))
97+ ])
98+
99+ result.reload = refresh
100+ result.pendingUpdates = updates
101+
102+ return result
103+
104+ // scoped
105+
106+ function refresh () {
107+ if (abortLastFeed) {
108+ abortLastFeed()
109+ }
110+ updates.set(0)
111+ sync.set(false)
112+ content.innerHTML = ''
113+
114+ var abortable = Abortable()
115+ abortLastFeed = abortable.abort
116+
117+ pull(
118+ FeedSummary(getStream, {windowSize, bumpFilter}, () => {
119+ sync.set(true)
120+ }),
121+ pull.asyncMap(ensureAuthor),
122+ pull.filter((item) => {
123+ if (filter) {
124+ return filter(item)
125+ } else {
126+ return true
127+ }
128+ }),
129+ abortable,
130+ Scroller(scrollElement, content, renderItem, false, false)
131+ )
132+ }
133+}
134+
135+function ensureAuthor (item, cb) {
136+ if (item.type === 'message' && !item.message) {
137+ sbot_get(item.messageId, (_, value) => {
138+ if (value) {
139+ item.author = value.author
140+ }
141+ cb(null, item)
142+ })
143+ } else {
144+ cb(null, item)
145+ }
146+}
147+
148+function renderItem (item) {
149+ if (item.type === 'message') {
150+ var meta = null
151+ var previousId = item.messageId
152+ var replies = item.replies.slice(-4).map((msg) => {
153+ var result = message_render(msg, {inContext: true, inSummary: true, previousId})
154+ previousId = msg.key
155+ return result
156+ })
157+ var renderedMessage = item.message ? message_render(item.message, {inContext: true}) : null
158+ if (renderedMessage) {
159+ if (item.lastUpdateType === 'reply' && item.repliesFrom.size) {
160+ meta = m('div.meta', {
161+ title: people_names(item.repliesFrom)
162+ }, [
163+ many_people(item.repliesFrom), ' replied'
164+ ])
165+ } else if (item.lastUpdateType === 'dig' && item.digs.size) {
166+ meta = m('div.meta', {
167+ title: people_names(item.digs)
168+ }, [
169+ many_people(item.digs), ' dug this message'
170+ ])
171+ }
172+
173+ return m('FeedEvent', [
174+ meta,
175+ renderedMessage,
176+ when(replies.length, [
177+ when(item.replies.length > replies.length,
178+ m('a.full', {href: `#${item.messageId}`}, ['View full thread'])
179+ ),
180+ m('div.replies', replies)
181+ ])
182+ ])
183+ } else {
184+ if (item.lastUpdateType === 'reply' && item.repliesFrom.size) {
185+ meta = m('div.meta', {
186+ title: people_names(item.repliesFrom)
187+ }, [
188+ many_people(item.repliesFrom), ' replied to ', message_link(item.messageId)
189+ ])
190+ } else if (item.lastUpdateType === 'dig' && item.digs.size) {
191+ meta = m('div.meta', {
192+ title: people_names(item.digs)
193+ }, [
194+ many_people(item.digs), ' dug ', message_link(item.messageId)
195+ ])
196+ }
197+
198+ if (meta || replies.length) {
199+ return m('FeedEvent', [
200+ meta, m('div.replies', replies)
201+ ])
202+ }
203+ }
204+ } else if (item.type === 'follow') {
205+ return m('FeedEvent -follow', [
206+ m('div.meta', {
207+ title: people_names(item.contacts)
208+ }, [
209+ person(item.id), ' followed ', many_people(item.contacts)
210+ ])
211+ ])
212+ }
213+
214+ return h('div')
215+}
old_modules/feed.jsView
@@ -1,0 +1,20 @@
1+var ref = require('ssb-ref')
2+var h = require('hyperscript')
3+var extend = require('xtend')
4+
5+var plugs = require('patchbay/plugs')
6+var sbot_user_feed = plugs.first(exports.sbot_user_feed = [])
7+var avatar_profile = plugs.first(exports.avatar_profile = [])
8+var feed_summary = plugs.first(exports.feed_summary = [])
9+
10+exports.screen_view = function (id) {
11+ if (ref.isFeed(id)) {
12+ return feed_summary((opts) => {
13+ return sbot_user_feed(extend(opts, {id: id}))
14+ }, [
15+ h('div', [avatar_profile(id)])
16+ ], {
17+ windowSize: 50
18+ })
19+ }
20+}
old_modules/git-mini-messages.jsView
@@ -1,0 +1,26 @@
1+var h = require('../lib/h')
2+var when = require('@mmckegg/mutant/when')
3+var plugs = require('patchbay/plugs')
4+var message_link = plugs.first(exports.message_link = [])
5+
6+exports.message_content = exports.message_content_mini = function (msg, sbot) {
7+ if (msg.value.content.type === 'git-update') {
8+ var commits = msg.value.content.commits || []
9+ return [
10+ h('a', {href: `#${msg.key}`, title: commitSummary(commits)}, [
11+ 'pushed',
12+ when(commits, [' ', pluralizeCommits(commits)])
13+ ]),
14+ ' to ',
15+ message_link(msg.value.content.repo)
16+ ]
17+ }
18+}
19+
20+function pluralizeCommits (commits) {
21+ return when(commits.length === 1, '1 commit', `${commits.length} commits`)
22+}
23+
24+function commitSummary (commits) {
25+ return commits.map(commit => `- ${commit.title}`).join('\n')
26+}
old_modules/index.jsView
@@ -1,0 +1,31 @@
1+var SbotApi = require('../api')
2+var extend = require('xtend')
3+var combine = require('depject')
4+var fs = require('fs')
5+var patchbayModules = require('patchbay/modules')
6+
7+module.exports = function (config, ssbClient, overrides) {
8+ var api = SbotApi(ssbClient, config)
9+ var localModules = getLocalModules()
10+ return combine(extend(patchbayModules, localModules, {
11+ 'sbot-api.js': api,
12+ 'blob-url.js': {
13+ blob_url: function (link) {
14+ var prefix = config.blobsPrefix != null ? config.blobsPrefix : `http://localhost:${config.blobsPort}`
15+ if (typeof link.link === 'string') {
16+ link = link.link
17+ }
18+ return `${prefix}/${encodeURIComponent(link)}`
19+ }
20+ }
21+ }, overrides))
22+}
23+
24+function getLocalModules () {
25+ return fs.readdirSync(__dirname).reduce(function (result, e) {
26+ if (e !== 'index.js' && /\js$/.test(e)) {
27+ result[e] = require('./' + e)
28+ }
29+ return result
30+ }, {})
31+}
old_modules/like.jsView
@@ -1,0 +1,72 @@
1+var h = require('../lib/h')
2+var computed = require('@mmckegg/mutant/computed')
3+var when = require('@mmckegg/mutant/when')
4+var plugs = require('patchbay/plugs')
5+var message_link = plugs.first(exports.message_link = [])
6+var get_id = plugs.first(exports.get_id = [])
7+var get_likes = plugs.first(exports.get_likes = [])
8+var publish = plugs.first(exports.sbot_publish = [])
9+var people_names = plugs.first(exports.people_names = [])
10+
11+exports.message_content = exports.message_content_mini = function (msg, sbot) {
12+ if (msg.value.content.type !== 'vote') return
13+ var link = msg.value.content.vote.link
14+ return [
15+ msg.value.content.vote.value > 0 ? 'dug' : 'undug',
16+ ' ', message_link(link)
17+ ]
18+}
19+
20+exports.message_meta = function (msg, sbot) {
21+ return computed(get_likes(msg.key), likeCount)
22+}
23+
24+exports.message_action = function (msg, sbot) {
25+ var id = get_id()
26+ var dug = computed([get_likes(msg.key), id], doesLike)
27+ dug(() => {})
28+
29+ if (msg.value.content.type !== 'vote') {
30+ return h('a.dig', {
31+ href: '#',
32+ 'ev-click': function () {
33+ var dig = dug() ? {
34+ type: 'vote',
35+ vote: { link: msg.key, value: 0, expression: 'Undig' }
36+ } : {
37+ type: 'vote',
38+ vote: { link: msg.key, value: 1, expression: 'Dig' }
39+ }
40+ if (msg.value.content.recps) {
41+ dig.recps = msg.value.content.recps.map(function (e) {
42+ return e && typeof e !== 'string' ? e.link : e
43+ })
44+ dig.private = true
45+ }
46+ publish(dig)
47+ }
48+ }, when(dug, 'Undig', 'Dig'))
49+ }
50+}
51+
52+function doesLike (likes, userId) {
53+ return likes && likes[userId] && likes[userId][0] || false
54+}
55+
56+function likeCount (data) {
57+ var likes = getLikes(data)
58+ if (likes.length) {
59+ return [' ', h('span.likes', {
60+ title: people_names(likes)
61+ }, ['+', h('strong', `${likes.length}`)])]
62+ }
63+}
64+
65+function getLikes (likes) {
66+ return Object.keys(likes).reduce((result, id) => {
67+ if (likes[id][0]) {
68+ result.push(id)
69+ }
70+ return result
71+ }, [])
72+}
old_modules/many-people.jsView
@@ -1,0 +1,31 @@
1+var plugs = require('patchbay/plugs')
2+var person = plugs.first(exports.person = [])
3+exports.many_people = manyPeople
4+
5+function manyPeople (ids) {
6+ ids = Array.from(ids)
7+ var featuredIds = ids.slice(-3).reverse()
8+
9+ if (ids.length) {
10+ if (ids.length > 3) {
11+ return [
12+ person(featuredIds[0]), ', ',
13+ person(featuredIds[1]),
14+ ' and ', ids.length - 2, ' others'
15+ ]
16+ } else if (ids.length === 3) {
17+ return [
18+ person(featuredIds[0]), ', ',
19+ person(featuredIds[1]), ' and ',
20+ person(featuredIds[2])
21+ ]
22+ } else if (ids.length === 2) {
23+ return [
24+ person(featuredIds[0]), ' and ',
25+ person(featuredIds[1])
26+ ]
27+ } else {
28+ return person(featuredIds[0])
29+ }
30+ }
31+}
old_modules/message-confirm.jsView
@@ -1,0 +1,51 @@
1+var lightbox = require('hyperlightbox')
2+var h = require('../lib/h')
3+var plugs = require('patchbay/plugs')
4+var get_id = plugs.first(exports.get_id = [])
5+var publish = plugs.first(exports.sbot_publish = [])
6+var message_render = plugs.first(exports.message_render = [])
7+
8+exports.message_confirm = function (content, cb) {
9+ cb = cb || function () {}
10+
11+ var lb = lightbox()
12+ document.body.appendChild(lb)
13+
14+ var msg = {
15+ value: {
16+ author: get_id(),
17+ previous: null,
18+ sequence: null,
19+ timestamp: Date.now(),
20+ content: content
21+ }
22+ }
23+
24+ var okay = h('button', {
25+ 'ev-click': function () {
26+ lb.remove()
27+ publish(content, cb)
28+ },
29+ 'ev-keydown': function (ev) {
30+ if (ev.keyCode === 27) cancel.click() // escape
31+ }
32+ }, [
33+ 'okay'
34+ ])
35+
36+ var cancel = h('button', {'ev-click': function () {
37+ lb.remove()
38+ cb(null)
39+ }}, [
40+ 'Cancel'
41+ ])
42+
43+ lb.show(h('MessageConfirm', [
44+ h('section', [
45+ message_render(msg)
46+ ]),
47+ h('footer', [okay, cancel])
48+ ]))
49+
50+ okay.focus()
51+}
old_modules/message-name.jsView
@@ -1,0 +1,44 @@
1+var plugs = require('patchbay/plugs')
2+var sbot_links = plugs.first(exports.sbot_links = [])
3+var get_id = plugs.first(exports.get_id = [])
4+var sbot_get = plugs.first(exports.sbot_get = [])
5+var getAvatar = require('ssb-avatar')
6+
7+exports.message_name = function (id, cb) {
8+ sbot_get(id, function (err, value) {
9+ if (err && err.name === 'NotFoundError') {
10+ return cb(null, id.substring(0, 10) + '...(missing)')
11+ } else if (value.content.type === 'post' && typeof value.content.text === 'string') {
12+ if (value.content.text.trim()) {
13+ return cb(null, titleFromMarkdown(value.content.text, 40))
14+ }
15+ } else if (value.content.type === 'git-repo') {
16+ return getRepoName(id, cb)
17+ } else if (typeof value.content.text === 'string') {
18+ return cb(null, value.content.type + ': ' + titleFromMarkdown(value.content.text, 30))
19+ }
20+
21+ return cb(null, id.substring(0, 10) + '...')
22+ })
23+}
24+
25+function titleFromMarkdown (text, max) {
26+ text = text.trim().split('\n', 2).join('\n')
27+ text = text.replace(/_|`|\*|\#|\[.*?\]|\(\S*?\)/g, '').trim()
28+ text = text.replace(/\:$/, '')
29+ text = text.trim().split('\n', 1)[0].trim()
30+ if (text.length > max) {
31+ text = text.substring(0, max - 2) + '...'
32+ }
33+ return text
34+}
35+
36+function getRepoName (id, cb) {
37+ getAvatar({
38+ links: sbot_links,
39+ get: sbot_get
40+ }, get_id(), id, function (err, avatar) {
41+ if (err) return cb(err)
42+ cb(null, avatar.name)
43+ })
44+}
old_modules/message.jsView
@@ -1,0 +1,119 @@
1+var h = require('../lib/h')
2+var when = require('@mmckegg/mutant/when')
3+
4+var plugs = require('patchbay/plugs')
5+var message_content = plugs.first(exports.message_content = [])
6+var message_content_mini = plugs.first(exports.message_content_mini = [])
7+var message_link = plugs.first(exports.message_link = [])
8+var avatar_image = plugs.first(exports.avatar_image = [])
9+var avatar_name = plugs.first(exports.avatar_name = [])
10+var avatar_link = plugs.first(exports.avatar_link = [])
11+var message_meta = plugs.map(exports.message_meta = [])
12+var message_main_meta = plugs.map(exports.message_main_meta = [])
13+var message_action = plugs.map(exports.message_action = [])
14+var contextMenu = require('../lib/context-menu')
15+
16+exports.data_render = function (msg) {
17+ var div = h('Message -data', {
18+ 'ev-contextmenu': contextMenu.bind(null, msg)
19+ }, [
20+ messageHeader(msg),
21+ h('section', [
22+ h('pre', [
23+ JSON.stringify(msg, null, 2)
24+ ])
25+ ])
26+ ])
27+ return div
28+}
29+
30+exports.message_render = function (msg, opts) {
31+ opts = opts || {}
32+ var inContext = opts.inContext
33+ var previousId = opts.previousId
34+ var inSummary = opts.inSummary
35+
36+ var elMini = message_content_mini(msg)
37+ var el = message_content(msg)
38+
39+ if (elMini && (!el || inSummary)) {
40+ var div = h('Message', {
41+ 'ev-contextmenu': contextMenu.bind(null, msg)
42+ }, [
43+ h('header', [
44+ h('div.mini', [
45+ avatar_link(msg.value.author, avatar_name(msg.value.author), ''),
46+ ' ', elMini
47+ ]),
48+ h('div.meta', [message_main_meta(msg)])
49+ ])
50+ ])
51+ div.setAttribute('tabindex', '0')
52+ return div
53+ }
54+
55+ if (!el) return
56+
57+ var classList = []
58+ var replyInfo = null
59+
60+ if (msg.value.content.root) {
61+ classList.push('-reply')
62+ if (!inContext) {
63+ replyInfo = h('span', ['in reply to ', message_link(msg.value.content.root)])
64+ } else if (previousId && last(msg.value.content.branch) && previousId !== last(msg.value.content.branch)) {
65+ replyInfo = h('span', ['in reply to ', message_link(last(msg.value.content.branch))])
66+ }
67+ }
68+
69+ var element = h('Message', {
70+ classList,
71+ 'ev-contextmenu': contextMenu.bind(null, msg),
72+ 'ev-keydown': function (ev) {
73+ // on enter, hit first meta.
74+ if (ev.keyCode === 13) {
75+ element.querySelector('.enter').click()
76+ }
77+ }
78+ }, [
79+ messageHeader(msg, replyInfo),
80+ h('section', [el]),
81+ when(msg.key, h('footer', [
82+ h('div.actions', [
83+ message_action(msg),
84+ h('a', {href: '#' + msg.key}, 'Reply')
85+ ])
86+ ]))
87+ ])
88+
89+ // ); hyperscript does not seem to set attributes correctly.
90+ element.setAttribute('tabindex', '0')
91+
92+ return element
93+}
94+
95+function messageHeader (msg, replyInfo) {
96+ return h('header', [
97+ h('div.main', [
98+ h('a.avatar', {href: `#${msg.value.author}`}, avatar_image(msg.value.author)),
99+ h('div.main', [
100+ h('div.name', [
101+ h('a', {href: `#${msg.value.author}`}, avatar_name(msg.value.author))
102+ ]),
103+ h('div.meta', [
104+ message_main_meta(msg),
105+ ' ', replyInfo
106+ ])
107+ ])
108+ ]),
109+ h('div.meta', message_meta(msg))
110+ ])
111+}
112+
113+function last (array) {
114+ if (Array.isArray(array)) {
115+ return array[array.length - 1]
116+ } else {
117+ return array
118+ }
119+}
old_modules/notifications.jsView
@@ -1,0 +1,148 @@
1+var pull = require('pull-stream')
2+var paramap = require('pull-paramap')
3+var plugs = require('patchbay/plugs')
4+var cont = require('cont')
5+var ref = require('ssb-ref')
6+
7+var sbot_log = plugs.first(exports.sbot_log = [])
8+var sbot_get = plugs.first(exports.sbot_get = [])
9+var sbot_user_feed = plugs.first(exports.sbot_user_feed = [])
10+var message_unbox = plugs.first(exports.message_unbox = [])
11+var get_id = plugs.first(exports.get_id = [])
12+var feed_summary = plugs.first(exports.feed_summary = [])
13+
14+exports.screen_view = function (path) {
15+ if (path === '/notifications') {
16+ var oldest = null
17+ var id = get_id()
18+ var ids = {
19+ [id]: true
20+ }
21+
22+ getFirstMessage(id, function (err, msg) {
23+ if (err) return console.error(err)
24+ if (!oldest || msg.value.timestamp < oldest) {
25+ oldest = msg.value.timestamp
26+ }
27+ })
28+
29+ return feed_summary((opts) => {
30+ if (opts.old === false) {
31+ return pull(
32+ sbot_log(opts),
33+ unbox(),
34+ notifications(ids),
35+ pull.filter()
36+ )
37+ } else {
38+ return pull(
39+ sbot_log(opts),
40+ unbox(),
41+ notifications(ids),
42+ pull.filter(),
43+ pull.take(function (msg) {
44+ // abort stream after we pass the oldest messages of our feeds
45+ return !oldest || msg.value.timestamp > oldest
46+ })
47+ )
48+ }
49+ }, [], {
50+ windowSize: 200,
51+ filter: (group) => {
52+ return (
53+ ((group.message || group.type !== 'message') && (group.author !== id || group.digs && group.digs.size)) || (
54+ group.repliesFrom && group.repliesFrom.size && (
55+ !group.repliesFrom.has(id) || group.repliesFrom.size > 1
56+ )
57+ )
58+ )
59+ }
60+ })
61+ }
62+}
63+
64+function unbox () {
65+ return pull(
66+ pull.map(function (msg) {
67+ return msg.value && typeof msg.value.content === 'string'
68+ ? message_unbox(msg)
69+ : msg
70+ }),
71+ pull.filter(Boolean)
72+ )
73+}
74+
75+function notifications (ourIds) {
76+ function linksToUs (link) {
77+ return link && link.link in ourIds
78+ }
79+
80+ function isOurMsg (id, cb) {
81+ if (!id) return cb(null, false)
82+ if (typeof id === 'object' && typeof id.link === 'string') id = id.link
83+ if (!ref.isMsg(id)) return cb(null, false)
84+ sbot_get(id, function (err, msg) {
85+ if (err && err.name === 'NotFoundError') cb(null, false)
86+ else if (err) cb(err)
87+ else if (msg.content.type === 'issue' || msg.content.type === 'pull-request') {
88+ isOurMsg(msg.content.repo || msg.content.project, cb)
89+ } else {
90+ cb(err, msg.author in ourIds)
91+ }
92+ })
93+ }
94+
95+ function isAnyOurMessage (msg, ids, cb) {
96+ cont.para(ids.map(function (id) {
97+ return function (cb) { isOurMsg(id, cb) }
98+ }))(function (err, results) {
99+ if (err) cb(err)
100+ else if (results.some(Boolean)) cb(null, msg)
101+ else cb()
102+ })
103+ }
104+
105+ return paramap(function (msg, cb) {
106+ var c = msg.value && msg.value.content
107+ if (!c || typeof c !== 'object') return cb()
108+ if (msg.value.author in ourIds) return cb(null, msg)
109+
110+ if (c.mentions && Array.isArray(c.mentions) && c.mentions.some(linksToUs)) {
111+ return cb(null, msg)
112+ }
113+
114+ if (msg.private) {
115+ return cb(null, msg)
116+ }
117+
118+ switch (c.type) {
119+ case 'post':
120+ if (c.branch || c.root) {
121+ return isAnyOurMessage(msg, [].concat(c.branch, c.root), cb)
122+ } else {
123+ return cb()
124+ }
125+ case 'contact':
126+ return cb(null, c.contact in ourIds ? msg : null)
127+ case 'vote':
128+ if (c.vote && c.vote.link)
129+ return isOurMsg(c.vote.link, function (err, isOurs) {
130+ cb(err, isOurs ? msg : null)
131+ })
132+ else return cb()
133+ case 'issue':
134+ case 'pull-request':
135+ return isOurMsg(c.project || c.repo, function (err, isOurs) {
136+ cb(err, isOurs ? msg : null)
137+ })
138+ case 'issue-edit':
139+ return isAnyOurMessage(msg, [c.issue].concat(c.issues), cb)
140+ default:
141+ cb()
142+ }
143+ }, 4)
144+}
145+
146+function getFirstMessage (feedId, cb) {
147+ sbot_user_feed({id: feedId, gte: 0, limit: 1})(null, cb)
148+}
old_modules/obs-connected.jsView
@@ -1,0 +1,29 @@
1+var MutantSet = require('@mmckegg/mutant/set')
2+var plugs = require('patchbay/plugs')
3+var sbot_gossip_peers = plugs.first(exports.sbot_gossip_peers = [])
4+
5+var cache = null
6+
7+exports.obs_connected = function () {
8+ if (cache) {
9+ return cache
10+ } else {
11+ var result = MutantSet([], {nextTick: true})
12+ // todo: make this clean up on unlisten
13+
14+ refresh()
15+ setInterval(refresh, 10e3)
16+
17+ cache = result
18+ return result
19+ }
20+
21+ // scope
22+
23+ function refresh () {
24+ sbot_gossip_peers((err, peers) => {
25+ if (err) throw console.log(err)
26+ result.set(peers.filter(x => x.state === 'connected').map(x => x.key))
27+ })
28+ }
29+}
old_modules/obs-following.jsView
@@ -1,0 +1,47 @@
1+var pull = require('pull-stream')
2+var computed = require('@mmckegg/mutant/computed')
3+var MutantPullReduce = require('../lib/mutant-pull-reduce')
4+var plugs = require('patchbay/plugs')
5+var sbot_user_feed = plugs.first(exports.sbot_user_feed = [])
6+var cache = {}
7+var throttle = require('@mmckegg/mutant/throttle')
8+
9+exports.obs_following = function (userId) {
10+ if (cache[userId]) {
11+ return cache[userId]
12+ } else {
13+ var stream = pull(
14+ sbot_user_feed({id: userId, live: true}),
15+ pull.filter((msg) => {
16+ return !msg.value || msg.value.content.type === 'contact'
17+ })
18+ )
19+
20+ var result = MutantPullReduce(stream, (result, msg) => {
21+ var c = msg.value.content
22+ if (c.contact) {
23+ if (typeof c.following === 'boolean') {
24+ if (c.following) {
25+ result.add(c.contact)
26+ } else {
27+ result.delete(c.contact)
28+ }
29+ }
30+ }
31+ return result
32+ }, {
33+ startValue: new Set(),
34+ nextTick: true
35+ })
36+
37+ var instance = throttle(result, 2000)
38+ instance.sync = result.sync
39+
40+ instance.has = function (value) {
41+ return computed(instance, x => x.has(value))
42+ }
43+
44+ cache[userId] = instance
45+ return instance
46+ }
47+}
old_modules/obs-local.jsView
@@ -1,0 +1,29 @@
1+var MutantSet = require('@mmckegg/mutant/set')
2+var plugs = require('patchbay/plugs')
3+var sbot_list_local = plugs.first(exports.sbot_list_local = [])
4+
5+var cache = null
6+
7+exports.obs_local = function () {
8+ if (cache) {
9+ return cache
10+ } else {
11+ var result = MutantSet([], {nextTick: true})
12+ // todo: make this clean up on unlisten
13+
14+ refresh()
15+ setInterval(refresh, 10e3)
16+
17+ cache = result
18+ return result
19+ }
20+
21+ // scope
22+
23+ function refresh () {
24+ sbot_list_local((err, keys) => {
25+ if (err) throw console.log(err)
26+ result.set(keys)
27+ })
28+ }
29+}
old_modules/obs-recently-updated-feeds.jsView
@@ -1,0 +1,36 @@
1+var pull = require('pull-stream')
2+var pullCat = require('pull-cat')
3+var computed = require('@mmckegg/mutant/computed')
4+var MutantPullReduce = require('../lib/mutant-pull-reduce')
5+var plugs = require('patchbay/plugs')
6+var sbot_log = plugs.first(exports.sbot_log = [])
7+var throttle = require('@mmckegg/mutant/throttle')
8+var hr = 60 * 60 * 1000
9+
10+exports.obs_recently_updated_feeds = function (limit) {
11+ var stream = pull(
12+ pullCat([
13+ sbot_log({reverse: true, limit: limit || 500}),
14+ sbot_log({old: false})
15+ ])
16+ )
17+
18+ var result = MutantPullReduce(stream, (result, msg) => {
19+ if (msg.value.timestamp && Date.now() - msg.value.timestamp < 24 * hr) {
20+ result.add(msg.value.author)
21+ }
22+ return result
23+ }, {
24+ startValue: new Set(),
25+ nextTick: true
26+ })
27+
28+ var instance = throttle(result, 2000)
29+ instance.sync = result.sync
30+
31+ instance.has = function (value) {
32+ return computed(instance, x => x.has(value))
33+ }
34+
35+ return instance
36+}
old_modules/obs-subscribed-channels.jsView
@@ -1,0 +1,50 @@
1+var pull = require('pull-stream')
2+var computed = require('@mmckegg/mutant/computed')
3+var MutantPullReduce = require('../lib/mutant-pull-reduce')
4+var plugs = require('patchbay/plugs')
5+var sbot_user_feed = plugs.first(exports.sbot_user_feed = [])
6+var cache = {}
7+var throttle = require('@mmckegg/mutant/throttle')
8+
9+exports.obs_subscribed_channels = function (userId) {
10+ if (cache[userId]) {
11+ return cache[userId]
12+ } else {
13+ var stream = pull(
14+ sbot_user_feed({id: userId, live: true}),
15+ pull.filter((msg) => {
16+ return !msg.value || msg.value.content.type === 'channel'
17+ })
18+ )
19+
20+ var result = MutantPullReduce(stream, (result, msg) => {
21+ var c = msg.value.content
22+ if (typeof c.channel === 'string' && c.channel) {
23+ var channel = c.channel.trim()
24+ if (channel) {
25+ if (typeof c.subscribed === 'boolean') {
26+ if (c.subscribed) {
27+ result.add(channel)
28+ } else {
29+ result.delete(channel)
30+ }
31+ }
32+ }
33+ }
34+ return result
35+ }, {
36+ startValue: new Set(),
37+ nextTick: true
38+ })
39+
40+ var instance = throttle(result, 2000)
41+ instance.sync = result.sync
42+
43+ instance.has = function (value) {
44+ return computed(instance, x => x.has(value))
45+ }
46+
47+ cache[userId] = instance
48+ return instance
49+ }
50+}
old_modules/people-names.jsView
@@ -1,0 +1,22 @@
1+var Value = require('@mmckegg/mutant/value')
2+var plugs = require('patchbay/plugs')
3+var signifier = plugs.first(exports.signifier = [])
4+var computed = require('@mmckegg/mutant/computed')
5+
6+exports.people_names = function (ids) {
7+ return computed(Array.from(ids).map(ObservName), join) || ''
8+}
9+
10+function join (...args) {
11+ return args.join('\n')
12+}
13+
14+function ObservName (id) {
15+ var obs = Value(id.slice(0, 10))
16+ signifier(id, (_, value) => {
17+ if (value && value.length) {
18+ obs.set(value[0].name)
19+ }
20+ })
21+ return obs
22+}
old_modules/person.jsView
@@ -1,0 +1,9 @@
1+var plugs = require('patchbay/plugs')
2+var avatar_name = plugs.first(exports.avatar_name = [])
3+var avatar_link = plugs.first(exports.avatar_link = [])
4+
5+exports.person = person
6+
7+function person (id) {
8+ return avatar_link(id, avatar_name(id), '')
9+}
old_modules/post.jsView
@@ -1,0 +1,13 @@
1+var h = require('hyperscript')
2+var plugs = require('patchbay/plugs')
3+var message_link = plugs.first(exports.message_link = [])
4+var markdown = plugs.first(exports.markdown = [])
5+
6+exports.message_content = function (data) {
7+ if(!data.value.content || !data.value.content.text) return
8+
9+ return h('div',
10+ markdown(data.value.content)
11+ )
12+
13+}
old_modules/private.jsView
@@ -1,0 +1,84 @@
1+var pull = require('pull-stream')
2+var ref = require('ssb-ref')
3+var plugs = require('patchbay/plugs')
4+var message_compose = plugs.first(exports.message_compose = [])
5+var sbot_log = plugs.first(exports.sbot_log = [])
6+var feed_summary = plugs.first(exports.feed_summary = [])
7+var message_unbox = plugs.first(exports.message_unbox = [])
8+var get_id = plugs.first(exports.get_id = [])
9+var avatar_image_link = plugs.first(exports.avatar_image_link = [])
10+var update_cache = plugs.first(exports.update_cache = [])
11+var h = require('../lib/h')
12+
13+exports.screen_view = function (path, sbot) {
14+ if (path === '/private') {
15+ var id = get_id()
16+
17+ return feed_summary((opts) => {
18+ return pull(
19+ sbot_log(opts),
20+ loosen(10), // release tight loops if they continue too long (avoid scroll jank)
21+ unbox(),
22+ pull.through((item) => {
23+ if (item.value) {
24+ update_cache(item)
25+ }
26+ })
27+ )
28+ }, [
29+ message_compose({type: 'post', recps: [], private: true}, {
30+ prepublish: function (msg) {
31+ msg.recps = [id].concat(msg.mentions).filter(function (e) {
32+ return ref.isFeed(typeof e === 'string' ? e : e.link)
33+ })
34+ if (!msg.recps.length) {
35+ throw new Error('cannot make private message without recipients - just mention the user in an at reply in the message you send')
36+ }
37+ return msg
38+ },
39+ placeholder: `Write a private message \n\n\n\nThis can only be read by yourself and people you have @mentioned.`
40+ })
41+ ], {
42+ windowSize: 1000
43+ })
44+ }
45+}
46+
47+exports.message_meta = function (msg) {
48+ if (msg.value.content.recps || msg.value.private) {
49+ return h('span.private', [
50+ map(msg.value.content.recps, function (id) {
51+ return avatar_image_link(typeof id === 'string' ? id : id.link, 'thumbnail')
52+ })
53+ ])
54+ }
55+}
56+
57+function unbox () {
58+ return pull(
59+ pull.filter(function (msg) {
60+ return typeof msg.value.content === 'string'
61+ }),
62+ pull.map(function (msg) {
63+ return message_unbox(msg) || { timestamp: msg.timestamp }
64+ })
65+ )
66+}
67+
68+function map (ary, iter) {
69+ if (Array.isArray(ary)) return ary.map(iter)
70+}
71+
72+function loosen (max) {
73+ var lastRelease = Date.now()
74+ return pull.asyncMap(function (item, cb) {
75+ if (Date.now() - lastRelease > max) {
76+ setImmediate(() => {
77+ lastRelease = Date.now()
78+ cb(null, item)
79+ })
80+ } else {
81+ cb(null, item)
82+ }
83+ })
84+}
old_modules/public.jsView
@@ -1,0 +1,225 @@
1+var MutantMap = require('@mmckegg/mutant/map')
2+var computed = require('@mmckegg/mutant/computed')
3+var when = require('@mmckegg/mutant/when')
4+var send = require('@mmckegg/mutant/send')
5+var pull = require('pull-stream')
6+var extend = require('xtend')
7+
8+var plugs = require('patchbay/plugs')
9+var h = require('../lib/h')
10+var message_compose = plugs.first(exports.message_compose = [])
11+var sbot_log = plugs.first(exports.sbot_log = [])
12+var sbot_feed = plugs.first(exports.sbot_feed = [])
13+var sbot_user_feed = plugs.first(exports.sbot_user_feed = [])
14+
15+var feed_summary = plugs.first(exports.feed_summary = [])
16+var obs_channels = plugs.first(exports.obs_channels = [])
17+var obs_subscribed_channels = plugs.first(exports.obs_subscribed_channels = [])
18+var get_id = plugs.first(exports.get_id = [])
19+var publish = plugs.first(exports.sbot_publish = [])
20+var obs_following = plugs.first(exports.obs_following = [])
21+var obs_recently_updated_feeds = plugs.first(exports.obs_recently_updated_feeds = [])
22+var avatar_image = plugs.first(exports.avatar_image = [])
23+var avatar_name = plugs.first(exports.avatar_name = [])
24+var obs_local = plugs.first(exports.obs_local = [])
25+var obs_connected = plugs.first(exports.obs_connected = [])
26+
27+exports.screen_view = function (path, sbot) {
28+ if (path === '/public') {
29+ var id = get_id()
30+ var channels = computed(obs_channels(), items => items.slice(0, 8), {comparer: arrayEq})
31+ var subscribedChannels = obs_subscribed_channels(id)
32+ var loading = computed(subscribedChannels.sync, x => !x)
33+ var connectedPeers = obs_connected()
34+ var localPeers = obs_local()
35+ var connectedPubs = computed([connectedPeers, localPeers], (c, l) => c.filter(x => !l.includes(x)))
36+ var following = obs_following(id)
37+
38+ var oldest = Date.now() - (2 * 24 * 60 * 60e3)
39+ getFirstMessage(id, (_, msg) => {
40+ if (msg) {
41+ // fall back to timestamp stream before this, give 48 hrs for feeds to stabilize
42+ if (msg.value.timestamp > oldest) {
43+ oldest = Date.now()
44+ }
45+ }
46+ })
47+
48+ var whoToFollow = computed([obs_following(id), obs_recently_updated_feeds(200)], (following, recent) => {
49+ return Array.from(recent).filter(x => x !== id && !following.has(x)).slice(0, 10)
50+ })
51+
52+ var feedSummary = feed_summary(getFeed, [
53+ message_compose({type: 'post'}, {placeholder: 'Write a public message'})
54+ ], {
55+ waitUntil: computed([
56+ following.sync,
57+ subscribedChannels.sync
58+ ], x => x.every(Boolean)),
59+ windowSize: 500,
60+ filter: (item) => {
61+ return (
62+ id === item.author ||
63+ following().has(item.author) ||
64+ subscribedChannels().has(item.channel) ||
65+ (item.repliesFrom && item.repliesFrom.has(id)) ||
66+ item.digs && item.digs.has(id)
67+ )
68+ },
69+ bumpFilter: (msg, group) => {
70+ if (!group.message) {
71+ return (
72+ isMentioned(id, msg.value.content.mentions) ||
73+ msg.value.author === id || (
74+ fromDay(msg, group.fromTime) && (
75+ following().has(msg.value.author) ||
76+ group.repliesFrom.has(id)
77+ )
78+ )
79+ )
80+ }
81+ return true
82+ }
83+ })
84+
85+ var result = h('SplitView', [
86+ h('div.side', [
87+ h('h2', 'Active Channels'),
88+ when(loading, [ h('Loading') ]),
89+ h('ChannelList', {
90+ hidden: loading
91+ }, [
92+ MutantMap(channels, (channel) => {
93+ var subscribed = subscribedChannels.has(channel.id)
94+ return h('a.channel', {
95+ href: `##${channel.id}`,
96+ classList: [
97+ when(subscribed, '-subscribed')
98+ ]
99+ }, [
100+ h('span.name', '#' + channel.id),
101+ when(subscribed,
102+ h('a -unsubscribe', {
103+ 'ev-click': send(unsubscribe, channel.id)
104+ }, 'Unsubscribe'),
105+ h('a -subscribe', {
106+ 'ev-click': send(subscribe, channel.id)
107+ }, 'Subscribe')
108+ )
109+ ])
110+ }, {maxTime: 5})
111+ ]),
112+
113+ when(computed(localPeers, x => x.length), h('h2', 'Local')),
114+ h('ProfileList', [
115+ MutantMap(localPeers, (id) => {
116+ return h('a.profile', {
117+ classList: [
118+ when(computed([connectedPeers, id], (p, id) => p.includes(id)), '-connected')
119+ ],
120+ href: `#${id}`
121+ }, [
122+ h('div.avatar', [avatar_image(id)]),
123+ h('div.main', [
124+ h('div.name', [ avatar_name(id) ])
125+ ])
126+ ])
127+ })
128+ ]),
129+
130+ when(computed(whoToFollow, x => x.length), h('h2', 'Who to follow')),
131+ h('ProfileList', [
132+ MutantMap(whoToFollow, (id) => {
133+ return h('a.profile', {
134+ href: `#${id}`
135+ }, [
136+ h('div.avatar', [avatar_image(id)]),
137+ h('div.main', [
138+ h('div.name', [ avatar_name(id) ])
139+ ])
140+ ])
141+ })
142+ ]),
143+
144+ when(computed(connectedPubs, x => x.length), h('h2', 'Connected Pubs')),
145+ h('ProfileList', [
146+ MutantMap(connectedPubs, (id) => {
147+ return h('a.profile', {
148+ classList: [ '-connected' ],
149+ href: `#${id}`
150+ }, [
151+ h('div.avatar', [avatar_image(id)]),
152+ h('div.main', [
153+ h('div.name', [ avatar_name(id) ])
154+ ])
155+ ])
156+ })
157+ ])
158+ ]),
159+ h('div.main', [ feedSummary ])
160+ ])
161+
162+ result.pendingUpdates = feedSummary.pendingUpdates
163+ result.reload = feedSummary.reload
164+
165+ return result
166+ }
167+
168+ // scoped
169+
170+ function getFeed (opts) {
171+ if (opts.lt && opts.lt < oldest) {
172+ opts = extend(opts, {lt: parseInt(opts.lt, 10)})
173+ return pull(
174+ sbot_feed(opts),
175+ pull.map((msg) => {
176+ if (msg.sync) {
177+ return msg
178+ } else {
179+ return {key: msg.key, value: msg.value, timestamp: msg.value.timestamp}
180+ }
181+ })
182+ )
183+ } else {
184+ return sbot_log(opts)
185+ }
186+ }
187+}
188+
189+function fromDay (msg, fromTime) {
190+ return (fromTime - msg.timestamp) < (24 * 60 * 60e3)
191+}
192+
193+function isMentioned (id, list) {
194+ if (Array.isArray(list)) {
195+ return list.includes(id)
196+ } else {
197+ return false
198+ }
199+}
200+
201+function subscribe (id) {
202+ publish({
203+ type: 'channel',
204+ channel: id,
205+ subscribed: true
206+ })
207+}
208+
209+function unsubscribe (id) {
210+ publish({
211+ type: 'channel',
212+ channel: id,
213+ subscribed: false
214+ })
215+}
216+
217+function arrayEq (a, b) {
218+ if (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a !== b) {
219+ return a.every((value, i) => value === b[i])
220+ }
221+}
222+
223+function getFirstMessage (feedId, cb) {
224+ sbot_user_feed({id: feedId, gte: 0, limit: 1})(null, cb)
225+}
old_modules/raw.jsView
@@ -1,0 +1,1 @@
1+// disable
old_modules/thread.jsView
@@ -1,0 +1,127 @@
1+var ui = require('patchbay/ui')
2+var pull = require('pull-stream')
3+var Cat = require('pull-cat')
4+var sort = require('ssb-sort')
5+var ref = require('ssb-ref')
6+var h = require('hyperscript')
7+var u = require('patchbay/util')
8+var Scroller = require('pull-scroll')
9+
10+
11+function once (cont) {
12+ var ended = false
13+ return function (abort, cb) {
14+ if(abort) return cb(abort)
15+ else if (ended) return cb(ended)
16+ else
17+ cont(function (err, data) {
18+ if(err) return cb(ended = err)
19+ ended = true
20+ cb(null, data)
21+ })
22+ }
23+}
24+
25+var plugs = require('patchbay/plugs')
26+
27+var message_render = plugs.first(exports.message_render = [])
28+var message_name = plugs.first(exports.message_name = [])
29+var message_compose = plugs.first(exports.message_compose = [])
30+var message_unbox = plugs.first(exports.message_unbox = [])
31+
32+var sbot_get = plugs.first(exports.sbot_get = [])
33+var sbot_links = plugs.first(exports.sbot_links = [])
34+var get_id = plugs.first(exports.get_id = [])
35+
36+function getThread (root, cb) {
37+ //in this case, it's inconvienent that panel only takes
38+ //a stream. maybe it would be better to accept an array?
39+
40+ sbot_get(root, function (err, value) {
41+ var msg = {key: root, value: value}
42+// if(value.content.root) return getThread(value.content.root, cb)
43+
44+ pull(
45+ sbot_links({rel: 'root', dest: root, values: true, keys: true}),
46+ pull.collect(function (err, ary) {
47+ if(err) return cb(err)
48+ ary.unshift(msg)
49+ cb(null, ary)
50+ })
51+ )
52+ })
53+
54+}
55+
56+exports.screen_view = function (id) {
57+ if(ref.isMsg(id)) {
58+ var meta = {
59+ type: 'post',
60+ root: id,
61+ branch: id //mutated when thread is loaded.
62+ }
63+
64+ var previousId = id
65+ var content = h('div.column.scroller__content')
66+ var div = h('div.column.scroller',
67+ {style: {'overflow-y': 'auto'}},
68+ h('div.scroller__wrapper',
69+ content,
70+ message_compose(meta, {shrink: false, placeholder: 'Write a reply'})
71+ )
72+ )
73+
74+ message_name(id, function (err, name) {
75+ div.title = name
76+ })
77+
78+ pull(
79+ sbot_links({
80+ rel: 'root', dest: id, keys: true, old: false
81+ }),
82+ pull.drain(function (msg) {
83+ loadThread() //redraw thread
84+ }, function () {} )
85+ )
86+
87+
88+ function loadThread () {
89+ getThread(id, function (err, thread) {
90+ //would probably be better keep an id for each message element
91+ //(i.e. message key) and then update it if necessary.
92+ //also, it may have moved (say, if you received a missing message)
93+ content.innerHTML = ''
94+ //decrypt
95+ thread = thread.map(function (msg) {
96+ return 'string' === typeof msg.value.content ? message_unbox(msg) : msg
97+ })
98+
99+ if(err) return content.appendChild(h('pre', err.stack))
100+ sort(thread).map((msg) => {
101+ var result = message_render(msg, {inContext: true, previousId})
102+ previousId = msg.key
103+ return result
104+ }).filter(Boolean).forEach(function (el) {
105+ content.appendChild(el)
106+ })
107+
108+ var branches = sort.heads(thread)
109+ meta.branch = branches.length > 1 ? branches : branches[0]
110+ meta.root = thread[0].value.content.root || thread[0].key
111+ meta.channel = thread[0].value.content.channel
112+
113+ var recps = thread[0].value.content.recps
114+ var private = thread[0].value.private
115+ if(private) {
116+ if(recps)
117+ meta.recps = recps
118+ else
119+ meta.recps = [thread[0].value.author, get_id()]
120+ }
121+ })
122+ }
123+
124+ loadThread()
125+ return div
126+ }
127+}
old_modules/timestamp.jsView
@@ -1,0 +1,20 @@
1+var h = require('hyperscript')
2+var human = require('human-time')
3+
4+function updateTimestampEl(el) {
5+ el.firstChild.nodeValue = human(new Date(el.timestamp))
6+ return el
7+}
8+
9+setInterval(function () {
10+ var els = [].slice.call(document.querySelectorAll('.pw__timestamp'))
11+ els.forEach(updateTimestampEl)
12+}, 60e3)
13+
14+exports.message_main_meta = function (msg) {
15+ return updateTimestampEl(h('a.enter.pw__timestamp', {
16+ href: '#'+msg.key,
17+ timestamp: msg.value.timestamp,
18+ title: new Date(msg.value.timestamp)
19+ }, ''))
20+}
old_styles/channel-list.mcssView
@@ -1,0 +1,73 @@
1+ChannelList {
2+ a.channel {
3+ display: flex;
4+ padding: 8px 10px;
5+ font-size: 110%;
6+ margin: 4px 0;
7+ background: rgba(255, 255, 255, 0.22);
8+ border-radius: 5px;
9+ position: relative
10+ transition: background-color 0.2s
11+ max-width: 250px;
12+
13+ background-repeat: no-repeat
14+ background-position: right
15+
16+ -subscribed {
17+ background-image: svg(subscribed)
18+ span.name {
19+ font-weight: bold
20+ }
21+ }
22+
23+ @svg subscribed {
24+ width: 20px
25+ height: 12px
26+ content: "<circle cx='6' stroke='#888' fill='none' cy='6' r='5' /> <circle cx='6' cy='6' r='3' fill='#888'/>"
27+ }
28+
29+ :hover {
30+ background: rgba(255, 255, 255, 0.4);
31+ text-decoration: none;
32+ a {
33+ transition: opacity 0.05s
34+ opacity: 1
35+ }
36+ }
37+
38+ span.name {
39+ flex: 1
40+ white-space: nowrap;
41+ min-width: 0;
42+ }
43+
44+ a {
45+ display: inline
46+ opacity: 0;
47+ font-size: 80%;
48+ background-color: rgb(112, 112, 112);
49+ transition: opacity 0.2s, background-color 0.4s
50+ padding: 9px 10px;
51+ color: white;
52+ border-radius: 4px;
53+ font-weight: bold;
54+ margin: -8px -10px -8px 4px;
55+ border-top-left-radius: 0;
56+ border-bottom-left-radius: 0;
57+ border-left: 2px solid rgba(255, 255, 255, 0.9);
58+ text-decoration: none
59+
60+ -subscribe {
61+ :hover {
62+ background-color: rgb(112, 184, 212);
63+ }
64+ }
65+
66+ -unsubscribe {
67+ :hover {
68+ background: rgb(212, 112, 112);
69+ }
70+ }
71+ }
72+ }
73+}
old_styles/feed-event.mcssView
@@ -1,0 +1,36 @@
1+FeedEvent {
2+ display: flex
3+ flex: 1
4+ flex-direction: column
5+ background: white
6+ margin-top: 10px
7+
8+ div {
9+ flex: 1
10+ }
11+
12+ a.full {
13+ display: block;
14+ padding: 10px;
15+ background: #daecd6;
16+ border-top: 1px solid #bbc9d2;
17+ border-bottom: 1px solid #bbc9d2;
18+ text-align: center;
19+ color: #759053;
20+ }
21+
22+ div.replies {
23+ font-size: 100%
24+ display: flex
25+ flex-direction: column
26+ div {
27+ flex: 1
28+ margin: 0
29+ }
30+ }
31+
32+ div.meta {
33+ font-size: 100%
34+ padding: 10px
35+ }
36+}
old_styles/loading.mcssView
@@ -1,0 +1,63 @@
1+Loading {
2+ height: 25%
3+ display: flex
4+ align-items: center
5+ justify-content: center
6+
7+ -inline {
8+ height: 16px
9+ width: 16px
10+ display: inline-block
11+ margin: -3px 3px
12+
13+ ::before {
14+ display: block
15+ height: 16px
16+ width: 16px
17+ }
18+ }
19+
20+ -large {
21+ ::before {
22+ height: 100px
23+ width: 100px
24+ }
25+ ::after {
26+ color: #CCC;
27+ content: 'Loading...'
28+ font-size: 200%
29+ }
30+ }
31+
32+ ::before {
33+ content: ' '
34+ height: 50px
35+ width: 50px
36+ background-image: svg(waitingIcon)
37+ background-repeat: no-repeat
38+ background-position: center
39+ background-size: contain
40+ animation: spin 3s infinite linear
41+ }
42+}
43+
44+@svg waitingIcon {
45+ width: 30px
46+ height: 30px
47+ content: "<circle cx='15' cy='15' r='10' /><circle cx='10' cy='10' r='2' /><circle cx='20' cy='20' r='3' />"
48+
49+ circle {
50+ stroke: #CCC
51+ stroke-width: 3px
52+ fill: none
53+ }
54+}
55+
56+@keyframes spin {
57+ 0% {
58+ transform: rotate(0deg);
59+ }
60+ 100% {
61+ transform: rotate(360deg);
62+ }
63+}
old_styles/message-confirm.mcssView
@@ -1,0 +1,18 @@
1+MessageConfirm {
2+ section {
3+ max-height: 80vh;
4+ overflow: auto;
5+ margin: -20px;
6+ padding: 20px;
7+ margin-bottom: 0;
8+ }
9+ footer {
10+ text-align: right;
11+ background: #e2e2e2;
12+ margin: 0px -20px -20px;
13+ padding: 5px;
14+ border-top: 1px solid #d6d6d6;
15+ position: relative;
16+ box-shadow: 0 0 6px rgba(51, 51, 51, 0.47);
17+ }
18+}
old_styles/message.mcssView
@@ -1,0 +1,166 @@
1+Message {
2+ display: flex
3+ flex-direction: column
4+ box-shadow: #dadada 1px 2px 8px
5+ border: 1px solid #f5f5f5
6+ background: white
7+ position: relative
8+ font-size: 120%
9+
10+ :focus {
11+ z-index: 1
12+ }
13+
14+ -data {
15+ header {
16+ div.main {
17+ font-size: 80%
18+ a.avatar {
19+ img {
20+ width: 25px
21+ }
22+ }
23+ }
24+ }
25+ (pre) {
26+ overflow: auto
27+ max-height: 200px
28+ }
29+ }
30+
31+ -reply {
32+ font-size: 100%
33+ header {
34+ div.main {
35+ a.avatar {
36+ img {
37+ width: 30px
38+ }
39+ }
40+ }
41+ }
42+ }
43+
44+ header {
45+ margin: 15px 15px
46+ display: flex
47+
48+ div.mini {
49+ flex: 1
50+ }
51+
52+ div.main {
53+ display: flex
54+ flex: 1
55+
56+ a.avatar {
57+ img {
58+ width: 50px
59+ }
60+ }
61+
62+ div.main {
63+ div.name {
64+ font-size: 120%
65+ a {
66+ color: #444
67+ font-weight: bold
68+ }
69+ }
70+ div.meta {
71+ font-size: 90%
72+ }
73+ margin-left: 10px
74+ }
75+
76+ div.meta {
77+
78+ }
79+ }
80+
81+ div.meta {
82+
83+ em {
84+ display: inline-block
85+ padding: 4px
86+ }
87+
88+ a.channel {
89+ display: inline-block
90+ padding: 4px
91+ }
92+
93+ span.likes {
94+ color: #ffffff;
95+ background: linear-gradient(45deg, #859c88, #87d47d);
96+ padding: 5px 8px;
97+ border-radius: 10px;
98+ display: inline-block;
99+ vertical-align: top;
100+ }
101+
102+ span.private {
103+ display: inline-block;
104+ margin: -3px -3px -3px 4px;
105+ border: 4px solid #525050;
106+ position: relative;
107+
108+ a {
109+ display: inline-block
110+
111+ img {
112+ margin: 0
113+ vertical-align: bottom
114+ border: none
115+ }
116+ }
117+
118+ :after {
119+ content: 'private';
120+ position: absolute;
121+ background: #525050;
122+ bottom: 0;
123+ left: -1px;
124+ font-size: 10px;
125+ padding: 2px 4px 0 2px;
126+ border-top-right-radius: 5px;
127+ color: white;
128+ font-weight: bold;
129+ pointer-events: none;
130+ white-space: nowrap
131+ }
132+ }
133+ }
134+ }
135+
136+ section {
137+ margin: 0 15px
138+ (img) {
139+ max-width: 100%
140+ }
141+ }
142+
143+ footer {
144+ background: #fdfdfd
145+ padding: 15px 15px
146+ text-align: right
147+
148+ div.actions {
149+ a {
150+ margin-left: 5px;
151+ color: #5f5f5f;
152+ padding: 3px 6px;
153+ background: white;
154+ border: 2px solid #DDD;
155+ border-radius: 4px;
156+
157+ :hover {
158+ background: #cbeeff;
159+ color: #3b7ba2;
160+ text-decoration: none;
161+ border-color: #8eafc1;
162+ }
163+ }
164+ }
165+ }
166+}
old_styles/notifier.mcssView
@@ -1,0 +1,24 @@
1+Notifier {
2+ padding: 10px;
3+ display: block;
4+ background: rgb(214, 228, 236);
5+ border-bottom: 1px solid rgb(187, 201, 210);
6+ text-align: center;
7+
8+ :hover {
9+ background: rgb(220, 242, 255);
10+ }
11+
12+ animation: 0.5s slide-in
13+ position: relative
14+
15+ -loader {
16+ background: rgb(255, 239, 217);
17+ border-bottom: 1px solid rgb(228, 220, 195);
18+ color: #10100c !important;
19+
20+ :hover {
21+ background: rgb(245, 229, 207);
22+ }
23+ }
24+}
old_styles/page-heading.mcssView
@@ -1,0 +1,43 @@
1+PageHeading {
2+ display: flex
3+ h1 {
4+ flex: 1
5+ }
6+ div.meta {
7+ a {
8+ font-size: 80%;
9+ background: rgb(112, 112, 112);
10+ border: 2px solid #313131;
11+ transition: opacity 0.2s;
12+ opacity: 0.6;
13+ padding: 6px 12px;
14+ color: white;
15+ border-radius: 4px;
16+ font-weight: bold;
17+ text-decoration: none;
18+ display: block;
19+
20+ -subscribe {
21+ background-color: rgb(88, 171, 204);
22+ border-color: #20699c;
23+ }
24+
25+ -unsubscribe {
26+ background-repeat: no-repeat
27+ background-position: right
28+ background-image: svg(subscribed)
29+ padding-right: 25px
30+ }
31+
32+ :hover {
33+ opacity: 1
34+ }
35+ }
36+ }
37+
38+ @svg subscribed {
39+ width: 20px
40+ height: 12px
41+ content: "<circle cx='6' stroke='#FFF' fill='none' cy='6' r='5' /> <circle cx='6' cy='6' r='3' fill='#FFF'/>"
42+ }
43+}
old_styles/patchbay-tweaks.cssView
@@ -1,0 +1,59 @@
1+.scroller__wrapper {
2+ width: 600px;
3+ padding: 20px;
4+}
5+
6+.compose__controls > input[type=file] {
7+ flex: 1;
8+ padding: 5px;
9+}
10+
11+a.avatar {
12+ display: inline;
13+ font-weight: bold;
14+ color: #222
15+}
16+
17+div.avatar > a.avatar {
18+ display: flex;
19+ font-size: 120%;
20+}
21+
22+img.emoji {
23+ width: 1.5em;
24+ height: 1.5em;
25+ align-content: center;
26+ margin-top: -0.2em;
27+}
28+
29+div.compose textarea {
30+ transition: height 0.1s
31+}
32+
33+div.lightbox {
34+ box-shadow: #5f5f5f 1px 2px 100px;
35+ bottom: inherit !important;
36+ overflow: hidden !important;
37+}
38+
39+div.suggest-box {
40+ background: #333;
41+}
42+
43+div.suggest-box ul {
44+ left: 390px;
45+ top: 87px;
46+ position: fixed;
47+ padding: 5px;
48+ border-radius: 3px;
49+ background: #fdfdfd;
50+ border: 1px solid #CCC;
51+}
52+
53+div.suggest-box li {
54+ padding: 3px;
55+}
56+
57+div.suggest-box strong {
58+ font-weight: normal;
59+}
old_styles/profile-list.mcssView
@@ -1,0 +1,51 @@
1+ProfileList {
2+ a.profile {
3+ display: flex;
4+ padding: 4px;
5+ font-size: 110%;
6+ margin: 4px 0;
7+ background: rgba(255, 255, 255, 0.22);
8+ border-radius: 5px;
9+ position: relative
10+ text-decoration: none
11+ transition: background-color 0.2s
12+
13+ background-repeat: no-repeat
14+ background-position: right
15+
16+ -connected {
17+ background-image: svg(connected)
18+ }
19+
20+ @svg connected {
21+ width: 20px
22+ height: 12px
23+ content: "<circle cx='6' stroke='none' fill='green' cy='6' r='5' />"
24+ }
25+
26+ :hover {
27+ background-color: rgba(255, 255, 255, 0.4);
28+ }
29+
30+ div.avatar {
31+ img {
32+ width: 40px
33+ height: 40px
34+ }
35+ }
36+
37+ div.main {
38+ display: flex
39+ flex-direction: column
40+ flex: 1
41+ margin-left: 10px
42+ justify-content: center
43+ div.name {
44+ font-weight: bold
45+ font-size: 110%
46+ color: #333
47+ -webkit-mask-image: linear-gradient(90deg, rgba(0,0,0,1) 90%, rgba(0,0,0,0))
48+ }
49+ }
50+ }
51+}
old_styles/split-view.mcssView
@@ -1,0 +1,28 @@
1+SplitView {
2+ display: flex
3+ flex: 3
4+ div.main {
5+ display: flex
6+ flex-direction: column
7+ flex: 1
8+ }
9+ div.side {
10+ min-width: 280px;
11+ padding: 20px;
12+ background: linear-gradient(100deg, #cee4ef, #efebeb);
13+ border-right: 1px solid #dcdcdc;
14+ overflow-y: auto;
15+
16+ h2 {
17+ margin-top: 20px
18+ margin-bottom: 8px
19+ color: #527b90;
20+ text-shadow: 0px 0px 3px #fff;
21+ font-weight: normal;
22+ span.sub {
23+ font-weight: normal
24+ font-size: 90%
25+ }
26+ }
27+ }
28+}

Built with git-ssb-web