git ssb

2+

mixmix / ticktack



Tree: f53d4720c0fe90f6e4f8f94d0f986812e6ca896b

Files: f53d4720c0fe90f6e4f8f94d0f986812e6ca896b / app / html / context.js

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

Built with git-ssb-web