Commit a6940a3d687c1a19beaf95c047f36bf32fbb8f2e
basic inbox view
mix irving committed on 9/10/2017, 8:06:50 AMFiles changed
.gitignore | added |
README.md | added |
index.js | added |
message/html/layout/inbox.js | added |
message/html/render/post.js | added |
package.json | added |
post/html/subject.js | added |
post/page/inbox.js | added |
router/sync/routes.js | added |
styles/mcss.js | added |
README.md | ||
---|---|---|
@@ -1,0 +1,10 @@ | ||
1 | +# patch-inbox | |
2 | + | |
3 | +intended to be patchcore friendly module for browsing and sending private message. | |
4 | + | |
5 | +currently it is dependent on patchbay as well. Interested to change this in the future. | |
6 | + | |
7 | +Search for `TODO` in codebase to see known areas for improvement. | |
8 | + | |
9 | + | |
10 | + |
index.js | ||
---|---|---|
@@ -1,0 +1,13 @@ | ||
1 | +const nest = require('depnest') | |
2 | + | |
3 | +module.exports = { | |
4 | + patchInbox: nest({ | |
5 | + 'message.html.layout': require('./message/html/layout/inbox'), | |
6 | + 'message.html.render': require('./message/html/render/post'), | |
7 | + 'post.html.subject': require('./post/html/subject'), | |
8 | + 'post.page.inbox': require('./post/page/inbox'), | |
9 | + 'router.sync.routes': require('./router/sync/routes'), | |
10 | + 'styles.mcss': require('./styles/mcss'), | |
11 | + }) | |
12 | +} | |
13 | + |
message/html/layout/inbox.js | ||
---|---|---|
@@ -1,0 +1,119 @@ | ||
1 | +const nest = require('depnest') | |
2 | +const { h, Value } = require('mutant') | |
3 | + | |
4 | +exports.gives = nest('message.html.layout') | |
5 | + | |
6 | +exports.needs = nest({ | |
7 | + 'about.html.image': 'first', | |
8 | + 'keys.sync.id': 'first', | |
9 | + 'message.html.backlinks': 'first', | |
10 | + 'message.html.author': 'first', | |
11 | + 'message.html.markdown': 'first', | |
12 | + 'message.html.meta': 'map', | |
13 | + 'message.html.timestamp': 'first', | |
14 | + 'post.html.subject': 'first', | |
15 | + 'app.sync.goTo': 'first', // TODO generalise - this is patchbay only | |
16 | +}) | |
17 | + | |
18 | +exports.create = (api) => { | |
19 | + return nest('message.html.layout', inboxLayout) | |
20 | + | |
21 | + function inboxLayout (msgRollup, { layout, content } = {}) { | |
22 | + if (layout !== 'inbox') return | |
23 | + | |
24 | + var rawMessage = Value(null) | |
25 | + | |
26 | + const { timestamp, author, meta } = api.message.html | |
27 | + const { image } = api.about.html | |
28 | + | |
29 | + const msgCount = msgRollup.replies.length + 1 | |
30 | + const rootMsg = msgRollup | |
31 | + const newMsg = getNewestMsg(msgRollup) | |
32 | + | |
33 | + const myId = api.keys.sync.id() | |
34 | + const recps = msgRollup.value.content.recps | |
35 | + .map(recp => { | |
36 | + // TODO check these things are feed links!!! | |
37 | + if (typeof recp === 'string') return recp | |
38 | + | |
39 | + if (recp.link) return recp.link | |
40 | + }) | |
41 | + .filter(key => key !== myId) | |
42 | + .filter(Boolean) | |
43 | + .reduce((sofar, el) => sofar.includes(el) ? sofar : [...sofar, el], []) //.uniq | |
44 | + | |
45 | + const showNewMsg = newMsg && newMsg.value.author !== myId | |
46 | + | |
47 | + // const dataset = newMsg | |
48 | + // ? { root: rootMsg.key, id: newMsg.key } | |
49 | + // : { id: root.key } | |
50 | + | |
51 | + const openMessage = () => api.app.sync.goTo({ key: rootMsg.key }) | |
52 | + | |
53 | + const card = h('Message -inbox-card', { // This is required for patchbay keyboard shortcut 'o' | |
54 | + attributes: { | |
55 | + tabindex: '0' | |
56 | + } | |
57 | + }, [ | |
58 | + h('section.recps', {}, [ | |
59 | + h('div.spacer', { className: getSpacerClass(recps) }), | |
60 | + h('div.recps', { className: getRecpsClass(recps) }, recps.map(image)), | |
61 | + ]), | |
62 | + h('section.content', { 'ev-click': openMessage }, [ | |
63 | + h('header', [ | |
64 | + h('span.count', `(${msgCount})`), | |
65 | + api.post.html.subject(rootMsg) | |
66 | + ]), | |
67 | + showNewMsg | |
68 | + ? h('div.update', [ | |
69 | + h('span.replySymbol', '►'), | |
70 | + messageContent(newMsg), | |
71 | + timestamp(newMsg || rootMsg), | |
72 | + ]) : '' | |
73 | + ]), | |
74 | + ]) | |
75 | + | |
76 | + return card | |
77 | + } | |
78 | + | |
79 | + function messageContent (msg) { | |
80 | + if (!msg.value.content || !msg.value.content.text) return | |
81 | + return api.post.html.subject(msg) | |
82 | + } | |
83 | +} | |
84 | + | |
85 | +function getNewestMsg (msg) { | |
86 | + if (!msg.replies || msg.replies.length === 0) return | |
87 | + | |
88 | + return msg.replies[msg.replies.length - 1] | |
89 | +} | |
90 | + | |
91 | +function getSpacerClass (recps) { | |
92 | + switch (recps.length) { | |
93 | + case 1: | |
94 | + return '-half' | |
95 | + case 3: | |
96 | + return '-half' | |
97 | + case 4: | |
98 | + return '-half' | |
99 | + case 5: | |
100 | + return '-quarter' | |
101 | + case 6: | |
102 | + return '-quarter' | |
103 | + default: | |
104 | + return '' | |
105 | + } | |
106 | +} | |
107 | + | |
108 | +function getRecpsClass (recps) { | |
109 | + switch (recps.length) { | |
110 | + case 1: | |
111 | + return '-inbox-large' | |
112 | + case 2: | |
113 | + return '-inbox-large' | |
114 | + default: | |
115 | + return '-inbox-small' | |
116 | + } | |
117 | +} | |
118 | + | |
119 | + |
message/html/render/post.js | ||
---|---|---|
@@ -1,0 +1,41 @@ | ||
1 | +var nest = require('depnest') | |
2 | +var h = require('mutant/h') | |
3 | + | |
4 | +exports.needs = nest({ | |
5 | + 'message.html': { | |
6 | + decorate: 'reduce', | |
7 | + layout: 'first', | |
8 | + link: 'first', | |
9 | + markdown: 'first' | |
10 | + } | |
11 | +}) | |
12 | + | |
13 | +exports.gives = nest('message.html.render') | |
14 | + | |
15 | +exports.create = function (api) { | |
16 | + return nest('message.html.render', function renderMessage (msg, opts) { | |
17 | + if (msg.value.content.type !== 'post') return | |
18 | + if (opts && opts.layout !== 'inbox') return | |
19 | + | |
20 | + var element = api.message.html.layout(msg, Object.assign({}, { | |
21 | + title: messageTitle(msg), | |
22 | + // content: messageContent(msg), // not needed | |
23 | + }, opts)) | |
24 | + | |
25 | + // decorate locally | |
26 | + if (msg.replies && msg.replies.length) { | |
27 | + element.dataset.root = msg.key | |
28 | + element.dataset.id = msg.replies[msg.replies.length-1].key | |
29 | + } else { | |
30 | + element.dataset.id = msg.key | |
31 | + } | |
32 | + | |
33 | + return element | |
34 | + }) | |
35 | + | |
36 | + function messageTitle (data) { | |
37 | + var root = data.value.content && data.value.content.root | |
38 | + return !root ? null : h('span', ['re: ', api.message.html.link(root)]) | |
39 | + } | |
40 | +} | |
41 | + |
package.json | ||
---|---|---|
@@ -1,0 +1,32 @@ | ||
1 | +{ | |
2 | + "name": "patch-inbox", | |
3 | + "version": "0.0.1", | |
4 | + "description": "an inbox for the patchcore ecosystem", | |
5 | + "main": "index.js", | |
6 | + "scripts": { | |
7 | + "test": "echo \"Error: no test specified\" && exit 1" | |
8 | + }, | |
9 | + "repository": { | |
10 | + "type": "git", | |
11 | + "url": "git+https://github.com/mixmix/patch-inbox.git" | |
12 | + }, | |
13 | + "keywords": [ | |
14 | + "patchcore", | |
15 | + "depject", | |
16 | + "scuttlebutt" | |
17 | + ], | |
18 | + "author": "mixmix", | |
19 | + "license": "AGPL-3.0", | |
20 | + "bugs": { | |
21 | + "url": "https://github.com/mixmix/patch-inbox/issues" | |
22 | + }, | |
23 | + "homepage": "https://github.com/mixmix/patch-inbox#readme", | |
24 | + "dependencies": { | |
25 | + "depnest": "^1.3.0", | |
26 | + "mutant": "^3.21.2", | |
27 | + "pull-next-step": "^1.0.0", | |
28 | + "pull-scroll": "^1.0.9", | |
29 | + "pull-stream": "^3.6.1", | |
30 | + "ssb-ref": "^2.7.1" | |
31 | + } | |
32 | +} |
post/html/subject.js | ||
---|---|---|
@@ -1,0 +1,41 @@ | ||
1 | +const nest = require('depnest') | |
2 | + | |
3 | +exports.gives = nest('post.html.subject') | |
4 | + | |
5 | +exports.needs = nest({ | |
6 | + 'message.html.markdown': 'first' | |
7 | +}) | |
8 | + | |
9 | +exports.create = function (api) { | |
10 | + return nest('post.html.subject', subject) | |
11 | + | |
12 | + function subject (msg) { | |
13 | + const { subject, text } = msg.value.content | |
14 | + if(!(subject || text)) return | |
15 | + | |
16 | + return api.message.html.markdown(firstLine(subject|| text)) | |
17 | + } | |
18 | + | |
19 | + function firstLine (text) { | |
20 | + if(text.length < 80 && !~text.indexOf('\n')) return text | |
21 | + | |
22 | + //get the first non-empty line | |
23 | + // var line = text.trim().split('\n').shift().trim() | |
24 | + | |
25 | + var line = text.trim().replace(/\n+/g, ' // ').trim() | |
26 | + | |
27 | + //always break on a space, so that links are preserved. | |
28 | + const leadingMentionsLength = countLeadingMentions(line) | |
29 | + const i = line.indexOf(' ', leadingMentionsLength + 80) | |
30 | + var sample = line.substring(0, ~i ? i : line.length) | |
31 | + | |
32 | + const ellipsis = (sample.length < line.length) ? '...' : '' | |
33 | + return sample + ellipsis | |
34 | + } | |
35 | + | |
36 | + function countLeadingMentions (str) { | |
37 | + return str.match(/^(\s*\[@[^\)]+\)\s*)*/)[0].length | |
38 | + // matches any number of pattern " [@...) " from start of line | |
39 | + } | |
40 | +} | |
41 | + |
post/page/inbox.js | ||
---|---|---|
@@ -1,0 +1,95 @@ | ||
1 | +const nest = require('depnest') | |
2 | +const { h, Value } = require('mutant') | |
3 | +const pull = require('pull-stream') | |
4 | +const Scroller = require('pull-scroll') | |
5 | +const next = require('pull-next-step') | |
6 | +const ref = require('ssb-ref') | |
7 | + | |
8 | +exports.gives = nest({ | |
9 | + 'post.page.inbox': true, | |
10 | + 'app.html.menuItem': true | |
11 | +}) | |
12 | + | |
13 | +exports.needs = nest({ | |
14 | + 'app.html': { | |
15 | + filter: 'first', | |
16 | + scroller: 'first' | |
17 | + }, | |
18 | + 'app.sync.goTo': 'first', | |
19 | + 'feed.pull.private': 'first', | |
20 | + 'feed.pull.rollup': 'first', | |
21 | + 'keys.sync.id': 'first', | |
22 | + 'message.html': { | |
23 | + // compose: 'first', | |
24 | + render: 'first' | |
25 | + } | |
26 | +}) | |
27 | + | |
28 | +exports.create = function (api) { | |
29 | + return nest({ | |
30 | + 'post.page.inbox': page, | |
31 | + 'app.html.menuItem': menuItem | |
32 | + }) | |
33 | + | |
34 | + function menuItem () { | |
35 | + return h('a', { | |
36 | + style: { order: 2 }, | |
37 | + 'ev-click': () => api.app.sync.goTo({ page: 'inbox' }) | |
38 | + }, '/inbox') | |
39 | + } | |
40 | + | |
41 | + function page (location) { | |
42 | + const id = api.keys.sync.id() | |
43 | + | |
44 | + // TODO - create a postNew page | |
45 | + // const composer = api.message.html.compose({ | |
46 | + // meta: { type: 'post' }, | |
47 | + // prepublish: meta => { | |
48 | + // meta.recps = [id, ...(meta.mentions || [])] | |
49 | + // .filter(m => ref.isFeed(typeof m === 'string' ? m : m.link)) | |
50 | + // return meta | |
51 | + // }, | |
52 | + // placeholder: 'Write a private message. \n\n@mention users in the first message to start a private thread.'} | |
53 | + // ) | |
54 | + | |
55 | + const newMsgCount = Value(0) | |
56 | + const { filterMenu, filterDownThrough, filterUpThrough, resetFeed } = api.app.html.filter(draw) | |
57 | + const { container, content } = api.app.html.scroller({ prepend: [ | |
58 | + h('div', { style: {'margin-left': '9rem', display: 'flex', 'align-items': 'baseline'} }, [ | |
59 | + h('button', { 'ev-click': draw, stlye: {'margin-left': 0} }, 'REFRESH'), | |
60 | + h('span', ['New Messages: ', newMsgCount]), | |
61 | + ]), | |
62 | + filterMenu | |
63 | + ] }) | |
64 | + | |
65 | + function draw () { | |
66 | + newMsgCount.set(0) | |
67 | + resetFeed({ container, content }) | |
68 | + | |
69 | + pull( | |
70 | + next(api.feed.pull.private, {old: false, limit: 100, property: ['value', 'timestamp']}), | |
71 | + filterDownThrough(), | |
72 | + pull.drain(msg => newMsgCount.set(newMsgCount() + 1)) | |
73 | + // TODO - better NEW MESSAGES | |
74 | + ) | |
75 | + | |
76 | + pull( | |
77 | + next(api.feed.pull.private, {reverse: true, limit: 100, live: false}, ['value', 'timestamp']), | |
78 | + filterUpThrough(), | |
79 | + pull.filter(msg => msg.value.content.recps), | |
80 | + api.feed.pull.rollup(), | |
81 | + Scroller(container, content, render, false, false) | |
82 | + ) | |
83 | + } | |
84 | + draw() | |
85 | + | |
86 | + function render (msgRollup) { | |
87 | + return api.message.html.render(msgRollup, { layout: 'inbox' }) | |
88 | + } | |
89 | + | |
90 | + container.title = '/inbox' | |
91 | + return container | |
92 | + } | |
93 | +} | |
94 | + | |
95 | + |
router/sync/routes.js | ||
---|---|---|
@@ -1,0 +1,22 @@ | ||
1 | +const nest = require('depnest') | |
2 | + | |
3 | +exports.gives = nest('router.sync.routes') | |
4 | + | |
5 | +exports.needs = nest({ | |
6 | + 'post.page.inbox': 'first' | |
7 | +}) | |
8 | + | |
9 | +exports.create = (api) => { | |
10 | + return nest('router.sync.routes', (sofar = []) => { | |
11 | + const pages = api.post.page | |
12 | + | |
13 | + // loc = location | |
14 | + const routes = [ | |
15 | + [ loc => loc.page === 'inbox', pages.inbox ], | |
16 | + // [ loc => loc.page === 'private', pages.private ], | |
17 | + ] | |
18 | + | |
19 | + return [...routes, ...sofar] | |
20 | + }) | |
21 | +} | |
22 | + |
styles/mcss.js | ||
---|---|---|
@@ -1,0 +1,136 @@ | ||
1 | +const nest = require('depnest') | |
2 | + | |
3 | +exports.gives = nest('styles.mcss') | |
4 | + | |
5 | +const inboxMcss = ` | |
6 | +Message -inbox-card { | |
7 | + padding: .5rem | |
8 | + display: flex | |
9 | + | |
10 | + section.recps { | |
11 | + width: 8rem | |
12 | + margin-right: 1rem | |
13 | + | |
14 | + display: flex | |
15 | + | |
16 | + div.spacer { | |
17 | + -quarter { | |
18 | + width: 2rem | |
19 | + } | |
20 | + | |
21 | + -half { | |
22 | + width: 4rem | |
23 | + } | |
24 | + } | |
25 | + | |
26 | + div.recps { | |
27 | + flex-grow: 1 | |
28 | + | |
29 | + // width: 4rem | |
30 | + height: 4rem | |
31 | + | |
32 | + display: flex | |
33 | + flex-wrap: wrap | |
34 | + justify-content: flex-end | |
35 | + | |
36 | + img.Avatar { | |
37 | + width: 1.9rem | |
38 | + height: 1.9rem | |
39 | + margin: 0 0 .1rem .1rem | |
40 | + } | |
41 | + | |
42 | + -inbox-large { | |
43 | + // width: 8rem | |
44 | + | |
45 | + img.Avatar { | |
46 | + width: 3.8rem | |
47 | + height: 3.8rem | |
48 | + margin: 0 0 0 .2rem | |
49 | + } | |
50 | + } | |
51 | + } | |
52 | + } | |
53 | + | |
54 | + section.content { | |
55 | + max-width: 40rem | |
56 | + margin: 0 | |
57 | + | |
58 | + header { | |
59 | + display: flex | |
60 | + align-items: baseline | |
61 | + | |
62 | + span { | |
63 | + color: #666 | |
64 | + word-break: normal | |
65 | + margin-right: .5rem | |
66 | + } | |
67 | + $markdownSmall | |
68 | + } | |
69 | + | |
70 | + div.update { | |
71 | + $markdownTiny | |
72 | + | |
73 | + display: flex | |
74 | + flex-wrap: wrap | |
75 | + margin-left: 2rem | |
76 | + | |
77 | + span.replySymcol { | |
78 | + color: #666 | |
79 | + margin-right: .3rem | |
80 | + } | |
81 | + | |
82 | + a.Timestamp { | |
83 | + font-size: .8rem | |
84 | + flex-basis: 100% | |
85 | + } | |
86 | + } | |
87 | + } | |
88 | +} | |
89 | + | |
90 | +Scroller { | |
91 | + div.wrapper { | |
92 | + section.content { | |
93 | + div.Message.-inbox-card { | |
94 | + border-bottom: initial | |
95 | + } | |
96 | + } | |
97 | + } | |
98 | +} | |
99 | + | |
100 | +$markdownSmall { | |
101 | + div.Markdown { | |
102 | + h1, h2, h3, h4, h5, h6, p { | |
103 | + font-size: 1rem | |
104 | + font-weight: 300 | |
105 | + margin: 0 | |
106 | + | |
107 | + (img) { max-width: 100% } | |
108 | + } | |
109 | + } | |
110 | +} | |
111 | + | |
112 | +$markdownTiny { | |
113 | + div.Markdown { | |
114 | + h1, h2, h3, h4, h5, h6, p { | |
115 | + color: #666 | |
116 | + font-size: .9rem | |
117 | + font-weight: 300 | |
118 | + margin: 0 | |
119 | + | |
120 | + (a) { color: #666 } | |
121 | + (img) { max-width: 100% } | |
122 | + } | |
123 | + } | |
124 | +} | |
125 | +` | |
126 | + | |
127 | +exports.create = (api) => { | |
128 | + return nest('styles.mcss', mcss) | |
129 | + | |
130 | + function mcss (sofar = {}) { | |
131 | + sofar['patchInbox.app.page.inbox'] = inboxMcss | |
132 | + | |
133 | + return sofar | |
134 | + } | |
135 | +} | |
136 | + |
Built with git-ssb-web