Commit d1c4f8d0850bbfbb59186b4b79726d7de9011998
merge
Dominic Tarr committed on 8/11/2017, 8:29:50 AMParent: 5eb78cc1badb9c34b623487a4ff5fe39b1d0ca87
Parent: db1b91bf9f68e6db25b325fd506058608428302e
Files changed
app/html/thread.mcss | changed |
app/page/home.js | changed |
app/page/home.mcss | changed |
app/page/page.mcss | changed |
app/page/private.js | changed |
styles/mixins.js | changed |
app/html/thread.mcss | ||
---|---|---|
@@ -1,19 +1,13 @@ | ||
1 | 1 | Thread { |
2 | - background-color: #f7f7f7 | |
3 | - font-family: 'arial' | |
4 | - padding: 1rem | |
5 | - | |
6 | - max-width: 1400px | |
7 | - | |
8 | 2 | display: flex |
9 | 3 | flex-direction: column |
10 | 4 | |
11 | 5 | div.my-chunk { |
12 | 6 | $chunk |
13 | 7 | |
14 | 8 | justify-content: space-between |
15 | - | |
9 | + | |
16 | 10 | div.avatar { |
17 | 11 | visibility: hidden |
18 | 12 | } |
19 | 13 | |
@@ -21,46 +15,24 @@ | ||
21 | 15 | div.msg-row { |
22 | 16 | div.msg { |
23 | 17 | $primaryColor |
24 | 18 | |
25 | - border-bottom-left-radius: .9rem | |
26 | - border-top-left-radius: .9rem | |
19 | + (a) { color: #4d3568 } | |
20 | + $roundLeft | |
27 | 21 | } |
28 | - :first-child { | |
29 | - div.msg { | |
30 | - border-top-right-radius: .9rem | |
31 | - } | |
32 | - } | |
33 | - :last-child { | |
34 | - div.msg { | |
35 | - border-bottom-right-radius: .9rem | |
36 | - } | |
37 | - } | |
38 | 22 | } |
39 | 23 | } |
40 | 24 | } |
41 | 25 | |
42 | 26 | div.other-chunk { |
43 | 27 | $chunk |
44 | 28 | |
45 | 29 | div.msgs { |
46 | - | |
47 | 30 | div.msg-row { |
48 | 31 | div.msg { |
49 | 32 | border: 1.5px #ddd solid |
50 | - border-bottom-right-radius: .9rem | |
51 | - border-top-right-radius: .9rem | |
33 | + $roundRight | |
52 | 34 | } |
53 | - :first-child { | |
54 | - div.msg { | |
55 | - border-top-left-radius: .9rem | |
56 | - } | |
57 | - } | |
58 | - :last-child { | |
59 | - div.msg { | |
60 | - border-bottom-left-radius: .9rem | |
61 | - } | |
62 | - } | |
63 | 35 | } |
64 | 36 | } |
65 | 37 | } |
66 | 38 | } |
@@ -84,13 +56,20 @@ | ||
84 | 56 | max-width: 80% |
85 | 57 | div.msg-row { |
86 | 58 | display: flex |
87 | 59 | |
60 | + :first-child { | |
61 | + div.msg { $roundTop } | |
62 | + } | |
63 | + :last-child { | |
64 | + div.msg { $roundBottom } | |
65 | + } | |
66 | + | |
88 | 67 | div.msg { |
89 | 68 | line-height: 1.2rem |
90 | 69 | background-color: #fff |
91 | 70 | padding: 0 .7rem |
92 | - margin-bottom: .1rem | |
71 | + margin-bottom: .1rem | |
93 | 72 | border-radius: .3rem |
94 | 73 | } |
95 | 74 | div.msg-spacer { |
96 | 75 | flex-grow: grow |
app/page/home.js | ||
---|---|---|
@@ -1,6 +1,6 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | -const { h } = require('mutant') | |
2 | +const { h, computed } = require('mutant') | |
3 | 3 | const {threadReduce} = require('ssb-reduce-stream') |
4 | 4 | const pull = require('pull-stream') |
5 | 5 | const isObject = require('lodash/isObject') |
6 | 6 | const isString = require('lodash/isString') |
@@ -12,18 +12,51 @@ | ||
12 | 12 | const Next = require('pull-next') |
13 | 13 | |
14 | 14 | exports.needs = nest({ |
15 | 15 | 'about.html.image': 'first', |
16 | + 'about.obs.name': 'first', | |
16 | 17 | 'app.html.nav': 'first', |
17 | 18 | 'sbot.pull.log': 'first', |
18 | 19 | 'history.sync.push': 'first', |
20 | + 'keys.sync.id': 'first', | |
19 | 21 | 'message.sync.unbox': 'first', |
22 | + 'message.html.markdown': 'first' | |
20 | 23 | }) |
21 | 24 | |
25 | +var strings = { | |
26 | + showMore: "Show More", | |
27 | + channels: "Channels", | |
28 | + directMessages: "Direct Messages", | |
29 | + replySymbol: "> " | |
30 | +} | |
31 | + | |
22 | 32 | function firstLine (text) { |
23 | 33 | if(text.length < 80 && !~text.indexOf('\n')) return text |
24 | 34 | |
25 | - return text.split('\n')[0].substring(0, 80) | |
35 | + var line = '' | |
36 | + var lineNumber = 0 | |
37 | + while (line.length === 0) { | |
38 | + const rawLine = text.split('\n')[lineNumber] | |
39 | + line = trimLeadingMentions(rawLine) | |
40 | + | |
41 | + lineNumber++ | |
42 | + } | |
43 | + | |
44 | + var sample = line.substring(0, 80) | |
45 | + if (hasBrokenLink(sample)) | |
46 | + sample = sample + line.substring(81).match(/[^\)]*\)/)[0] | |
47 | + | |
48 | + return sample | |
49 | + | |
50 | + function trimLeadingMentions (str) { | |
51 | + return str.replace(/^(\s*\[@[^\)]+\)\s*)*/, '') | |
52 | + // deletes any number of pattern " [@...) " from start of line | |
53 | + } | |
54 | + | |
55 | + function hasBrokenLink (str) { | |
56 | + return /\[[^\]]*\]\([^\)]*$/.test(str) | |
57 | + // matches "[name](start_of_link" | |
58 | + } | |
26 | 59 | } |
27 | 60 | |
28 | 61 | exports.create = (api) => { |
29 | 62 | return nest('app.page.home', function (location) { |
@@ -32,37 +65,65 @@ | ||
32 | 65 | var container = h('div.container', []) |
33 | 66 | |
34 | 67 | function subject (msg) { |
35 | 68 | const { subject, text } = msg.value.content |
36 | - return firstLine(subject|| text) | |
69 | + return api.message.html.markdown(firstLine(subject|| text)) | |
37 | 70 | } |
38 | 71 | |
39 | 72 | function link(location) { |
40 | 73 | return {'ev-click': () => api.history.sync.push(location)} |
41 | 74 | } |
42 | 75 | |
43 | - function item (name, thread) { | |
76 | + function item (context, thread, opts = {}) { | |
44 | 77 | if(!thread.value) return |
78 | + | |
79 | + const subjectEl = h('div.subject', [ | |
80 | + opts.nameRecipients | |
81 | + ? h('div.recps', buildRecipientNames(thread).map(recp => h('div.recp', recp))) | |
82 | + : null, | |
83 | + subject(thread) | |
84 | + ]) | |
85 | + | |
45 | 86 | const lastReply = thread.replies && last(thread.replies) |
87 | + const replyEl = lastReply | |
88 | + ? h('div.reply', [ | |
89 | + h('div.replySymbol', strings.replySymbol), | |
90 | + subject(lastReply) | |
91 | + ]) | |
92 | + : null | |
46 | 93 | |
47 | - return h('div.threadLink', link(thread), [ | |
48 | - name, | |
49 | - h('div.subject', [subject(thread)]), | |
50 | - lastReply ? h('div.reply', [subject(lastReply)]) : null | |
94 | + | |
95 | + // REFACTOR: move this to a template? | |
96 | + function buildRecipientNames (thread) { | |
97 | + const myId = api.keys.sync.id() | |
98 | + | |
99 | + return thread.value.content.recps | |
100 | + .map(link => isString(link) ? link : link.link) | |
101 | + .filter(link => link !== myId) | |
102 | + .map(api.about.obs.name) | |
103 | + } | |
104 | + | |
105 | + return h('div.thread', link(thread), [ | |
106 | + h('div.context', context), | |
107 | + h('div.content', [ | |
108 | + subjectEl, | |
109 | + replyEl | |
110 | + ]) | |
51 | 111 | ]) |
52 | 112 | } |
53 | 113 | |
54 | - function threadGroup (threads, obj, toName) { | |
114 | + function threadGroup (threads, obj, toContext, opts) { | |
55 | 115 | // threads = a state object for all the types of threads |
56 | - // obj = a map of keys to root ids, where key ∈ (channel | group | concatenated list of pubkeys) | |
57 | - // toName = fn that derives a name from a particular thread | |
116 | + // obj = a map of keys to root ids, where key (channel | group | concatenated list of pubkeys) | |
117 | + // toContext = fn that derives the context of the group | |
118 | + // opts = { nameRecipients } | |
58 | 119 | |
59 | - var groupEl = h('div.group') | |
120 | + var groupEl = h('div.threads') | |
60 | 121 | for(var k in obj) { |
61 | 122 | var id = obj[k] |
62 | 123 | var thread = get(threads, ['roots', id]) |
63 | 124 | if(thread && thread.value) { |
64 | - var el = item(toName(k, thread), thread) | |
125 | + var el = item(toContext(k, thread), thread, opts) | |
65 | 126 | if(el) groupEl.appendChild(el) |
66 | 127 | } |
67 | 128 | } |
68 | 129 | return groupEl |
@@ -92,43 +153,51 @@ | ||
92 | 153 | lastTimestamp = data.timestamp |
93 | 154 | if(isObject(data.value.content)) return data |
94 | 155 | return api.message.sync.unbox(data) |
95 | 156 | }), |
96 | - pull.filter(Boolean), | |
97 | - function (read) { | |
98 | - return function (abort, cb) { | |
99 | - read(abort, function (err, data) { | |
100 | - try { | |
101 | - cb(err, data) | |
102 | - } catch (err) { | |
103 | - console.error(err) | |
104 | - read(err, function () {}) | |
105 | - } | |
106 | - }) | |
107 | - } | |
108 | - } | |
157 | + pull.filter(Boolean) | |
109 | 158 | ), |
110 | - function render (threadsState) { | |
111 | - update(threadsState) | |
159 | + function render (threads) { | |
160 | + update(threads) | |
112 | 161 | morphdom(container, |
113 | 162 | h('div.container', [ |
114 | - threadGroup( | |
115 | - threadsState, | |
116 | - threadsState.private, | |
117 | - function (_, msg) { | |
118 | - // NB: msg passed in is actually a 'thread', but only care about root msg | |
119 | - return h('div.recps', [ | |
120 | - msg.value.content.recps.map(function (link) { | |
121 | - return api.about.html.image(isString(link) ? link : link.link) | |
122 | - }) | |
123 | - ]) | |
124 | - } | |
125 | - ), | |
126 | - threadGroup( | |
127 | - threadsState, | |
128 | - threadsState.channels, | |
129 | - ch => h('h2.title', '#'+ch) | |
130 | - ) | |
163 | + //private section | |
164 | + h('section.updates -directMessage', [ | |
165 | + h('h2', strings.directMessages), | |
166 | + threadGroup( | |
167 | + threads, | |
168 | + threads.private, | |
169 | + function (_, msg) { | |
170 | + // NB: msg passed in is actually a 'thread', but only care about root msg | |
171 | + const myId = api.keys.sync.id() | |
172 | + | |
173 | + return msg.value.content.recps | |
174 | + .map(link => isString(link) ? link : link.link) | |
175 | + .filter(link => link !== myId) | |
176 | + .map(api.about.html.image) | |
177 | + }, | |
178 | + { nameRecipients: true } | |
179 | + ) | |
180 | + ]), | |
181 | + //channels section | |
182 | + h('section.updates -channel', [ | |
183 | + h('h2', strings.channels), | |
184 | + threadGroup( | |
185 | + threads, | |
186 | + threads.channels, | |
187 | + ch => '#'+ch | |
188 | + ) | |
189 | + ]), | |
190 | + //group section | |
191 | + h('section.updates -group', [ | |
192 | + h('h2', 'Groups'), | |
193 | + 'TODO: complete + enable when groups are live' | |
194 | + // threadGroup( | |
195 | + // threads, | |
196 | + // threads.groups, | |
197 | + // toName ... | |
198 | + // ) | |
199 | + ]) | |
131 | 200 | ]) |
132 | 201 | ) |
133 | 202 | return container |
134 | 203 | }, |
@@ -138,9 +207,9 @@ | ||
138 | 207 | return h('Page -home', [ |
139 | 208 | h('h1', 'Home'), |
140 | 209 | api.app.html.nav(), |
141 | 210 | threadsObs, |
142 | - h('button', {'ev-click': threadsObs.more}, ['Show More']) | |
211 | + h('button', {'ev-click': threadsObs.more}, [strings.showMore]) | |
143 | 212 | ]) |
144 | 213 | }) |
145 | 214 | } |
146 | 215 |
app/page/home.mcss | ||
---|---|---|
@@ -1,17 +1,115 @@ | ||
1 | 1 | Page -home { |
2 | - h1 {} | |
3 | 2 | |
4 | 3 | div.container { |
5 | - div.group { | |
6 | - div.threadLink { | |
7 | - div.recps { | |
8 | - img { | |
9 | - $smallAvatar | |
4 | + $primaryBackground | |
5 | + | |
6 | + section.updates { | |
7 | + | |
8 | + -directMessage { | |
9 | + $homePageSection | |
10 | + | |
11 | + div.threads { | |
12 | + div.thread { | |
13 | + div.content { | |
14 | + div.subject { | |
15 | + display: flex | |
16 | + | |
17 | + div.recps { | |
18 | + display: flex | |
19 | + font-weight: 600 | |
20 | + margin-right: .4rem | |
21 | + | |
22 | + div.recp { | |
23 | + margin-right: .4rem | |
24 | + } | |
25 | + } | |
26 | + } | |
27 | + } | |
10 | 28 | } |
11 | 29 | } |
12 | 30 | } |
31 | + | |
32 | + -channel { | |
33 | + $homePageSection | |
34 | + | |
35 | + div.threads { | |
36 | + div.thread { | |
37 | + div.context { | |
38 | + background: #fff | |
39 | + min-width: 8rem | |
40 | + padding: .1rem .3rem | |
41 | + border: 1px solid #ddd | |
42 | + border-radius: 2px | |
43 | + } | |
44 | + } | |
45 | + } | |
46 | + } | |
47 | + | |
48 | + -group { | |
49 | + $homePageSection | |
50 | + } | |
13 | 51 | } |
14 | 52 | } |
15 | 53 | } |
16 | 54 | |
55 | +$homePageSection { | |
56 | + display: flex | |
57 | + flex-direction: column | |
58 | + align-items: center | |
59 | + margin-bottom: 1.5rem | |
17 | 60 | |
61 | + h2 { | |
62 | + color: #888 | |
63 | + font-size: 1rem | |
64 | + text-align: center | |
65 | + padding-bottom: .3rem | |
66 | + width: 420px | |
67 | + border-bottom: 1px solid #aaa | |
68 | + margin-top: 0 | |
69 | + } | |
70 | + | |
71 | + div.threads { | |
72 | + div.thread { | |
73 | + display: flex | |
74 | + align-items: center | |
75 | + | |
76 | + margin-bottom: .5rem | |
77 | + | |
78 | + div.context { | |
79 | + display: flex | |
80 | + margin-right: 1rem | |
81 | + | |
82 | + img { | |
83 | + $smallAvatar | |
84 | + margin-right: .5rem | |
85 | + } | |
86 | + } | |
87 | + | |
88 | + div.content { | |
89 | + flex-grow: .8 | |
90 | + background: #fff | |
91 | + padding: 1rem | |
92 | + border: 1px solid #ddd | |
93 | + border-radius: 1rem | |
94 | + | |
95 | + div.subject { | |
96 | + font-size: 1.2rem | |
97 | + margin-bottom: .3rem | |
98 | + | |
99 | + $largeMarkdown | |
100 | + } | |
101 | + div.reply { | |
102 | + display: flex | |
103 | + color: #444 | |
104 | + | |
105 | + div.replySymbol { | |
106 | + margin-left: .7rem | |
107 | + margin-right: .3rem | |
108 | + } | |
109 | + | |
110 | + $smallMarkdown | |
111 | + } | |
112 | + } | |
113 | + } | |
114 | + } | |
115 | +} |
app/page/page.mcss | ||
---|---|---|
@@ -2,6 +2,15 @@ | ||
2 | 2 | h1 { |
3 | 3 | margin-left: 1rem |
4 | 4 | } |
5 | 5 | |
6 | + div.Nav {} | |
7 | + | |
8 | + div.container { | |
9 | + $primaryBackground | |
10 | + padding: 1rem | |
11 | + | |
12 | + max-width: 1400px | |
13 | + | |
14 | + } | |
6 | 15 | } |
7 | 16 |
app/page/private.js | ||
---|---|---|
@@ -18,8 +18,8 @@ | ||
18 | 18 | |
19 | 19 | return h('Page -private', [ |
20 | 20 | h('h1', 'Private message'), |
21 | 21 | api.app.html.nav(), |
22 | - thread | |
22 | + h('div.container', thread) | |
23 | 23 | ]) |
24 | 24 | } |
25 | 25 | } |
styles/mixins.js | ||
---|---|---|
@@ -18,8 +18,12 @@ | ||
18 | 18 | $colorSubtle { |
19 | 19 | color: #222 |
20 | 20 | } |
21 | 21 | |
22 | +$primaryBackground { | |
23 | + background-color: #f7f7f7 | |
24 | +} | |
25 | + | |
22 | 26 | $smallAvatar { |
23 | 27 | width: 3rem |
24 | 28 | height: 3rem |
25 | 29 | border-radius: 1.5rem |
@@ -29,5 +33,45 @@ | ||
29 | 33 | width: 6rem |
30 | 34 | height: 6rem |
31 | 35 | border-radius: 3rem |
32 | 36 | } |
37 | + | |
38 | +$smallMarkdown { | |
39 | + div.Markdown { | |
40 | + h1, h2, h3, h4, h5, h6, p { | |
41 | + font-size: 1rem | |
42 | + font-weight: 300 | |
43 | + margin: 0 | |
44 | + } | |
45 | + } | |
46 | +} | |
47 | + | |
48 | +$largeMarkdown { | |
49 | + div.Markdown { | |
50 | + h1, h2, h3, h4, h5, h6, p { | |
51 | + font-size: 1.2rem | |
52 | + font-weight: 300 | |
53 | + margin: 0 | |
54 | + } | |
55 | + } | |
56 | +} | |
57 | + | |
58 | +$roundLeft { | |
59 | + border-top-left-radius: .9rem | |
60 | + border-bottom-left-radius: .9rem | |
61 | +} | |
62 | + | |
63 | +$roundRight { | |
64 | + border-top-right-radius: .9rem | |
65 | + border-bottom-right-radius: .9rem | |
66 | +} | |
67 | + | |
68 | +$roundTop { | |
69 | + border-top-left-radius: .9rem | |
70 | + border-top-right-radius: .9rem | |
71 | +} | |
72 | + | |
73 | +$roundBottom { | |
74 | + border-bottom-left-radius: .9rem | |
75 | + border-bottom-right-radius: .9rem | |
76 | +} | |
33 | 77 | ` |
Built with git-ssb-web