Files: e57c5f14597311bb19cb42a5208dcf94188919ba / app / html / sideNav / sideNavDiscovery.js
10634 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 | 'sbot.obs.localPeers': 'first', |
24 | 'translations.sync.strings': 'first', |
25 | 'unread.sync.isUnread': 'first' |
26 | }) |
27 | |
28 | exports.create = (api) => { |
29 | var recentMsgCache = MutantArray() |
30 | var usersLastMsgCache = Dict() // { id: [ msgs ] } |
31 | var unreadMsgsCache = Dict() // { id: [ msgs ] } |
32 | |
33 | return nest({ |
34 | //intercept markUnread and remove them from the cache. |
35 | 'unread.sync.markUnread': function (msg) { |
36 | unreadMsgsCache.get(msg.value.content.root || msg.key) |
37 | .delete(msg.key) |
38 | unreadMsgsCache.get(msg.value.author) |
39 | .delete(msg.key) |
40 | }, |
41 | 'app.html.sideNav': sideNav, |
42 | }) |
43 | |
44 | function isMatch (location) { |
45 | if (location.page) { |
46 | if (location.page.match(/^blog/)) return true |
47 | if (location.page.match(/^thread/)) return true |
48 | if (location.page.match(/^user/)) return true |
49 | if (location.page.match(/^channel/)) return true |
50 | } |
51 | if (location.key) { |
52 | return true |
53 | } |
54 | return false |
55 | } |
56 | |
57 | function sideNav (location) { |
58 | if (!isMatch(location)) return |
59 | |
60 | const strings = api.translations.sync.strings() |
61 | const myKey = api.keys.sync.id() |
62 | |
63 | var nearby = api.sbot.obs.localPeers() |
64 | |
65 | // Unread message counts |
66 | function updateCache (cache, msg) { |
67 | if(api.unread.sync.isUnread(msg)) |
68 | cache.add(msg.key) |
69 | else |
70 | cache.delete(msg.key) |
71 | } |
72 | |
73 | function updateUnreadMsgsCache (msg) { |
74 | updateCache(getUnreadMsgsCache(msg.value.author), msg) |
75 | updateCache(getUnreadMsgsCache(msg.value.content.root || msg.key), msg) |
76 | } |
77 | |
78 | pull( |
79 | next(api.feed.pull.private, {reverse: true, limit: 1000, live: false, property: ['value', 'timestamp']}), |
80 | privateMsgFilter(), |
81 | pull.drain(updateUnreadMsgsCache) |
82 | ) |
83 | |
84 | pull( |
85 | next(api.feed.pull.private, {old: false, live: true, property: ['value', 'timestamp']}), |
86 | privateMsgFilter(), |
87 | pull.drain(updateUnreadMsgsCache) |
88 | ) |
89 | |
90 | return h('SideNav -discovery', [ |
91 | LevelOneSideNav(), |
92 | LevelTwoSideNav() |
93 | ]) |
94 | |
95 | function LevelOneSideNav () { |
96 | function isDiscoverLocation (loc) { |
97 | const PAGES_UNDER_DISCOVER = ['blogIndex', 'blogShow', 'userShow'] |
98 | |
99 | if (PAGES_UNDER_DISCOVER.includes(location.page)) return true |
100 | if (location.page === 'threadNew') return false |
101 | if (location.page === 'channelSubscriptions') return false |
102 | if (get(location, 'value.private') === undefined) return true |
103 | return false |
104 | } |
105 | |
106 | const prepend = [ |
107 | // Nearby |
108 | computed(nearby, n => !isEmpty(n) ? h('header', strings.peopleNearby) : null), |
109 | map(nearby, feedId => Option({ |
110 | notifications: notifications(feedId), |
111 | imageEl: api.about.html.avatar(feedId, 'small'), |
112 | label: api.about.obs.name(feedId), |
113 | selected: location.feed === feedId && !isDiscoverLocation(location), |
114 | location: computed(recentMsgCache, recent => { |
115 | const lastMsg = recent.find(msg => msg.value.author === feedId) |
116 | return lastMsg |
117 | ? Object.assign(lastMsg, { feed: feedId }) |
118 | : { page: 'threadNew', feed: feedId } |
119 | }), |
120 | }), { comparer: (a, b) => a === b }), |
121 | |
122 | // --------------------- |
123 | computed(nearby, n => !isEmpty(n) ? h('hr') : null), |
124 | |
125 | // Discover |
126 | Option({ |
127 | // notifications: '!', //TODO - count this! |
128 | // imageEl: h('i.fa.fa-binoculars'), |
129 | imageEl: h('i', [ |
130 | h('img', { src: path.join(__dirname, '../../../assets', 'discover.png') }) |
131 | ]), |
132 | label: strings.blogIndex.title, |
133 | selected: isDiscoverLocation(location), |
134 | location: { page: 'blogIndex' }, |
135 | }), |
136 | |
137 | // My subscriptions |
138 | Option({ |
139 | imageEl: h('i', [ |
140 | h('img', { src: path.join(__dirname, '../../../assets', 'my_subscribed.png') }) |
141 | ]), |
142 | label: strings.subscriptions.user, |
143 | selected: location.page === 'channelSubscriptions' && location.scope === 'user', |
144 | location: { page: 'channelSubscriptions', scope: 'user' }, |
145 | }), |
146 | |
147 | // Friends subscriptions |
148 | Option({ |
149 | imageEl: h('i', [ |
150 | h('img', { src: path.join(__dirname, '../../../assets', 'friends_subscribed.png') }) |
151 | ]), |
152 | label: strings.subscriptions.friends, |
153 | selected: location.page === 'channelSubscriptions' && location.scope === 'friends', |
154 | location: { page: 'channelSubscriptions', scope: 'friends' }, |
155 | }) |
156 | ] |
157 | |
158 | return api.app.html.scroller({ |
159 | classList: [ 'level', '-one' ], |
160 | prepend, |
161 | stream: api.feed.pull.private, |
162 | filter: privateMsgFilter, |
163 | store: recentMsgCache, |
164 | updateTop: updateRecentMsgCache, |
165 | updateBottom: updateRecentMsgCache, |
166 | render: (msgObs) => { |
167 | const msg = resolve(msgObs) |
168 | const { author } = msg.value |
169 | if (nearby.has(author)) return |
170 | |
171 | return Option({ |
172 | //the number of threads with each peer |
173 | notifications: notifications(author), |
174 | imageEl: api.about.html.avatar(author), |
175 | label: api.about.obs.name(author), |
176 | selected: location.feed === author, |
177 | location: Object.assign({}, msg, { feed: author }) // TODO make obs? |
178 | }) |
179 | } |
180 | }) |
181 | |
182 | function updateRecentMsgCache (soFar, newMsg) { |
183 | soFar.transaction(() => { |
184 | const { author, timestamp } = newMsg.value |
185 | const index = indexOf(soFar, (msg) => author === resolve(msg).value.author) |
186 | var object = Value() |
187 | |
188 | if (index >= 0) { |
189 | // reference already exists, lets use this instead! |
190 | const existingMsg = soFar.get(index) |
191 | |
192 | if (resolve(existingMsg).value.timestamp > timestamp) return |
193 | // but abort if the existing reference is newer |
194 | |
195 | object = existingMsg |
196 | soFar.deleteAt(index) |
197 | } |
198 | |
199 | object.set(newMsg) |
200 | |
201 | const justOlderPosition = indexOf(soFar, (msg) => timestamp > resolve(msg).value.timestamp) |
202 | if (justOlderPosition > -1) { |
203 | soFar.insert(object, justOlderPosition) |
204 | } else { |
205 | soFar.push(object) |
206 | } |
207 | }) |
208 | } |
209 | |
210 | } |
211 | |
212 | function getUnreadMsgsCache (key) { |
213 | var cache = unreadMsgsCache.get(key) |
214 | if (!cache) { |
215 | cache = Set () |
216 | unreadMsgsCache.put(key, cache) |
217 | } |
218 | return cache |
219 | } |
220 | |
221 | function notifications (key) { |
222 | return computed(getUnreadMsgsCache(key), cache => cache.length) |
223 | } |
224 | |
225 | function LevelTwoSideNav () { |
226 | const { key, value, feed: targetUser, page } = location |
227 | const root = get(value, 'content.root', key) |
228 | if (!targetUser) return |
229 | if (page === 'userShow') return |
230 | |
231 | |
232 | const prepend = Option({ |
233 | selected: page === 'threadNew', |
234 | location: {page: 'threadNew', feed: targetUser}, |
235 | label: h('Button', strings.threadNew.action.new), |
236 | }) |
237 | |
238 | var userLastMsgCache = usersLastMsgCache.get(targetUser) |
239 | if (!userLastMsgCache) { |
240 | userLastMsgCache = MutantArray() |
241 | usersLastMsgCache.put(targetUser, userLastMsgCache) |
242 | } |
243 | |
244 | return api.app.html.scroller({ |
245 | classList: [ 'level', '-two' ], |
246 | prepend, |
247 | stream: api.feed.pull.private, |
248 | filter: () => pull( |
249 | pull.filter(msg => !msg.value.content.root), |
250 | 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 | ) |
256 | ), |
257 | store: userLastMsgCache, |
258 | updateTop: updateLastMsgCache, |
259 | 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 | } |
269 | }) |
270 | |
271 | function updateLastMsgCache (soFar, newMsg) { |
272 | soFar.transaction(() => { |
273 | const { timestamp } = newMsg.value |
274 | const index = indexOf(soFar, (msg) => timestamp === resolve(msg).value.timestamp) |
275 | |
276 | if (index >= 0) return |
277 | // if reference already exists, abort |
278 | |
279 | var object = Value(newMsg) |
280 | |
281 | const justOlderPosition = indexOf(soFar, (msg) => timestamp > resolve(msg).value.timestamp) |
282 | if (justOlderPosition > -1) { |
283 | soFar.insert(object, justOlderPosition) |
284 | } else { |
285 | soFar.push(object) |
286 | } |
287 | }) |
288 | } |
289 | } |
290 | |
291 | function Option ({ notifications = 0, imageEl, label, location, selected }) { |
292 | const className = selected ? '-selected' : '' |
293 | function goToLocation (e) { |
294 | e.preventDefault() |
295 | e.stopPropagation() |
296 | api.history.sync.push(resolve(location)) |
297 | } |
298 | |
299 | if (!imageEl) { |
300 | return h('Option', { className, 'ev-click': goToLocation }, [ |
301 | when(notifications, h('div.spacer', h('div.alert', notifications))), |
302 | h('div.label', label) |
303 | ]) |
304 | } |
305 | |
306 | return h('Option', { className }, [ |
307 | h('div.circle', [ |
308 | when(notifications, h('div.alert', notifications)), |
309 | imageEl |
310 | ]), |
311 | h('div.label', { 'ev-click': goToLocation }, label) |
312 | ]) |
313 | } |
314 | |
315 | function privateMsgFilter () { |
316 | return pull( |
317 | pull.filter(msg => msg.value.content.type === 'post'), |
318 | pull.filter(msg => msg.value.author != myKey), |
319 | pull.filter(msg => msg.value.content.recps) |
320 | ) |
321 | } |
322 | } |
323 | } |
324 | |
325 | function indexOf (array, fn) { |
326 | for (var i = 0; i < array.getLength(); i++) { |
327 | if (fn(array.get(i))) { |
328 | return i |
329 | } |
330 | } |
331 | return -1 |
332 | } |
333 | |
334 | |
335 |
Built with git-ssb-web