Commit cb2a914cb1b67be199e798ca44039444c43341fc
initial
Dominic Tarr committed on 7/4/2019, 5:09:07 PMFiles changed
LICENSE | added |
README.md | added |
channel-link.js | added |
friends.js | added |
index.js | added |
message-link.js | added |
messages/post.js | added |
messages/vote.js | added |
package.json | added |
private.js | added |
public.js | added |
thread.js | added |
translations.js | added |
LICENSE | ||
---|---|---|
@@ -1,0 +1,22 @@ | ||
1 … | +Copyright (c) 2019 Dominic Tarr | |
2 … | + | |
3 … | +Permission is hereby granted, free of charge, | |
4 … | +to any person obtaining a copy of this software and | |
5 … | +associated documentation files (the "Software"), to | |
6 … | +deal in the Software without restriction, including | |
7 … | +without limitation the rights to use, copy, modify, | |
8 … | +merge, publish, distribute, sublicense, and/or sell | |
9 … | +copies of the Software, and to permit persons to whom | |
10 … | +the Software is furnished to do so, | |
11 … | +subject to the following conditions: | |
12 … | + | |
13 … | +The above copyright notice and this permission notice | |
14 … | +shall be included in all copies or substantial portions of the Software. | |
15 … | + | |
16 … | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
17 … | +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |
18 … | +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
19 … | +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR | |
20 … | +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |
21 … | +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
22 … | +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
README.md | ||
---|---|---|
@@ -1,0 +1,7 @@ | ||
1 … | +# yap-patch | |
2 … | + | |
3 … | +yap plugin for patchwork threads and public/private feeds. | |
4 … | + | |
5 … | +## License | |
6 … | + | |
7 … | +MIT |
channel-link.js | ||
---|---|---|
@@ -1,0 +1,6 @@ | ||
1 … | + | |
2 … | +module.exports = function (sbot) { | |
3 … | + return function (channel) { | |
4 … | + return ['a', {href: '/public?channel='+encodeURIComponent(channel)}, '#'+channel] | |
5 … | + } | |
6 … | +} |
friends.js | ||
---|---|---|
@@ -1,0 +1,47 @@ | ||
1 … | +var toUrl = require('yap-util').toUrl | |
2 … | + | |
3 … | +module.exports = function (sbot) { | |
4 … | + return function (opts, apply, req) { | |
5 … | + var tr = require('./translations')(req.cookies.lang) | |
6 … | + | |
7 … | + return function (cb) { | |
8 … | + var max = 1.5 | |
9 … | + sbot.friends.hops({start: opts.id, reverse: false, max: max}, function (err, follows) { | |
10 … | + if(err) return cb(err) | |
11 … | + sbot.friends.hops({start: opts.id, reverse: true, max: max}, function (err, followers) { | |
12 … | + if(err) return cb(err) | |
13 … | + var friends = {} | |
14 … | + for(var k in followers) | |
15 … | + if(followers[k] <= 0 || followers[k] > max) delete followers[k] | |
16 … | + for(var k in follows) { | |
17 … | + if(followers[k] <= 0 || follows[k] > max) delete follows[k] | |
18 … | + else if(follows[k] > 0 && followers[k] > 0) { | |
19 … | + friends[k] = follows[k] | |
20 … | + delete follows[k] | |
21 … | + delete followers[k] | |
22 … | + } | |
23 … | + } | |
24 … | + var limit = 25 | |
25 … | + function group (label, list) { | |
26 … | + return [ | |
27 … | + 'div.'+label, | |
28 … | + ['h2', tr(label)], | |
29 … | + ].concat(list.slice(0, limit).map(function (e) { | |
30 … | + return apply('avatar', {id: e, image: true, name: false, href: toUrl('friends', {id: e})}) | |
31 … | + })).concat( | |
32 … | + //TODO make this a link to a page showing friends. | |
33 … | + list.length > limit ? '...' + tr('AndMore', list.length-limit) : '' | |
34 … | + ) | |
35 … | + } | |
36 … | + | |
37 … | + cb(null, [ | |
38 … | + ['h1', tr('FriendsOf'), ' ', apply('avatar', {id: opts.id, image: false, name: true})], | |
39 … | + group('Friends', Object.keys(friends)), | |
40 … | + group('Follows', Object.keys(follows)), | |
41 … | + group('Followers', Object.keys(followers)) | |
42 … | + ]) | |
43 … | + }) | |
44 … | + }) | |
45 … | + } | |
46 … | + } | |
47 … | +} |
index.js | ||
---|---|---|
@@ -1,0 +1,28 @@ | ||
1 … | +module.exports = function (sbot) { | |
2 … | + return function (use) { | |
3 … | + //view (and filtered views) on the raw log | |
4 … | + use('public', require('./public')(sbot)) | |
5 … | + use('public/menu', function (opts, apply, req) { | |
6 … | + var tr = require('./translations')(req.cookies.lang) | |
7 … | + return ['a', {href: '/patch/public'}, tr('Public')] | |
8 … | + }) | |
9 … | +// use.map('link', 'feed', 'public') | |
10 … | +// use.map('link', 'msg', '/message') | |
11 … | + use('messages/post', require('./messages/post')(sbot)) | |
12 … | + use('messages/vote', require('./messages/vote')(sbot)) | |
13 … | + use.map('messages', 'post', 'messages/post') | |
14 … | + use.map('messages', 'vote', 'messages/vote') | |
15 … | + | |
16 … | + use.list('menu', 'public/menu') | |
17 … | + use('thread', require('./thread')(sbot)) | |
18 … | + use('private', require('./private')(sbot)) | |
19 … | + use('private/menu', function (opts, apply, req) { | |
20 … | + var tr = require('./translations')(req.cookies.lang) | |
21 … | + return ['a', {href: '/patch/private'}, tr('Private')] | |
22 … | + }) | |
23 … | + use.list('menu', 'private/menu') | |
24 … | + use('friends', require('./friends')(sbot)) | |
25 … | + use('messageLink', require('./message-link')(sbot)) | |
26 … | + use('channelLink', require('./channel-link')(sbot)) | |
27 … | + } | |
28 … | +} |
message-link.js | ||
---|---|---|
@@ -1,0 +1,23 @@ | ||
1 … | +var msum = require('markdown-summary') | |
2 … | + | |
3 … | +module.exports = function (sbot) { | |
4 … | + return function (data, apply) { | |
5 … | + | |
6 … | + function link (data) { | |
7 … | + return ['a', | |
8 … | + {href: apply.toUrl('message', {id: data.key})}, | |
9 … | + msum.title(data.value.content.text) | |
10 … | + ] | |
11 … | + } | |
12 … | + | |
13 … | + if(data.key && data.value && data.value.content && data.value.content.type) | |
14 … | + return link(data) | |
15 … | + else if(data.id) | |
16 … | + return function (cb) { | |
17 … | + sbot.get(data, function (err, msg) { | |
18 … | + var _data = {key: data.id, value: msg} | |
19 … | + cb(null, link(data)) | |
20 … | + }) | |
21 … | + } | |
22 … | + } | |
23 … | +} |
messages/post.js | ||
---|---|---|
@@ -1,0 +1,40 @@ | ||
1 … | +var ref = require('ssb-ref') | |
2 … | +var niceAgo = require('nice-ago') | |
3 … | +var htmlEscape = require('html-escape') | |
4 … | + | |
5 … | +var u = require('yap-util') | |
6 … | +var toUrl = u.toUrl | |
7 … | + | |
8 … | +module.exports = u.createRenderer(function render (data, apply) { | |
9 … | + var since = apply.since | |
10 … | + var time = data.value.timestamp || data.timestamp | |
11 … | + return ['div.Message', | |
12 … | + apply.cacheAttrs(toUrl('message', {id: data.key}), data.key, since), | |
13 … | + ['div.MessageSide', | |
14 … | + apply('avatar', {id: data.value.author, name: false, image: true}), | |
15 … | + ['a', { | |
16 … | + href: toUrl('message', {id: data.key}), | |
17 … | + title: new Date(time)+'\n'+data.key | |
18 … | + }, | |
19 … | + ''+niceAgo(Date.now(), time) | |
20 … | + ] | |
21 … | + ], | |
22 … | + ['div.MessageMain', | |
23 … | + ['div.MessageMeta', | |
24 … | + apply('avatar', {id: data.value.author, name: true, image: false}), | |
25 … | + /* | |
26 … | + h('label.type', data.value.content.type), | |
27 … | + */ | |
28 … | + ['label.msgId', data.key], | |
29 … | + | |
30 … | + data.value.content.channel | |
31 … | + ? ['a', {href: toUrl('patch/public', {channel: data.value.content.channel})}, '#'+data.value.content.channel] | |
32 … | + : '', | |
33 … | + | |
34 … | + ['a', {href: toUrl('patch/thread', {id: data.value.content.root || data.key})}, 'Thread'] | |
35 … | + | |
36 … | + ], | |
37 … | + ['div.MessageContent', u.markdown(data.value.content)] | |
38 … | + ] | |
39 … | + ] | |
40 … | +}) |
messages/vote.js | ||
---|---|---|
@@ -1,0 +1,14 @@ | ||
1 … | +var u = require('yap-util') | |
2 … | +module.exports = u.createRenderer(function (data, apply) { | |
3 … | + return ['div.Message', | |
4 … | + //currently, a vote message has no need for a cache tag. | |
5 … | + //unless we decide to show other peers that have liked this. | |
6 … | + //u.cacheTag(toUrl('message', {id: data.key}), data.key, ), | |
7 … | + ['h3', 'Yup!'], | |
8 … | + ['div.EmbeddedMessage', | |
9 … | + apply('message', {id: data.value.content.vote && data.value.content.vote.link}), | |
10 … | + ] | |
11 … | + ] | |
12 … | +}) | |
13 … | + | |
14 … | + |
package.json | ||
---|---|---|
@@ -1,0 +1,25 @@ | ||
1 … | +{ | |
2 … | + "name": "yap-patch", | |
3 … | + "description": "", | |
4 … | + "version": "1.0.0", | |
5 … | + "homepage": "https://github.com/dominictarr/yap-patch", | |
6 … | + "repository": { | |
7 … | + "type": "git", | |
8 … | + "url": "git://github.com/dominictarr/yap-patch.git" | |
9 … | + }, | |
10 … | + "dependencies": { | |
11 … | + "markdown-summary": "^1.0.3", | |
12 … | + "nice-ago": "^1.0.1", | |
13 … | + "pull-append": "^1.0.0", | |
14 … | + "pull-stream": "^3.6.13", | |
15 … | + "ssb-ref": "^2.13.9", | |
16 … | + "ssb-sort": "^1.1.3", | |
17 … | + "yap-util": "^1.0.3" | |
18 … | + }, | |
19 … | + "devDependencies": {}, | |
20 … | + "scripts": { | |
21 … | + "test": "tape test/*.js" | |
22 … | + }, | |
23 … | + "author": "Dominic Tarr @EMovhfIrFk4NihAKnRNhrfRaqIhBv1Wj8pTxJNgvCCY=.ed25519", | |
24 … | + "license": "MIT" | |
25 … | +} |
private.js | ||
---|---|---|
@@ -1,0 +1,7 @@ | ||
1 … | + | |
2 … | + | |
3 … | +module.exports = function (sbot) { | |
4 … | + return function (opts, apply) { | |
5 … | + return apply('patch/public', Object.assign({}, opts, {private: true})) | |
6 … | + } | |
7 … | +} |
public.js | ||
---|---|---|
@@ -1,0 +1,96 @@ | ||
1 … | +var pull = require('pull-stream') | |
2 … | +var ref = require('ssb-ref') | |
3 … | +var Append = require('pull-append') | |
4 … | + | |
5 … | +var sort = require('ssb-sort') | |
6 … | + | |
7 … | +var u = require('yap-util') | |
8 … | + | |
9 … | +toUrl = u.toUrl | |
10 … | + | |
11 … | +//var render = require('./message').render | |
12 … | +var niceAgo = require('nice-ago') | |
13 … | + | |
14 … | +function merge() { | |
15 … | + return Object.assign.apply(null, [{}].concat([].slice.call(arguments))) | |
16 … | +} | |
17 … | + | |
18 … | +module.exports = function (sbot) { | |
19 … | + return function (opts, apply, req) { | |
20 … | + var tr = require('./translations')(req.cookies.lang) | |
21 … | + var self = this | |
22 … | + opts.reverse = opts.reverse != 'false' | |
23 … | + // var min = !isNaN(+opts.lt) ? +opts.lt : Date.now() | |
24 … | + // var max = !isNaN(+opts.gt) ? +opts.gt : 0 | |
25 … | + | |
26 … | + var min = Date.now() | |
27 … | + var max = 0 | |
28 … | + | |
29 … | + var type = 'patch/'+(opts.private ? 'private' : 'public') | |
30 … | + var Type = (opts.private ? 'Private' : 'Public') | |
31 … | + | |
32 … | + return function (cb) { | |
33 … | + //grab a handle on the since value _before_ we make the | |
34 … | + //query, because then we avoid the race condition | |
35 … | + //where something isn't included in the query but | |
36 … | + //is in the since. | |
37 … | + var since = self.since | |
38 … | + var _opts = u.createQuery(opts, {limit: 20, reverse: opts.reverse}) | |
39 … | + | |
40 … | + pull( | |
41 … | + sbot.query.read(_opts), | |
42 … | + pull.filter(function (v) { | |
43 … | + return v.value.content.type === 'post' | |
44 … | + }), | |
45 … | + pull.collect(function (err, ary) { | |
46 … | + if(err) return cb(err) | |
47 … | + ary = ary.sort(function (a, b) { | |
48 … | + return b.value.timestamp - a.value.timestamp | |
49 … | + }).map(function (data) { | |
50 … | + min = Math.min(data.value.timestamp, min) | |
51 … | + max = Math.max(data.value.timestamp, max) | |
52 … | + return apply('message', data) | |
53 … | + }) | |
54 … | + var nav_opts = {} | |
55 … | + if(opts.author) nav_opts.author = opts.author | |
56 … | + if(opts.channel) nav_opts.channel = opts.channel | |
57 … | + if(opts.private) nav_opts.private = opts.private | |
58 … | + | |
59 … | + var nav = ['span', | |
60 … | + opts.author ? | |
61 … | + apply('avatar', { | |
62 … | + id: opts.author, | |
63 … | + name: true, | |
64 … | + image: false, | |
65 … | + href: toUrl('friends', {id: opts.author}) | |
66 … | + }): '', | |
67 … | + ' ', | |
68 … | + | |
69 … | + //load previous from a url, so that it can be updated by coherence | |
70 … | + apply('more', Object.assign({ | |
71 … | + href: toUrl(type, Object.assign({}, nav_opts, { gt: max })), | |
72 … | + label: '<< '+ niceAgo(Date.now(), max), | |
73 … | + title: 'after '+ new Date(max).toString() | |
74 … | + }, nav_opts, {gt: max })), | |
75 … | + ' + ', | |
76 … | + apply('more', Object.assign({ | |
77 … | + href: toUrl(type, Object.assign({}, nav_opts, { lt: min })), | |
78 … | + label: niceAgo(Date.now(), min) + ' >>', | |
79 … | + title: 'before '+ new Date(min).toString() | |
80 … | + }, nav_opts, {lt: min })), ' ', | |
81 … | + ['a', | |
82 … | + { | |
83 … | + href: toUrl('compose', {private: opts.private, content: {channel: opts.channel}}), | |
84 … | + title: new Date(max).toString() | |
85 … | + }, | |
86 … | + tr('Compose') | |
87 … | + ] | |
88 … | + ] | |
89 … | + ary.unshift(nav) | |
90 … | + ary.push(nav) | |
91 … | + cb(null, ['div.' + Type, ['title', Type], ary]) | |
92 … | + }) | |
93 … | + ) | |
94 … | + } | |
95 … | + } | |
96 … | +} |
thread.js | ||
---|---|---|
@@ -1,0 +1,165 @@ | ||
1 … | +var pull = require('pull-stream') | |
2 … | +var ref = require('ssb-ref') | |
3 … | +var msum = require('markdown-summary') | |
4 … | +var sort = require('ssb-sort') | |
5 … | +var u = require('yap-util') | |
6 … | + | |
7 … | +var toUrl = u.toUrl | |
8 … | + | |
9 … | +function uniqueRecps (recps) { | |
10 … | + if(!recps || !recps.length) return | |
11 … | + recps = recps.map(function (e) { | |
12 … | + return 'string' === typeof e ? e : e.link | |
13 … | + }) | |
14 … | + .filter(Boolean) | |
15 … | + return recps.filter(function (id, i) { | |
16 … | + return !~recps.indexOf(id, i+1) | |
17 … | + }) | |
18 … | +} | |
19 … | + | |
20 … | +function getThread(sbot, id, cb) { | |
21 … | + pull( | |
22 … | + sbot.query.read({ | |
23 … | + //hack so that ssb-query filters on post but uses | |
24 … | + //indexes for root. | |
25 … | + query: [{$filter: { | |
26 … | + value: { content: {root: id} } | |
27 … | + }},{$filter: { | |
28 … | + value: { content: {type: 'post'} } | |
29 … | + }}] | |
30 … | + }), | |
31 … | + pull.collect(function (err, ary) { | |
32 … | + if(err) return cb(err) | |
33 … | + cb(null, ary) | |
34 … | + }) | |
35 … | + ) | |
36 … | +} | |
37 … | + | |
38 … | +function isObject(o) { | |
39 … | + return o && 'object' === typeof o | |
40 … | +} | |
41 … | +var isArray = Array.isArray | |
42 … | + | |
43 … | +function backlinks (sbot, id, cb) { | |
44 … | + var likes = [], backlinks = [] | |
45 … | + pull( | |
46 … | + sbot.links({dest: id, values: true}), | |
47 … | + pull.drain(function (e) { | |
48 … | + var content = e.value.content | |
49 … | + var vote = content.vote | |
50 … | + if(isObject(vote) && | |
51 … | + vote.value == 1 && vote.link == id) | |
52 … | + likes.push(e) | |
53 … | + else if(content.type == 'post' && isArray(content.mentions)) { | |
54 … | + for(var i in content.mentions) { | |
55 … | + var m = content.mentions[i] | |
56 … | + if(m && m.link == id) { | |
57 … | + backlinks.push(e) | |
58 … | + return //if something links twice, don't back link it twice | |
59 … | + } | |
60 … | + } | |
61 … | + } | |
62 … | + }, function () { | |
63 … | + cb(null, likes, backlinks) | |
64 … | + }) | |
65 … | + ) | |
66 … | +} | |
67 … | + | |
68 … | + | |
69 … | +module.exports = function (sbot) { | |
70 … | + return function (opts, apply, req) { | |
71 … | + var context = req.cookies | |
72 … | + var since = apply.since | |
73 … | + var tr = require('./translations')(context.lang) | |
74 … | + return function (cb) { | |
75 … | + var cacheTime = 0 | |
76 … | + if(!ref.isMsg(opts.id)) | |
77 … | + return cb(new Error('expected valid msg id as id')) | |
78 … | + sbot.get({id:opts.id, private: true}, function (err, msg) { | |
79 … | + if(err) return cb(err) | |
80 … | + var data = {key: opts.id, value: msg, timestamp: msg.timestamp || Date.now() } | |
81 … | + if(data.value.content.root) | |
82 … | + cb(null, apply('message', data)) //just show one message | |
83 … | + else if(data.value.content.type != 'post') | |
84 … | + cb(null, apply('message', data)) //just show one message | |
85 … | + else | |
86 … | + getThread(sbot, opts.id, function (err, ary) { | |
87 … | + var root = opts.id | |
88 … | + ary = [data].concat(ary || []) | |
89 … | + var branch = sort.heads(ary) | |
90 … | + var recps = uniqueRecps(ary[0].value.content.recps) | |
91 … | + | |
92 … | + ary.unshift(data) | |
93 … | + var o = {}, cacheTime | |
94 … | + ary = ary.filter(function (e) { | |
95 … | + if(o[e.key]) return false | |
96 … | + return o[e.key] = true | |
97 … | + }) | |
98 … | + sort(ary) | |
99 … | + var recipients = ' ' | |
100 … | + if(ary[0].value.content.recps) | |
101 … | + recipients = ['div.Recipients', tr('ThreadRecipients'), | |
102 … | + ary[0].value.content.recps.map(function (e) { | |
103 … | + return apply('avatar', e) | |
104 … | + })] | |
105 … | + cb(null, | |
106 … | + ['div.thread', | |
107 … | + apply.cacheAttrs(apply.toUrl('thread', opts), data.key, since), | |
108 … | + ary[0].value.content.text && ['title', msum.title(ary[0].value.content.text)], | |
109 … | + recipients, | |
110 … | + ary.map(function (data) { | |
111 … | + return ['div.MessageContainer', | |
112 … | + apply('message', data), | |
113 … | + function (cb) { | |
114 … | + backlinks(sbot, data.key, function (err, likes, backlinks) { | |
115 … | + if(err) return cb(err) | |
116 … | + | |
117 … | + var expression = tr('Like') | |
118 … | + cb(null, ['div.MessageExtra', | |
119 … | + apply('publish', { | |
120 … | + id: context.id, | |
121 … | + suggestedRecps: data.value.author, | |
122 … | + content: { | |
123 … | + type: 'vote', | |
124 … | + root: root, branch: branch, | |
125 … | + vote: { | |
126 … | + link:data.key, value: 1, | |
127 … | + expression: expression | |
128 … | + }, | |
129 … | + channel: data.value.content.channel, | |
130 … | + recps: recps | |
131 … | + }, | |
132 … | + name: expression + ' ' + (likes.length ? '('+likes.length+')' : '') | |
133 … | + }), | |
134 … | + (backlinks.length ? | |
135 … | + ['ul.MessageBacklinks', | |
136 … | + backlinks.map(function (e) { | |
137 … | + return ['li', apply('avatar', {id:e.value.author}), | |
138 … | + ' ', | |
139 … | + apply('/patch/messageLink', e), | |
140 … | + ' ', | |
141 … | + e.value.content.channel && apply('/patch/channelLink', e.value.content.channel) | |
142 … | + ] | |
143 … | + }) | |
144 … | + ] : '') | |
145 … | + ]) | |
146 … | + }) | |
147 … | + } | |
148 … | + ] | |
149 … | + }), | |
150 … | + apply('compose', { | |
151 … | + content: { | |
152 … | + type: 'post', | |
153 … | + root: root, | |
154 … | + recps: recps, | |
155 … | + branch: branch, | |
156 … | + channel: ary[0].value.content.channel | |
157 … | + } | |
158 … | + }) | |
159 … | + ] | |
160 … | + ) | |
161 … | + }) | |
162 … | + }) | |
163 … | + } | |
164 … | + } | |
165 … | +} |
Built with git-ssb-web