git ssb

2+

mixmix / ticktack



Tree: 2f8b5a42a327baeb86c4cc6af56c65848948d0ec

Files: 2f8b5a42a327baeb86c4cc6af56c65848948d0ec / app / html / sideNav / sideNavDiscovery.js

10534 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 (get(location, 'value.private') === undefined) return true
102 return false
103 }
104
105 const prepend = [
106 // Nearby
107 computed(nearby, n => !isEmpty(n) ? h('header', strings.peopleNearby) : null),
108 map(nearby, feedId => Option({
109 notifications: notifications(feedId),
110 imageEl: api.about.html.avatar(feedId, 'small'),
111 label: api.about.obs.name(feedId),
112 selected: location.feed === feedId && !isDiscoverLocation(location),
113 location: computed(recentMsgCache, recent => {
114 const lastMsg = recent.find(msg => msg.value.author === feedId)
115 return lastMsg
116 ? Object.assign(lastMsg, { feed: feedId })
117 : { page: 'threadNew', feed: feedId }
118 }),
119 }), { comparer: (a, b) => a === b }),
120
121 // ---------------------
122 computed(nearby, n => !isEmpty(n) ? h('hr') : null),
123
124 // Discover
125 Option({
126 // notifications: '!', //TODO - count this!
127 // imageEl: h('i.fa.fa-binoculars'),
128 imageEl: h('i', [
129 h('img', { src: path.join(__dirname, '../../../assets', 'discover.png') })
130 ]),
131 label: strings.blogIndex.title,
132 selected: isDiscoverLocation(location),
133 location: { page: 'blogIndex' },
134 }),
135
136 // My subscriptions
137 Option({
138 imageEl: h('i', [
139 h('img', { src: path.join(__dirname, '../../../assets', 'discover.png') })
140 ]),
141 label: strings.subscriptions.user,
142 selected: location.page === 'channelSubscriptions' && !location.scope,
143 location: { page: 'channelSubscriptions', scope: 'user' },
144 }),
145
146 // Friends subscriptions
147 Option({
148 imageEl: h('i', [
149 h('img', { src: path.join(__dirname, '../../../assets', 'discover.png') })
150 ]),
151 label: strings.subscriptions.friends,
152 selected: location.page === 'channelSubscriptions' && location.scope === 'friends',
153 location: { page: 'channelSubscriptions', scope: 'friends' },
154 })
155 ]
156
157 return api.app.html.scroller({
158 classList: [ 'level', '-one' ],
159 prepend,
160 stream: api.feed.pull.private,
161 filter: privateMsgFilter,
162 store: recentMsgCache,
163 updateTop: updateRecentMsgCache,
164 updateBottom: updateRecentMsgCache,
165 render: (msgObs) => {
166 const msg = resolve(msgObs)
167 const { author } = msg.value
168 if (nearby.has(author)) return
169
170 return Option({
171 //the number of threads with each peer
172 notifications: notifications(author),
173 imageEl: api.about.html.avatar(author),
174 label: api.about.obs.name(author),
175 selected: location.feed === author,
176 location: Object.assign({}, msg, { feed: author }) // TODO make obs?
177 })
178 }
179 })
180
181 function updateRecentMsgCache (soFar, newMsg) {
182 soFar.transaction(() => {
183 const { author, timestamp } = newMsg.value
184 const index = indexOf(soFar, (msg) => author === resolve(msg).value.author)
185 var object = Value()
186
187 if (index >= 0) {
188 // reference already exists, lets use this instead!
189 const existingMsg = soFar.get(index)
190
191 if (resolve(existingMsg).value.timestamp > timestamp) return
192 // but abort if the existing reference is newer
193
194 object = existingMsg
195 soFar.deleteAt(index)
196 }
197
198 object.set(newMsg)
199
200 const justOlderPosition = indexOf(soFar, (msg) => timestamp > resolve(msg).value.timestamp)
201 if (justOlderPosition > -1) {
202 soFar.insert(object, justOlderPosition)
203 } else {
204 soFar.push(object)
205 }
206 })
207 }
208
209 }
210
211 function getUnreadMsgsCache (key) {
212 var cache = unreadMsgsCache.get(key)
213 if (!cache) {
214 cache = Set ()
215 unreadMsgsCache.put(key, cache)
216 }
217 return cache
218 }
219
220 function notifications (key) {
221 return computed(getUnreadMsgsCache(key), cache => cache.length)
222 }
223
224 function LevelTwoSideNav () {
225 const { key, value, feed: targetUser, page } = location
226 const root = get(value, 'content.root', key)
227 if (!targetUser) return
228 if (page === 'userShow') return
229
230
231 const prepend = Option({
232 selected: page === 'threadNew',
233 location: {page: 'threadNew', feed: targetUser},
234 label: h('Button', strings.threadNew.action.new),
235 })
236
237 var userLastMsgCache = usersLastMsgCache.get(targetUser)
238 if (!userLastMsgCache) {
239 userLastMsgCache = MutantArray()
240 usersLastMsgCache.put(targetUser, userLastMsgCache)
241 }
242
243 return api.app.html.scroller({
244 classList: [ 'level', '-two' ],
245 prepend,
246 stream: api.feed.pull.private,
247 filter: () => pull(
248 pull.filter(msg => !msg.value.content.root),
249 pull.filter(msg => msg.value.content.type === 'post'),
250 pull.filter(msg => msg.value.content.recps),
251 pull.filter(msg => msg.value.content.recps
252 .map(recp => typeof recp === 'object' ? recp.link : recp)
253 .some(recp => recp === targetUser)
254 )
255 ),
256 store: userLastMsgCache,
257 updateTop: updateLastMsgCache,
258 updateBottom: updateLastMsgCache,
259 render: (rootMsgObs) => {
260 const rootMsg = resolve(rootMsgObs)
261 return Option({
262 notifications: notifications(rootMsg.key),
263 label: api.message.html.subject(rootMsg),
264 selected: rootMsg.key === root,
265 location: Object.assign(rootMsg, { feed: targetUser }),
266 })
267 }
268 })
269
270 function updateLastMsgCache (soFar, newMsg) {
271 soFar.transaction(() => {
272 const { timestamp } = newMsg.value
273 const index = indexOf(soFar, (msg) => timestamp === resolve(msg).value.timestamp)
274
275 if (index >= 0) return
276 // if reference already exists, abort
277
278 var object = Value(newMsg)
279
280 const justOlderPosition = indexOf(soFar, (msg) => timestamp > resolve(msg).value.timestamp)
281 if (justOlderPosition > -1) {
282 soFar.insert(object, justOlderPosition)
283 } else {
284 soFar.push(object)
285 }
286 })
287 }
288 }
289
290 function Option ({ notifications = 0, imageEl, label, location, selected }) {
291 const className = selected ? '-selected' : ''
292 function goToLocation (e) {
293 e.preventDefault()
294 e.stopPropagation()
295 api.history.sync.push(resolve(location))
296 }
297
298 if (!imageEl) {
299 return h('Option', { className, 'ev-click': goToLocation }, [
300 when(notifications, h('div.spacer', h('div.alert', notifications))),
301 h('div.label', label)
302 ])
303 }
304
305 return h('Option', { className }, [
306 h('div.circle', [
307 when(notifications, h('div.alert', notifications)),
308 imageEl
309 ]),
310 h('div.label', { 'ev-click': goToLocation }, label)
311 ])
312 }
313
314 function privateMsgFilter () {
315 return pull(
316 pull.filter(msg => msg.value.content.type === 'post'),
317 pull.filter(msg => msg.value.author != myKey),
318 pull.filter(msg => msg.value.content.recps)
319 )
320 }
321 }
322}
323
324function indexOf (array, fn) {
325 for (var i = 0; i < array.getLength(); i++) {
326 if (fn(array.get(i))) {
327 return i
328 }
329 }
330 return -1
331}
332
333
334

Built with git-ssb-web