git ssb

2+

mixmix / ticktack



Tree: e57c5f14597311bb19cb42a5208dcf94188919ba

Files: e57c5f14597311bb19cb42a5208dcf94188919ba / app / html / sideNav / sideNavDiscovery.js

10634 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 'sbot.obs.localPeers': 'first',
24 'translations.sync.strings': 'first',
25 'unread.sync.isUnread': 'first'
26})
27
28exports.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
325function 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