git ssb

2+

mixmix / ticktack



Tree: 0b0275502152a806a995b36fd4d29e8c3df1195e

Files: 0b0275502152a806a995b36fd4d29e8c3df1195e / app / html / sideNav / sideNavDiscovery.js

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

Built with git-ssb-web