git ssb

2+

mixmix / ticktack



Tree: 9b70108d00646124112662ec83dd9733d7afca7d

Files: 9b70108d00646124112662ec83dd9733d7afca7d / app / html / context.js

9096 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('app.html.context')
10
11exports.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
26exports.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
282function 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