Commit d092b707950e5fda7d051ad6913a8cbd99c22955
main feed with rollups and sidebar using patchcore!
Matt McKegg committed on 2/14/2017, 5:47:55 AMParent: 8397cee5bd7b17b9f2ebf584813ed0c0a035023d
Files changed
lib/window.js | ||
---|---|---|
@@ -13,9 +13,9 @@ | ||
13 | 13 | window.webContents.on('dom-ready', function () { |
14 | 14 | window.webContents.executeJavaScript(` |
15 | 15 | var electron = require('electron') |
16 | 16 | var rootView = require(${JSON.stringify(path)}) |
17 | - var h = require('../lib/h') | |
17 | + var h = require('mutant/h') | |
18 | 18 | |
19 | 19 | require('../lib/context-menu') |
20 | 20 | electron.webFrame.setZoomLevelLimits(1, 1) |
21 | 21 |
lib/feed-summary.js | ||
---|---|---|
@@ -1,194 +1,0 @@ | ||
1 | -var pull = require('pull-stream') | |
2 | -var pullPushable = require('pull-pushable') | |
3 | -var pullNext = require('pull-next') | |
4 | -var SortedArray = require('sorted-array-functions') | |
5 | - | |
6 | -module.exports = FeedSummary | |
7 | - | |
8 | -function FeedSummary (source, opts, cb) { | |
9 | - var bumpFilter = opts && opts.bumpFilter | |
10 | - var windowSize = opts && opts.windowSize || 1000 | |
11 | - var last = null | |
12 | - var returned = false | |
13 | - var done = false | |
14 | - return pullNext(() => { | |
15 | - if (!done) { | |
16 | - var next = {reverse: true, limit: windowSize, live: false} | |
17 | - if (last) { | |
18 | - next.lt = last.timestamp || last.value.sequence | |
19 | - } | |
20 | - var pushable = pullPushable() | |
21 | - pull( | |
22 | - source(next), | |
23 | - pull.collect((err, values) => { | |
24 | - if (err) throw err | |
25 | - if (!values.length) { | |
26 | - done = true | |
27 | - pushable.end() | |
28 | - if (!returned) cb && cb() | |
29 | - returned = true | |
30 | - } else { | |
31 | - var fromTime = last && last.timestamp || Date.now() | |
32 | - last = values[values.length - 1] | |
33 | - groupMessages(values, fromTime, bumpFilter, (err, result) => { | |
34 | - if (err) throw err | |
35 | - result.forEach(v => pushable.push(v)) | |
36 | - pushable.end() | |
37 | - if (!returned) cb && cb() | |
38 | - returned = true | |
39 | - }) | |
40 | - } | |
41 | - }) | |
42 | - ) | |
43 | - } | |
44 | - return pushable | |
45 | - }) | |
46 | -} | |
47 | - | |
48 | -function groupMessages (messages, fromTime, bumpFilter, cb) { | |
49 | - var follows = {} | |
50 | - var messageUpdates = {} | |
51 | - reverseForEach(messages, function (msg) { | |
52 | - if (!msg.value) return | |
53 | - var c = msg.value.content | |
54 | - if (c.type === 'contact') { | |
55 | - updateContact(msg, follows) | |
56 | - } else if (c.type === 'vote') { | |
57 | - if (c.vote && c.vote.link) { | |
58 | - // only show digs of posts added in the current window | |
59 | - // and only for the main post | |
60 | - const group = messageUpdates[c.vote.link] | |
61 | - if (group) { | |
62 | - if (c.vote.value > 0) { | |
63 | - group.lastUpdateType = 'dig' | |
64 | - group.digs.add(msg.value.author) | |
65 | - // only bump when filter passes | |
66 | - if (!bumpFilter || bumpFilter(msg, group)) { | |
67 | - group.updated = msg.timestamp | |
68 | - } | |
69 | - } else { | |
70 | - group.digs.delete(msg.value.author) | |
71 | - if (group.lastUpdateType === 'dig' && !group.digs.size && !group.replies.length) { | |
72 | - group.lastUpdateType = 'reply' | |
73 | - } | |
74 | - } | |
75 | - } | |
76 | - } | |
77 | - } else { | |
78 | - if (c.root || (c.type === 'git-update' && c.repo)) { | |
79 | - const group = ensureMessage(c.root || c.repo, messageUpdates) | |
80 | - group.fromTime = fromTime | |
81 | - group.lastUpdateType = 'reply' | |
82 | - group.repliesFrom.add(msg.value.author) | |
83 | - SortedArray.add(group.replies, msg, compareUserTimestamp) | |
84 | - //group.replies.push(msg) | |
85 | - group.channel = group.channel || msg.value.content.channel | |
86 | - | |
87 | - // only bump when filter passes | |
88 | - if (!bumpFilter || bumpFilter(msg, group)) { | |
89 | - group.updated = msg.timestamp || msg.value.sequence | |
90 | - } | |
91 | - } else { | |
92 | - const group = ensureMessage(msg.key, messageUpdates) | |
93 | - group.fromTime = fromTime | |
94 | - group.lastUpdateType = 'post' | |
95 | - group.updated = msg.timestamp || msg.value.sequence | |
96 | - group.author = msg.value.author | |
97 | - group.channel = msg.value.content.channel | |
98 | - group.message = msg | |
99 | - } | |
100 | - } | |
101 | - }, () => { | |
102 | - var result = [] | |
103 | - Object.keys(follows).forEach((key) => { | |
104 | - if (follows[key].updated) { | |
105 | - SortedArray.add(result, follows[key], compareUpdated) | |
106 | - } | |
107 | - }) | |
108 | - Object.keys(messageUpdates).forEach((key) => { | |
109 | - if (messageUpdates[key].updated) { | |
110 | - SortedArray.add(result, messageUpdates[key], compareUpdated) | |
111 | - } | |
112 | - }) | |
113 | - cb(null, result) | |
114 | - }) | |
115 | -} | |
116 | - | |
117 | -function compareUpdated (a, b) { | |
118 | - return b.updated - a.updated | |
119 | -} | |
120 | - | |
121 | -function reverseForEach (items, fn, cb) { | |
122 | - var i = items.length - 1 | |
123 | - nextBatch() | |
124 | - | |
125 | - function nextBatch () { | |
126 | - var start = Date.now() | |
127 | - while (i >= 0) { | |
128 | - fn(items[i], i) | |
129 | - i -= 1 | |
130 | - if (Date.now() - start > 10) break | |
131 | - } | |
132 | - | |
133 | - if (i > 0) { | |
134 | - setImmediate(nextBatch) | |
135 | - } else { | |
136 | - cb && cb() | |
137 | - } | |
138 | - } | |
139 | -} | |
140 | - | |
141 | -function updateContact (msg, groups) { | |
142 | - var c = msg.value.content | |
143 | - var id = msg.value.author | |
144 | - var group = groups[id] | |
145 | - if (c.contact) { | |
146 | - if (c.following) { | |
147 | - if (!group) { | |
148 | - group = groups[id] = { | |
149 | - type: 'follow', | |
150 | - lastUpdateType: null, | |
151 | - contacts: new Set(), | |
152 | - updated: 0, | |
153 | - author: id, | |
154 | - id: id | |
155 | - } | |
156 | - } | |
157 | - group.contacts.add(c.contact) | |
158 | - group.updated = msg.timestamp || msg.value.sequence | |
159 | - } else { | |
160 | - if (group) { | |
161 | - group.contacts.delete(c.contact) | |
162 | - if (!group.contacts.size) { | |
163 | - delete groups[id] | |
164 | - } | |
165 | - } | |
166 | - } | |
167 | - } | |
168 | -} | |
169 | - | |
170 | -function ensureMessage (id, groups) { | |
171 | - var group = groups[id] | |
172 | - if (!group) { | |
173 | - group = groups[id] = { | |
174 | - type: 'message', | |
175 | - repliesFrom: new Set(), | |
176 | - replies: [], | |
177 | - message: null, | |
178 | - messageId: id, | |
179 | - digs: new Set(), | |
180 | - updated: 0 | |
181 | - } | |
182 | - } | |
183 | - return group | |
184 | -} | |
185 | - | |
186 | -function compareUserTimestamp (a, b) { | |
187 | - var isClose = !a.timestamp || !b.timestamp || Math.abs(a.timestamp - b.timestamp) < (10 * 60e3) | |
188 | - if (isClose) { | |
189 | - // recieved close together, use provided timestamps | |
190 | - return a.value.timestamp - b.value.timestamp | |
191 | - } else { | |
192 | - return a.timestamp - b.timestamp | |
193 | - } | |
194 | -} |
lib/h.js | ||
---|---|---|
@@ -1,1 +1,0 @@ | ||
1 | -module.exports = require('micro-css/h')(require('mutant/html-element')) |
lib/once-true.js | ||
---|---|---|
@@ -1,17 +1,0 @@ | ||
1 | -var watch = require('mutant/watch') | |
2 | -module.exports = function onceTrue (value, fn) { | |
3 | - var done = false | |
4 | - var release = watch(value, (v) => { | |
5 | - if (v && !done) { | |
6 | - done = true | |
7 | - setImmediate(doRelease) | |
8 | - fn(v) | |
9 | - } | |
10 | - }, { nextTick: true }) | |
11 | - | |
12 | - return release | |
13 | - | |
14 | - function doRelease () { | |
15 | - release() | |
16 | - } | |
17 | -} |
lib/pull-scroll.js | ||
---|---|---|
@@ -1,126 +1,0 @@ | ||
1 | -// FROM: https://raw.githubusercontent.com/dominictarr/pull-scroll/master/index.js | |
2 | -var pull = require('pull-stream') | |
3 | -var Pause = require('pull-pause') | |
4 | -var isVisible = require('is-visible').isVisible | |
5 | - | |
6 | -var next = 'undefined' === typeof setImmediate ? setTimeout : setImmediate | |
7 | - | |
8 | -function isBottom (scroller, buffer) { | |
9 | - var rect = scroller.getBoundingClientRect() | |
10 | - var topmax = scroller.scrollTopMax || (scroller.scrollHeight - rect.height) | |
11 | - return scroller.scrollTop >= | |
12 | - + ((topmax) - (buffer || 0)) | |
13 | -} | |
14 | - | |
15 | -function isTop (scroller, buffer) { | |
16 | - return scroller.scrollTop <= (buffer || 0) | |
17 | -} | |
18 | - | |
19 | -function isFilled(content) { | |
20 | - return ( | |
21 | - !isVisible(content) | |
22 | - //check if the scroller is not visible. | |
23 | - // && content.getBoundingClientRect().height == 0 | |
24 | - //and has children. if there are no children, | |
25 | - //it might be size zero because it hasn't started yet. | |
26 | -// && | |
27 | - && content.children.length > 20 | |
28 | - //&& !isVisible(scroller) | |
29 | - ) | |
30 | -} | |
31 | - | |
32 | -function isEnd(scroller, buffer, top) { | |
33 | - //if the element is display none, don't read anything into it. | |
34 | - return (top ? isTop : isBottom)(scroller, buffer) | |
35 | -} | |
36 | - | |
37 | -function append(scroller, list, el, top, sticky) { | |
38 | - if(!el) return | |
39 | - var s = scroller.scrollHeight | |
40 | - if(top && list.firstChild) | |
41 | - list.insertBefore(el, list.firstChild) | |
42 | - else | |
43 | - list.appendChild(el) | |
44 | - | |
45 | - //scroll down by the height of the thing added. | |
46 | - //if it added to the top (in non-sticky mode) | |
47 | - //or added it to the bottom (in sticky mode) | |
48 | - if(top !== sticky) { | |
49 | - var st = list.scrollTop, d = (scroller.scrollHeight - s) + 1 | |
50 | - scroller.scrollTop = scroller.scrollTop + d | |
51 | - } | |
52 | -} | |
53 | - | |
54 | -function overflow (el) { | |
55 | - return el.style.overflowY || el.style.overflow || (function () { | |
56 | - var style = getComputedStyle(el) | |
57 | - return style.overflowY || el.style.overflow | |
58 | - })() | |
59 | -} | |
60 | - | |
61 | -var buffer = 1000 | |
62 | -module.exports = function Scroller(scroller, content, render, top, sticky, cb) { | |
63 | - //if second argument is a function, | |
64 | - //it means the scroller and content elements are the same. | |
65 | - if('function' === typeof content) { | |
66 | - cb = sticky | |
67 | - top = render | |
68 | - render = content | |
69 | - content = scroller | |
70 | - } | |
71 | - | |
72 | - if(!cb) cb = function (err) { if(err) throw err } | |
73 | - | |
74 | - var f = overflow(scroller) | |
75 | - if(!/auto|scroll/.test(f)) | |
76 | - throw new Error('scroller.style.overflowY must be scroll or auto, was:' + f + '!') | |
77 | - scroller.addEventListener('scroll', scroll) | |
78 | - var pause = Pause(function () {}), queue = [] | |
79 | - | |
80 | - //apply some changes to the dom, but ensure that | |
81 | - //`element` is at the same place on screen afterwards. | |
82 | - | |
83 | - function add () { | |
84 | - if(queue.length) | |
85 | - append(scroller, content, render(queue.shift()), top, sticky) | |
86 | - } | |
87 | - | |
88 | - function scroll (ev) { | |
89 | - if (isEnd(scroller, buffer, top) || isFilled(content)) { | |
90 | - pause.resume() | |
91 | - add() | |
92 | - } | |
93 | - } | |
94 | - | |
95 | - // pause.pause() | |
96 | - // | |
97 | - // //wait until the scroller has been added to the document | |
98 | - // next(function next () { | |
99 | - // if(scroller.parentElement) pause.resume() | |
100 | - // else setTimeout(next, 100) | |
101 | - // }) | |
102 | - | |
103 | - var stream = pull( | |
104 | - pause, | |
105 | - pull.drain(function (e) { | |
106 | - queue.push(e) | |
107 | - //we don't know the scroll bar positions if it's display none | |
108 | - //so we have to wait until it becomes visible again. | |
109 | - if(!isVisible(content)) { | |
110 | - if(content.children.length < 20) add() | |
111 | - } | |
112 | - else if(isEnd(scroller, buffer, top)) add() | |
113 | - | |
114 | - if(queue.length > 10) pause.pause() | |
115 | - }, function (err) { | |
116 | - scroller.removeEventListener('scroll', scroll) | |
117 | - if(err) console.error(err) | |
118 | - cb ? cb(err) : console.error(err) | |
119 | - }) | |
120 | - ) | |
121 | - | |
122 | - stream.visible = add | |
123 | - | |
124 | - return stream | |
125 | - | |
126 | -} |
main-window.js | ||
---|---|---|
@@ -1,32 +1,227 @@ | ||
1 | +var combine = require('depject') | |
2 | +var entry = require('depject/entry') | |
3 | +var electron = require('electron') | |
4 | +var h = require('mutant/h') | |
5 | +var Value = require('mutant/value') | |
6 | +var when = require('mutant/when') | |
7 | +var computed = require('mutant/computed') | |
8 | +var toCollection = require('mutant/dict-to-collection') | |
9 | +var MutantDict = require('mutant/dict') | |
10 | +var MutantMap = require('mutant/map') | |
11 | +var Url = require('url') | |
1 | 12 | var insertCss = require('insert-css') |
13 | +var nest = require('depnest') | |
2 | 14 | |
3 | 15 | module.exports = function (config) { |
4 | - var modules = require('depject')( | |
16 | + var sockets = combine( | |
5 | 17 | overrideConfig(config), |
6 | - require('patchbay/modules_extra'), | |
7 | - require('patchbay/modules_basic'), | |
8 | - require('patchbay/modules_core'), | |
9 | - require('./modules') | |
18 | + require('./modules'), | |
19 | + require('./plugs'), | |
20 | + require('patchcore') | |
10 | 21 | ) |
22 | + var api = entry(sockets, nest({ | |
23 | + 'page.html.render': 'first' | |
24 | + })) | |
11 | 25 | |
12 | - process.nextTick(() => { | |
13 | - insertCss(modules.styles[0]() + require('./styles')) | |
26 | + var renderPage = api.page.html.render | |
27 | + | |
28 | + var searchTimer = null | |
29 | + var searchBox = h('input.search', { | |
30 | + type: 'search', | |
31 | + placeholder: 'word, @key, #channel' | |
14 | 32 | }) |
15 | 33 | |
16 | - return modules.app[0]() | |
17 | -} | |
34 | + searchBox.oninput = function () { | |
35 | + clearTimeout(searchTimer) | |
36 | + searchTimer = setTimeout(doSearch, 500) | |
37 | + } | |
18 | 38 | |
19 | -function overrideConfig (config) { | |
20 | - return { | |
21 | - config: { | |
22 | - gives: {'config': true}, | |
23 | - create: function (api) { | |
24 | - return { | |
25 | - config () { | |
26 | - return config | |
27 | - } | |
28 | - } | |
39 | + searchBox.onfocus = function () { | |
40 | + if (searchBox.value) { | |
41 | + doSearch() | |
42 | + } | |
43 | + } | |
44 | + | |
45 | + var forwardHistory = [] | |
46 | + var backHistory = [] | |
47 | + | |
48 | + var views = MutantDict({ | |
49 | + // preload tabs (and subscribe to update notifications) | |
50 | + '/public': renderPage('/public'), | |
51 | + // '/private': renderPage('/private'), | |
52 | + // [ssbClient.id]: renderPage(ssbClient.id), | |
53 | + // '/notifications': renderPage('/notifications') | |
54 | + }) | |
55 | + | |
56 | + var lastViewed = {} | |
57 | + | |
58 | + // delete cached view after 30 mins of last seeing | |
59 | + setInterval(() => { | |
60 | + views.keys().forEach((view) => { | |
61 | + if (lastViewed[view] !== true && Date.now() - lastViewed[view] > (30 * 60e3) && view !== currentView()) { | |
62 | + views.delete(view) | |
29 | 63 | } |
64 | + }) | |
65 | + }, 60e3) | |
66 | + | |
67 | + var canGoForward = Value(false) | |
68 | + var canGoBack = Value(false) | |
69 | + var currentView = Value('/public') | |
70 | + | |
71 | + var mainElement = h('div.main', MutantMap(toCollection(views), (item) => { | |
72 | + return h('div.view', { | |
73 | + hidden: computed([item.key, currentView], (a, b) => a !== b) | |
74 | + }, [ item.value ]) | |
75 | + })) | |
76 | + | |
77 | + insertCss(require('./styles')) | |
78 | + | |
79 | + return h('div', { | |
80 | + classList: `MainWindow -${process.platform}`, | |
81 | + events: { | |
82 | + click: catchLinks | |
30 | 83 | } |
84 | + }, [ | |
85 | + h('div.top', [ | |
86 | + h('span.history', [ | |
87 | + h('a', { | |
88 | + 'ev-click': goBack, | |
89 | + classList: [ when(canGoBack, '-active') ] | |
90 | + }, '<'), | |
91 | + h('a', { | |
92 | + 'ev-click': goForward, | |
93 | + classList: [ when(canGoForward, '-active') ] | |
94 | + }, '>') | |
95 | + ]), | |
96 | + h('span.nav', [ | |
97 | + tab('Public', '/public'), | |
98 | + //tab('Private', '/private') | |
99 | + ]), | |
100 | + h('span.appTitle', ['Patchwork']), | |
101 | + h('span', [ searchBox ]), | |
102 | + h('span.nav', [ | |
103 | + // tab('Profile', ssbClient.id), | |
104 | + // tab('Mentions', '/notifications') | |
105 | + ]) | |
106 | + ]), | |
107 | + mainElement | |
108 | + ]) | |
109 | + | |
110 | + // scoped | |
111 | + | |
112 | + function catchLinks (ev) { | |
113 | + if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.defaultPrevented) { | |
114 | + return true | |
115 | + } | |
116 | + | |
117 | + var anchor = null | |
118 | + for (var n = ev.target; n.parentNode; n = n.parentNode) { | |
119 | + if (n.nodeName === 'A') { | |
120 | + anchor = n | |
121 | + break | |
122 | + } | |
123 | + } | |
124 | + if (!anchor) return true | |
125 | + | |
126 | + var href = anchor.getAttribute('href') | |
127 | + | |
128 | + if (href) { | |
129 | + var url = Url.parse(href) | |
130 | + if (url.host) { | |
131 | + electron.shell.openUrl(href) | |
132 | + } else if (href !== '#') { | |
133 | + setView(href) | |
134 | + } | |
135 | + } | |
136 | + | |
137 | + ev.preventDefault() | |
138 | + ev.stopPropagation() | |
31 | 139 | } |
140 | + | |
141 | + function tab (name, view) { | |
142 | + var instance = views.get(view) | |
143 | + lastViewed[view] = true | |
144 | + return h('a', { | |
145 | + 'ev-click': function (ev) { | |
146 | + if (instance.pendingUpdates && instance.pendingUpdates() && instance.reload) { | |
147 | + instance.reload() | |
148 | + } | |
149 | + }, | |
150 | + href: view, | |
151 | + classList: [ | |
152 | + when(selected(view), '-selected') | |
153 | + ] | |
154 | + }, [ | |
155 | + name, | |
156 | + when(instance.pendingUpdates, [ | |
157 | + ' (', instance.pendingUpdates, ')' | |
158 | + ]) | |
159 | + ]) | |
160 | + } | |
161 | + | |
162 | + function goBack () { | |
163 | + if (backHistory.length) { | |
164 | + canGoForward.set(true) | |
165 | + forwardHistory.push(currentView()) | |
166 | + currentView.set(backHistory.pop()) | |
167 | + canGoBack.set(backHistory.length > 0) | |
168 | + } | |
169 | + } | |
170 | + | |
171 | + function goForward () { | |
172 | + if (forwardHistory.length) { | |
173 | + backHistory.push(currentView()) | |
174 | + currentView.set(forwardHistory.pop()) | |
175 | + canGoForward.set(forwardHistory.length > 0) | |
176 | + canGoBack.set(true) | |
177 | + } | |
178 | + } | |
179 | + | |
180 | + function setView (view) { | |
181 | + if (!views.has(view)) { | |
182 | + views.put(view, renderPage(view)) | |
183 | + } | |
184 | + | |
185 | + if (lastViewed[view] !== true) { | |
186 | + lastViewed[view] = Date.now() | |
187 | + } | |
188 | + | |
189 | + if (currentView() && lastViewed[currentView()] !== true) { | |
190 | + lastViewed[currentView()] = Date.now() | |
191 | + } | |
192 | + | |
193 | + if (view !== currentView()) { | |
194 | + canGoForward.set(false) | |
195 | + canGoBack.set(true) | |
196 | + forwardHistory.length = 0 | |
197 | + backHistory.push(currentView()) | |
198 | + currentView.set(view) | |
199 | + } | |
200 | + } | |
201 | + | |
202 | + function doSearch () { | |
203 | + var value = searchBox.value.trim() | |
204 | + if (value.startsWith('/') || value.startsWith('?') || value.startsWith('@') || value.startsWith('#') || value.startsWith('%')) { | |
205 | + setView(value) | |
206 | + } else if (value.trim()) { | |
207 | + setView(`?${value.trim()}`) | |
208 | + } else { | |
209 | + setView('/public') | |
210 | + } | |
211 | + } | |
212 | + | |
213 | + function selected (view) { | |
214 | + return computed([currentView, view], (currentView, view) => { | |
215 | + return currentView === view | |
216 | + }) | |
217 | + } | |
32 | 218 | } |
219 | + | |
220 | +function overrideConfig (config) { | |
221 | + return [{ | |
222 | + gives: nest('config.sync.load'), | |
223 | + create: function (api) { | |
224 | + return nest('config.sync.load', () => config) | |
225 | + } | |
226 | + }] | |
227 | +} |
modules/app.js | ||
---|---|---|
@@ -1,194 +1,0 @@ | ||
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/obs/recent.js | ||
---|---|---|
@@ -1,0 +1,41 @@ | ||
1 | +var nest = require('depnest') | |
2 | +var { Value, Dict, Struct, computed } = require('mutant') | |
3 | + | |
4 | +exports.gives = nest({ | |
5 | + 'sbot.hook.feed': true, | |
6 | + 'channel.obs.recent': true | |
7 | +}) | |
8 | + | |
9 | +exports.create = function (api) { | |
10 | + var channelsLookup = Dict() | |
11 | + | |
12 | + var recentChannels = computed(channelsLookup, (lookup) => { | |
13 | + var values = Object.keys(lookup).map(x => lookup[x]).sort((a, b) => b.updatedAt - a.updatedAt) | |
14 | + return values | |
15 | + }, {nextTick: true}) | |
16 | + | |
17 | + return nest({ | |
18 | + 'sbot.hook.feed': (msg) => { | |
19 | + if (msg.key && msg.value && msg.value.content) { | |
20 | + var c = msg.value.content | |
21 | + if (c.type === 'post' && typeof c.channel === 'string') { | |
22 | + var name = c.channel.trim() | |
23 | + if (name) { | |
24 | + var channel = channelsLookup.get(name) | |
25 | + if (!channel) { | |
26 | + channel = Struct({ | |
27 | + id: name, | |
28 | + updatedAt: Value() | |
29 | + }) | |
30 | + channelsLookup.put(name, channel) | |
31 | + } | |
32 | + if (channel.updatedAt() < msg.timestamp) { | |
33 | + channel.updatedAt.set(msg.timestamp) | |
34 | + } | |
35 | + } | |
36 | + } | |
37 | + } | |
38 | + }, | |
39 | + 'channel.obs.recent': () => recentChannels | |
40 | + }) | |
41 | +} |
modules/channel/obs/subscribed.js | ||
---|---|---|
@@ -1,0 +1,65 @@ | ||
1 | +var pull = require('pull-stream') | |
2 | +var computed = require('mutant/computed') | |
3 | +var MutantPullReduce = require('../../../lib/mutant-pull-reduce') | |
4 | +var nest = require('depnest') | |
5 | + | |
6 | +var throttle = require('mutant/throttle') | |
7 | + | |
8 | +exports.needs = nest({ | |
9 | + 'sbot.pull.userFeed': 'first' | |
10 | +}) | |
11 | + | |
12 | +exports.gives = nest({ | |
13 | + 'channel.obs': ['subscribed'] | |
14 | +}) | |
15 | + | |
16 | +exports.create = function (api) { | |
17 | + var cache = {} | |
18 | + | |
19 | + return nest({ | |
20 | + 'channel.obs': {subscribed} | |
21 | + }) | |
22 | + | |
23 | + function subscribed (userId) { | |
24 | + if (cache[userId]) { | |
25 | + return cache[userId] | |
26 | + } else { | |
27 | + var stream = pull( | |
28 | + api.sbot.pull.userFeed({id: userId, live: true}), | |
29 | + pull.filter((msg) => { | |
30 | + return !msg.value || msg.value.content.type === 'channel' | |
31 | + }) | |
32 | + ) | |
33 | + | |
34 | + var result = MutantPullReduce(stream, (result, msg) => { | |
35 | + var c = msg.value.content | |
36 | + if (typeof c.channel === 'string' && c.channel) { | |
37 | + var channel = c.channel.trim() | |
38 | + if (channel) { | |
39 | + if (typeof c.subscribed === 'boolean') { | |
40 | + if (c.subscribed) { | |
41 | + result.add(channel) | |
42 | + } else { | |
43 | + result.delete(channel) | |
44 | + } | |
45 | + } | |
46 | + } | |
47 | + } | |
48 | + return result | |
49 | + }, { | |
50 | + startValue: new Set(), | |
51 | + nextTick: true | |
52 | + }) | |
53 | + | |
54 | + var instance = throttle(result, 2000) | |
55 | + instance.sync = result.sync | |
56 | + | |
57 | + instance.has = function (value) { | |
58 | + return computed(instance, x => x.has(value)) | |
59 | + } | |
60 | + | |
61 | + cache[userId] = instance | |
62 | + return instance | |
63 | + } | |
64 | + } | |
65 | +} |
modules/cache.js | ||
---|---|---|
@@ -1,65 +1,0 @@ | ||
1 | -var Value = require('mutant/value') | |
2 | -var computed = require('mutant/computed') | |
3 | -var MutantDict = require('mutant/dict') | |
4 | -var MutantStruct = require('mutant/struct') | |
5 | - | |
6 | -exports.gives = { | |
7 | - cache: { | |
8 | - update_from: true, | |
9 | - get_likes: true, | |
10 | - obs_channels: true | |
11 | - } | |
12 | -} | |
13 | - | |
14 | -exports.create = function (api) { | |
15 | - var likesLookup = {} | |
16 | - var channelsLookup = MutantDict() | |
17 | - | |
18 | - var obs_channels = computed(channelsLookup, (lookup) => { | |
19 | - var values = Object.keys(lookup).map(x => lookup[x]).sort((a, b) => b.updatedAt - a.updatedAt) | |
20 | - return values | |
21 | - }, {nextTick: true}) | |
22 | - | |
23 | - return { | |
24 | - cache: { | |
25 | - get_likes, obs_channels, update_from | |
26 | - } | |
27 | - } | |
28 | - | |
29 | - function get_likes (msgId) { | |
30 | - if (!likesLookup[msgId]) { | |
31 | - likesLookup[msgId] = Value({}) | |
32 | - } | |
33 | - return likesLookup[msgId] | |
34 | - } | |
35 | - | |
36 | - function update_from (msg) { | |
37 | - if (msg.key && msg.value && msg.value.content) { | |
38 | - var c = msg.value.content | |
39 | - if (c.type === 'vote') { | |
40 | - if (msg.value.content.vote && msg.value.content.vote.link) { | |
41 | - var likes = get_likes(msg.value.content.vote.link)() | |
42 | - if (!likes[msg.value.author] || likes[msg.value.author][1] < msg.timestamp) { | |
43 | - likes[msg.value.author] = [msg.value.content.vote.value > 0, msg.timestamp] | |
44 | - get_likes(msg.value.content.vote.link).set(likes) | |
45 | - } | |
46 | - } | |
47 | - } else if (c.type === 'post' && typeof c.channel === 'string') { | |
48 | - var name = c.channel.trim() | |
49 | - if (name) { | |
50 | - var channel = channelsLookup.get(name) | |
51 | - if (!channel) { | |
52 | - channel = MutantStruct({ | |
53 | - id: name, | |
54 | - updatedAt: Value() | |
55 | - }) | |
56 | - channelsLookup.put(name, channel) | |
57 | - } | |
58 | - if (channel.updatedAt() < msg.timestamp) { | |
59 | - channel.updatedAt.set(msg.timestamp) | |
60 | - } | |
61 | - } | |
62 | - } | |
63 | - } | |
64 | - } | |
65 | -} |
modules/feed/html/rollup.js | ||
---|---|---|
@@ -1,0 +1,230 @@ | ||
1 | +var Value = require('mutant/value') | |
2 | +var when = require('mutant/when') | |
3 | +var computed = require('mutant/computed') | |
4 | +var h = require('mutant/h') | |
5 | +var MutantArray = require('mutant/array') | |
6 | +var Abortable = require('pull-abortable') | |
7 | +var pull = require('pull-stream') | |
8 | +var nest = require('depnest') | |
9 | + | |
10 | +var onceTrue = require('mutant/once-true') | |
11 | +var Scroller = require('pull-scroll') | |
12 | + | |
13 | +exports.needs = nest({ | |
14 | + 'message.html': { | |
15 | + render: 'first', | |
16 | + link: 'first' | |
17 | + }, | |
18 | + 'sbot.async.get': 'first', | |
19 | + 'keys.sync.id': 'first', | |
20 | + feed: { | |
21 | + 'html.rollup': 'first', | |
22 | + 'pull.summary': 'first' | |
23 | + }, | |
24 | + profile: { | |
25 | + 'html.person': 'first', | |
26 | + 'html.manyPeople': 'first', | |
27 | + 'obs.names': 'first' | |
28 | + } | |
29 | +}) | |
30 | + | |
31 | +exports.gives = nest({ | |
32 | + 'feed.html': ['rollup'] | |
33 | +}) | |
34 | + | |
35 | +exports.create = function (api) { | |
36 | + return nest({ | |
37 | + 'feed.html': { rollup } | |
38 | + }) | |
39 | + function rollup (getStream, opts) { | |
40 | + var sync = Value(false) | |
41 | + var updates = Value(0) | |
42 | + | |
43 | + var filter = opts && opts.filter | |
44 | + var bumpFilter = opts && opts.bumpFilter | |
45 | + var windowSize = opts && opts.windowSize | |
46 | + var waitFor = opts && opts.waitFor || true | |
47 | + | |
48 | + var updateLoader = h('a', { | |
49 | + className: 'Notifier -loader', | |
50 | + href: '#', | |
51 | + 'ev-click': refresh | |
52 | + }, [ | |
53 | + 'Show ', | |
54 | + h('strong', [updates]), ' ', | |
55 | + when(computed(updates, a => a === 1), 'update', 'updates') | |
56 | + ]) | |
57 | + | |
58 | + var content = h('section.content') | |
59 | + | |
60 | + var container = h('Scroller', { style: { overflow: 'auto' } }, [ | |
61 | + h('div.wrapper', [ | |
62 | + h('section.prepend', opts.prepend), | |
63 | + content | |
64 | + ]) | |
65 | + ]) | |
66 | + | |
67 | + setTimeout(refresh, 10) | |
68 | + | |
69 | + onceTrue(waitFor, () => { | |
70 | + pull( | |
71 | + getStream({old: false}), | |
72 | + pull.drain((item) => { | |
73 | + var type = item && item.value && item.value.content.type | |
74 | + if (type && type !== 'vote') { | |
75 | + if (item.value && item.value.author === api.keys.sync.id() && !updates()) { | |
76 | + return refresh() | |
77 | + } | |
78 | + if (filter) { | |
79 | + var update = (item.value.content.type === 'post' && item.value.content.root) ? { | |
80 | + type: 'message', | |
81 | + messageId: item.value.content.root, | |
82 | + channel: item.value.content.channel | |
83 | + } : { | |
84 | + type: 'message', | |
85 | + author: item.value.author, | |
86 | + channel: item.value.content.channel, | |
87 | + messageId: item.key | |
88 | + } | |
89 | + | |
90 | + ensureAuthor(update, (err, update) => { | |
91 | + if (!err) { | |
92 | + if (filter(update)) { | |
93 | + updates.set(updates() + 1) | |
94 | + } | |
95 | + } | |
96 | + }) | |
97 | + } else { | |
98 | + updates.set(updates() + 1) | |
99 | + } | |
100 | + } | |
101 | + }) | |
102 | + ) | |
103 | + }) | |
104 | + | |
105 | + var abortLastFeed = null | |
106 | + | |
107 | + var result = MutantArray([ | |
108 | + when(updates, updateLoader), | |
109 | + container | |
110 | + //when(sync, container, h('div', {className: 'Loading -large'})) | |
111 | + ]) | |
112 | + | |
113 | + result.reload = refresh | |
114 | + result.pendingUpdates = updates | |
115 | + | |
116 | + return result | |
117 | + | |
118 | + // scoped | |
119 | + | |
120 | + function refresh () { | |
121 | + if (abortLastFeed) { | |
122 | + abortLastFeed() | |
123 | + } | |
124 | + updates.set(0) | |
125 | + sync.set(false) | |
126 | + content.innerHTML = '' | |
127 | + | |
128 | + var abortable = Abortable() | |
129 | + abortLastFeed = abortable.abort | |
130 | + | |
131 | + pull( | |
132 | + api.feed.pull.summary(getStream, {windowSize, bumpFilter}, () => { | |
133 | + sync.set(true) | |
134 | + }), | |
135 | + pull.asyncMap(ensureAuthor), | |
136 | + pull.filter((item) => { | |
137 | + if (filter) { | |
138 | + return filter(item) | |
139 | + } else { | |
140 | + return true | |
141 | + } | |
142 | + }), | |
143 | + abortable, | |
144 | + Scroller(container, content, renderItem, false, false) | |
145 | + ) | |
146 | + } | |
147 | + } | |
148 | + | |
149 | + function ensureAuthor (item, cb) { | |
150 | + if (item.type === 'message' && !item.message) { | |
151 | + api.sbot.async.get(item.messageId, (_, value) => { | |
152 | + if (value) { | |
153 | + item.author = value.author | |
154 | + } | |
155 | + cb(null, item) | |
156 | + }) | |
157 | + } else { | |
158 | + cb(null, item) | |
159 | + } | |
160 | + } | |
161 | + | |
162 | + function renderItem (item) { | |
163 | + if (item.type === 'message') { | |
164 | + var meta = null | |
165 | + var previousId = item.messageId | |
166 | + var replies = item.replies.slice(-4).map((msg) => { | |
167 | + var result = api.message.html.render(msg, {inContext: true, inSummary: true, previousId}) | |
168 | + previousId = msg.key | |
169 | + return result | |
170 | + }) | |
171 | + var renderedMessage = item.message ? api.message.html.render(item.message, {inContext: true}) : null | |
172 | + if (renderedMessage) { | |
173 | + if (item.lastUpdateType === 'reply' && item.repliesFrom.size) { | |
174 | + meta = h('div.meta', { | |
175 | + title: api.profile.obs.names(item.repliesFrom) | |
176 | + }, [ | |
177 | + api.profile.html.manyPeople(item.repliesFrom), ' replied' | |
178 | + ]) | |
179 | + } else if (item.lastUpdateType === 'dig' && item.digs.size) { | |
180 | + meta = h('div.meta', { | |
181 | + title: api.profile.obs.names(item.digs) | |
182 | + }, [ | |
183 | + api.profile.html.manyPeople(item.digs), ' dug this message' | |
184 | + ]) | |
185 | + } | |
186 | + | |
187 | + return h('div', {className: 'FeedEvent'}, [ | |
188 | + meta, | |
189 | + renderedMessage, | |
190 | + when(replies.length, [ | |
191 | + when(item.replies.length > replies.length, | |
192 | + h('a.full', {href: `#${item.messageId}`}, ['View full thread']) | |
193 | + ), | |
194 | + h('div.replies', replies) | |
195 | + ]) | |
196 | + ]) | |
197 | + } else { | |
198 | + if (item.lastUpdateType === 'reply' && item.repliesFrom.size) { | |
199 | + meta = h('div.meta', { | |
200 | + title: api.profile.obs.names(item.repliesFrom) | |
201 | + }, [ | |
202 | + api.profile.html.manyPeople(item.repliesFrom), ' replied to ', api.message.html.link(item.messageId) | |
203 | + ]) | |
204 | + } else if (item.lastUpdateType === 'dig' && item.digs.size) { | |
205 | + meta = h('div.meta', { | |
206 | + title: api.profile.obs.names(item.digs) | |
207 | + }, [ | |
208 | + api.profile.html.manyPeople(item.digs), ' dug ', api.message.html.link(item.messageId) | |
209 | + ]) | |
210 | + } | |
211 | + | |
212 | + if (meta || replies.length) { | |
213 | + return h('div', {className: 'FeedEvent'}, [ | |
214 | + meta, h('div.replies', replies) | |
215 | + ]) | |
216 | + } | |
217 | + } | |
218 | + } else if (item.type === 'follow') { | |
219 | + return h('div', {className: 'FeedEvent -follow'}, [ | |
220 | + h('div.meta', { | |
221 | + title: api.profile.obs.names(item.contacts) | |
222 | + }, [ | |
223 | + api.profile.html.person(item.id), ' followed ', api.profile.html.manyPeople(item.contacts) | |
224 | + ]) | |
225 | + ]) | |
226 | + } | |
227 | + | |
228 | + return h('div') | |
229 | + } | |
230 | +} |
modules/feed/pull/summary.js | ||
---|---|---|
@@ -1,0 +1,204 @@ | ||
1 | +var pull = require('pull-stream') | |
2 | +var pullPushable = require('pull-pushable') | |
3 | +var pullNext = require('pull-next') | |
4 | +var SortedArray = require('sorted-array-functions') | |
5 | +var nest = require('depnest') | |
6 | + | |
7 | +exports.gives = nest({ | |
8 | + 'feed.pull': [ 'summary' ] | |
9 | +}) | |
10 | + | |
11 | +exports.create = function () { | |
12 | + return nest({ | |
13 | + 'feed.pull': { summary } | |
14 | + }) | |
15 | +} | |
16 | + | |
17 | +function summary (source, opts, cb) { | |
18 | + var bumpFilter = opts && opts.bumpFilter | |
19 | + var windowSize = opts && opts.windowSize || 1000 | |
20 | + var last = null | |
21 | + var returned = false | |
22 | + var done = false | |
23 | + return pullNext(() => { | |
24 | + if (!done) { | |
25 | + var next = {reverse: true, limit: windowSize, live: false} | |
26 | + if (last) { | |
27 | + next.lt = last.timestamp || last.value.sequence | |
28 | + } | |
29 | + var pushable = pullPushable() | |
30 | + pull( | |
31 | + source(next), | |
32 | + pull.collect((err, values) => { | |
33 | + if (err) throw err | |
34 | + if (!values.length) { | |
35 | + done = true | |
36 | + pushable.end() | |
37 | + if (!returned) cb && cb() | |
38 | + returned = true | |
39 | + } else { | |
40 | + var fromTime = last && last.timestamp || Date.now() | |
41 | + last = values[values.length - 1] | |
42 | + groupMessages(values, fromTime, bumpFilter, (err, result) => { | |
43 | + if (err) throw err | |
44 | + result.forEach(v => pushable.push(v)) | |
45 | + pushable.end() | |
46 | + if (!returned) cb && cb() | |
47 | + returned = true | |
48 | + }) | |
49 | + } | |
50 | + }) | |
51 | + ) | |
52 | + } | |
53 | + return pushable | |
54 | + }) | |
55 | +} | |
56 | + | |
57 | +function groupMessages (messages, fromTime, bumpFilter, cb) { | |
58 | + var follows = {} | |
59 | + var messageUpdates = {} | |
60 | + reverseForEach(messages, function (msg) { | |
61 | + if (!msg.value) return | |
62 | + var c = msg.value.content | |
63 | + if (c.type === 'contact') { | |
64 | + updateContact(msg, follows) | |
65 | + } else if (c.type === 'vote') { | |
66 | + if (c.vote && c.vote.link) { | |
67 | + // only show digs of posts added in the current window | |
68 | + // and only for the main post | |
69 | + const group = messageUpdates[c.vote.link] | |
70 | + if (group) { | |
71 | + if (c.vote.value > 0) { | |
72 | + group.lastUpdateType = 'dig' | |
73 | + group.digs.add(msg.value.author) | |
74 | + // only bump when filter passes | |
75 | + if (!bumpFilter || bumpFilter(msg, group)) { | |
76 | + group.updated = msg.timestamp | |
77 | + } | |
78 | + } else { | |
79 | + group.digs.delete(msg.value.author) | |
80 | + if (group.lastUpdateType === 'dig' && !group.digs.size && !group.replies.length) { | |
81 | + group.lastUpdateType = 'reply' | |
82 | + } | |
83 | + } | |
84 | + } | |
85 | + } | |
86 | + } else { | |
87 | + if (c.root || (c.type === 'git-update' && c.repo)) { | |
88 | + const group = ensureMessage(c.root || c.repo, messageUpdates) | |
89 | + group.fromTime = fromTime | |
90 | + group.lastUpdateType = 'reply' | |
91 | + group.repliesFrom.add(msg.value.author) | |
92 | + SortedArray.add(group.replies, msg, compareUserTimestamp) | |
93 | + //group.replies.push(msg) | |
94 | + group.channel = group.channel || msg.value.content.channel | |
95 | + | |
96 | + // only bump when filter passes | |
97 | + if (!bumpFilter || bumpFilter(msg, group)) { | |
98 | + group.updated = msg.timestamp || msg.value.sequence | |
99 | + } | |
100 | + } else { | |
101 | + const group = ensureMessage(msg.key, messageUpdates) | |
102 | + group.fromTime = fromTime | |
103 | + group.lastUpdateType = 'post' | |
104 | + group.updated = msg.timestamp || msg.value.sequence | |
105 | + group.author = msg.value.author | |
106 | + group.channel = msg.value.content.channel | |
107 | + group.message = msg | |
108 | + group.boxed = typeof msg.value.content === 'string' | |
109 | + } | |
110 | + } | |
111 | + }, () => { | |
112 | + var result = [] | |
113 | + Object.keys(follows).forEach((key) => { | |
114 | + if (follows[key].updated) { | |
115 | + SortedArray.add(result, follows[key], compareUpdated) | |
116 | + } | |
117 | + }) | |
118 | + Object.keys(messageUpdates).forEach((key) => { | |
119 | + if (messageUpdates[key].updated) { | |
120 | + SortedArray.add(result, messageUpdates[key], compareUpdated) | |
121 | + } | |
122 | + }) | |
123 | + cb(null, result) | |
124 | + }) | |
125 | +} | |
126 | + | |
127 | +function compareUpdated (a, b) { | |
128 | + return b.updated - a.updated | |
129 | +} | |
130 | + | |
131 | +function reverseForEach (items, fn, cb) { | |
132 | + var i = items.length - 1 | |
133 | + nextBatch() | |
134 | + | |
135 | + function nextBatch () { | |
136 | + var start = Date.now() | |
137 | + while (i >= 0) { | |
138 | + fn(items[i], i) | |
139 | + i -= 1 | |
140 | + if (Date.now() - start > 10) break | |
141 | + } | |
142 | + | |
143 | + if (i > 0) { | |
144 | + setImmediate(nextBatch) | |
145 | + } else { | |
146 | + cb && cb() | |
147 | + } | |
148 | + } | |
149 | +} | |
150 | + | |
151 | +function updateContact (msg, groups) { | |
152 | + var c = msg.value.content | |
153 | + var id = msg.value.author | |
154 | + var group = groups[id] | |
155 | + if (c.contact) { | |
156 | + if (c.following) { | |
157 | + if (!group) { | |
158 | + group = groups[id] = { | |
159 | + type: 'follow', | |
160 | + lastUpdateType: null, | |
161 | + contacts: new Set(), | |
162 | + updated: 0, | |
163 | + author: id, | |
164 | + id: id | |
165 | + } | |
166 | + } | |
167 | + group.contacts.add(c.contact) | |
168 | + group.updated = msg.timestamp || msg.value.sequence | |
169 | + } else { | |
170 | + if (group) { | |
171 | + group.contacts.delete(c.contact) | |
172 | + if (!group.contacts.size) { | |
173 | + delete groups[id] | |
174 | + } | |
175 | + } | |
176 | + } | |
177 | + } | |
178 | +} | |
179 | + | |
180 | +function ensureMessage (id, groups) { | |
181 | + var group = groups[id] | |
182 | + if (!group) { | |
183 | + group = groups[id] = { | |
184 | + type: 'message', | |
185 | + repliesFrom: new Set(), | |
186 | + replies: [], | |
187 | + message: null, | |
188 | + messageId: id, | |
189 | + digs: new Set(), | |
190 | + updated: 0 | |
191 | + } | |
192 | + } | |
193 | + return group | |
194 | +} | |
195 | + | |
196 | +function compareUserTimestamp (a, b) { | |
197 | + var isClose = !a.timestamp || !b.timestamp || Math.abs(a.timestamp - b.timestamp) < (10 * 60e3) | |
198 | + if (isClose) { | |
199 | + // recieved close together, use provided timestamps | |
200 | + return a.value.timestamp - b.value.timestamp | |
201 | + } else { | |
202 | + return a.timestamp - b.timestamp | |
203 | + } | |
204 | +} |
modules/feed-summary.js | ||
---|---|---|
@@ -1,220 +1,0 @@ | ||
1 | -var Value = require('mutant/value') | |
2 | -var when = require('mutant/when') | |
3 | -var computed = require('mutant/computed') | |
4 | -var MutantArray = require('mutant/array') | |
5 | -var Abortable = require('pull-abortable') | |
6 | -var onceTrue = require('../lib/once-true') | |
7 | -var pull = require('pull-stream') | |
8 | - | |
9 | -var Scroller = require('../lib/pull-scroll') | |
10 | -var FeedSummary = require('../lib/feed-summary') | |
11 | - | |
12 | -var h = require('../lib/h') | |
13 | - | |
14 | -exports.needs = { | |
15 | - message: { | |
16 | - render: 'first', | |
17 | - link: 'first' | |
18 | - }, | |
19 | - sbot: { | |
20 | - get: 'first', | |
21 | - get_id: 'first' | |
22 | - }, | |
23 | - helpers: { | |
24 | - build_scroller: 'first' | |
25 | - }, | |
26 | - person: 'first', | |
27 | - many_people: 'first', | |
28 | - people_names: 'first' | |
29 | -} | |
30 | - | |
31 | -exports.gives = { | |
32 | - feed_summary: true | |
33 | -} | |
34 | - | |
35 | -exports.create = function (api) { | |
36 | - return { | |
37 | - feed_summary (getStream, prepend, opts) { | |
38 | - var sync = Value(false) | |
39 | - var updates = Value(0) | |
40 | - | |
41 | - var filter = opts && opts.filter | |
42 | - var bumpFilter = opts && opts.bumpFilter | |
43 | - var windowSize = opts && opts.windowSize | |
44 | - var waitFor = opts && opts.waitFor || true | |
45 | - | |
46 | - var updateLoader = h('a Notifier -loader', { | |
47 | - href: '#', | |
48 | - 'ev-click': refresh | |
49 | - }, [ | |
50 | - 'Show ', | |
51 | - h('strong', [updates]), ' ', | |
52 | - when(computed(updates, a => a === 1), 'update', 'updates') | |
53 | - ]) | |
54 | - | |
55 | - var { container, content } = api.helpers.build_scroller({ prepend }) | |
56 | - | |
57 | - setTimeout(refresh, 10) | |
58 | - | |
59 | - onceTrue(waitFor, () => { | |
60 | - pull( | |
61 | - getStream({old: false}), | |
62 | - pull.drain((item) => { | |
63 | - var type = item && item.value && item.value.content.type | |
64 | - if (type && type !== 'vote') { | |
65 | - if (item.value && item.value.author === api.sbot.get_id() && !updates()) { | |
66 | - return refresh() | |
67 | - } | |
68 | - if (filter) { | |
69 | - var update = (item.value.content.type === 'post' && item.value.content.root) ? { | |
70 | - type: 'message', | |
71 | - messageId: item.value.content.root, | |
72 | - channel: item.value.content.channel | |
73 | - } : { | |
74 | - type: 'message', | |
75 | - author: item.value.author, | |
76 | - channel: item.value.content.channel, | |
77 | - messageId: item.key | |
78 | - } | |
79 | - | |
80 | - ensureAuthor(update, (err, update) => { | |
81 | - if (!err) { | |
82 | - if (filter(update)) { | |
83 | - updates.set(updates() + 1) | |
84 | - } | |
85 | - } | |
86 | - }) | |
87 | - } else { | |
88 | - updates.set(updates() + 1) | |
89 | - } | |
90 | - } | |
91 | - }) | |
92 | - ) | |
93 | - }) | |
94 | - | |
95 | - var abortLastFeed = null | |
96 | - | |
97 | - var result = MutantArray([ | |
98 | - when(updates, updateLoader), | |
99 | - when(sync, container, h('Loading -large')) | |
100 | - ]) | |
101 | - | |
102 | - result.reload = refresh | |
103 | - result.pendingUpdates = updates | |
104 | - | |
105 | - return result | |
106 | - | |
107 | - // scoped | |
108 | - | |
109 | - function refresh () { | |
110 | - if (abortLastFeed) { | |
111 | - abortLastFeed() | |
112 | - } | |
113 | - updates.set(0) | |
114 | - sync.set(false) | |
115 | - content.innerHTML = '' | |
116 | - | |
117 | - var abortable = Abortable() | |
118 | - abortLastFeed = abortable.abort | |
119 | - | |
120 | - pull( | |
121 | - FeedSummary(getStream, {windowSize, bumpFilter}, () => { | |
122 | - sync.set(true) | |
123 | - }), | |
124 | - pull.asyncMap(ensureAuthor), | |
125 | - pull.filter((item) => { | |
126 | - if (filter) { | |
127 | - return filter(item) | |
128 | - } else { | |
129 | - return true | |
130 | - } | |
131 | - }), | |
132 | - abortable, | |
133 | - Scroller(container, content, renderItem, false, false) | |
134 | - ) | |
135 | - } | |
136 | - } | |
137 | - } | |
138 | - | |
139 | - function ensureAuthor (item, cb) { | |
140 | - if (item.type === 'message' && !item.message) { | |
141 | - api.sbot.get(item.messageId, (_, value) => { | |
142 | - if (value) { | |
143 | - item.author = value.author | |
144 | - } | |
145 | - cb(null, item) | |
146 | - }) | |
147 | - } else { | |
148 | - cb(null, item) | |
149 | - } | |
150 | - } | |
151 | - | |
152 | - function renderItem (item) { | |
153 | - if (item.type === 'message') { | |
154 | - var meta = null | |
155 | - var previousId = item.messageId | |
156 | - var replies = item.replies.slice(-4).map((msg) => { | |
157 | - var result = api.message.render(msg, {inContext: true, inSummary: true, previousId}) | |
158 | - previousId = msg.key | |
159 | - return result | |
160 | - }) | |
161 | - var renderedMessage = item.message ? api.message.render(item.message, {inContext: true}) : null | |
162 | - if (renderedMessage) { | |
163 | - if (item.lastUpdateType === 'reply' && item.repliesFrom.size) { | |
164 | - meta = h('div.meta', { | |
165 | - title: api.people_names(item.repliesFrom) | |
166 | - }, [ | |
167 | - api.many_people(item.repliesFrom), ' replied' | |
168 | - ]) | |
169 | - } else if (item.lastUpdateType === 'dig' && item.digs.size) { | |
170 | - meta = h('div.meta', { | |
171 | - title: api.people_names(item.digs) | |
172 | - }, [ | |
173 | - api.many_people(item.digs), ' dug this message' | |
174 | - ]) | |
175 | - } | |
176 | - | |
177 | - return h('FeedEvent', [ | |
178 | - meta, | |
179 | - renderedMessage, | |
180 | - when(replies.length, [ | |
181 | - when(item.replies.length > replies.length, | |
182 | - h('a.full', {href: `#${item.messageId}`}, ['View full thread']) | |
183 | - ), | |
184 | - h('div.replies', replies) | |
185 | - ]) | |
186 | - ]) | |
187 | - } else { | |
188 | - if (item.lastUpdateType === 'reply' && item.repliesFrom.size) { | |
189 | - meta = h('div.meta', { | |
190 | - title: api.people_names(item.repliesFrom) | |
191 | - }, [ | |
192 | - api.many_people(item.repliesFrom), ' replied to ', api.message.link(item.messageId) | |
193 | - ]) | |
194 | - } else if (item.lastUpdateType === 'dig' && item.digs.size) { | |
195 | - meta = h('div.meta', { | |
196 | - title: api.people_names(item.digs) | |
197 | - }, [ | |
198 | - api.many_people(item.digs), ' dug ', api.message.link(item.messageId) | |
199 | - ]) | |
200 | - } | |
201 | - | |
202 | - if (meta || replies.length) { | |
203 | - return h('FeedEvent', [ | |
204 | - meta, h('div.replies', replies) | |
205 | - ]) | |
206 | - } | |
207 | - } | |
208 | - } else if (item.type === 'follow') { | |
209 | - return h('FeedEvent -follow', [ | |
210 | - h('div.meta', { | |
211 | - title: api.people_names(item.contacts) | |
212 | - }, [ | |
213 | - api.person(item.id), ' followed ', api.many_people(item.contacts) | |
214 | - ]) | |
215 | - ]) | |
216 | - } | |
217 | - | |
218 | - return h('div') | |
219 | - } | |
220 | -} |
modules/helpers/blob-url.js | ||
---|---|---|
@@ -1,22 +1,0 @@ | ||
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.js | ||
---|---|---|
@@ -1,30 +1,0 @@ | ||
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/page/html/render/notifications.js |
---|
modules/page/html/render/private.js |
---|
modules/page/html/render/profile.js |
---|
modules/page/html/render/public.js | ||
---|---|---|
@@ -1,0 +1,249 @@ | ||
1 | +var nest = require('depnest') | |
2 | +var { h, send, when, computed, map } = require('mutant') | |
3 | +var extend = require('xtend') | |
4 | +var pull = require('pull-stream') | |
5 | + | |
6 | +exports.gives = nest({ | |
7 | + 'page.html.render': true | |
8 | +}) | |
9 | + | |
10 | +exports.needs = nest({ | |
11 | + sbot: { | |
12 | + pull: { | |
13 | + log: 'first', | |
14 | + feed: 'first', | |
15 | + userFeed: 'first' | |
16 | + }, | |
17 | + async: { | |
18 | + publish: 'first' | |
19 | + }, | |
20 | + obs: { | |
21 | + connectedPeers: 'first', | |
22 | + localPeers: 'first' | |
23 | + } | |
24 | + }, | |
25 | + 'about.html.image': 'first', | |
26 | + 'about.obs.name': 'first', | |
27 | + | |
28 | + 'feed.html.rollup': 'first', | |
29 | + 'profile.obs': { | |
30 | + following: 'first', | |
31 | + recentlyUpdated: 'first' | |
32 | + }, | |
33 | + 'channel.obs': { | |
34 | + subscribed: 'first', | |
35 | + recent: 'first' | |
36 | + }, | |
37 | + 'keys.sync.id': 'first' | |
38 | +}) | |
39 | + | |
40 | +exports.create = function (api) { | |
41 | + return nest('page.html.render', page) | |
42 | + | |
43 | + function page (path) { | |
44 | + if (path !== '/public') return // "/" is a sigil for "page" | |
45 | + | |
46 | + var id = api.keys.sync.id() | |
47 | + var following = api.profile.obs.following(id) | |
48 | + var subscribedChannels = api.channel.obs.subscribed(id) | |
49 | + var loading = computed(subscribedChannels.sync, x => !x) | |
50 | + var channels = computed(api.channel.obs.recent(), items => items.slice(0, 8), {comparer: arrayEq}) | |
51 | + var connectedPeers = api.sbot.obs.connectedPeers() | |
52 | + var localPeers = api.sbot.obs.localPeers() | |
53 | + var connectedPubs = computed([connectedPeers, localPeers], (c, l) => c.filter(x => !l.includes(x))) | |
54 | + | |
55 | + var oldest = Date.now() - (2 * 24 * 60 * 60e3) | |
56 | + getFirstMessage(id, (_, msg) => { | |
57 | + if (msg) { | |
58 | + // fall back to timestamp stream before this, give 48 hrs for feeds to stabilize | |
59 | + if (msg.value.timestamp > oldest) { | |
60 | + oldest = Date.now() | |
61 | + } | |
62 | + } | |
63 | + }) | |
64 | + | |
65 | + return h('div.SplitView', [ | |
66 | + h('div.side', [ | |
67 | + getSidebar() | |
68 | + ]), | |
69 | + h('div.main', [ | |
70 | + getFeedView() | |
71 | + ]) | |
72 | + ]) | |
73 | + | |
74 | + function getFeedView () { | |
75 | + return api.feed.html.rollup(getFeed, { | |
76 | + waitUntil: computed([ | |
77 | + following.sync, | |
78 | + subscribedChannels.sync | |
79 | + ], (...x) => x.every(Boolean)), | |
80 | + windowSize: 500, | |
81 | + filter: (item) => { | |
82 | + return !item.boxed && ( | |
83 | + id === item.author || | |
84 | + following().has(item.author) || | |
85 | + subscribedChannels().has(item.channel) || | |
86 | + (item.repliesFrom && item.repliesFrom.has(id)) || | |
87 | + item.digs && item.digs.has(id) | |
88 | + ) | |
89 | + }, | |
90 | + bumpFilter: (msg, group) => { | |
91 | + if (!group.message) { | |
92 | + return ( | |
93 | + isMentioned(id, msg.value.content.mentions) || | |
94 | + msg.value.author === id || ( | |
95 | + fromDay(msg, group.fromTime) && ( | |
96 | + following().has(msg.value.author) || | |
97 | + group.repliesFrom.has(id) | |
98 | + ) | |
99 | + ) | |
100 | + ) | |
101 | + } | |
102 | + return true | |
103 | + } | |
104 | + }) | |
105 | + } | |
106 | + | |
107 | + function getSidebar () { | |
108 | + var whoToFollow = computed([following, api.profile.obs.recentlyUpdated(200)], (following, recent) => { | |
109 | + return Array.from(recent).filter(x => x !== id && !following.has(x)).slice(0, 10) | |
110 | + }) | |
111 | + return [ | |
112 | + h('h2', 'Active Channels'), | |
113 | + when(loading, [ h('Loading') ]), | |
114 | + h('div', { | |
115 | + classList: 'ChannelList', | |
116 | + hidden: loading | |
117 | + }, [ | |
118 | + map(channels, (channel) => { | |
119 | + var subscribed = subscribedChannels.has(channel.id) | |
120 | + return h('a.channel', { | |
121 | + href: `##${channel.id}`, | |
122 | + classList: [ | |
123 | + when(subscribed, '-subscribed') | |
124 | + ] | |
125 | + }, [ | |
126 | + h('span.name', '#' + channel.id), | |
127 | + when(subscribed, | |
128 | + h('a -unsubscribe', { | |
129 | + 'ev-click': send(unsubscribe, channel.id) | |
130 | + }, 'Unsubscribe'), | |
131 | + h('a -subscribe', { | |
132 | + 'ev-click': send(subscribe, channel.id) | |
133 | + }, 'Subscribe') | |
134 | + ) | |
135 | + ]) | |
136 | + }, {maxTime: 5}) | |
137 | + ]), | |
138 | + | |
139 | + when(computed(localPeers, x => x.length), h('h2', 'Local')), | |
140 | + h('div', { | |
141 | + classList: 'ProfileList' | |
142 | + }, [ | |
143 | + map(localPeers, (id) => { | |
144 | + return h('a.profile', { | |
145 | + classList: [ | |
146 | + when(computed([connectedPeers, id], (p, id) => p.includes(id)), '-connected') | |
147 | + ], | |
148 | + href: `#${id}` | |
149 | + }, [ | |
150 | + h('div.avatar', [api.about.html.image(id)]), | |
151 | + h('div.main', [ | |
152 | + h('div.name', [ api.about.obs.name(id) ]) | |
153 | + ]) | |
154 | + ]) | |
155 | + }) | |
156 | + ]), | |
157 | + | |
158 | + when(computed(whoToFollow, x => x.length), h('h2', 'Who to follow')), | |
159 | + h('div', { | |
160 | + classList: 'ProfileList' | |
161 | + }, [ | |
162 | + map(whoToFollow, (id) => { | |
163 | + return h('a.profile', { | |
164 | + href: `#${id}` | |
165 | + }, [ | |
166 | + h('div.avatar', [api.about.html.image(id)]), | |
167 | + h('div.main', [ | |
168 | + h('div.name', [ api.about.obs.name(id) ]) | |
169 | + ]) | |
170 | + ]) | |
171 | + }) | |
172 | + ]), | |
173 | + | |
174 | + when(computed(connectedPubs, x => x.length), h('h2', 'Connected Pubs')), | |
175 | + h('div', { | |
176 | + classList: 'ProfileList' | |
177 | + }, [ | |
178 | + map(connectedPubs, (id) => { | |
179 | + return h('a.profile', { | |
180 | + classList: [ '-connected' ], | |
181 | + href: `#${id}` | |
182 | + }, [ | |
183 | + h('div.avatar', [api.about.html.image(id)]), | |
184 | + h('div.main', [ | |
185 | + h('div.name', [ api.about.obs.name(id) ]) | |
186 | + ]) | |
187 | + ]) | |
188 | + }) | |
189 | + ]) | |
190 | + ] | |
191 | + } | |
192 | + | |
193 | + function getFeed (opts) { | |
194 | + if (opts.lt && opts.lt < oldest) { | |
195 | + opts = extend(opts, {lt: parseInt(opts.lt, 10)}) | |
196 | + return pull( | |
197 | + api.sbot.pull.feed(opts), | |
198 | + pull.map((msg) => { | |
199 | + if (msg.sync) { | |
200 | + return msg | |
201 | + } else { | |
202 | + return {key: msg.key, value: msg.value, timestamp: msg.value.timestamp} | |
203 | + } | |
204 | + }) | |
205 | + ) | |
206 | + } else { | |
207 | + return api.sbot.pull.log(opts) | |
208 | + } | |
209 | + } | |
210 | + | |
211 | + function getFirstMessage (feedId, cb) { | |
212 | + api.sbot.pull.userFeed({id: feedId, gte: 0, limit: 1})(null, cb) | |
213 | + } | |
214 | + | |
215 | + function subscribe (id) { | |
216 | + api.sbot.async.publish({ | |
217 | + type: 'channel', | |
218 | + channel: id, | |
219 | + subscribed: true | |
220 | + }) | |
221 | + } | |
222 | + | |
223 | + function unsubscribe (id) { | |
224 | + api.sbot.async.publish({ | |
225 | + type: 'channel', | |
226 | + channel: id, | |
227 | + subscribed: false | |
228 | + }) | |
229 | + } | |
230 | + } | |
231 | +} | |
232 | + | |
233 | +function isMentioned (id, list) { | |
234 | + if (Array.isArray(list)) { | |
235 | + return list.includes(id) | |
236 | + } else { | |
237 | + return false | |
238 | + } | |
239 | +} | |
240 | + | |
241 | +function fromDay (msg, fromTime) { | |
242 | + return (fromTime - msg.timestamp) < (24 * 60 * 60e3) | |
243 | +} | |
244 | + | |
245 | +function arrayEq (a, b) { | |
246 | + if (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a !== b) { | |
247 | + return a.every((value, i) => value === b[i]) | |
248 | + } | |
249 | +} |
modules/profile/html/manyPeople.js | ||
---|---|---|
@@ -1,0 +1,43 @@ | ||
1 | +var nest = require('depnest') | |
2 | + | |
3 | +exports.needs = nest({ | |
4 | + 'profile.html.person': 'first' | |
5 | +}) | |
6 | + | |
7 | +exports.gives = nest({ | |
8 | + 'profile.html': [ 'manyPeople' ] | |
9 | +}) | |
10 | + | |
11 | +exports.create = function (api) { | |
12 | + return nest({ | |
13 | + 'profile.html': { manyPeople } | |
14 | + }) | |
15 | + | |
16 | + function manyPeople (ids) { | |
17 | + ids = Array.from(ids) | |
18 | + var featuredIds = ids.slice(-3).reverse() | |
19 | + | |
20 | + if (ids.length) { | |
21 | + if (ids.length > 3) { | |
22 | + return [ | |
23 | + api.profile.html.person(featuredIds[0]), ', ', | |
24 | + api.profile.html.person(featuredIds[1]), | |
25 | + ' and ', ids.length - 2, ' others' | |
26 | + ] | |
27 | + } else if (ids.length === 3) { | |
28 | + return [ | |
29 | + api.profile.html.person(featuredIds[0]), ', ', | |
30 | + api.profile.html.person(featuredIds[1]), ' and ', | |
31 | + api.profile.html.person(featuredIds[2]) | |
32 | + ] | |
33 | + } else if (ids.length === 2) { | |
34 | + return [ | |
35 | + api.profile.html.person(featuredIds[0]), ' and ', | |
36 | + api.profile.html.person(featuredIds[1]) | |
37 | + ] | |
38 | + } else { | |
39 | + return api.profile.html.person(featuredIds[0]) | |
40 | + } | |
41 | + } | |
42 | + } | |
43 | +} |
modules/profile/html/person.js | ||
---|---|---|
@@ -1,0 +1,22 @@ | ||
1 | +var nest = require('depnest') | |
2 | +var h = require('mutant/h') | |
3 | + | |
4 | +exports.needs = nest({ | |
5 | + 'about.obs.name': 'first' | |
6 | +}) | |
7 | + | |
8 | +exports.gives = nest({ | |
9 | + 'profile.html': ['person'] | |
10 | +}) | |
11 | + | |
12 | +exports.create = function (api) { | |
13 | + return nest({ | |
14 | + 'profile.html': {person} | |
15 | + }) | |
16 | + | |
17 | + function person (id) { | |
18 | + return h('a', {classList: 'ProfileLink', href: id}, [ | |
19 | + '@', api.about.obs.name(id) | |
20 | + ]) | |
21 | + } | |
22 | +} |
modules/profile/obs/following.js | ||
---|---|---|
@@ -1,0 +1,61 @@ | ||
1 | +var pull = require('pull-stream') | |
2 | +var computed = require('mutant/computed') | |
3 | +var MutantPullReduce = require('../../../lib/mutant-pull-reduce') | |
4 | +var throttle = require('mutant/throttle') | |
5 | +var nest = require('depnest') | |
6 | + | |
7 | +exports.needs = nest({ | |
8 | + 'sbot.pull.userFeed': 'first' | |
9 | +}) | |
10 | + | |
11 | +exports.gives = nest({ | |
12 | + 'profile.obs': ['following'] | |
13 | +}) | |
14 | + | |
15 | +exports.create = function (api) { | |
16 | + var cache = {} | |
17 | + | |
18 | + return nest({ | |
19 | + 'profile.obs': {following} | |
20 | + }) | |
21 | + | |
22 | + function following (userId) { | |
23 | + if (cache[userId]) { | |
24 | + return cache[userId] | |
25 | + } else { | |
26 | + var stream = pull( | |
27 | + api.sbot.pull.userFeed({id: userId, live: true}), | |
28 | + pull.filter((msg) => { | |
29 | + return !msg.value || msg.value.content.type === 'contact' | |
30 | + }) | |
31 | + ) | |
32 | + | |
33 | + var result = MutantPullReduce(stream, (result, msg) => { | |
34 | + var c = msg.value.content | |
35 | + if (c.contact) { | |
36 | + if (typeof c.following === 'boolean') { | |
37 | + if (c.following) { | |
38 | + result.add(c.contact) | |
39 | + } else { | |
40 | + result.delete(c.contact) | |
41 | + } | |
42 | + } | |
43 | + } | |
44 | + return result | |
45 | + }, { | |
46 | + startValue: new Set(), | |
47 | + nextTick: true | |
48 | + }) | |
49 | + | |
50 | + var instance = throttle(result, 2000) | |
51 | + instance.sync = result.sync | |
52 | + | |
53 | + instance.has = function (value) { | |
54 | + return computed(instance, x => x.has(value)) | |
55 | + } | |
56 | + | |
57 | + cache[userId] = instance | |
58 | + return instance | |
59 | + } | |
60 | + } | |
61 | +} |
modules/profile/obs/names.js | ||
---|---|---|
@@ -1,0 +1,24 @@ | ||
1 | +var nest = require('depnest') | |
2 | +var computed = require('mutant/computed') | |
3 | + | |
4 | +exports.needs = nest({ | |
5 | + 'about.obs.name': 'first' | |
6 | +}) | |
7 | + | |
8 | +exports.gives = nest({ | |
9 | + 'profile.obs': [ 'names' ] | |
10 | +}) | |
11 | + | |
12 | +exports.create = function (api) { | |
13 | + return nest({ | |
14 | + 'profile.obs': { names } | |
15 | + }) | |
16 | + | |
17 | + function names (ids) { | |
18 | + return computed(Array.from(ids).map(api.about.obs.name), join) || '' | |
19 | + } | |
20 | +} | |
21 | + | |
22 | +function join (...args) { | |
23 | + return args.join('\n') | |
24 | +} |
modules/profile/obs/recently-updated.js | ||
---|---|---|
@@ -1,0 +1,43 @@ | ||
1 | +var pull = require('pull-stream') | |
2 | +var pullCat = require('pull-cat') | |
3 | +var computed = require('mutant/computed') | |
4 | +var MutantPullReduce = require('../../../lib/mutant-pull-reduce') | |
5 | +var throttle = require('mutant/throttle') | |
6 | +var nest = require('depnest') | |
7 | +var hr = 60 * 60 * 1000 | |
8 | + | |
9 | +exports.needs = nest({ | |
10 | + 'sbot.pull.log': 'first' | |
11 | +}) | |
12 | + | |
13 | +exports.gives = nest('profile.obs.recentlyUpdated') | |
14 | + | |
15 | +exports.create = function (api) { | |
16 | + return nest('profile.obs.recentlyUpdated', function (limit) { | |
17 | + var stream = pull( | |
18 | + pullCat([ | |
19 | + api.sbot.pull.log({reverse: true, limit: limit || 500}), | |
20 | + api.sbot.pull.log({old: false}) | |
21 | + ]) | |
22 | + ) | |
23 | + | |
24 | + var result = MutantPullReduce(stream, (result, msg) => { | |
25 | + if (msg.value.timestamp && Date.now() - msg.value.timestamp < 24 * hr) { | |
26 | + result.add(msg.value.author) | |
27 | + } | |
28 | + return result | |
29 | + }, { | |
30 | + startValue: new Set(), | |
31 | + nextTick: true | |
32 | + }) | |
33 | + | |
34 | + var instance = throttle(result, 2000) | |
35 | + instance.sync = result.sync | |
36 | + | |
37 | + instance.has = function (value) { | |
38 | + return computed(instance, x => x.has(value)) | |
39 | + } | |
40 | + | |
41 | + return instance | |
42 | + }) | |
43 | +} |
modules/many-people.js | ||
---|---|---|
@@ -1,39 +1,0 @@ | ||
1 | -exports.needs = { | |
2 | - person: 'first' | |
3 | -} | |
4 | - | |
5 | -exports.gives = { | |
6 | - many_people: true | |
7 | -} | |
8 | - | |
9 | -exports.create = function (api) { | |
10 | - return { | |
11 | - many_people (ids) { | |
12 | - ids = Array.from(ids) | |
13 | - var featuredIds = ids.slice(-3).reverse() | |
14 | - | |
15 | - if (ids.length) { | |
16 | - if (ids.length > 3) { | |
17 | - return [ | |
18 | - api.person(featuredIds[0]), ', ', | |
19 | - api.person(featuredIds[1]), | |
20 | - ' and ', ids.length - 2, ' others' | |
21 | - ] | |
22 | - } else if (ids.length === 3) { | |
23 | - return [ | |
24 | - api.person(featuredIds[0]), ', ', | |
25 | - api.person(featuredIds[1]), ' and ', | |
26 | - api.person(featuredIds[2]) | |
27 | - ] | |
28 | - } else if (ids.length === 2) { | |
29 | - return [ | |
30 | - api.person(featuredIds[0]), ' and ', | |
31 | - api.person(featuredIds[1]) | |
32 | - ] | |
33 | - } else { | |
34 | - return api.person(featuredIds[0]) | |
35 | - } | |
36 | - } | |
37 | - } | |
38 | - } | |
39 | -} |
modules/message/render.js | ||
---|---|---|
@@ -1,137 +1,0 @@ | ||
1 | -var when = require('mutant/when') | |
2 | -var h = require('../../lib/h') | |
3 | -var contextMenu = require('../../lib/context-menu') | |
4 | - | |
5 | -exports.needs = { | |
6 | - message: { | |
7 | - content: 'first', | |
8 | - content_mini: 'first', | |
9 | - link: 'first', | |
10 | - meta: 'map', | |
11 | - main_meta: 'map', | |
12 | - action: 'map' | |
13 | - }, | |
14 | - about: { | |
15 | - image: 'first', | |
16 | - name: 'first', | |
17 | - link: 'first', | |
18 | - name_link: 'first' | |
19 | - } | |
20 | -} | |
21 | - | |
22 | -exports.gives = { | |
23 | - message: { | |
24 | - data_render: true, | |
25 | - render: true | |
26 | - } | |
27 | -} | |
28 | - | |
29 | -exports.create = function (api) { | |
30 | - return { | |
31 | - message: { | |
32 | - data_render (msg) { | |
33 | - var div = h('Message -data', { | |
34 | - 'ev-contextmenu': contextMenu.bind(null, msg) | |
35 | - }, [ | |
36 | - messageHeader(msg), | |
37 | - h('section', [ | |
38 | - h('pre', [ | |
39 | - JSON.stringify(msg, null, 2) | |
40 | - ]) | |
41 | - ]) | |
42 | - ]) | |
43 | - return div | |
44 | - }, | |
45 | - | |
46 | - render (msg, opts) { | |
47 | - opts = opts || {} | |
48 | - var inContext = opts.inContext | |
49 | - var previousId = opts.previousId | |
50 | - var inSummary = opts.inSummary | |
51 | - | |
52 | - var elMini = api.message.content_mini(msg) | |
53 | - var el = api.message.content(msg) | |
54 | - | |
55 | - if (elMini && (!el || inSummary)) { | |
56 | - var div = h('Message', { | |
57 | - 'ev-contextmenu': contextMenu.bind(null, msg) | |
58 | - }, [ | |
59 | - h('header', [ | |
60 | - h('div.mini', [ | |
61 | - api.about.link(msg.value.author, api.about.name(msg.value.author), ''), | |
62 | - ' ', elMini | |
63 | - ]), | |
64 | - h('div.meta', [api.message.main_meta(msg)]) | |
65 | - ]) | |
66 | - ]) | |
67 | - div.setAttribute('tabindex', '0') | |
68 | - return div | |
69 | - } | |
70 | - | |
71 | - if (!el) return | |
72 | - | |
73 | - var classList = [] | |
74 | - var replyInfo = null | |
75 | - | |
76 | - if (msg.value.content.root) { | |
77 | - classList.push('-reply') | |
78 | - if (!inContext) { | |
79 | - replyInfo = h('span', ['in reply to ', api.message.link(msg.value.content.root)]) | |
80 | - } else if (previousId && last(msg.value.content.branch) && previousId !== last(msg.value.content.branch)) { | |
81 | - replyInfo = h('span', ['in reply to ', api.message.link(last(msg.value.content.branch))]) | |
82 | - } | |
83 | - } | |
84 | - | |
85 | - var element = h('Message', { | |
86 | - classList, | |
87 | - 'ev-contextmenu': contextMenu.bind(null, msg), | |
88 | - 'ev-keydown': function (ev) { | |
89 | - // on enter, hit first meta. | |
90 | - if (ev.keyCode === 13) { | |
91 | - element.querySelector('.enter').click() | |
92 | - } | |
93 | - } | |
94 | - }, [ | |
95 | - messageHeader(msg, replyInfo), | |
96 | - h('section', [el]), | |
97 | - when(msg.key, h('footer', [ | |
98 | - h('div.actions', [ | |
99 | - api.message.action(msg) | |
100 | - ]) | |
101 | - ])) | |
102 | - ]) | |
103 | - | |
104 | - // ); hyperscript does not seem to set attributes correctly. | |
105 | - element.setAttribute('tabindex', '0') | |
106 | - | |
107 | - return element | |
108 | - } | |
109 | - } | |
110 | - } | |
111 | - | |
112 | - function messageHeader (msg, replyInfo) { | |
113 | - return h('header', [ | |
114 | - h('div.main', [ | |
115 | - h('a.avatar', {href: `#${msg.value.author}`}, api.about.image(msg.value.author)), | |
116 | - h('div.main', [ | |
117 | - h('div.name', [ | |
118 | - h('a', {href: `#${msg.value.author}`}, api.about.name(msg.value.author)) | |
119 | - ]), | |
120 | - h('div.meta', [ | |
121 | - api.message.main_meta(msg), | |
122 | - ' ', replyInfo | |
123 | - ]) | |
124 | - ]) | |
125 | - ]), | |
126 | - h('div.meta', api.message.meta(msg)) | |
127 | - ]) | |
128 | - } | |
129 | -} | |
130 | - | |
131 | -function last (array) { | |
132 | - if (Array.isArray(array)) { | |
133 | - return array[array.length - 1] | |
134 | - } else { | |
135 | - return array | |
136 | - } | |
137 | -} |
modules/message/timestamp.js | ||
---|---|---|
@@ -1,21 +1,0 @@ | ||
1 | -exports.needs = { | |
2 | - helpers: { | |
3 | - timestamp: 'first' | |
4 | - } | |
5 | -} | |
6 | - | |
7 | -exports.gives = { | |
8 | - message: { | |
9 | - main_meta: true | |
10 | - } | |
11 | -} | |
12 | - | |
13 | -exports.create = function (api) { | |
14 | - return { | |
15 | - message: { | |
16 | - main_meta (msg) { | |
17 | - return api.helpers.timestamp(msg) | |
18 | - } | |
19 | - } | |
20 | - } | |
21 | -} |
modules/obs-connected.js | ||
---|---|---|
@@ -1,41 +1,0 @@ | ||
1 | -var MutantSet = require('mutant/set') | |
2 | - | |
3 | -exports.needs = { | |
4 | - sbot: { | |
5 | - gossip_peers: 'first' | |
6 | - } | |
7 | -} | |
8 | - | |
9 | -exports.gives = { | |
10 | - obs_connected: true | |
11 | -} | |
12 | - | |
13 | -exports.create = function (api) { | |
14 | - var cache = null | |
15 | - | |
16 | - return { | |
17 | - obs_connected () { | |
18 | - if (cache) { | |
19 | - return cache | |
20 | - } else { | |
21 | - var result = MutantSet([], {nextTick: true}) | |
22 | - // todo: make this clean up on unlisten | |
23 | - | |
24 | - refresh() | |
25 | - setInterval(refresh, 10e3) | |
26 | - | |
27 | - cache = result | |
28 | - return result | |
29 | - } | |
30 | - | |
31 | - // scope | |
32 | - | |
33 | - function refresh () { | |
34 | - api.sbot.gossip_peers((err, peers) => { | |
35 | - if (err) throw console.log(err) | |
36 | - result.set(peers.filter(x => x.state === 'connected').map(x => x.key)) | |
37 | - }) | |
38 | - } | |
39 | - } | |
40 | - } | |
41 | -} |
modules/obs-following.js | ||
---|---|---|
@@ -1,61 +1,0 @@ | ||
1 | -var pull = require('pull-stream') | |
2 | -var computed = require('mutant/computed') | |
3 | -var MutantPullReduce = require('../lib/mutant-pull-reduce') | |
4 | -var throttle = require('mutant/throttle') | |
5 | - | |
6 | -exports.needs = { | |
7 | - sbot: { | |
8 | - user_feed: 'first' | |
9 | - } | |
10 | -} | |
11 | - | |
12 | -exports.gives = { | |
13 | - obs_following: true | |
14 | -} | |
15 | - | |
16 | -exports.create = function (api) { | |
17 | - var cache = {} | |
18 | - | |
19 | - return { | |
20 | - obs_following (userId) { | |
21 | - if (cache[userId]) { | |
22 | - return cache[userId] | |
23 | - } else { | |
24 | - var stream = pull( | |
25 | - api.sbot.user_feed({id: userId, live: true}), | |
26 | - pull.filter((msg) => { | |
27 | - return !msg.value || msg.value.content.type === 'contact' | |
28 | - }) | |
29 | - ) | |
30 | - | |
31 | - var result = MutantPullReduce(stream, (result, msg) => { | |
32 | - var c = msg.value.content | |
33 | - if (c.contact) { | |
34 | - if (typeof c.following === 'boolean') { | |
35 | - if (c.following) { | |
36 | - result.add(c.contact) | |
37 | - } else { | |
38 | - result.delete(c.contact) | |
39 | - } | |
40 | - } | |
41 | - } | |
42 | - return result | |
43 | - }, { | |
44 | - startValue: new Set(), | |
45 | - nextTick: true | |
46 | - }) | |
47 | - | |
48 | - var instance = throttle(result, 2000) | |
49 | - instance.sync = result.sync | |
50 | - | |
51 | - instance.has = function (value) { | |
52 | - return computed(instance, x => x.has(value)) | |
53 | - } | |
54 | - | |
55 | - cache[userId] = instance | |
56 | - return instance | |
57 | - } | |
58 | - } | |
59 | - | |
60 | - } | |
61 | -} |
modules/obs-local.js | ||
---|---|---|
@@ -1,43 +1,0 @@ | ||
1 | -var MutantSet = require('mutant/set') | |
2 | -var plugs = require('patchbay/plugs') | |
3 | -var sbot_list_local = plugs.first(exports.sbot_list_local = []) | |
4 | - | |
5 | -exports.needs = { | |
6 | - sbot: { | |
7 | - list_local: 'first' | |
8 | - } | |
9 | -} | |
10 | - | |
11 | -exports.gives = { | |
12 | - obs_local: true | |
13 | -} | |
14 | - | |
15 | -exports.create = function (api) { | |
16 | - var cache = null | |
17 | - | |
18 | - return { | |
19 | - obs_local () { | |
20 | - if (cache) { | |
21 | - return cache | |
22 | - } else { | |
23 | - var result = MutantSet([], {nextTick: true}) | |
24 | - // todo: make this clean up on unlisten | |
25 | - | |
26 | - refresh() | |
27 | - setInterval(refresh, 10e3) | |
28 | - | |
29 | - cache = result | |
30 | - return result | |
31 | - } | |
32 | - | |
33 | - // scope | |
34 | - | |
35 | - function refresh () { | |
36 | - sbot_list_local((err, keys) => { | |
37 | - if (err) throw console.log(err) | |
38 | - result.set(keys) | |
39 | - }) | |
40 | - } | |
41 | - } | |
42 | - } | |
43 | -} |
modules/obs-recently-updated-feeds.js | ||
---|---|---|
@@ -1,48 +1,0 @@ | ||
1 | -var pull = require('pull-stream') | |
2 | -var pullCat = require('pull-cat') | |
3 | -var computed = require('mutant/computed') | |
4 | -var MutantPullReduce = require('../lib/mutant-pull-reduce') | |
5 | -var throttle = require('mutant/throttle') | |
6 | -var hr = 60 * 60 * 1000 | |
7 | - | |
8 | -exports.needs = { | |
9 | - sbot: { | |
10 | - log: 'first' | |
11 | - } | |
12 | -} | |
13 | - | |
14 | -exports.gives = { | |
15 | - obs_recently_updated_feeds: true | |
16 | -} | |
17 | - | |
18 | -exports.create = function (api) { | |
19 | - return { | |
20 | - obs_recently_updated_feeds (limit) { | |
21 | - var stream = pull( | |
22 | - pullCat([ | |
23 | - api.sbot.log({reverse: true, limit: limit || 500}), | |
24 | - api.sbot.log({old: false}) | |
25 | - ]) | |
26 | - ) | |
27 | - | |
28 | - var result = MutantPullReduce(stream, (result, msg) => { | |
29 | - if (msg.value.timestamp && Date.now() - msg.value.timestamp < 24 * hr) { | |
30 | - result.add(msg.value.author) | |
31 | - } | |
32 | - return result | |
33 | - }, { | |
34 | - startValue: new Set(), | |
35 | - nextTick: true | |
36 | - }) | |
37 | - | |
38 | - var instance = throttle(result, 2000) | |
39 | - instance.sync = result.sync | |
40 | - | |
41 | - instance.has = function (value) { | |
42 | - return computed(instance, x => x.has(value)) | |
43 | - } | |
44 | - | |
45 | - return instance | |
46 | - } | |
47 | - } | |
48 | -} |
modules/obs-subscribed-channels.js | ||
---|---|---|
@@ -1,64 +1,0 @@ | ||
1 | -var pull = require('pull-stream') | |
2 | -var computed = require('mutant/computed') | |
3 | -var MutantPullReduce = require('../lib/mutant-pull-reduce') | |
4 | - | |
5 | -var throttle = require('mutant/throttle') | |
6 | - | |
7 | -exports.needs = { | |
8 | - sbot: { | |
9 | - user_feed: 'first' | |
10 | - } | |
11 | -} | |
12 | - | |
13 | -exports.gives = { | |
14 | - obs_subscribed_channels: true | |
15 | -} | |
16 | - | |
17 | -exports.create = function (api) { | |
18 | - var cache = {} | |
19 | - return { | |
20 | - obs_subscribed_channels (userId) { | |
21 | - if (cache[userId]) { | |
22 | - return cache[userId] | |
23 | - } else { | |
24 | - var stream = pull( | |
25 | - api.sbot.user_feed({id: userId, live: true}), | |
26 | - pull.filter((msg) => { | |
27 | - return !msg.value || msg.value.content.type === 'channel' | |
28 | - }) | |
29 | - ) | |
30 | - | |
31 | - var result = MutantPullReduce(stream, (result, msg) => { | |
32 | - var c = msg.value.content | |
33 | - if (typeof c.channel === 'string' && c.channel) { | |
34 | - var channel = c.channel.trim() | |
35 | - if (channel) { | |
36 | - if (typeof c.subscribed === 'boolean') { | |
37 | - if (c.subscribed) { | |
38 | - result.add(channel) | |
39 | - } else { | |
40 | - result.delete(channel) | |
41 | - } | |
42 | - } | |
43 | - } | |
44 | - } | |
45 | - return result | |
46 | - }, { | |
47 | - startValue: new Set(), | |
48 | - nextTick: true | |
49 | - }) | |
50 | - | |
51 | - var instance = throttle(result, 2000) | |
52 | - instance.sync = result.sync | |
53 | - | |
54 | - instance.has = function (value) { | |
55 | - return computed(instance, x => x.has(value)) | |
56 | - } | |
57 | - | |
58 | - cache[userId] = instance | |
59 | - return instance | |
60 | - } | |
61 | - } | |
62 | - | |
63 | - } | |
64 | -} |
modules/pages/public.js | ||
---|---|---|
@@ -1,247 +1,0 @@ | ||
1 | -var MutantMap = require('mutant/map') | |
2 | -var computed = require('mutant/computed') | |
3 | -var when = require('mutant/when') | |
4 | -var send = require('mutant/send') | |
5 | -var pull = require('pull-stream') | |
6 | -var extend = require('xtend') | |
7 | -var h = require('../../lib/h') | |
8 | - | |
9 | -exports.needs = { | |
10 | - message: { | |
11 | - render: 'first', | |
12 | - compose: 'first', | |
13 | - publish: 'first' | |
14 | - }, | |
15 | - sbot: { | |
16 | - get_id: 'first', | |
17 | - log: 'first', | |
18 | - feed: 'first', | |
19 | - user_feed: 'first' | |
20 | - }, | |
21 | - cache: { | |
22 | - obs_channels: 'first' | |
23 | - }, | |
24 | - helpers: { | |
25 | - build_scroller: 'first' | |
26 | - }, | |
27 | - about: { | |
28 | - image: 'first', | |
29 | - name: 'first' | |
30 | - }, | |
31 | - feed_summary: 'first', | |
32 | - obs_subscribed_channels: 'first', | |
33 | - obs_following: 'first', | |
34 | - obs_recently_updated_feeds: 'first', | |
35 | - obs_local: 'first', | |
36 | - obs_connected: 'first' | |
37 | -} | |
38 | - | |
39 | -exports.gives = { | |
40 | - page: true | |
41 | -} | |
42 | - | |
43 | -exports.create = function (api) { | |
44 | - return { | |
45 | - | |
46 | - page (path) { | |
47 | - if (path !== '/public') return | |
48 | - var id = api.sbot.get_id() | |
49 | - var channels = computed(api.cache.obs_channels, items => items.slice(0, 8), {comparer: arrayEq}) | |
50 | - var subscribedChannels = api.obs_subscribed_channels(id) | |
51 | - var loading = computed(subscribedChannels.sync, x => !x) | |
52 | - var connectedPeers = api.obs_connected() | |
53 | - var localPeers = api.obs_local() | |
54 | - var connectedPubs = computed([connectedPeers, localPeers], (c, l) => c.filter(x => !l.includes(x))) | |
55 | - var following = api.obs_following(id) | |
56 | - | |
57 | - var oldest = Date.now() - (2 * 24 * 60 * 60e3) | |
58 | - getFirstMessage(id, (_, msg) => { | |
59 | - if (msg) { | |
60 | - // fall back to timestamp stream before this, give 48 hrs for feeds to stabilize | |
61 | - if (msg.value.timestamp > oldest) { | |
62 | - oldest = Date.now() | |
63 | - } | |
64 | - } | |
65 | - }) | |
66 | - | |
67 | - var whoToFollow = computed([api.obs_following(id), api.obs_recently_updated_feeds(200)], (following, recent) => { | |
68 | - return Array.from(recent).filter(x => x !== id && !following.has(x)).slice(0, 10) | |
69 | - }) | |
70 | - | |
71 | - var feedSummary = api.feed_summary(getFeed, [ | |
72 | - api.message.compose({type: 'post'}, {placeholder: 'Write a public message'}) | |
73 | - ], { | |
74 | - waitUntil: computed([ | |
75 | - following.sync, | |
76 | - subscribedChannels.sync | |
77 | - ], x => x.every(Boolean)), | |
78 | - windowSize: 500, | |
79 | - filter: (item) => { | |
80 | - return ( | |
81 | - id === item.author || | |
82 | - following().has(item.author) || | |
83 | - subscribedChannels().has(item.channel) || | |
84 | - (item.repliesFrom && item.repliesFrom.has(id)) || | |
85 | - item.digs && item.digs.has(id) | |
86 | - ) | |
87 | - }, | |
88 | - bumpFilter: (msg, group) => { | |
89 | - if (!group.message) { | |
90 | - return ( | |
91 | - isMentioned(id, msg.value.content.mentions) || | |
92 | - msg.value.author === id || ( | |
93 | - fromDay(msg, group.fromTime) && ( | |
94 | - following().has(msg.value.author) || | |
95 | - group.repliesFrom.has(id) | |
96 | - ) | |
97 | - ) | |
98 | - ) | |
99 | - } | |
100 | - return true | |
101 | - } | |
102 | - }) | |
103 | - | |
104 | - var result = h('SplitView', [ | |
105 | - h('div.side', [ | |
106 | - h('h2', 'Active Channels'), | |
107 | - when(loading, [ h('Loading') ]), | |
108 | - h('ChannelList', { | |
109 | - hidden: loading | |
110 | - }, [ | |
111 | - MutantMap(channels, (channel) => { | |
112 | - var subscribed = subscribedChannels.has(channel.id) | |
113 | - return h('a.channel', { | |
114 | - href: `##${channel.id}`, | |
115 | - classList: [ | |
116 | - when(subscribed, '-subscribed') | |
117 | - ] | |
118 | - }, [ | |
119 | - h('span.name', '#' + channel.id), | |
120 | - when(subscribed, | |
121 | - h('a -unsubscribe', { | |
122 | - 'ev-click': send(unsubscribe, channel.id) | |
123 | - }, 'Unsubscribe'), | |
124 | - h('a -subscribe', { | |
125 | - 'ev-click': send(subscribe, channel.id) | |
126 | - }, 'Subscribe') | |
127 | - ) | |
128 | - ]) | |
129 | - }, {maxTime: 5}) | |
130 | - ]), | |
131 | - | |
132 | - when(computed(localPeers, x => x.length), h('h2', 'Local')), | |
133 | - h('ProfileList', [ | |
134 | - MutantMap(localPeers, (id) => { | |
135 | - return h('a.profile', { | |
136 | - classList: [ | |
137 | - when(computed([connectedPeers, id], (p, id) => p.includes(id)), '-connected') | |
138 | - ], | |
139 | - href: `#${id}` | |
140 | - }, [ | |
141 | - h('div.avatar', [api.about.image(id)]), | |
142 | - h('div.main', [ | |
143 | - h('div.name', [ api.about.name(id) ]) | |
144 | - ]) | |
145 | - ]) | |
146 | - }) | |
147 | - ]), | |
148 | - | |
149 | - when(computed(whoToFollow, x => x.length), h('h2', 'Who to follow')), | |
150 | - h('ProfileList', [ | |
151 | - MutantMap(whoToFollow, (id) => { | |
152 | - return h('a.profile', { | |
153 | - href: `#${id}` | |
154 | - }, [ | |
155 | - h('div.avatar', [api.about.image(id)]), | |
156 | - h('div.main', [ | |
157 | - h('div.name', [ api.about.name(id) ]) | |
158 | - ]) | |
159 | - ]) | |
160 | - }) | |
161 | - ]), | |
162 | - | |
163 | - when(computed(connectedPubs, x => x.length), h('h2', 'Connected Pubs')), | |
164 | - h('ProfileList', [ | |
165 | - MutantMap(connectedPubs, (id) => { | |
166 | - return h('a.profile', { | |
167 | - classList: [ '-connected' ], | |
168 | - href: `#${id}` | |
169 | - }, [ | |
170 | - h('div.avatar', [api.about.image(id)]), | |
171 | - h('div.main', [ | |
172 | - h('div.name', [ api.about.name(id) ]) | |
173 | - ]) | |
174 | - ]) | |
175 | - }) | |
176 | - ]) | |
177 | - ]), | |
178 | - h('div.main', [ feedSummary ]) | |
179 | - ]) | |
180 | - | |
181 | - result.pendingUpdates = feedSummary.pendingUpdates | |
182 | - result.reload = feedSummary.reload | |
183 | - | |
184 | - return result | |
185 | - | |
186 | - // scoped | |
187 | - | |
188 | - function getFeed (opts) { | |
189 | - if (opts.lt && opts.lt < oldest) { | |
190 | - opts = extend(opts, {lt: parseInt(opts.lt, 10)}) | |
191 | - return pull( | |
192 | - api.sbot.feed(opts), | |
193 | - pull.map((msg) => { | |
194 | - if (msg.sync) { | |
195 | - return msg | |
196 | - } else { | |
197 | - return {key: msg.key, value: msg.value, timestamp: msg.value.timestamp} | |
198 | - } | |
199 | - }) | |
200 | - ) | |
201 | - } else { | |
202 | - return api.sbot.log(opts) | |
203 | - } | |
204 | - } | |
205 | - } | |
206 | - } | |
207 | - | |
208 | - // scoped | |
209 | - | |
210 | - function subscribe (id) { | |
211 | - api.message.publish({ | |
212 | - type: 'channel', | |
213 | - channel: id, | |
214 | - subscribed: true | |
215 | - }) | |
216 | - } | |
217 | - | |
218 | - function unsubscribe (id) { | |
219 | - api.message.publish({ | |
220 | - type: 'channel', | |
221 | - channel: id, | |
222 | - subscribed: false | |
223 | - }) | |
224 | - } | |
225 | - | |
226 | - function getFirstMessage (feedId, cb) { | |
227 | - api.sbot.user_feed({id: feedId, gte: 0, limit: 1})(null, cb) | |
228 | - } | |
229 | -} | |
230 | - | |
231 | -function fromDay (msg, fromTime) { | |
232 | - return (fromTime - msg.timestamp) < (24 * 60 * 60e3) | |
233 | -} | |
234 | - | |
235 | -function isMentioned (id, list) { | |
236 | - if (Array.isArray(list)) { | |
237 | - return list.includes(id) | |
238 | - } else { | |
239 | - return false | |
240 | - } | |
241 | -} | |
242 | - | |
243 | -function arrayEq (a, b) { | |
244 | - if (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a !== b) { | |
245 | - return a.every((value, i) => value === b[i]) | |
246 | - } | |
247 | -} |
modules/people-names.js | ||
---|---|---|
@@ -1,36 +1,0 @@ | ||
1 | -var Value = require('mutant/value') | |
2 | -var computed = require('mutant/computed') | |
3 | - | |
4 | -exports.needs = { | |
5 | - about: { | |
6 | - signifier: 'first' | |
7 | - } | |
8 | -} | |
9 | - | |
10 | -exports.gives = { | |
11 | - people_names: true | |
12 | -} | |
13 | - | |
14 | -exports.create = function (api) { | |
15 | - return { | |
16 | - people_names (ids) { | |
17 | - return computed(Array.from(ids).map(ObservName), join) || '' | |
18 | - } | |
19 | - } | |
20 | - | |
21 | - // scoped | |
22 | - | |
23 | - function ObservName (id) { | |
24 | - var obs = Value(id.slice(0, 10)) | |
25 | - api.about.signifier(id, (_, value) => { | |
26 | - if (value && value.length) { | |
27 | - obs.set(value[0].name) | |
28 | - } | |
29 | - }) | |
30 | - return obs | |
31 | - } | |
32 | -} | |
33 | - | |
34 | -function join (...args) { | |
35 | - return args.join('\n') | |
36 | -} |
modules/person.js | ||
---|---|---|
@@ -1,18 +1,0 @@ | ||
1 | -exports.needs = { | |
2 | - about: { | |
3 | - name: 'first', | |
4 | - link: 'first' | |
5 | - } | |
6 | -} | |
7 | - | |
8 | -exports.gives = { | |
9 | - person: true | |
10 | -} | |
11 | - | |
12 | -exports.create = function (api) { | |
13 | - return { | |
14 | - person (id) { | |
15 | - return api.about.link(id, api.about.name(id), '') | |
16 | - } | |
17 | - } | |
18 | -} |
modules/raw.js | ||
---|---|---|
@@ -1,9 +1,0 @@ | ||
1 | -// disable raw view for now | |
2 | - | |
3 | -exports.gives = { | |
4 | - | |
5 | -} | |
6 | - | |
7 | -exports.create = function () { | |
8 | - return {} | |
9 | -} |
modules/sbot.js | ||
---|---|---|
@@ -1,207 +1,0 @@ | ||
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 | - cache: { | |
34 | - update_from: 'first' | |
35 | - } | |
36 | -} | |
37 | - | |
38 | -exports.gives = { | |
39 | -// connection_status: true, | |
40 | - sbot: { | |
41 | - blobs_add: true, | |
42 | - links: true, | |
43 | - links2: true, | |
44 | - query: true, | |
45 | - fulltext_search: true, | |
46 | - get: true, | |
47 | - log: true, | |
48 | - user_feed: true, | |
49 | - gossip_peers: true, | |
50 | - gossip_connect: true, | |
51 | - progress: true, | |
52 | - publish: true, | |
53 | - whoami: true, | |
54 | - | |
55 | - // additional | |
56 | - get_id: true, | |
57 | - feed: true, | |
58 | - list_local: true | |
59 | - } | |
60 | -} | |
61 | - | |
62 | -exports.create = function (api) { | |
63 | - | |
64 | - var sbot = null | |
65 | - var config = api.config() | |
66 | - | |
67 | - var rec = Reconnect(function (isConn) { | |
68 | - function notify (value) { | |
69 | - isConn(value); api.connection_status(value) | |
70 | - } | |
71 | - | |
72 | - createClient(config.keys, config, function (err, _sbot) { | |
73 | - if(err) | |
74 | - return notify(err) | |
75 | - | |
76 | - sbot = _sbot | |
77 | - sbot.on('closed', function () { | |
78 | - sbot = null | |
79 | - notify(new Error('closed')) | |
80 | - }) | |
81 | - | |
82 | - notify() | |
83 | - }) | |
84 | - }) | |
85 | - | |
86 | - var internal = { | |
87 | - getLatest: rec.async(function (id, cb) { | |
88 | - sbot.getLatest(id, cb) | |
89 | - }), | |
90 | - add: rec.async(function (msg, cb) { | |
91 | - sbot.add(msg, cb) | |
92 | - }) | |
93 | - } | |
94 | - | |
95 | - var feed = createFeed(internal, keys, {remote: true}) | |
96 | - | |
97 | - return { | |
98 | - // connection_status, | |
99 | - sbot: { | |
100 | - blobs_add: rec.sink(function (cb) { | |
101 | - return pull( | |
102 | - Hash(function (err, id) { | |
103 | - if(err) return cb(err) | |
104 | - //completely UGLY hack to tell when the blob has been sucessfully written... | |
105 | - var start = Date.now(), n = 5 | |
106 | - ;(function next () { | |
107 | - setTimeout(function () { | |
108 | - sbot.blobs.has(id, function (err, has) { | |
109 | - if(has) return cb(null, id) | |
110 | - if(n--) next() | |
111 | - else cb(new Error('write failed')) | |
112 | - }) | |
113 | - }, Date.now() - start) | |
114 | - })() | |
115 | - }), | |
116 | - sbot.blobs.add() | |
117 | - ) | |
118 | - }), | |
119 | - links: rec.source(function (query) { | |
120 | - return sbot.links(query) | |
121 | - }), | |
122 | - links2: rec.source(function (query) { | |
123 | - return sbot.links2.read(query) | |
124 | - }), | |
125 | - query: rec.source(function (query) { | |
126 | - return sbot.query.read(query) | |
127 | - }), | |
128 | - log: rec.source(function (opts) { | |
129 | - return pull( | |
130 | - sbot.createLogStream(opts), | |
131 | - pull.through(function (e) { | |
132 | - CACHE[e.key] = CACHE[e.key] || e.value | |
133 | - api.cache.update_from(e) | |
134 | - }) | |
135 | - ) | |
136 | - }), | |
137 | - user_feed: rec.source(function (opts) { | |
138 | - return sbot.createUserStream(opts) | |
139 | - }), | |
140 | - fulltext_search: rec.source(function (opts) { | |
141 | - return sbot.fulltext.search(opts) | |
142 | - }), | |
143 | - get: rec.async(function (key, cb) { | |
144 | - if('function' !== typeof cb) | |
145 | - throw new Error('cb must be function') | |
146 | - if(CACHE[key]) cb(null, CACHE[key]) | |
147 | - else sbot.get(key, function (err, value) { | |
148 | - if(err) return cb(err) | |
149 | - cb(null, CACHE[key] = value) | |
150 | - }) | |
151 | - }), | |
152 | - gossip_peers: rec.async(function (cb) { | |
153 | - sbot.gossip.peers(cb) | |
154 | - }), | |
155 | - //liteclient won't have permissions for this | |
156 | - gossip_connect: rec.async(function (opts, cb) { | |
157 | - sbot.gossip.connect(opts, cb) | |
158 | - }), | |
159 | - progress: rec.source(function () { | |
160 | - return sbot.replicate.changes() | |
161 | - }), | |
162 | - publish: rec.async(function (content, cb) { | |
163 | - if(content.recps) | |
164 | - content = ssbKeys.box(content, content.recps.map(function (e) { | |
165 | - return ref.isFeed(e) ? e : e.link | |
166 | - })) | |
167 | - else if(content.mentions) | |
168 | - content.mentions.forEach(function (mention) { | |
169 | - if(ref.isBlob(mention.link)) { | |
170 | - sbot.blobs.push(mention.link, function (err) { | |
171 | - if(err) console.error(err) | |
172 | - }) | |
173 | - } | |
174 | - }) | |
175 | - | |
176 | - feed.add(content, function (err, msg) { | |
177 | - if(err) console.error(err) | |
178 | - else if(!cb) console.log(msg) | |
179 | - cb && cb(err, msg) | |
180 | - }) | |
181 | - }), | |
182 | - whoami: rec.async(function (cb) { | |
183 | - sbot.whoami(cb) | |
184 | - }), | |
185 | - | |
186 | - // ADDITIONAL: | |
187 | - | |
188 | - feed: rec.source(function (opts) { | |
189 | - return pull( | |
190 | - sbot.createFeedStream(opts), | |
191 | - pull.through(function (e) { | |
192 | - CACHE[e.key] = CACHE[e.key] || e.value | |
193 | - api.cache.update_from(e) | |
194 | - }) | |
195 | - ) | |
196 | - }), | |
197 | - | |
198 | - get_id: function () { | |
199 | - return keys.id | |
200 | - }, | |
201 | - | |
202 | - list_local: rec.async(function (cb) { | |
203 | - return sbot.local.list(cb) | |
204 | - }) | |
205 | - } | |
206 | - } | |
207 | -} |
modules/vote/like.js | ||
---|---|---|
@@ -1,92 +1,0 @@ | ||
1 | -var h = require('../../lib/h') | |
2 | -var computed = require('mutant/computed') | |
3 | -var when = require('mutant/when') | |
4 | - | |
5 | -exports.needs = { | |
6 | - sbot: { | |
7 | - get_id: 'first' | |
8 | - }, | |
9 | - message: { | |
10 | - link: 'first', | |
11 | - publish: 'first' | |
12 | - }, | |
13 | - cache: { | |
14 | - get_likes: 'first' | |
15 | - }, | |
16 | - people_names: 'first' | |
17 | -} | |
18 | - | |
19 | -exports.gives = { | |
20 | - message: { | |
21 | - action: true, | |
22 | - content: true, | |
23 | - meta: true | |
24 | - } | |
25 | -} | |
26 | - | |
27 | -exports.create = function (api) { | |
28 | - return { | |
29 | - message: { | |
30 | - content (msg) { | |
31 | - if (msg.value.content.type !== 'vote') return | |
32 | - var link = msg.value.content.vote.link | |
33 | - return [ | |
34 | - msg.value.content.vote.value > 0 ? 'dug' : 'undug', | |
35 | - ' ', api.message.link(link) | |
36 | - ] | |
37 | - }, | |
38 | - meta (msg) { | |
39 | - return computed(api.cache.get_likes(msg.key), likeCount) | |
40 | - }, | |
41 | - action (msg) { | |
42 | - var id = api.sbot.get_id() | |
43 | - var dug = computed([api.cache.get_likes(msg.key), id], doesLike) | |
44 | - dug(() => {}) | |
45 | - | |
46 | - if (msg.value.content.type !== 'vote') { | |
47 | - return h('a.dig', { | |
48 | - href: '#', | |
49 | - 'ev-click': function () { | |
50 | - var dig = dug() ? { | |
51 | - type: 'vote', | |
52 | - vote: { link: msg.key, value: 0, expression: 'Undig' } | |
53 | - } : { | |
54 | - type: 'vote', | |
55 | - vote: { link: msg.key, value: 1, expression: 'Dig' } | |
56 | - } | |
57 | - if (msg.value.content.recps) { | |
58 | - dig.recps = msg.value.content.recps.map(function (e) { | |
59 | - return e && typeof e !== 'string' ? e.link : e | |
60 | - }) | |
61 | - dig.private = true | |
62 | - } | |
63 | - api.message.publish(dig) | |
64 | - } | |
65 | - }, when(dug, 'Undig', 'Dig')) | |
66 | - } | |
67 | - } | |
68 | - } | |
69 | - } | |
70 | - | |
71 | - function likeCount (data) { | |
72 | - var likes = getLikes(data) | |
73 | - if (likes.length) { | |
74 | - return [' ', h('span.likes', { | |
75 | - title: api.people_names(likes) | |
76 | - }, ['+', h('strong', `${likes.length}`)])] | |
77 | - } | |
78 | - } | |
79 | -} | |
80 | - | |
81 | -function doesLike (likes, userId) { | |
82 | - return likes && likes[userId] && likes[userId][0] || false | |
83 | -} | |
84 | - | |
85 | -function getLikes (likes) { | |
86 | - return Object.keys(likes).reduce((result, id) => { | |
87 | - if (likes[id][0]) { | |
88 | - result.push(id) | |
89 | - } | |
90 | - return result | |
91 | - }, []) | |
92 | -} |
package.json | ||
---|---|---|
@@ -13,23 +13,26 @@ | ||
13 | 13 | "author": "Secure Scuttlebutt Consortium", |
14 | 14 | "license": "GPL", |
15 | 15 | "dependencies": { |
16 | 16 | "atomic-file": "^0.1.0", |
17 | + "bulk-require": "^1.0.0", | |
18 | + "catch-links": "^2.0.1", | |
17 | 19 | "data-uri-to-buffer": "0.0.4", |
18 | 20 | "deep-equal": "^1.0.1", |
21 | + "depject": "github:mmckegg/depject#mattject", | |
22 | + "depnest": "^1.0.2", | |
19 | 23 | "electron-default-menu": "~1.0.0", |
20 | 24 | "graphmitter": "^1.6.3", |
21 | 25 | "has-network": "0.0.0", |
22 | 26 | "insert-css": "~1.0.0", |
23 | 27 | "is-visible": "^2.1.1", |
24 | 28 | "level": "~1.4.0", |
25 | 29 | "level-memview": "0.0.0", |
26 | - "micro-css": "~0.6.2", | |
27 | - "mutant": "~3.12.0", | |
30 | + "micro-css": "^1.0.0", | |
31 | + "mutant": "^3.14.0", | |
28 | 32 | "non-private-ip": "^1.4.1", |
29 | 33 | "on-change-network": "0.0.2", |
30 | 34 | "on-wakeup": "^1.0.1", |
31 | - "patchbay": "github:ssbc/patchbay#module_restructure", | |
32 | 35 | "prebuild": "github:mmckegg/prebuild#use-npm-conf", |
33 | 36 | "pull-abortable": "^4.1.0", |
34 | 37 | "pull-file": "~1.0.0", |
35 | 38 | "pull-identify-filetype": "^1.1.0", |
@@ -37,15 +40,17 @@ | ||
37 | 40 | "pull-notify": "^0.1.1", |
38 | 41 | "pull-pause": "0.0.0", |
39 | 42 | "pull-ping": "^2.0.2", |
40 | 43 | "pull-pushable": "^2.0.1", |
44 | + "pull-scroll": "^1.0.3", | |
41 | 45 | "pull-stream": "~3.4.5", |
42 | 46 | "scuttlebot": "^9.4.3", |
43 | 47 | "sorted-array-functions": "~1.0.0", |
44 | 48 | "ssb-avatar": "^0.2.0", |
45 | 49 | "ssb-blobs": "~0.1.7", |
46 | 50 | "ssb-keys": "~7.0.0", |
47 | 51 | "ssb-links": "~2.0.0", |
52 | + "ssb-msgs": "^5.2.0", | |
48 | 53 | "ssb-query": "~0.1.1", |
49 | 54 | "ssb-ref": "~2.6.2", |
50 | 55 | "ssb-sort": "^1.0.0", |
51 | 56 | "statistics": "^3.3.0" |
styles/feed-event.mcss | ||
---|---|---|
@@ -2,9 +2,10 @@ | ||
2 | 2 | display: flex |
3 | 3 | flex: 1 |
4 | 4 | flex-direction: column |
5 | 5 | background: white |
6 | - margin-top: 10px | |
6 | + max-width: 700px | |
7 | + margin: 10px auto | |
7 | 8 | |
8 | 9 | div { |
9 | 10 | flex: 1 |
10 | 11 | } |
styles/message.mcss | ||
---|---|---|
@@ -96,9 +96,9 @@ | ||
96 | 96 | padding: 5px 8px; |
97 | 97 | border-radius: 10px; |
98 | 98 | display: inline-block; |
99 | 99 | vertical-align: top; |
100 | - margin: -4px 0; | |
100 | + margin: -2px 0; | |
101 | 101 | } |
102 | 102 | |
103 | 103 | span.private { |
104 | 104 | display: inline-block; |
styles/split-view.mcss | ||
---|---|---|
@@ -4,8 +4,9 @@ | ||
4 | 4 | div.main { |
5 | 5 | display: flex |
6 | 6 | flex-direction: column |
7 | 7 | flex: 1 |
8 | + overflow-y: scroll | |
8 | 9 | } |
9 | 10 | div.side { |
10 | 11 | min-width: 280px; |
11 | 12 | padding: 20px; |
styles/markdown.mcss | ||
---|---|---|
@@ -1,0 +1,9 @@ | ||
1 | +Markdown { | |
2 | + word-break: break-word | |
3 | + (pre) { | |
4 | + overflow: auto; | |
5 | + padding: 10px; | |
6 | + background: #fbfbfb; | |
7 | + border: 1px solid #EEE; | |
8 | + } | |
9 | +} |
styles/scroller.mcss | ||
---|---|---|
@@ -1,0 +1,16 @@ | ||
1 | +Scroller { | |
2 | + display: flex | |
3 | + flex-direction: column | |
4 | + | |
5 | + overflow: auto | |
6 | + width: 100% | |
7 | + height: 100% | |
8 | + min-height: 0px | |
9 | + | |
10 | + div.wrapper { | |
11 | + flex: 1 | |
12 | + width: 600px | |
13 | + margin-left: auto | |
14 | + margin-right: auto | |
15 | + } | |
16 | +} |
api/index.js | ||
---|---|---|
@@ -1,155 +1,0 @@ | ||
1 | -var pull = require('pull-stream') | |
2 | -var ssbKeys = require('ssb-keys') | |
3 | -var ref = require('ssb-ref') | |
4 | -var InfoCache = require('./info-cache') | |
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 | - | |
24 | -var createFeed = require('ssb-feed') | |
25 | -var cache = CACHE = {} | |
26 | - | |
27 | -module.exports = function (sbot, opts) { | |
28 | - var connection_status = [] | |
29 | - var keys = opts.keys | |
30 | - var infoCache = InfoCache() | |
31 | - | |
32 | - var internal = { | |
33 | - getLatest: function (id, cb) { | |
34 | - sbot.getLatest(id, cb) | |
35 | - }, | |
36 | - add: function (msg, cb) { | |
37 | - sbot.add(msg, cb) | |
38 | - } | |
39 | - } | |
40 | - | |
41 | - var feed = createFeed(internal, keys, {remote: true}) | |
42 | - | |
43 | - setImmediate((x) => { | |
44 | - connection_status.forEach(fn => fn()) | |
45 | - }) | |
46 | - | |
47 | - return { | |
48 | - connection_status: connection_status, | |
49 | - get_id: function () { | |
50 | - return sbot.id | |
51 | - }, | |
52 | - get_likes: function (id) { | |
53 | - return infoCache.getLikes(id) | |
54 | - }, | |
55 | - obs_channels: function () { | |
56 | - return infoCache.channels | |
57 | - }, | |
58 | - update_cache: function (msg) { | |
59 | - infoCache.updateFrom(msg) | |
60 | - }, | |
61 | - sbot_blobs_add: function (cb) { | |
62 | - return pull( | |
63 | - Hash(function (err, id) { | |
64 | - if(err) return cb(err) | |
65 | - //completely UGLY hack to tell when the blob has been sucessfully written... | |
66 | - var start = Date.now(), n = 5 | |
67 | - ;(function next () { | |
68 | - setTimeout(function () { | |
69 | - sbot.blobs.has(id, function (err, has) { | |
70 | - if(has) return cb(null, id) | |
71 | - if(n--) next() | |
72 | - else cb(new Error('write failed')) | |
73 | - }) | |
74 | - }, Date.now() - start) | |
75 | - })() | |
76 | - }), | |
77 | - sbot.blobs.add() | |
78 | - ) | |
79 | - }, | |
80 | - sbot_links: function (query) { | |
81 | - return sbot.links(query) | |
82 | - }, | |
83 | - sbot_links2: function (query) { | |
84 | - return sbot.links2.read(query) | |
85 | - }, | |
86 | - sbot_query: function (query) { | |
87 | - return sbot.query.read(query) | |
88 | - }, | |
89 | - sbot_log: function (opts) { | |
90 | - return pull( | |
91 | - sbot.createLogStream(opts), | |
92 | - pull.through(function (e) { | |
93 | - CACHE[e.key] = CACHE[e.key] || e.value | |
94 | - infoCache.updateFrom(e) | |
95 | - }) | |
96 | - ) | |
97 | - }, | |
98 | - sbot_user_feed: function (opts) { | |
99 | - return sbot.createUserStream(opts) | |
100 | - }, | |
101 | - sbot_get: function (key, cb) { | |
102 | - if(CACHE[key] && CACHE[key].value) cb(null, CACHE[key].value) | |
103 | - else sbot.get(key, function (err, value) { | |
104 | - if(err) return cb(err) | |
105 | - CACHE[key] = {key, value} | |
106 | - cb(null, value) | |
107 | - }) | |
108 | - }, | |
109 | - sbot_gossip_peers: function (cb) { | |
110 | - sbot.gossip.peers(cb) | |
111 | - }, | |
112 | - //liteclient won't have permissions for this | |
113 | - sbot_gossip_connect: function (opts, cb) { | |
114 | - sbot.gossip.connect(opts, cb) | |
115 | - }, | |
116 | - sbot_publish: function (content, cb) { | |
117 | - if(content.recps) | |
118 | - content = ssbKeys.box(content, content.recps.map(function (e) { | |
119 | - return ref.isFeed(e) ? e : e.link | |
120 | - })) | |
121 | - else if(content.mentions) | |
122 | - content.mentions.forEach(function (mention) { | |
123 | - if(ref.isBlob(mention.link)) { | |
124 | - sbot.blobs.push(mention.link, function (err) { | |
125 | - if(err) console.error(err) | |
126 | - }) | |
127 | - } | |
128 | - }) | |
129 | - | |
130 | - feed.add(content, function (err, msg) { | |
131 | - if(err) console.error(err) | |
132 | - else if(!cb) console.log(msg) | |
133 | - cb && cb(err, msg) | |
134 | - }) | |
135 | - }, | |
136 | - sbot_whoami: function (cb) { | |
137 | - sbot.whoami(cb) | |
138 | - }, | |
139 | - sbot_progress: function () { | |
140 | - return sbot.replicate.changes() | |
141 | - }, | |
142 | - sbot_feed: function (opts) { | |
143 | - return pull( | |
144 | - sbot.createFeedStream(opts), | |
145 | - pull.through(function (e) { | |
146 | - CACHE[e.key] = CACHE[e.key] || e.value | |
147 | - infoCache.updateFrom(e) | |
148 | - }) | |
149 | - ) | |
150 | - }, | |
151 | - sbot_list_local: function (cb) { | |
152 | - return sbot.local.list(cb) | |
153 | - } | |
154 | - } | |
155 | -} |
old_modules/about.js | ||
---|---|---|
@@ -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 | -} |
old_modules/app.js | ||
---|---|---|
@@ -1,198 +1,0 @@ | ||
1 | -var Modules = require('./modules') | |
2 | -var h = require('./lib/h') | |
3 | -var Value = require('mutant/value') | |
4 | -var when = require('mutant/when') | |
5 | -var computed = require('mutant/computed') | |
6 | -var toCollection = require('mutant/dict-to-collection') | |
7 | -var MutantDict = require('mutant/dict') | |
8 | -var MutantMap = require('mutant/map') | |
9 | -var watch = require('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.js | ||
---|---|---|
@@ -1,78 +1,0 @@ | ||
1 | -var when = require('mutant/when') | |
2 | -var send = require('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.js | ||
---|---|---|
@@ -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 | -} |
old_modules/feed.js | ||
---|---|---|
@@ -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 | -} |
old_modules/git-mini-messages.js | ||
---|---|---|
@@ -1,26 +1,0 @@ | ||
1 | -var h = require('../lib/h') | |
2 | -var when = require('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.js | ||
---|---|---|
@@ -1,31 +1,0 @@ | ||
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/message-confirm.js | ||
---|---|---|
@@ -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 | -} |
old_modules/message-name.js | ||
---|---|---|
@@ -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 | -} |
old_modules/notifications.js | ||
---|---|---|
@@ -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 | -} |
old_modules/post.js | ||
---|---|---|
@@ -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 | -} |
old_modules/private.js | ||
---|---|---|
@@ -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 | -} |
old_modules/public.js | ||
---|---|---|
@@ -1,225 +1,0 @@ | ||
1 | -var MutantMap = require('mutant/map') | |
2 | -var computed = require('mutant/computed') | |
3 | -var when = require('mutant/when') | |
4 | -var send = require('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.js | ||
---|---|---|
@@ -1,1 +1,0 @@ | ||
1 | -// disable |
old_modules/thread.js | ||
---|---|---|
@@ -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 | -} |
old_modules/timestamp.js | ||
---|---|---|
@@ -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 | -} |
old_styles/message-confirm.mcss | ||
---|---|---|
@@ -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 | -} |
old_styles/patchbay-tweaks.css | ||
---|---|---|
@@ -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 | -} |
plugs/blob/sync/url.js | ||
---|---|---|
@@ -1,0 +1,18 @@ | ||
1 | +var nest = require('depnest') | |
2 | + | |
3 | +exports.needs = nest({ | |
4 | + 'config.sync.load': 'first' | |
5 | +}) | |
6 | + | |
7 | +exports.gives = nest('blob.sync.url') | |
8 | + | |
9 | +exports.create = function (api) { | |
10 | + return nest('blob.sync.url', function (link) { | |
11 | + var config = api.config.sync.load() | |
12 | + var prefix = config.blobsPrefix != null ? config.blobsPrefix : `http://localhost:${config.blobsPort}` | |
13 | + if (typeof link.link === 'string') { | |
14 | + link = link.link | |
15 | + } | |
16 | + return `${prefix}/${encodeURIComponent(link)}` | |
17 | + }) | |
18 | +} |
plugs/emoji/sync/url.js | ||
---|---|---|
@@ -1,0 +1,10 @@ | ||
1 | +var emojis = require('emoji-named-characters') | |
2 | +var nest = require('depnest') | |
3 | + | |
4 | +exports.gives = nest('emoji.sync.url') | |
5 | + | |
6 | +exports.create = function (api) { | |
7 | + return nest('emoji.sync.url', (emoji) => { | |
8 | + return emoji in emojis && `img/emoji/${emoji}.png` | |
9 | + }) | |
10 | +} |
plugs/index.js | ||
---|---|---|
@@ -1,0 +1,3 @@ | ||
1 | +module.exports = { | |
2 | + plugs: require('bulk-require')(__dirname, ['**/!(index).js']) | |
3 | +} |
plugs/message/html/layout/default.js | ||
---|---|---|
@@ -1,0 +1,76 @@ | ||
1 | +const { when, h } = require('mutant') | |
2 | +var nest = require('depnest') | |
3 | + | |
4 | +exports.needs = nest({ | |
5 | + 'profile.html.person': 'first', | |
6 | + 'message.html': { | |
7 | + link: 'first', | |
8 | + meta: 'map', | |
9 | + action: 'map', | |
10 | + timestamp: 'first' | |
11 | + }, | |
12 | + 'about.html.image': 'first' | |
13 | +}) | |
14 | + | |
15 | +exports.gives = nest('message.html.layout') | |
16 | + | |
17 | +exports.create = function (api) { | |
18 | + return nest('message.html.layout', layout) | |
19 | + | |
20 | + function layout (msg, opts) { | |
21 | + if (!(opts.layout === undefined || opts.layout === 'default' || opts.layout === 'mini')) return | |
22 | + | |
23 | + var classList = ['Message'] | |
24 | + var replyInfo = null | |
25 | + | |
26 | + if (msg.value.content.root) { | |
27 | + classList.push('-reply') | |
28 | + if (!opts.previousId) { | |
29 | + replyInfo = h('span', ['in reply to ', api.message.html.link(msg.value.content.root)]) | |
30 | + } else if (opts.previousId && last(msg.value.content.branch) && opts.previousId !== last(msg.value.content.branch)) { | |
31 | + replyInfo = h('span', ['in reply to ', api.message.html.link(last(msg.value.content.branch))]) | |
32 | + } | |
33 | + } | |
34 | + | |
35 | + return h('div', { | |
36 | + classList | |
37 | + }, [ | |
38 | + messageHeader(msg, replyInfo), | |
39 | + h('section', [opts.content]), | |
40 | + when(msg.key, h('footer', [ | |
41 | + h('div.actions', [ | |
42 | + api.message.html.action(msg) | |
43 | + ]) | |
44 | + ])) | |
45 | + ]) | |
46 | + | |
47 | + // scoped | |
48 | + | |
49 | + function messageHeader (msg, replyInfo) { | |
50 | + return h('header', [ | |
51 | + h('div.main', [ | |
52 | + h('a.avatar', {href: `${msg.value.author}`}, [ | |
53 | + api.about.html.image(msg.value.author) | |
54 | + ]), | |
55 | + h('div.main', [ | |
56 | + h('div.name', [ | |
57 | + api.profile.html.person(msg.value.author) | |
58 | + ]), | |
59 | + h('div.meta', [ | |
60 | + api.message.html.timestamp(msg), ' ', replyInfo | |
61 | + ]) | |
62 | + ]) | |
63 | + ]), | |
64 | + h('div.meta', api.message.html.meta(msg)) | |
65 | + ]) | |
66 | + } | |
67 | + } | |
68 | +} | |
69 | + | |
70 | +function last (array) { | |
71 | + if (Array.isArray(array)) { | |
72 | + return array[array.length - 1] | |
73 | + } else { | |
74 | + return array | |
75 | + } | |
76 | +} |
plugs/message/html/meta/likes.js | ||
---|---|---|
@@ -1,0 +1,21 @@ | ||
1 | +var nest = require('depnest') | |
2 | +var { h, computed } = require('mutant') | |
3 | +exports.gives = nest('message.html.meta') | |
4 | +exports.needs = nest({ | |
5 | + 'message.obs.likes': 'first', | |
6 | + 'profile.obs.names': 'first' | |
7 | +}) | |
8 | + | |
9 | +exports.create = function (api) { | |
10 | + return nest('message.html.meta', function likes (msg) { | |
11 | + return computed(api.message.obs.likes(msg.key), likeCount) | |
12 | + }) | |
13 | + | |
14 | + function likeCount (likes) { | |
15 | + if (likes.length) { | |
16 | + return [' ', h('span.likes', { | |
17 | + title: api.profile.obs.names(likes) | |
18 | + }, ['+', h('strong', `${likes.length}`)])] | |
19 | + } | |
20 | + } | |
21 | +} |
Built with git-ssb-web