Commit 6cedff7c896fdc49a0f8be9b4f479c7704797374
Merge pull request #97 from ticktackim/multi_pm_sidebar
show multi-msgs in sidebarmix irving authored on 2/12/2018, 1:05:31 AM
GitHub committed on 2/12/2018, 1:05:31 AM
Parent: 65bba059de67096d23a74b9e419174aa97fa4977
Parent: 0f137965c8946bbf7e1023f2f6d9e3168fcf88d9
Files changed
about/html/avatar.mcss | changed |
app/html/sideNav/sideNav.mcss | changed |
app/html/sideNav/sideNavDiscovery.js | changed |
app/page/threadNew.js | changed |
app/page/userShow.js | changed |
app/page/userShow.mcss | changed |
message/html/subject.js | changed |
message/index.js | changed |
message/sync/getParticipants.js | added |
router/sync/routes.js | changed |
styles/mixins.js | changed |
about/html/avatar.mcss | ||
---|---|---|
@@ -5,8 +5,12 @@ | ||
5 | 5 | -tiny { |
6 | 6 | $circleTiny |
7 | 7 | } |
8 | 8 | |
9 | + -halfSmall { | |
10 | + $circleHalfSmall | |
11 | + } | |
12 | + | |
9 | 13 | -small { |
10 | 14 | $circleSmall |
11 | 15 | } |
12 | 16 |
app/html/sideNav/sideNav.mcss | ||
---|---|---|
@@ -56,9 +56,9 @@ | ||
56 | 56 | position: relative |
57 | 57 | width: 1.2rem |
58 | 58 | height: 1.2rem |
59 | 59 | border-radius: 1rem |
60 | - top: -.2rem | |
60 | + /* top: -.2rem */ | |
61 | 61 | left: -.2rem |
62 | 62 | |
63 | 63 | background-color: red |
64 | 64 | color: #fff |
@@ -92,13 +92,23 @@ | ||
92 | 92 | justify-content: center |
93 | 93 | align-items: center |
94 | 94 | } |
95 | 95 | |
96 | + img {} | |
97 | + div.many-images { | |
98 | + /* this width refernces avatarSmall */ | |
99 | + width: 2.8rem | |
100 | + height: 2.8rem | |
101 | + | |
102 | + display: flex | |
103 | + flex-wrap: wrap | |
104 | + justify-content: center | |
105 | + align-items: center | |
106 | + | |
107 | + img { | |
108 | + } | |
109 | + } | |
96 | 110 | |
97 | - a img { | |
98 | - | |
99 | - } | |
100 | - | |
101 | 111 | i { |
102 | 112 | $circleSmall |
103 | 113 | $colorPrimary |
104 | 114 | font-size: 1.3rem |
app/html/sideNav/sideNavDiscovery.js | ||
---|---|---|
@@ -19,8 +19,9 @@ | ||
19 | 19 | 'keys.sync.id': 'first', |
20 | 20 | 'history.sync.push': 'first', |
21 | 21 | 'history.obs.store': 'first', |
22 | 22 | 'message.html.subject': 'first', |
23 | + 'message.sync.getParticipants': 'first', | |
23 | 24 | 'sbot.obs.localPeers': 'first', |
24 | 25 | 'translations.sync.strings': 'first', |
25 | 26 | 'unread.sync.isUnread': 'first' |
26 | 27 | }) |
@@ -58,10 +59,10 @@ | ||
58 | 59 | if (!isMatch(location)) return |
59 | 60 | |
60 | 61 | const strings = api.translations.sync.strings() |
61 | 62 | const myKey = api.keys.sync.id() |
62 | - | |
63 | 63 | var nearby = api.sbot.obs.localPeers() |
64 | + const getParticipants = api.message.sync.getParticipants | |
64 | 65 | |
65 | 66 | // Unread message counts |
66 | 67 | function updateCache (cache, msg) { |
67 | 68 | if(api.unread.sync.isUnread(msg)) |
@@ -70,10 +71,13 @@ | ||
70 | 71 | cache.delete(msg.key) |
71 | 72 | } |
72 | 73 | |
73 | 74 | function updateUnreadMsgsCache (msg) { |
74 | - updateCache(getUnreadMsgsCache(msg.value.author), msg) | |
75 | - updateCache(getUnreadMsgsCache(msg.value.content.root || msg.key), msg) | |
75 | + const participantsKey = getParticipants(msg).key | |
76 | + updateCache(getUnreadMsgsCache(participantsKey), msg) | |
77 | + | |
78 | + const rootKey = get(msg, 'value.content.root', msg.key) | |
79 | + updateCache(getUnreadMsgsCache(rootKey), msg) | |
76 | 80 | } |
77 | 81 | |
78 | 82 | pull( |
79 | 83 | next(api.feed.pull.private, {reverse: true, limit: 1000, live: false, property: ['value', 'timestamp']}), |
@@ -109,24 +113,22 @@ | ||
109 | 113 | map(nearby, feedId => Option({ |
110 | 114 | notifications: notifications(feedId), |
111 | 115 | imageEl: api.about.html.avatar(feedId, 'small'), |
112 | 116 | label: api.about.obs.name(feedId), |
113 | - selected: location.feed === feedId && !isDiscoverLocation(location), | |
117 | + selected: get(location, 'participants', []).join() === feedId && !isDiscoverLocation(location), | |
114 | 118 | location: computed(recentMsgCache, recent => { |
115 | 119 | const lastMsg = recent.find(msg => msg.value.author === feedId) |
116 | 120 | return lastMsg |
117 | - ? Object.assign(lastMsg, { feed: feedId }) | |
118 | - : { page: 'threadNew', feed: feedId } | |
121 | + ? Object.assign(lastMsg, { participants: [feedId] }) | |
122 | + : { page: 'threadNew', participants: [feedId] } | |
119 | 123 | }), |
120 | 124 | }), { comparer: (a, b) => a === b }), |
121 | 125 | |
122 | 126 | // --------------------- |
123 | 127 | computed(nearby, n => !isEmpty(n) ? h('hr') : null), |
124 | 128 | |
125 | 129 | // Discover |
126 | 130 | Option({ |
127 | - // notifications: '!', //TODO - count this! | |
128 | - // imageEl: h('i.fa.fa-binoculars'), | |
129 | 131 | imageEl: h('i', [ |
130 | 132 | h('img', { src: path.join(__dirname, '../../../assets', 'discover.png') }) |
131 | 133 | ]), |
132 | 134 | label: strings.blogIndex.title, |
@@ -162,28 +164,48 @@ | ||
162 | 164 | filter: privateMsgFilter, |
163 | 165 | store: recentMsgCache, |
164 | 166 | updateTop: updateRecentMsgCache, |
165 | 167 | updateBottom: updateRecentMsgCache, |
166 | - render: (msgObs) => { | |
167 | - const msg = resolve(msgObs) | |
168 | - const { author } = msg.value | |
169 | - if (nearby.has(author)) return | |
168 | + render | |
169 | + }) | |
170 | + | |
171 | + function render (msgObs) { | |
172 | + const msg = resolve(msgObs) | |
173 | + const participants = getParticipants(msg) | |
174 | + // TODO msg has been decorated with a flat participantsKey, could re-hydrate | |
170 | 175 | |
176 | + if (participants.length === 1 && nearby.has(participants.key)) return | |
177 | + const locParticipantsKey = get(location, 'participants', []).join(' ') //TODO collect logic | |
178 | + | |
179 | + if (participants.length === 1) { | |
180 | + const author = participants[0] | |
171 | 181 | return Option({ |
172 | 182 | //the number of threads with each peer |
173 | 183 | notifications: notifications(author), |
174 | 184 | imageEl: api.about.html.avatar(author), |
175 | 185 | label: api.about.obs.name(author), |
176 | - selected: location.feed === author, | |
177 | - location: Object.assign({}, msg, { feed: author }) // TODO make obs? | |
186 | + selected: locParticipantsKey === author, | |
187 | + location: Object.assign({}, msg, { participants }) // TODO make obs? | |
178 | 188 | }) |
179 | 189 | } |
180 | - }) | |
190 | + else { | |
191 | + const rootMsg = get(msg, 'value.content.root', msg) | |
192 | + return Option({ | |
193 | + //the number of threads with each peer | |
194 | + notifications: notifications(participants), | |
195 | + imageEl: participants.map(p => api.about.html.avatar(p, 'halfSmall')), | |
196 | + label: api.message.html.subject(rootMsg), | |
197 | + selected: locParticipantsKey === participants.key, | |
198 | + location: Object.assign({}, msg, { participants }) // TODO make obs? | |
199 | + }) | |
200 | + } | |
201 | + } | |
181 | 202 | |
182 | 203 | function updateRecentMsgCache (soFar, newMsg) { |
183 | 204 | soFar.transaction(() => { |
184 | - const { author, timestamp } = newMsg.value | |
185 | - const index = indexOf(soFar, (msg) => author === resolve(msg).value.author) | |
205 | + const { timestamp } = newMsg.value | |
206 | + newMsg.participantsKey = getParticipants(newMsg).key | |
207 | + const index = indexOf(soFar, (msg) => newMsg.participantsKey === resolve(msg).participantsKey) | |
186 | 208 | var object = Value() |
187 | 209 | |
188 | 210 | if (index >= 0) { |
189 | 211 | // reference already exists, lets use this instead! |
@@ -218,28 +240,31 @@ | ||
218 | 240 | return cache |
219 | 241 | } |
220 | 242 | |
221 | 243 | function notifications (key) { |
244 | + key = typeof key === 'string' | |
245 | + ? key | |
246 | + : key.key // participants.key case | |
222 | 247 | return computed(getUnreadMsgsCache(key), cache => cache.length) |
223 | 248 | } |
224 | 249 | |
225 | 250 | function LevelTwoSideNav () { |
226 | - const { key, value, feed: targetUser, page } = location | |
251 | + const { key, value, participants, page } = location | |
227 | 252 | const root = get(value, 'content.root', key) |
228 | - if (!targetUser) return | |
253 | + if (isEmpty(participants)) return | |
229 | 254 | if (page === 'userShow') return |
230 | 255 | |
231 | - | |
232 | 256 | const prepend = Option({ |
233 | 257 | selected: page === 'threadNew', |
234 | - location: {page: 'threadNew', feed: targetUser}, | |
258 | + location: {page: 'threadNew', participants}, | |
235 | 259 | label: h('Button', strings.threadNew.action.new), |
236 | 260 | }) |
237 | 261 | |
238 | - var userLastMsgCache = usersLastMsgCache.get(targetUser) | |
262 | + var participantsKey = participants.join(' ') // TODO collect this repeated logic | |
263 | + var userLastMsgCache = usersLastMsgCache.get(participantsKey) | |
239 | 264 | if (!userLastMsgCache) { |
240 | 265 | userLastMsgCache = MutantArray() |
241 | - usersLastMsgCache.put(targetUser, userLastMsgCache) | |
266 | + usersLastMsgCache.put(participantsKey, userLastMsgCache) | |
242 | 267 | } |
243 | 268 | |
244 | 269 | return api.app.html.scroller({ |
245 | 270 | classList: [ 'level', '-two' ], |
@@ -247,28 +272,27 @@ | ||
247 | 272 | stream: api.feed.pull.private, |
248 | 273 | filter: () => pull( |
249 | 274 | pull.filter(msg => !msg.value.content.root), |
250 | 275 | pull.filter(msg => msg.value.content.type === 'post'), |
251 | - pull.filter(msg => msg.value.content.recps), | |
252 | - pull.filter(msg => msg.value.content.recps | |
253 | - .map(recp => typeof recp === 'object' ? recp.link : recp) | |
254 | - .some(recp => recp === targetUser) | |
255 | - ) | |
276 | + pull.filter(msg => getParticipants(msg).key === participantsKey) | |
256 | 277 | ), |
257 | 278 | store: userLastMsgCache, |
258 | 279 | updateTop: updateLastMsgCache, |
259 | 280 | updateBottom: updateLastMsgCache, |
260 | - render: (rootMsgObs) => { | |
261 | - const rootMsg = resolve(rootMsgObs) | |
262 | - return Option({ | |
263 | - notifications: notifications(rootMsg.key), | |
264 | - label: api.message.html.subject(rootMsg), | |
265 | - selected: rootMsg.key === root, | |
266 | - location: Object.assign(rootMsg, { feed: targetUser }), | |
267 | - }) | |
268 | - } | |
281 | + render | |
269 | 282 | }) |
270 | 283 | |
284 | + function render (rootMsgObs) { | |
285 | + const rootMsg = resolve(rootMsgObs) | |
286 | + const participants = getParticipants(rootMsg) | |
287 | + return Option({ | |
288 | + notifications: notifications(rootMsg.key), | |
289 | + label: api.message.html.subject(rootMsg), | |
290 | + selected: rootMsg.key === root, | |
291 | + location: Object.assign(rootMsg, { participants }), | |
292 | + }) | |
293 | + } | |
294 | + | |
271 | 295 | function updateLastMsgCache (soFar, newMsg) { |
272 | 296 | soFar.transaction(() => { |
273 | 297 | const { timestamp } = newMsg.value |
274 | 298 | const index = indexOf(soFar, (msg) => timestamp === resolve(msg).value.timestamp) |
@@ -305,18 +329,19 @@ | ||
305 | 329 | |
306 | 330 | return h('Option', { className }, [ |
307 | 331 | h('div.circle', [ |
308 | 332 | when(notifications, h('div.alert', notifications)), |
309 | - imageEl | |
333 | + Array.isArray(imageEl) | |
334 | + ? h('div.many-images', imageEl.slice(0,4)) // not ideal? not enough space to show more though | |
335 | + : imageEl | |
310 | 336 | ]), |
311 | 337 | h('div.label', { 'ev-click': goToLocation }, label) |
312 | 338 | ]) |
313 | 339 | } |
314 | 340 | |
315 | 341 | function privateMsgFilter () { |
316 | 342 | return pull( |
317 | 343 | pull.filter(msg => msg.value.content.type === 'post'), |
318 | - pull.filter(msg => msg.value.author != myKey), | |
319 | 344 | pull.filter(msg => msg.value.content.recps) |
320 | 345 | ) |
321 | 346 | } |
322 | 347 | } |
app/page/threadNew.js | ||
---|---|---|
@@ -1,7 +1,8 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | 2 | const { h, Struct, Value, Array: MutantArray, computed, map, resolve } = require('mutant') |
3 | 3 | const suggestBox = require('suggest-box') |
4 | +const isEmpty = require('lodash/isEmpty') | |
4 | 5 | |
5 | 6 | exports.gives = nest('app.page.threadNew') |
6 | 7 | |
7 | 8 | exports.needs = nest({ |
@@ -21,24 +22,29 @@ | ||
21 | 22 | |
22 | 23 | return nest('app.page.threadNew', threadNew) |
23 | 24 | |
24 | 25 | function threadNew (location) { |
25 | - const { feed, channel } = location | |
26 | - | |
27 | - if (feed) return threadNewFeed(location) | |
26 | + if (isEmpty(location.participants)) return | |
27 | + | |
28 | + return threadNewFeed(location) | |
28 | 29 | } |
29 | 30 | |
30 | 31 | function threadNewFeed (location) { |
31 | 32 | const strings = api.translations.sync.strings() |
32 | 33 | const myId = api.keys.sync.id() |
33 | 34 | |
34 | - const { feed } = location | |
35 | + const { participants } = location | |
35 | 36 | |
36 | 37 | const meta = Struct({ |
37 | 38 | type: 'post', |
38 | 39 | recps: MutantArray ([ |
39 | 40 | myId, |
40 | - { link: feed, name: resolve(api.about.obs.name(feed)) } | |
41 | + ...participants.map(p => { | |
42 | + return { | |
43 | + link: p, | |
44 | + name: resolve(api.about.obs.name(p)) | |
45 | + } | |
46 | + }) | |
41 | 47 | ]), |
42 | 48 | subject: Value() |
43 | 49 | }) |
44 | 50 |
app/page/userShow.js | ||
---|---|---|
@@ -41,9 +41,9 @@ | ||
41 | 41 | { page: 'userEdit', feed }, |
42 | 42 | // h('i.fa.fa-pencil') |
43 | 43 | h('img', { src: path.join(__dirname, '../../assets', 'edit.png') }) |
44 | 44 | ) |
45 | - const directMessageButton = Link({ page: 'threadNew', feed }, h('Button', strings.userShow.action.directMessage)) | |
45 | + const directMessageButton = Link({ page: 'threadNew', participants: [feed] }, h('Button', strings.userShow.action.directMessage)) | |
46 | 46 | |
47 | 47 | const BLOG_TYPES = ['blog', 'post'] |
48 | 48 | |
49 | 49 | // TODO return some of this ? |
@@ -69,10 +69,10 @@ | ||
69 | 69 | ]), |
70 | 70 | h('div.introduction', computed(api.about.obs.description(feed), d => api.message.html.markdown(d || ''))), |
71 | 71 | feed !== myId |
72 | 72 | ? h('div.actions', [ |
73 | + h('div.directMessage', directMessageButton), | |
73 | 74 | api.contact.html.follow(feed), |
74 | - h('div.directMessage', directMessageButton), | |
75 | 75 | api.contact.html.block(feed) |
76 | 76 | ]) |
77 | 77 | : '', |
78 | 78 | ]), |
app/page/userShow.mcss | ||
---|---|---|
@@ -28,18 +28,20 @@ | ||
28 | 28 | } |
29 | 29 | |
30 | 30 | div.actions { |
31 | 31 | display: flex |
32 | + | |
33 | + div { | |
34 | + margin: 0 .5rem | |
35 | + } | |
32 | 36 | |
33 | 37 | div.Follow { |
34 | - margin-right: 1rem | |
35 | 38 | } |
36 | 39 | |
37 | 40 | div.directMessage { |
38 | 41 | } |
39 | 42 | |
40 | 43 | div.Block { |
41 | - margin-left: 1rem | |
42 | 44 | } |
43 | 45 | } |
44 | 46 | } |
45 | 47 | } |
message/html/subject.js | ||
---|---|---|
@@ -1,23 +1,48 @@ | ||
1 | 1 | const nest = require('depnest') |
2 | -const { h, when, send, resolve, Value, computed } = require('mutant') | |
2 | +const { computed, Value } = require('mutant') | |
3 | 3 | const { title } = require('markdown-summary') |
4 | +const { isMsg } = require('ssb-ref') | |
4 | 5 | |
5 | 6 | |
6 | 7 | exports.gives = nest('message.html.subject') |
7 | 8 | |
8 | 9 | exports.needs = nest({ |
9 | 10 | 'message.html.markdown': 'first', |
11 | + 'message.sync.unbox': 'first', | |
12 | + 'sbot.async.get': 'first', | |
10 | 13 | }) |
11 | 14 | |
12 | 15 | exports.create = function (api) { |
13 | 16 | return nest('message.html.subject', subject) |
14 | 17 | |
15 | 18 | function subject (msg) { |
19 | + if (msg === undefined) debugger | |
20 | + // test if it's a message ref, or a full message object | |
21 | + if (isMsg(msg)) { | |
22 | + var subject = Value() | |
23 | + | |
24 | + api.sbot.async.get(msg, (err, value) => { | |
25 | + if (err) throw err | |
26 | + | |
27 | + subject.set(getMsgSubject({ | |
28 | + key: msg, | |
29 | + value: api.message.sync.unbox(value) | |
30 | + })) | |
31 | + }) | |
32 | + | |
33 | + return subject | |
34 | + } | |
35 | + else | |
36 | + return getMsgSubject(msg) | |
37 | + } | |
38 | + | |
39 | + function getMsgSubject (msg) { | |
16 | 40 | const { subject, text } = msg.value.content |
17 | 41 | if(!(subject || text)) return |
18 | 42 | |
19 | 43 | return subject |
20 | 44 | ? api.message.html.markdown(subject) |
21 | 45 | : api.message.html.markdown(title(text)) |
22 | 46 | } |
23 | 47 | } |
48 | + |
message/index.js | ||
---|---|---|
@@ -7,7 +7,10 @@ | ||
7 | 7 | compose: require('./html/compose'), |
8 | 8 | likes: require('./html/likes'), |
9 | 9 | subject: require('./html/subject'), |
10 | 10 | timeago: require('./html/timeago') |
11 | - } | |
11 | + }, | |
12 | + sync: { | |
13 | + getParticipants: require('./sync/getParticipants'), | |
14 | + }, | |
12 | 15 | } |
13 | 16 |
message/sync/getParticipants.js | ||
---|---|---|
@@ -1,0 +1,27 @@ | ||
1 | +const nest = require('depnest') | |
2 | +const get = require('lodash/get') | |
3 | + | |
4 | +exports.gives = nest('message.sync.getParticipants') | |
5 | + | |
6 | +exports.needs = nest({ | |
7 | + 'keys.sync.id': 'first', | |
8 | +}) | |
9 | + | |
10 | +exports.create = function (api) { | |
11 | + return nest('message.sync.getParticipants', getParticipants) | |
12 | + | |
13 | + function getParticipants (msg) { | |
14 | + const myKey = api.keys.sync.id() | |
15 | + | |
16 | + var participants = get(msg, 'value.content.recps') | |
17 | + .map(r => typeof r === 'string' ? r : r.link) | |
18 | + .filter(r => r != myKey) | |
19 | + .sort() | |
20 | + | |
21 | + participants.key = participants.join(' ') | |
22 | + return participants | |
23 | + } | |
24 | +} | |
25 | + | |
26 | + | |
27 | + |
router/sync/routes.js | ||
---|---|---|
@@ -59,9 +59,9 @@ | ||
59 | 59 | [ location => location.page === 'addressBook', pages.addressBook ], |
60 | 60 | |
61 | 61 | // Private Thread pages |
62 | 62 | // [ location => location.page === 'threadNew' && location.channel, pages.threadNew ], |
63 | - [ location => location.page === 'threadNew' && isFeed(location.feed), pages.threadNew ], | |
63 | + [ location => location.page === 'threadNew' && location.participants.every(isFeed), pages.threadNew ], | |
64 | 64 | [ location => isMsg(location.key), pages.threadShow ], |
65 | 65 | |
66 | 66 | // User pages |
67 | 67 | // [ location => location.page === 'userFind', pages.userFind ], |
Built with git-ssb-web