git ssb

0+

Dominic / yap-patch



Commit cb2a914cb1b67be199e798ca44039444c43341fc

initial

Dominic Tarr committed on 7/4/2019, 5:09:07 PM

Files changed

LICENSEadded
README.mdadded
channel-link.jsadded
friends.jsadded
index.jsadded
message-link.jsadded
messages/post.jsadded
messages/vote.jsadded
package.jsonadded
private.jsadded
public.jsadded
thread.jsadded
translations.jsadded
LICENSEView
@@ -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.mdView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsonView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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 +}
translations.jsView
@@ -1,0 +1,3 @@
1 +module.exports = function (lang) {
2 + return function (string) { return string }
3 +}

Built with git-ssb-web