git ssb

2+

mixmix / ticktack



Tree: 8c68dbb9a3b69a0cf0e0dd503ef059a873189cd5

Files: 8c68dbb9a3b69a0cf0e0dd503ef059a873189cd5 / app / html / sideNav / sideNavDiscovery.js

11883 bytesRaw
1const nest = require('depnest')
2const { h, computed, map, when, Dict, Array: MutantArray, Value, Set, resolve } = require('mutant')
3const pull = require('pull-stream')
4const next = require('pull-next-step')
5const get = require('lodash/get')
6const isEmpty = require('lodash/isEmpty')
7const path = require('path')
8
9exports.gives = nest({
10 'app.html.sideNav': true,
11 'unread.sync.markUnread': true
12})
13
14exports.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
29exports.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
350function 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