git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Commit d092b707950e5fda7d051ad6913a8cbd99c22955

main feed with rollups and sidebar using patchcore!

Matt McKegg committed on 2/14/2017, 5:47:55 AM
Parent: 8397cee5bd7b17b9f2ebf584813ed0c0a035023d

Files changed

lib/window.jschanged
lib/feed-summary.jsdeleted
lib/h.jsdeleted
lib/once-true.jsdeleted
lib/pull-scroll.jsdeleted
main-window.jschanged
modules/app.jsdeleted
modules/channel/obs/recent.jsadded
modules/channel/obs/subscribed.jsadded
modules/cache.jsdeleted
modules/feed/html/rollup.jsadded
modules/feed/pull/summary.jsadded
modules/feed-summary.jsdeleted
modules/helpers/blob-url.jsdeleted
modules/helpers/emoji.jsdeleted
modules/page/html/render/notifications.jsadded
modules/page/html/render/private.jsadded
modules/page/html/render/profile.jsadded
modules/page/html/render/public.jsadded
modules/profile/html/manyPeople.jsadded
modules/profile/html/person.jsadded
modules/profile/obs/following.jsadded
modules/profile/obs/names.jsadded
modules/profile/obs/recently-updated.jsadded
modules/many-people.jsdeleted
modules/message/render.jsdeleted
modules/message/timestamp.jsdeleted
modules/obs-connected.jsdeleted
modules/obs-following.jsdeleted
modules/obs-local.jsdeleted
modules/obs-recently-updated-feeds.jsdeleted
modules/obs-subscribed-channels.jsdeleted
modules/pages/public.jsdeleted
modules/people-names.jsdeleted
modules/person.jsdeleted
modules/raw.jsdeleted
modules/sbot.jsdeleted
modules/vote/like.jsdeleted
package.jsonchanged
styles/feed-event.mcsschanged
styles/message.mcsschanged
styles/split-view.mcsschanged
styles/markdown.mcssadded
styles/profile-link.mcssadded
styles/scroller.mcssadded
api/index.jsdeleted
old_modules/about.jsdeleted
old_modules/app.jsdeleted
old_modules/channel.jsdeleted
old_modules/data-feed.jsdeleted
old_modules/feed.jsdeleted
old_modules/git-mini-messages.jsdeleted
old_modules/index.jsdeleted
old_modules/message-confirm.jsdeleted
old_modules/message-name.jsdeleted
old_modules/notifications.jsdeleted
old_modules/post.jsdeleted
old_modules/private.jsdeleted
old_modules/public.jsdeleted
old_modules/raw.jsdeleted
old_modules/thread.jsdeleted
old_modules/timestamp.jsdeleted
old_styles/message-confirm.mcssdeleted
old_styles/patchbay-tweaks.cssdeleted
plugs/blob/sync/url.jsadded
plugs/emoji/sync/url.jsadded
plugs/index.jsadded
plugs/message/html/layout/default.jsadded
plugs/message/html/meta/likes.jsadded
lib/window.jsView
@@ -13,9 +13,9 @@
1313 window.webContents.on('dom-ready', function () {
1414 window.webContents.executeJavaScript(`
1515 var electron = require('electron')
1616 var rootView = require(${JSON.stringify(path)})
17- var h = require('../lib/h')
17+ var h = require('mutant/h')
1818
1919 require('../lib/context-menu')
2020 electron.webFrame.setZoomLevelLimits(1, 1)
2121
lib/feed-summary.jsView
@@ -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.jsView
@@ -1,1 +1,0 @@
1-module.exports = require('micro-css/h')(require('mutant/html-element'))
lib/once-true.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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')
112 var insertCss = require('insert-css')
13+var nest = require('depnest')
214
315 module.exports = function (config) {
4- var modules = require('depject')(
16+ var sockets = combine(
517 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')
1021 )
22+ var api = entry(sockets, nest({
23+ 'page.html.render': 'first'
24+ }))
1125
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'
1432 })
1533
16- return modules.app[0]()
17-}
34+ searchBox.oninput = function () {
35+ clearTimeout(searchTimer)
36+ searchTimer = setTimeout(doSearch, 500)
37+ }
1838
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)
2963 }
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
3083 }
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()
31139 }
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+ }
32218 }
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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
modules/page/html/render/private.jsView
modules/page/html/render/profile.jsView
modules/page/html/render/public.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsonView
@@ -13,23 +13,26 @@
1313 "author": "Secure Scuttlebutt Consortium",
1414 "license": "GPL",
1515 "dependencies": {
1616 "atomic-file": "^0.1.0",
17+ "bulk-require": "^1.0.0",
18+ "catch-links": "^2.0.1",
1719 "data-uri-to-buffer": "0.0.4",
1820 "deep-equal": "^1.0.1",
21+ "depject": "github:mmckegg/depject#mattject",
22+ "depnest": "^1.0.2",
1923 "electron-default-menu": "~1.0.0",
2024 "graphmitter": "^1.6.3",
2125 "has-network": "0.0.0",
2226 "insert-css": "~1.0.0",
2327 "is-visible": "^2.1.1",
2428 "level": "~1.4.0",
2529 "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",
2832 "non-private-ip": "^1.4.1",
2933 "on-change-network": "0.0.2",
3034 "on-wakeup": "^1.0.1",
31- "patchbay": "github:ssbc/patchbay#module_restructure",
3235 "prebuild": "github:mmckegg/prebuild#use-npm-conf",
3336 "pull-abortable": "^4.1.0",
3437 "pull-file": "~1.0.0",
3538 "pull-identify-filetype": "^1.1.0",
@@ -37,15 +40,17 @@
3740 "pull-notify": "^0.1.1",
3841 "pull-pause": "0.0.0",
3942 "pull-ping": "^2.0.2",
4043 "pull-pushable": "^2.0.1",
44+ "pull-scroll": "^1.0.3",
4145 "pull-stream": "~3.4.5",
4246 "scuttlebot": "^9.4.3",
4347 "sorted-array-functions": "~1.0.0",
4448 "ssb-avatar": "^0.2.0",
4549 "ssb-blobs": "~0.1.7",
4650 "ssb-keys": "~7.0.0",
4751 "ssb-links": "~2.0.0",
52+ "ssb-msgs": "^5.2.0",
4853 "ssb-query": "~0.1.1",
4954 "ssb-ref": "~2.6.2",
5055 "ssb-sort": "^1.0.0",
5156 "statistics": "^3.3.0"
styles/feed-event.mcssView
@@ -2,9 +2,10 @@
22 display: flex
33 flex: 1
44 flex-direction: column
55 background: white
6- margin-top: 10px
6+ max-width: 700px
7+ margin: 10px auto
78
89 div {
910 flex: 1
1011 }
styles/message.mcssView
@@ -96,9 +96,9 @@
9696 padding: 5px 8px;
9797 border-radius: 10px;
9898 display: inline-block;
9999 vertical-align: top;
100- margin: -4px 0;
100+ margin: -2px 0;
101101 }
102102
103103 span.private {
104104 display: inline-block;
styles/split-view.mcssView
@@ -4,8 +4,9 @@
44 div.main {
55 display: flex
66 flex-direction: column
77 flex: 1
8+ overflow-y: scroll
89 }
910 div.side {
1011 min-width: 280px;
1112 padding: 20px;
styles/markdown.mcssView
@@ -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/profile-link.mcssView
@@ -1,0 +1,4 @@
1+ProfileLink {
2+ font-weight: bold
3+ color: black
4+}
styles/scroller.mcssView
@@ -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.jsView
@@ -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.jsView
@@ -1,37 +1,0 @@
1-var h = require('hyperscript')
2-
3-function idLink (id) {
4- return h('a', {href:'#'+id}, id.slice(0, 10))
5-}
6-
7-function asLink (ln) {
8- return 'string' === typeof ln ? ln : ln.link
9-}
10-
11-var plugs = require('patchbay/plugs')
12-var blob_url = plugs.first(exports.blob_url = [])
13-var avatar_name = plugs.first(exports.avatar_name = [])
14-var avatar_link = plugs.first(exports.avatar_link = [])
15-
16-exports.message_content = function (msg) {
17- if(msg.value.content.type !== 'about' || !msg.value.content.about) return
18-
19- if(!msg.value.content.image && !msg.value.content.name)
20- return
21-
22- var about = msg.value.content
23- var id = msg.value.content.about
24- return h('p',
25- about.about === msg.value.author
26- ? h('span', 'self-identifies ')
27- : h('span', 'identifies ', about.name ? idLink(id) : avatar_link(id, avatar_name(id))),
28- ' as ',
29- h('a', {href:"#"+about.about},
30- about.name || null,
31- about.image
32- ? h('img.avatar--fullsize', {src: blob_url(about.image)})
33- : null
34- )
35- )
36-
37-}
old_modules/app.jsView
@@ -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.jsView
@@ -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.jsView
@@ -1,32 +1,0 @@
1-var h = require('hyperscript')
2-var u = require('patchbay/util')
3-var pull = require('pull-stream')
4-var Scroller = require('pull-scroll')
5-
6-var plugs = require('patchbay/plugs')
7-var sbot_log = plugs.first(exports.sbot_log = [])
8-var data_render = plugs.first(exports.data_render = [])
9-
10-exports.screen_view = function (path, sbot) {
11- if(path === '/data-feed' || path === '/data') {
12- var content = h('div.column.scroller__content')
13- var div = h('div.column.scroller',
14- {style: {'overflow':'auto'}},
15- h('div.scroller__wrapper',
16- content
17- )
18- )
19-
20- pull(
21- u.next(sbot_log, {old: false, limit: 100}),
22- Scroller(div, content, data_render, true, false)
23- )
24-
25- pull(
26- u.next(sbot_log, {reverse: true, limit: 100, live: false}),
27- Scroller(div, content, data_render, false, false)
28- )
29-
30- return div
31- }
32-}
old_modules/feed.jsView
@@ -1,20 +1,0 @@
1-var ref = require('ssb-ref')
2-var h = require('hyperscript')
3-var extend = require('xtend')
4-
5-var plugs = require('patchbay/plugs')
6-var sbot_user_feed = plugs.first(exports.sbot_user_feed = [])
7-var avatar_profile = plugs.first(exports.avatar_profile = [])
8-var feed_summary = plugs.first(exports.feed_summary = [])
9-
10-exports.screen_view = function (id) {
11- if (ref.isFeed(id)) {
12- return feed_summary((opts) => {
13- return sbot_user_feed(extend(opts, {id: id}))
14- }, [
15- h('div', [avatar_profile(id)])
16- ], {
17- windowSize: 50
18- })
19- }
20-}
old_modules/git-mini-messages.jsView
@@ -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.jsView
@@ -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.jsView
@@ -1,51 +1,0 @@
1-var lightbox = require('hyperlightbox')
2-var h = require('../lib/h')
3-var plugs = require('patchbay/plugs')
4-var get_id = plugs.first(exports.get_id = [])
5-var publish = plugs.first(exports.sbot_publish = [])
6-var message_render = plugs.first(exports.message_render = [])
7-
8-exports.message_confirm = function (content, cb) {
9- cb = cb || function () {}
10-
11- var lb = lightbox()
12- document.body.appendChild(lb)
13-
14- var msg = {
15- value: {
16- author: get_id(),
17- previous: null,
18- sequence: null,
19- timestamp: Date.now(),
20- content: content
21- }
22- }
23-
24- var okay = h('button', {
25- 'ev-click': function () {
26- lb.remove()
27- publish(content, cb)
28- },
29- 'ev-keydown': function (ev) {
30- if (ev.keyCode === 27) cancel.click() // escape
31- }
32- }, [
33- 'okay'
34- ])
35-
36- var cancel = h('button', {'ev-click': function () {
37- lb.remove()
38- cb(null)
39- }}, [
40- 'Cancel'
41- ])
42-
43- lb.show(h('MessageConfirm', [
44- h('section', [
45- message_render(msg)
46- ]),
47- h('footer', [okay, cancel])
48- ]))
49-
50- okay.focus()
51-}
old_modules/message-name.jsView
@@ -1,44 +1,0 @@
1-var plugs = require('patchbay/plugs')
2-var sbot_links = plugs.first(exports.sbot_links = [])
3-var get_id = plugs.first(exports.get_id = [])
4-var sbot_get = plugs.first(exports.sbot_get = [])
5-var getAvatar = require('ssb-avatar')
6-
7-exports.message_name = function (id, cb) {
8- sbot_get(id, function (err, value) {
9- if (err && err.name === 'NotFoundError') {
10- return cb(null, id.substring(0, 10) + '...(missing)')
11- } else if (value.content.type === 'post' && typeof value.content.text === 'string') {
12- if (value.content.text.trim()) {
13- return cb(null, titleFromMarkdown(value.content.text, 40))
14- }
15- } else if (value.content.type === 'git-repo') {
16- return getRepoName(id, cb)
17- } else if (typeof value.content.text === 'string') {
18- return cb(null, value.content.type + ': ' + titleFromMarkdown(value.content.text, 30))
19- }
20-
21- return cb(null, id.substring(0, 10) + '...')
22- })
23-}
24-
25-function titleFromMarkdown (text, max) {
26- text = text.trim().split('\n', 2).join('\n')
27- text = text.replace(/_|`|\*|\#|\[.*?\]|\(\S*?\)/g, '').trim()
28- text = text.replace(/\:$/, '')
29- text = text.trim().split('\n', 1)[0].trim()
30- if (text.length > max) {
31- text = text.substring(0, max - 2) + '...'
32- }
33- return text
34-}
35-
36-function getRepoName (id, cb) {
37- getAvatar({
38- links: sbot_links,
39- get: sbot_get
40- }, get_id(), id, function (err, avatar) {
41- if (err) return cb(err)
42- cb(null, avatar.name)
43- })
44-}
old_modules/notifications.jsView
@@ -1,148 +1,0 @@
1-var pull = require('pull-stream')
2-var paramap = require('pull-paramap')
3-var plugs = require('patchbay/plugs')
4-var cont = require('cont')
5-var ref = require('ssb-ref')
6-
7-var sbot_log = plugs.first(exports.sbot_log = [])
8-var sbot_get = plugs.first(exports.sbot_get = [])
9-var sbot_user_feed = plugs.first(exports.sbot_user_feed = [])
10-var message_unbox = plugs.first(exports.message_unbox = [])
11-var get_id = plugs.first(exports.get_id = [])
12-var feed_summary = plugs.first(exports.feed_summary = [])
13-
14-exports.screen_view = function (path) {
15- if (path === '/notifications') {
16- var oldest = null
17- var id = get_id()
18- var ids = {
19- [id]: true
20- }
21-
22- getFirstMessage(id, function (err, msg) {
23- if (err) return console.error(err)
24- if (!oldest || msg.value.timestamp < oldest) {
25- oldest = msg.value.timestamp
26- }
27- })
28-
29- return feed_summary((opts) => {
30- if (opts.old === false) {
31- return pull(
32- sbot_log(opts),
33- unbox(),
34- notifications(ids),
35- pull.filter()
36- )
37- } else {
38- return pull(
39- sbot_log(opts),
40- unbox(),
41- notifications(ids),
42- pull.filter(),
43- pull.take(function (msg) {
44- // abort stream after we pass the oldest messages of our feeds
45- return !oldest || msg.value.timestamp > oldest
46- })
47- )
48- }
49- }, [], {
50- windowSize: 200,
51- filter: (group) => {
52- return (
53- ((group.message || group.type !== 'message') && (group.author !== id || group.digs && group.digs.size)) || (
54- group.repliesFrom && group.repliesFrom.size && (
55- !group.repliesFrom.has(id) || group.repliesFrom.size > 1
56- )
57- )
58- )
59- }
60- })
61- }
62-}
63-
64-function unbox () {
65- return pull(
66- pull.map(function (msg) {
67- return msg.value && typeof msg.value.content === 'string'
68- ? message_unbox(msg)
69- : msg
70- }),
71- pull.filter(Boolean)
72- )
73-}
74-
75-function notifications (ourIds) {
76- function linksToUs (link) {
77- return link && link.link in ourIds
78- }
79-
80- function isOurMsg (id, cb) {
81- if (!id) return cb(null, false)
82- if (typeof id === 'object' && typeof id.link === 'string') id = id.link
83- if (!ref.isMsg(id)) return cb(null, false)
84- sbot_get(id, function (err, msg) {
85- if (err && err.name === 'NotFoundError') cb(null, false)
86- else if (err) cb(err)
87- else if (msg.content.type === 'issue' || msg.content.type === 'pull-request') {
88- isOurMsg(msg.content.repo || msg.content.project, cb)
89- } else {
90- cb(err, msg.author in ourIds)
91- }
92- })
93- }
94-
95- function isAnyOurMessage (msg, ids, cb) {
96- cont.para(ids.map(function (id) {
97- return function (cb) { isOurMsg(id, cb) }
98- }))(function (err, results) {
99- if (err) cb(err)
100- else if (results.some(Boolean)) cb(null, msg)
101- else cb()
102- })
103- }
104-
105- return paramap(function (msg, cb) {
106- var c = msg.value && msg.value.content
107- if (!c || typeof c !== 'object') return cb()
108- if (msg.value.author in ourIds) return cb(null, msg)
109-
110- if (c.mentions && Array.isArray(c.mentions) && c.mentions.some(linksToUs)) {
111- return cb(null, msg)
112- }
113-
114- if (msg.private) {
115- return cb(null, msg)
116- }
117-
118- switch (c.type) {
119- case 'post':
120- if (c.branch || c.root) {
121- return isAnyOurMessage(msg, [].concat(c.branch, c.root), cb)
122- } else {
123- return cb()
124- }
125- case 'contact':
126- return cb(null, c.contact in ourIds ? msg : null)
127- case 'vote':
128- if (c.vote && c.vote.link)
129- return isOurMsg(c.vote.link, function (err, isOurs) {
130- cb(err, isOurs ? msg : null)
131- })
132- else return cb()
133- case 'issue':
134- case 'pull-request':
135- return isOurMsg(c.project || c.repo, function (err, isOurs) {
136- cb(err, isOurs ? msg : null)
137- })
138- case 'issue-edit':
139- return isAnyOurMessage(msg, [c.issue].concat(c.issues), cb)
140- default:
141- cb()
142- }
143- }, 4)
144-}
145-
146-function getFirstMessage (feedId, cb) {
147- sbot_user_feed({id: feedId, gte: 0, limit: 1})(null, cb)
148-}
old_modules/post.jsView
@@ -1,13 +1,0 @@
1-var h = require('hyperscript')
2-var plugs = require('patchbay/plugs')
3-var message_link = plugs.first(exports.message_link = [])
4-var markdown = plugs.first(exports.markdown = [])
5-
6-exports.message_content = function (data) {
7- if(!data.value.content || !data.value.content.text) return
8-
9- return h('div',
10- markdown(data.value.content)
11- )
12-
13-}
old_modules/private.jsView
@@ -1,84 +1,0 @@
1-var pull = require('pull-stream')
2-var ref = require('ssb-ref')
3-var plugs = require('patchbay/plugs')
4-var message_compose = plugs.first(exports.message_compose = [])
5-var sbot_log = plugs.first(exports.sbot_log = [])
6-var feed_summary = plugs.first(exports.feed_summary = [])
7-var message_unbox = plugs.first(exports.message_unbox = [])
8-var get_id = plugs.first(exports.get_id = [])
9-var avatar_image_link = plugs.first(exports.avatar_image_link = [])
10-var update_cache = plugs.first(exports.update_cache = [])
11-var h = require('../lib/h')
12-
13-exports.screen_view = function (path, sbot) {
14- if (path === '/private') {
15- var id = get_id()
16-
17- return feed_summary((opts) => {
18- return pull(
19- sbot_log(opts),
20- loosen(10), // release tight loops if they continue too long (avoid scroll jank)
21- unbox(),
22- pull.through((item) => {
23- if (item.value) {
24- update_cache(item)
25- }
26- })
27- )
28- }, [
29- message_compose({type: 'post', recps: [], private: true}, {
30- prepublish: function (msg) {
31- msg.recps = [id].concat(msg.mentions).filter(function (e) {
32- return ref.isFeed(typeof e === 'string' ? e : e.link)
33- })
34- if (!msg.recps.length) {
35- throw new Error('cannot make private message without recipients - just mention the user in an at reply in the message you send')
36- }
37- return msg
38- },
39- placeholder: `Write a private message \n\n\n\nThis can only be read by yourself and people you have @mentioned.`
40- })
41- ], {
42- windowSize: 1000
43- })
44- }
45-}
46-
47-exports.message_meta = function (msg) {
48- if (msg.value.content.recps || msg.value.private) {
49- return h('span.private', [
50- map(msg.value.content.recps, function (id) {
51- return avatar_image_link(typeof id === 'string' ? id : id.link, 'thumbnail')
52- })
53- ])
54- }
55-}
56-
57-function unbox () {
58- return pull(
59- pull.filter(function (msg) {
60- return typeof msg.value.content === 'string'
61- }),
62- pull.map(function (msg) {
63- return message_unbox(msg) || { timestamp: msg.timestamp }
64- })
65- )
66-}
67-
68-function map (ary, iter) {
69- if (Array.isArray(ary)) return ary.map(iter)
70-}
71-
72-function loosen (max) {
73- var lastRelease = Date.now()
74- return pull.asyncMap(function (item, cb) {
75- if (Date.now() - lastRelease > max) {
76- setImmediate(() => {
77- lastRelease = Date.now()
78- cb(null, item)
79- })
80- } else {
81- cb(null, item)
82- }
83- })
84-}
old_modules/public.jsView
@@ -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.jsView
@@ -1,1 +1,0 @@
1-// disable
old_modules/thread.jsView
@@ -1,127 +1,0 @@
1-var ui = require('patchbay/ui')
2-var pull = require('pull-stream')
3-var Cat = require('pull-cat')
4-var sort = require('ssb-sort')
5-var ref = require('ssb-ref')
6-var h = require('hyperscript')
7-var u = require('patchbay/util')
8-var Scroller = require('pull-scroll')
9-
10-
11-function once (cont) {
12- var ended = false
13- return function (abort, cb) {
14- if(abort) return cb(abort)
15- else if (ended) return cb(ended)
16- else
17- cont(function (err, data) {
18- if(err) return cb(ended = err)
19- ended = true
20- cb(null, data)
21- })
22- }
23-}
24-
25-var plugs = require('patchbay/plugs')
26-
27-var message_render = plugs.first(exports.message_render = [])
28-var message_name = plugs.first(exports.message_name = [])
29-var message_compose = plugs.first(exports.message_compose = [])
30-var message_unbox = plugs.first(exports.message_unbox = [])
31-
32-var sbot_get = plugs.first(exports.sbot_get = [])
33-var sbot_links = plugs.first(exports.sbot_links = [])
34-var get_id = plugs.first(exports.get_id = [])
35-
36-function getThread (root, cb) {
37- //in this case, it's inconvienent that panel only takes
38- //a stream. maybe it would be better to accept an array?
39-
40- sbot_get(root, function (err, value) {
41- var msg = {key: root, value: value}
42-// if(value.content.root) return getThread(value.content.root, cb)
43-
44- pull(
45- sbot_links({rel: 'root', dest: root, values: true, keys: true}),
46- pull.collect(function (err, ary) {
47- if(err) return cb(err)
48- ary.unshift(msg)
49- cb(null, ary)
50- })
51- )
52- })
53-
54-}
55-
56-exports.screen_view = function (id) {
57- if(ref.isMsg(id)) {
58- var meta = {
59- type: 'post',
60- root: id,
61- branch: id //mutated when thread is loaded.
62- }
63-
64- var previousId = id
65- var content = h('div.column.scroller__content')
66- var div = h('div.column.scroller',
67- {style: {'overflow-y': 'auto'}},
68- h('div.scroller__wrapper',
69- content,
70- message_compose(meta, {shrink: false, placeholder: 'Write a reply'})
71- )
72- )
73-
74- message_name(id, function (err, name) {
75- div.title = name
76- })
77-
78- pull(
79- sbot_links({
80- rel: 'root', dest: id, keys: true, old: false
81- }),
82- pull.drain(function (msg) {
83- loadThread() //redraw thread
84- }, function () {} )
85- )
86-
87-
88- function loadThread () {
89- getThread(id, function (err, thread) {
90- //would probably be better keep an id for each message element
91- //(i.e. message key) and then update it if necessary.
92- //also, it may have moved (say, if you received a missing message)
93- content.innerHTML = ''
94- //decrypt
95- thread = thread.map(function (msg) {
96- return 'string' === typeof msg.value.content ? message_unbox(msg) : msg
97- })
98-
99- if(err) return content.appendChild(h('pre', err.stack))
100- sort(thread).map((msg) => {
101- var result = message_render(msg, {inContext: true, previousId})
102- previousId = msg.key
103- return result
104- }).filter(Boolean).forEach(function (el) {
105- content.appendChild(el)
106- })
107-
108- var branches = sort.heads(thread)
109- meta.branch = branches.length > 1 ? branches : branches[0]
110- meta.root = thread[0].value.content.root || thread[0].key
111- meta.channel = thread[0].value.content.channel
112-
113- var recps = thread[0].value.content.recps
114- var private = thread[0].value.private
115- if(private) {
116- if(recps)
117- meta.recps = recps
118- else
119- meta.recps = [thread[0].value.author, get_id()]
120- }
121- })
122- }
123-
124- loadThread()
125- return div
126- }
127-}
old_modules/timestamp.jsView
@@ -1,20 +1,0 @@
1-var h = require('hyperscript')
2-var human = require('human-time')
3-
4-function updateTimestampEl(el) {
5- el.firstChild.nodeValue = human(new Date(el.timestamp))
6- return el
7-}
8-
9-setInterval(function () {
10- var els = [].slice.call(document.querySelectorAll('.pw__timestamp'))
11- els.forEach(updateTimestampEl)
12-}, 60e3)
13-
14-exports.message_main_meta = function (msg) {
15- return updateTimestampEl(h('a.enter.pw__timestamp', {
16- href: '#'+msg.key,
17- timestamp: msg.value.timestamp,
18- title: new Date(msg.value.timestamp)
19- }, ''))
20-}
old_styles/message-confirm.mcssView
@@ -1,18 +1,0 @@
1-MessageConfirm {
2- section {
3- max-height: 80vh;
4- overflow: auto;
5- margin: -20px;
6- padding: 20px;
7- margin-bottom: 0;
8- }
9- footer {
10- text-align: right;
11- background: #e2e2e2;
12- margin: 0px -20px -20px;
13- padding: 5px;
14- border-top: 1px solid #d6d6d6;
15- position: relative;
16- box-shadow: 0 0 6px rgba(51, 51, 51, 0.47);
17- }
18-}
old_styles/patchbay-tweaks.cssView
@@ -1,59 +1,0 @@
1-.scroller__wrapper {
2- width: 600px;
3- padding: 20px;
4-}
5-
6-.compose__controls > input[type=file] {
7- flex: 1;
8- padding: 5px;
9-}
10-
11-a.avatar {
12- display: inline;
13- font-weight: bold;
14- color: #222
15-}
16-
17-div.avatar > a.avatar {
18- display: flex;
19- font-size: 120%;
20-}
21-
22-img.emoji {
23- width: 1.5em;
24- height: 1.5em;
25- align-content: center;
26- margin-top: -0.2em;
27-}
28-
29-div.compose textarea {
30- transition: height 0.1s
31-}
32-
33-div.lightbox {
34- box-shadow: #5f5f5f 1px 2px 100px;
35- bottom: inherit !important;
36- overflow: hidden !important;
37-}
38-
39-div.suggest-box {
40- background: #333;
41-}
42-
43-div.suggest-box ul {
44- left: 390px;
45- top: 87px;
46- position: fixed;
47- padding: 5px;
48- border-radius: 3px;
49- background: #fdfdfd;
50- border: 1px solid #CCC;
51-}
52-
53-div.suggest-box li {
54- padding: 3px;
55-}
56-
57-div.suggest-box strong {
58- font-weight: normal;
59-}
plugs/blob/sync/url.jsView
@@ -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.jsView
@@ -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.jsView
@@ -1,0 +1,3 @@
1+module.exports = {
2+ plugs: require('bulk-require')(__dirname, ['**/!(index).js'])
3+}
plugs/message/html/layout/default.jsView
@@ -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.jsView
@@ -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