Files: 8c68dbb9a3b69a0cf0e0dd503ef059a873189cd5 / app / html / sideNav / sideNavDiscovery.js
11883 bytesRaw
1 | const nest = require('depnest') |
2 | const { h, computed, map, when, Dict, Array: MutantArray, Value, Set, resolve } = require('mutant') |
3 | const pull = require('pull-stream') |
4 | const next = require('pull-next-step') |
5 | const get = require('lodash/get') |
6 | const isEmpty = require('lodash/isEmpty') |
7 | const path = require('path') |
8 | |
9 | exports.gives = nest({ |
10 | 'app.html.sideNav': true, |
11 | 'unread.sync.markUnread': true |
12 | }) |
13 | |
14 | exports.needs = nest({ |
15 | 'app.html.scroller': 'first', |
16 | 'about.html.avatar': 'first', |
17 | 'about.obs.name': 'first', |
18 | 'feed.pull.private': 'first', |
19 | 'keys.sync.id': 'first', |
20 | 'history.sync.push': 'first', |
21 | 'history.obs.store': 'first', |
22 | 'message.html.subject': 'first', |
23 | 'message.sync.getParticipants': 'first', |
24 | 'sbot.obs.localPeers': 'first', |
25 | 'translations.sync.strings': 'first', |
26 | 'unread.sync.isUnread': 'first' |
27 | }) |
28 | |
29 | exports.create = (api) => { |
30 | var recentMsgCache = MutantArray() |
31 | var usersLastMsgCache = Dict() // { id: [ msgs ] } |
32 | var unreadMsgsCache = Dict() // { id: [ msgs ] } |
33 | |
34 | return nest({ |
35 | //intercept markUnread and remove them from the cache. |
36 | 'unread.sync.markUnread': function (msg) { |
37 | unreadMsgsCache.get(msg.value.content.root || msg.key) |
38 | .delete(msg.key) |
39 | unreadMsgsCache.get(msg.value.author) |
40 | .delete(msg.key) |
41 | }, |
42 | 'app.html.sideNav': sideNav, |
43 | }) |
44 | |
45 | function isMatch (location) { |
46 | if (location.page) { |
47 | if (location.page.match(/^blog/)) return true |
48 | if (location.page.match(/^thread/)) return true |
49 | if (location.page.match(/^user/)) return true |
50 | if (location.page.match(/^channel/)) return true |
51 | } |
52 | if (location.key) { |
53 | return true |
54 | } |
55 | return false |
56 | } |
57 | |
58 | function sideNav (location) { |
59 | if (!isMatch(location)) return |
60 | |
61 | const strings = api.translations.sync.strings() |
62 | const myKey = api.keys.sync.id() |
63 | var nearby = api.sbot.obs.localPeers() |
64 | const getParticipants = api.message.sync.getParticipants |
65 | |
66 | // Unread message counts |
67 | function updateCache (cache, msg) { |
68 | if(api.unread.sync.isUnread(msg)) |
69 | cache.add(msg.key) |
70 | else |
71 | cache.delete(msg.key) |
72 | } |
73 | |
74 | function updateUnreadMsgsCache (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) |
80 | } |
81 | |
82 | pull( |
83 | next(api.feed.pull.private, {reverse: true, limit: 1000, live: false, property: ['value', 'timestamp']}), |
84 | privateMsgFilter(), |
85 | pull.drain(updateUnreadMsgsCache) |
86 | ) |
87 | |
88 | pull( |
89 | next(api.feed.pull.private, {old: false, live: true, property: ['value', 'timestamp']}), |
90 | privateMsgFilter(), |
91 | pull.drain(updateUnreadMsgsCache) |
92 | ) |
93 | |
94 | return h('SideNav -discovery', [ |
95 | LevelOneSideNav(), |
96 | LevelTwoSideNav() |
97 | ]) |
98 | |
99 | function LevelOneSideNav () { |
100 | function isDiscoverLocation (loc) { |
101 | const PAGES_UNDER_DISCOVER = ['blogIndex', 'blogShow', 'userShow'] |
102 | |
103 | if (PAGES_UNDER_DISCOVER.includes(location.page)) return true |
104 | if (location.page === 'threadNew') return false |
105 | if (location.page === 'channelSubscriptions') return false |
106 | if (get(location, 'value.private') === undefined) return true |
107 | return false |
108 | } |
109 | |
110 | const prepend = [ |
111 | // Nearby |
112 | computed(nearby, n => !isEmpty(n) ? h('header', strings.peopleNearby) : null), |
113 | map(nearby, feedId => Option({ |
114 | notifications: notifications(feedId), |
115 | imageEl: api.about.html.avatar(feedId, 'small'), |
116 | label: api.about.obs.name(feedId), |
117 | selected: get(location, 'participants', []).join() === feedId && !isDiscoverLocation(location), |
118 | location: computed(recentMsgCache, recent => { |
119 | const lastMsg = recent.find(msg => msg.value.author === feedId) |
120 | return lastMsg |
121 | ? Object.assign(lastMsg, { participants: [feedId] }) |
122 | : { page: 'threadNew', participants: [feedId] } |
123 | }), |
124 | }), { comparer: (a, b) => a === b }), |
125 | |
126 | // --------------------- |
127 | computed(nearby, n => !isEmpty(n) ? h('hr') : null), |
128 | |
129 | // Discover |
130 | Option({ |
131 | imageEl: h('i', [ |
132 | h('img', { src: path.join(__dirname, '../../../assets', 'discover.png') }) |
133 | ]), |
134 | label: strings.blogIndex.title, |
135 | selected: isDiscoverLocation(location), |
136 | location: { page: 'blogIndex' }, |
137 | }), |
138 | |
139 | // My subscriptions |
140 | Option({ |
141 | imageEl: h('i', [ |
142 | h('img', { src: path.join(__dirname, '../../../assets', 'my_subscribed.png') }) |
143 | ]), |
144 | label: strings.subscriptions.user, |
145 | selected: location.page === 'channelSubscriptions' && location.scope === 'user', |
146 | location: { page: 'channelSubscriptions', scope: 'user' }, |
147 | }), |
148 | |
149 | // Friends subscriptions |
150 | Option({ |
151 | imageEl: h('i', [ |
152 | h('img', { src: path.join(__dirname, '../../../assets', 'friends_subscribed.png') }) |
153 | ]), |
154 | label: strings.subscriptions.friends, |
155 | selected: location.page === 'channelSubscriptions' && location.scope === 'friends', |
156 | location: { page: 'channelSubscriptions', scope: 'friends' }, |
157 | }) |
158 | ] |
159 | |
160 | return api.app.html.scroller({ |
161 | classList: [ 'level', '-one' ], |
162 | prepend, |
163 | stream: api.feed.pull.private, |
164 | filter: privateMsgFilter, |
165 | store: recentMsgCache, |
166 | updateTop: updateRecentMsgCache, |
167 | updateBottom: updateRecentMsgCache, |
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 |
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] |
181 | return Option({ |
182 | //the number of threads with each peer |
183 | notifications: notifications(author), |
184 | imageEl: api.about.html.avatar(author), |
185 | label: api.about.obs.name(author), |
186 | selected: locParticipantsKey === author, |
187 | location: Object.assign({}, msg, { participants }) // TODO make obs? |
188 | }) |
189 | } |
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 | } |
202 | |
203 | function updateRecentMsgCache (soFar, newMsg) { |
204 | soFar.transaction(() => { |
205 | const { timestamp } = newMsg.value |
206 | newMsg.participantsKey = getParticipants(newMsg).key |
207 | const index = indexOf(soFar, (msg) => newMsg.participantsKey === resolve(msg).participantsKey) |
208 | var object = Value() |
209 | |
210 | if (index >= 0) { |
211 | // reference already exists, lets use this instead! |
212 | const existingMsg = soFar.get(index) |
213 | |
214 | if (resolve(existingMsg).value.timestamp > timestamp) return |
215 | // but abort if the existing reference is newer |
216 | |
217 | object = existingMsg |
218 | soFar.deleteAt(index) |
219 | } |
220 | |
221 | object.set(newMsg) |
222 | |
223 | const justOlderPosition = indexOf(soFar, (msg) => timestamp > resolve(msg).value.timestamp) |
224 | if (justOlderPosition > -1) { |
225 | soFar.insert(object, justOlderPosition) |
226 | } else { |
227 | soFar.push(object) |
228 | } |
229 | }) |
230 | } |
231 | |
232 | } |
233 | |
234 | function getUnreadMsgsCache (key) { |
235 | var cache = unreadMsgsCache.get(key) |
236 | if (!cache) { |
237 | cache = Set () |
238 | unreadMsgsCache.put(key, cache) |
239 | } |
240 | return cache |
241 | } |
242 | |
243 | function notifications (key) { |
244 | key = typeof key === 'string' |
245 | ? key |
246 | : key.key // participants.key case |
247 | return computed(getUnreadMsgsCache(key), cache => cache.length) |
248 | } |
249 | |
250 | function LevelTwoSideNav () { |
251 | const { key, value, participants, page } = location |
252 | const root = get(value, 'content.root', key) |
253 | if (isEmpty(participants)) return |
254 | if (page === 'userShow') return |
255 | |
256 | const prepend = Option({ |
257 | selected: page === 'threadNew', |
258 | location: {page: 'threadNew', participants}, |
259 | label: h('Button', strings.threadNew.action.new), |
260 | }) |
261 | |
262 | var participantsKey = participants.join(' ') // TODO collect this repeated logic |
263 | var userLastMsgCache = usersLastMsgCache.get(participantsKey) |
264 | if (!userLastMsgCache) { |
265 | userLastMsgCache = MutantArray() |
266 | usersLastMsgCache.put(participantsKey, userLastMsgCache) |
267 | } |
268 | |
269 | return api.app.html.scroller({ |
270 | classList: [ 'level', '-two' ], |
271 | prepend, |
272 | stream: api.feed.pull.private, |
273 | filter: () => pull( |
274 | pull.filter(msg => !msg.value.content.root), |
275 | pull.filter(msg => msg.value.content.type === 'post'), |
276 | pull.filter(msg => getParticipants(msg).key === participantsKey) |
277 | ), |
278 | store: userLastMsgCache, |
279 | updateTop: updateLastMsgCache, |
280 | updateBottom: updateLastMsgCache, |
281 | render |
282 | }) |
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 | |
295 | function updateLastMsgCache (soFar, newMsg) { |
296 | soFar.transaction(() => { |
297 | const { timestamp } = newMsg.value |
298 | const index = indexOf(soFar, (msg) => timestamp === resolve(msg).value.timestamp) |
299 | |
300 | if (index >= 0) return |
301 | // if reference already exists, abort |
302 | |
303 | var object = Value(newMsg) |
304 | |
305 | const justOlderPosition = indexOf(soFar, (msg) => timestamp > resolve(msg).value.timestamp) |
306 | if (justOlderPosition > -1) { |
307 | soFar.insert(object, justOlderPosition) |
308 | } else { |
309 | soFar.push(object) |
310 | } |
311 | }) |
312 | } |
313 | } |
314 | |
315 | function Option ({ notifications = 0, imageEl, label, location, selected }) { |
316 | const className = selected ? '-selected' : '' |
317 | function goToLocation (e) { |
318 | e.preventDefault() |
319 | e.stopPropagation() |
320 | api.history.sync.push(resolve(location)) |
321 | } |
322 | |
323 | if (!imageEl) { |
324 | return h('Option', { className, 'ev-click': goToLocation }, [ |
325 | when(notifications, h('div.spacer', h('div.alert', notifications))), |
326 | h('div.label', label) |
327 | ]) |
328 | } |
329 | |
330 | return h('Option', { className }, [ |
331 | h('div.circle', [ |
332 | when(notifications, h('div.alert', notifications)), |
333 | Array.isArray(imageEl) |
334 | ? h('div.many-images', imageEl.slice(0,4)) // not ideal? not enough space to show more though |
335 | : imageEl |
336 | ]), |
337 | h('div.label', { 'ev-click': goToLocation }, label) |
338 | ]) |
339 | } |
340 | |
341 | function privateMsgFilter () { |
342 | return pull( |
343 | pull.filter(msg => msg.value.content.type === 'post'), |
344 | pull.filter(msg => msg.value.content.recps) |
345 | ) |
346 | } |
347 | } |
348 | } |
349 | |
350 | function indexOf (array, fn) { |
351 | for (var i = 0; i < array.getLength(); i++) { |
352 | if (fn(array.get(i))) { |
353 | return i |
354 | } |
355 | } |
356 | return -1 |
357 | } |
358 | |
359 | |
360 |
Built with git-ssb-web