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