Commit 1ed42bd9f36becb191c331ca40892cc3262c8e08
initial
Dominic Tarr committed on 5/11/2016, 1:27:27 AMFiles changed
.travis.yml | added |
LICENSE | added |
README.md | added |
index.js | added |
modules/avatar.js | added |
modules/like.js | added |
modules/message.js | added |
modules/names.js | added |
modules/post.js | added |
modules/timestamp.js | added |
package.json | added |
util.js | added |
LICENSE | ||
---|---|---|
@@ -1,0 +1,22 @@ | ||
1 | +Copyright (c) 2016 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,76 @@ | ||
1 | +# patchboard | |
2 | + | |
3 | +Prototype of a pluggable patchwork. | |
4 | + | |
5 | +Patchboard uses [depject](https://npm.im/depject) to provide | |
6 | +a highly composable api. all scripts in the `./modules` directory | |
7 | +are loaded and combined using [depject](https://npm.im/depject) | |
8 | + | |
9 | +This makes in very easy to create say, a renderer for a new message type, | |
10 | +or switch to a different method for choosing user names. | |
11 | + | |
12 | +Currently, this is a proof of concept and only renders the 100 most recent | |
13 | +messages. This should obviously be replaced with a module that can | |
14 | +scroll properly through a feed, which should plug into another module | |
15 | +that gives tabs or something like that. | |
16 | + | |
17 | + | |
18 | +## overview | |
19 | + | |
20 | +Currently, the main module is `message.js` which plugs into | |
21 | +the `message_render` socket, and provides `message_content`, `avatar`, | |
22 | +`message_meta` and `message_action` hooks. | |
23 | + | |
24 | +`avatar.js` plugs into `avatar`, and provides the `avatar_name` socket. | |
25 | +it just returns a link to the public key, labled with what it gets back | |
26 | +from `avatar_name` socket. this is in turn provided by the `names.js` module. | |
27 | + | |
28 | +Two modules plug into `message_content`, `post.js` and `like.js` | |
29 | + | |
30 | +No plugs into the `message_action` socket have been implemented yet, | |
31 | +but whatever is returned from this will inserted into the dom at the bottom | |
32 | +of the message (by the `message` module) so this would be the plug to | |
33 | +use for implementing a like/+1/fav/dig/yup button, or a reply button. | |
34 | + | |
35 | +## other ideas | |
36 | + | |
37 | +Editable messages would probably need to plug into several sockets. | |
38 | +firstly they would render content differently, so probably use the `message_content` socket. | |
39 | +secondly they would need to show edit state, which would probably use `message_meta` | |
40 | +and finially they'd need to provide the ability to edit the message! | |
41 | +that would use `message_action` | |
42 | + | |
43 | +Implementing a "events" message type would be easy, just implement another | |
44 | +plug for `message_content`, that renders events. | |
45 | + | |
46 | +Instead of reading all the modules in a directory, it would be better | |
47 | +to load these from configuration. Then, modules could be distributed | |
48 | +as browserify bundles, and distributed over ssb. Configuration | |
49 | +could just be a list of hashes - but you could also disable specific | |
50 | +sockets or plugs if necessary (leaving them unconnected). | |
51 | + | |
52 | +Then, that configuration could be shared over ssb! | |
53 | + | |
54 | +## higher level ui | |
55 | + | |
56 | +Instead of just taking the latest 100 messages, what would actually be useful | |
57 | +is ways to efficiently view messages, open threads, etc. | |
58 | +but if we can create a plug for rendering a stream of messages, | |
59 | +we can provide a socket for that in a module that implements tabs, or | |
60 | +columns, or whatever. | |
61 | + | |
62 | +## Running | |
63 | + | |
64 | +``` | |
65 | +# assuming that patchwork@2.8 is already running... | |
66 | +git clone https://github.com/dominictarr/patchboard.git | |
67 | +cd patchboard | |
68 | +npm install electro electron-prebuilt -g | |
69 | +patchwork plugins.install ssb-links # must have patchwork >=2.8 | |
70 | +electro index.js | |
71 | +``` | |
72 | + | |
73 | + | |
74 | +## License | |
75 | + | |
76 | +MIT |
index.js | ||
---|---|---|
@@ -1,0 +1,37 @@ | ||
1 | +var h = require('hyperscript') | |
2 | +var pull = require('pull-stream') | |
3 | +var combine = require('depject') | |
4 | +var fs = require('fs') | |
5 | +var path = require('path') | |
6 | + | |
7 | +var modules = fs.readdirSync(path.join(__dirname, 'modules')) | |
8 | + .map(function (e) { return require('./modules/'+e) }) | |
9 | + | |
10 | +var renderers = [] | |
11 | +modules.unshift({message_render: renderers}) | |
12 | + | |
13 | +combine(modules) | |
14 | + | |
15 | +var u = require('./util') | |
16 | + | |
17 | +require('ssb-client')(function (err, sbot) { | |
18 | + if(err) throw err | |
19 | + pull( | |
20 | + sbot.createLogStream({reverse: true, limit: 100}), | |
21 | + pull.drain(function (data) { | |
22 | + | |
23 | + var el = u.first(renderers, function (render) { | |
24 | + return render(data, sbot) | |
25 | + }) | |
26 | + | |
27 | + if('string' === typeof el) el = document.createTextNode(el) | |
28 | + if(el) document.body.appendChild(el) | |
29 | + }) | |
30 | + ) | |
31 | +}) | |
32 | + | |
33 | + | |
34 | + | |
35 | + | |
36 | + | |
37 | + |
modules/avatar.js | ||
---|---|---|
@@ -1,0 +1,12 @@ | ||
1 | + | |
2 | +var h = require('hyperscript') | |
3 | +var u = require('../util') | |
4 | + | |
5 | +exports.avatar = function (author, sbot) { | |
6 | + return h('a', {href:'#'}, u.first(exports.avatar_name, function (plug) { | |
7 | + return plug(author, sbot) | |
8 | + })) | |
9 | +} | |
10 | + | |
11 | +exports.avatar_name = [] | |
12 | + |
modules/like.js | ||
---|---|---|
@@ -1,0 +1,9 @@ | ||
1 | + | |
2 | +var h = require('hyperscript') | |
3 | + | |
4 | +exports.message_content = function (msg) { | |
5 | + if(msg.value.content && msg.value.content.type === 'vote') | |
6 | + return h('div', msg.value.content.vote.value > 0 ? 'yup' : 'nah', | |
7 | + h('a', {href: '#/msg/'+msg.value.content.vote.link}, msg.key) | |
8 | + ) | |
9 | +} |
modules/message.js | ||
---|---|---|
@@ -1,0 +1,34 @@ | ||
1 | +var h = require('hyperscript') | |
2 | +var u = require('../util') | |
3 | + | |
4 | +exports.message_render = function (msg, sbot) { | |
5 | + var el = u.first(exports.message_content, function (fn) { | |
6 | + return fn(msg) | |
7 | + }) | |
8 | + | |
9 | + if(el) console.log(el) | |
10 | + | |
11 | + function map (plugs, value) { | |
12 | + return plugs.map(function (plug) { | |
13 | + return plug(value, sbot) | |
14 | + }).filter(Boolean) | |
15 | + } | |
16 | + | |
17 | + if(el) | |
18 | + return h('div.message', | |
19 | + h('div.title', | |
20 | + h('div.avatar', map(exports.avatar, msg.value.author)), | |
21 | + h('div.metadata', map(exports.message_meta, msg)) | |
22 | + ), | |
23 | + h('div.content', el), | |
24 | + h('div.footer', | |
25 | + h('div.actions', map(exports.message_actions)) | |
26 | + ) | |
27 | + ) | |
28 | +} | |
29 | + | |
30 | +exports.message_content = [] | |
31 | +exports.avatar = [] | |
32 | +exports.message_meta = [] | |
33 | +exports.message_action = [] | |
34 | + |
modules/names.js | ||
---|---|---|
@@ -1,0 +1,39 @@ | ||
1 | +var h = require('hyperscript') | |
2 | +var pull = require('pull-stream') | |
3 | + | |
4 | +function all(stream, cb) { | |
5 | + pull(stream, pull.collect(cb)) | |
6 | +} | |
7 | + | |
8 | +exports.avatar = | |
9 | +function name (id, sbot) { | |
10 | + var n = h('span', id.substring(0, 10)) | |
11 | + //choose the most popular name for this person. | |
12 | + //for anything like this you'll see I have used sbot.links2 | |
13 | + //which is the ssb-links plugin. as you'll see the query interface | |
14 | + //is pretty powerful! | |
15 | + //TODO: "most popular" name is easily gameable. | |
16 | + //must come up with something better than this. | |
17 | + /* | |
18 | + filter(rel: ['mentions', prefix('@')]) | |
19 | + .reduce(name: rel[1], value: count()) | |
20 | + */ | |
21 | + | |
22 | + all( | |
23 | + sbot.links2.read({query: [ | |
24 | + {$filter: {rel: ['mentions', {$prefix: '@'}], dest: id}}, | |
25 | + {$reduce: { name: ['rel', 1], count: {$count: true} | |
26 | + }} | |
27 | + ]}), | |
28 | + function (err, names) { | |
29 | + if(err) throw err | |
30 | + console.log(names) | |
31 | + n.textContent = names.reduce(function (max, item) { | |
32 | + return max.count > item.count ? max : item | |
33 | + }, {name: id.substring(0, 10), count: 0}).name | |
34 | + }) | |
35 | + | |
36 | + return n | |
37 | + | |
38 | +} | |
39 | + |
modules/post.js | ||
---|---|---|
@@ -1,0 +1,17 @@ | ||
1 | +var markdown = require('ssb-markdown') | |
2 | +var h = require('hyperscript') | |
3 | + | |
4 | +//render a message | |
5 | + | |
6 | +exports.message_content = function (data, sbot) { | |
7 | + if(data.value.content && data.value.content.text) { | |
8 | + var d = h('div'/*, data.value.root ? */) | |
9 | + d.innerHTML = | |
10 | + markdown.block(data.value.content.text, data.value.content.mentions) | |
11 | + return d | |
12 | + } | |
13 | +} | |
14 | + | |
15 | + | |
16 | + | |
17 | + |
modules/timestamp.js | ||
---|---|---|
@@ -1,0 +1,6 @@ | ||
1 | +var h = require('hyperscript') | |
2 | +var moment = require('moment') | |
3 | + | |
4 | +exports.message_meta = function (msg) { | |
5 | + return h('a', {href: '#/'+msg.key}, moment(msg.value.timestamp).fromNow()) | |
6 | +} |
package.json | ||
---|---|---|
@@ -1,0 +1,24 @@ | ||
1 | +{ | |
2 | + "name": "patchboard", | |
3 | + "description": "", | |
4 | + "version": "0.0.0", | |
5 | + "homepage": "https://github.com/dominictarr/patchboard", | |
6 | + "repository": { | |
7 | + "type": "git", | |
8 | + "url": "git://github.com/dominictarr/patchboard.git" | |
9 | + }, | |
10 | + "dependencies": { | |
11 | + "hyperscript": "^1.4.7", | |
12 | + "moment": "^2.13.0", | |
13 | + "pull-paramap": "^1.1.6", | |
14 | + "pull-stream": "^3.3.2", | |
15 | + "ssb-client": "^3.0.1", | |
16 | + "ssb-markdown": "^3.0.0" | |
17 | + }, | |
18 | + "devDependencies": {}, | |
19 | + "scripts": { | |
20 | + "test": "set -e; for t in test/*.js; do node $t; done" | |
21 | + }, | |
22 | + "author": "Dominic Tarr <dominic.tarr@gmail.com> (http://dominictarr.com)", | |
23 | + "license": "MIT" | |
24 | +} |
util.js | ||
---|---|---|
@@ -1,0 +1,18 @@ | ||
1 | +function first (list, test) { | |
2 | + for(var i in list) { | |
3 | + var value = test(list[i], i, list) | |
4 | + if(value) return value | |
5 | + } | |
6 | +} | |
7 | + | |
8 | +function decorate (list, value, caller) { | |
9 | + caller = caller || function (d,e,v) { return d(e, v) } | |
10 | + | |
11 | + return list.reduce(function (element, decorator) { | |
12 | + return caller(decorator, element, value) || element | |
13 | + }, null) | |
14 | +} | |
15 | + | |
16 | +exports.first = first | |
17 | + | |
18 | +exports.decorate = decorate |
Built with git-ssb-web