git ssb

0+

wanderer🌟 / %yCkm4no/U8C2k0Z658j…



forked from mixmix / patch-inbox

Commit a6940a3d687c1a19beaf95c047f36bf32fbb8f2e

basic inbox view

mix irving committed on 9/10/2017, 8:06:50 AM

Files changed

.gitignoreadded
README.mdadded
index.jsadded
message/html/layout/inbox.jsadded
message/html/render/post.jsadded
package.jsonadded
post/html/subject.jsadded
post/page/inbox.jsadded
router/sync/routes.jsadded
styles/mcss.jsadded
.gitignoreView
@@ -1,0 +1,2 @@
1+node_modules
2+
README.mdView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsonView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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.jsView
@@ -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