git ssb

2+

mixmix / ticktack



Tree: d6d6eae9c07646a979ad0e986e126984fbd0f2cd

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

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

Built with git-ssb-web