git ssb

2+

mixmix / ticktack



Tree: 52acefb26cfdc545a5bef98c649d3cfbec4ac257

Files: 52acefb26cfdc545a5bef98c649d3cfbec4ac257 / app / html / context.js

8272 bytesRaw
1const nest = require('depnest')
2const { h, computed, map, when, Dict, dictToCollection, Array: MutantArray, Value, resolve } = require('mutant')
3const pull = require('pull-stream')
4const next = require('pull-next-step')
5const get = require('lodash/get')
6const isEmpty = require('lodash/isEmpty')
7
8exports.gives = nest('app.html.context')
9
10exports.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
25exports.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
252function 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