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