Commit d6d6eae9c07646a979ad0e986e126984fbd0f2cd
rename app.html.context app.html.sideBar
mix authored on 1/24/2018, 2:15:36 AMmix irving committed on 1/24/2018, 10:39:07 AM
Parent: 26360d1629d97859509df5393f68412875874595
Files changed
app/html/context.js | deleted |
app/html/context.mcss | deleted |
app/html/sideNav/sideNav.mcss | added |
app/html/sideNav/sideNavDiscovery.js | added |
app/html/sideNav/sideNavDiscovery.mcss | added |
app/index.js | changed |
app/page/blogIndex.js | changed |
app/page/blogNew.js | changed |
app/page/blogSearch.js | changed |
app/page/blogShow.js | changed |
app/page/threadNew.js | changed |
app/page/threadShow.js | changed |
app/page/userShow.js | changed |
app/html/context.js | ||
---|---|---|
@@ -1,305 +1,0 @@ | ||
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({ | |
10 | - 'app.html.context': true, | |
11 | - 'unread.sync.markUnread': true | |
12 | -}) | |
13 | - | |
14 | -exports.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 | - | |
29 | -exports.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 | - | |
296 | -function 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 | - |
app/html/context.mcss | ||
---|---|---|
@@ -1,139 +1,0 @@ | ||
1 | -Context { | |
2 | - flex-shrink: 0 | |
3 | - flex-grow: 0 | |
4 | - overflow: hidden | |
5 | - $backgroundPrimaryText | |
6 | - | |
7 | - display: flex | |
8 | - | |
9 | - div.level { | |
10 | - width: 13rem | |
11 | - overflow-y: auto | |
12 | - overflow-x: hidden | |
13 | - | |
14 | - border-right: 1px gainsboro solid | |
15 | - | |
16 | - section { | |
17 | - header { | |
18 | - $colorFontSubtle | |
19 | - padding: .5rem 1rem | |
20 | - } | |
21 | - | |
22 | - div.Option {} | |
23 | - | |
24 | - hr { | |
25 | - border: 1px solid gainsboro | |
26 | - border-bottom: none | |
27 | - margin: 0 | |
28 | - } | |
29 | - } | |
30 | - | |
31 | - -one {} | |
32 | - -two { | |
33 | - section { | |
34 | - div.Option { | |
35 | - padding: 0 1rem | |
36 | - } | |
37 | - } | |
38 | - } | |
39 | - } | |
40 | -} | |
41 | - | |
42 | -Option { | |
43 | - min-width: 8rem | |
44 | - padding: .5rem 1rem | |
45 | - display: flex | |
46 | - align-items: center | |
47 | - | |
48 | - :hover { | |
49 | - cursor: pointer | |
50 | - $backgroundSelected | |
51 | - } | |
52 | - | |
53 | - -selected { | |
54 | - $backgroundSelected | |
55 | - } | |
56 | - | |
57 | - div.spacer { | |
58 | - display: flex | |
59 | - align-self: center | |
60 | - div.alert { | |
61 | - position: relative | |
62 | - width: 1.2rem | |
63 | - height: 1.2rem | |
64 | - border-radius: 1rem | |
65 | - top: -.2rem | |
66 | - left: -.2rem | |
67 | - | |
68 | - background-color: red | |
69 | - color: #fff | |
70 | - font-size: .8rem | |
71 | - | |
72 | - display: flex | |
73 | - justify-content: center | |
74 | - align-items: center | |
75 | - align-self: flex-start | |
76 | - } | |
77 | - } | |
78 | - | |
79 | - div.circle { | |
80 | - width: 3.6rem | |
81 | - position: relative | |
82 | - | |
83 | - div.alert { | |
84 | - position: absolute | |
85 | - width: 1.2rem | |
86 | - height: 1.2rem | |
87 | - border-radius: 1rem | |
88 | - top: -.2rem | |
89 | - left: -.2rem | |
90 | - | |
91 | - background-color: red | |
92 | - color: #fff | |
93 | - font-size: .8rem | |
94 | - | |
95 | - display: flex | |
96 | - justify-content: center | |
97 | - align-items: center | |
98 | - } | |
99 | - | |
100 | - | |
101 | - a img { | |
102 | - | |
103 | - } | |
104 | - | |
105 | - i { | |
106 | - $circleSmall | |
107 | - $colorPrimary | |
108 | - font-size: 1.3rem | |
109 | - display: flex | |
110 | - justify-content: center | |
111 | - align-items: center | |
112 | - } | |
113 | - } | |
114 | - | |
115 | - div.label { | |
116 | - $markdownSmall | |
117 | - (a) { | |
118 | - $colorFontBasic | |
119 | - :hover { text-decoration: none } | |
120 | - } | |
121 | - | |
122 | - flex-grow: 1 | |
123 | - | |
124 | - display: flex | |
125 | - align-items: center | |
126 | - min-height: 3rem | |
127 | - | |
128 | - div.Button { | |
129 | - flex-grow: 1 | |
130 | - } | |
131 | - } | |
132 | -} | |
133 | - | |
134 | - | |
135 | - | |
136 | - | |
137 | - | |
138 | - | |
139 | - |
app/html/sideNav/sideNav.mcss | ||
---|---|---|
@@ -1,0 +1,35 @@ | ||
1 | +SideNav { | |
2 | + flex-shrink: 0 | |
3 | + flex-grow: 0 | |
4 | + overflow: hidden | |
5 | + $backgroundPrimaryText | |
6 | + | |
7 | + display: flex | |
8 | + | |
9 | + div.level { | |
10 | + width: 13rem | |
11 | + overflow-y: auto | |
12 | + overflow-x: hidden | |
13 | + | |
14 | + border-right: 1px gainsboro solid | |
15 | + | |
16 | + section { | |
17 | + header { | |
18 | + $colorFontSubtle | |
19 | + padding: .5rem 1rem | |
20 | + } | |
21 | + | |
22 | + div.Option {} | |
23 | + | |
24 | + hr { | |
25 | + border: 1px solid gainsboro | |
26 | + border-bottom: none | |
27 | + margin: 0 | |
28 | + } | |
29 | + } | |
30 | + | |
31 | + -one {} | |
32 | + -two {} | |
33 | + } | |
34 | +} | |
35 | + |
app/html/sideNav/sideNavDiscovery.js | ||
---|---|---|
@@ -1,0 +1,295 @@ | ||
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({ | |
10 | + 'app.html.sideNav': true, | |
11 | + 'unread.sync.markUnread': true | |
12 | +}) | |
13 | + | |
14 | +exports.needs = nest({ | |
15 | + 'app.html.scroller': 'first', | |
16 | + 'about.html.avatar': 'first', | |
17 | + 'about.obs.name': 'first', | |
18 | + 'feed.pull.private': 'first', | |
19 | + 'keys.sync.id': 'first', | |
20 | + 'history.sync.push': 'first', | |
21 | + 'message.html.subject': 'first', | |
22 | + 'sbot.obs.localPeers': 'first', | |
23 | + 'translations.sync.strings': 'first', | |
24 | + 'unread.sync.isUnread': 'first' | |
25 | +}) | |
26 | + | |
27 | +exports.create = (api) => { | |
28 | + var recentMsgCache = MutantArray() | |
29 | + var usersLastMsgCache = Dict() // { id: [ msgs ] } | |
30 | + var unreadMsgsCache = Dict() // { id: [ msgs ] } | |
31 | + | |
32 | + return nest({ | |
33 | + //intercept markUnread and remove them from the cache. | |
34 | + 'unread.sync.markUnread': function (msg) { | |
35 | + unreadMsgsCache.get(msg.value.content.root || msg.key) | |
36 | + .delete(msg.key) | |
37 | + unreadMsgsCache.get(msg.value.author) | |
38 | + .delete(msg.key) | |
39 | + }, | |
40 | + 'app.html.sideNav': sideNav, | |
41 | + }) | |
42 | + | |
43 | + function sideNav (location) { | |
44 | + const strings = api.translations.sync.strings() | |
45 | + const myKey = api.keys.sync.id() | |
46 | + | |
47 | + var nearby = api.sbot.obs.localPeers() | |
48 | + | |
49 | + // Unread message counts | |
50 | + function updateCache (cache, msg) { | |
51 | + if(api.unread.sync.isUnread(msg)) | |
52 | + cache.add(msg.key) | |
53 | + else | |
54 | + cache.delete(msg.key) | |
55 | + } | |
56 | + | |
57 | + function updateUnreadMsgsCache (msg) { | |
58 | + updateCache(getUnreadMsgsCache(msg.value.author), msg) | |
59 | + updateCache(getUnreadMsgsCache(msg.value.content.root || msg.key), msg) | |
60 | + } | |
61 | + | |
62 | + pull( | |
63 | + next(api.feed.pull.private, {reverse: true, limit: 1000, live: false, property: ['value', 'timestamp']}), | |
64 | + privateMsgFilter(), | |
65 | + pull.drain(updateUnreadMsgsCache) | |
66 | + ) | |
67 | + | |
68 | + pull( | |
69 | + next(api.feed.pull.private, {old: false, live: true, property: ['value', 'timestamp']}), | |
70 | + privateMsgFilter(), | |
71 | + pull.drain(updateUnreadMsgsCache) | |
72 | + ) | |
73 | + | |
74 | + return h('SideNav -discovery', [ | |
75 | + LevelOneSideNav(), | |
76 | + LevelTwoSideNav() | |
77 | + ]) | |
78 | + | |
79 | + function LevelOneSideNav () { | |
80 | + function isDiscoverSideNav (loc) { | |
81 | + const PAGES_UNDER_DISCOVER = ['blogIndex', 'blogShow', 'userShow'] | |
82 | + | |
83 | + return PAGES_UNDER_DISCOVER.includes(location.page) | |
84 | + || get(location, 'value.private') === undefined | |
85 | + } | |
86 | + | |
87 | + const prepend = [ | |
88 | + // Nearby | |
89 | + computed(nearby, n => !isEmpty(n) ? h('header', strings.peopleNearby) : null), | |
90 | + map(nearby, feedId => Option({ | |
91 | + notifications: notifications(feedId), | |
92 | + imageEl: api.about.html.avatar(feedId, 'small'), | |
93 | + label: api.about.obs.name(feedId), | |
94 | + selected: location.feed === feedId, | |
95 | + location: computed(recentMsgCache, recent => { | |
96 | + const lastMsg = recent.find(msg => msg.value.author === feedId) | |
97 | + return lastMsg | |
98 | + ? Object.assign(lastMsg, { feed: feedId }) | |
99 | + : { page: 'threadNew', feed: feedId } | |
100 | + }), | |
101 | + }), { comparer: (a, b) => a === b }), | |
102 | + | |
103 | + // --------------------- | |
104 | + computed(nearby, n => !isEmpty(n) ? h('hr') : null), | |
105 | + | |
106 | + // Discover | |
107 | + Option({ | |
108 | + // notifications: '!', //TODO - count this! | |
109 | + // imageEl: h('i.fa.fa-binoculars'), | |
110 | + imageEl: h('i', [ | |
111 | + h('img', { src: path.join(__dirname, '../../../assets', 'discover.png') }) | |
112 | + ]), | |
113 | + label: strings.blogIndex.title, | |
114 | + selected: isDiscoverSideNav(location), | |
115 | + location: { page: 'blogIndex' }, | |
116 | + }) | |
117 | + ] | |
118 | + | |
119 | + return api.app.html.scroller({ | |
120 | + classList: [ 'level', '-one' ], | |
121 | + prepend, | |
122 | + stream: api.feed.pull.private, | |
123 | + filter: privateMsgFilter, | |
124 | + store: recentMsgCache, | |
125 | + updateTop: updateRecentMsgCache, | |
126 | + updateBottom: updateRecentMsgCache, | |
127 | + render: (msgObs) => { | |
128 | + const msg = resolve(msgObs) | |
129 | + const { author } = msg.value | |
130 | + if (nearby.has(author)) return | |
131 | + | |
132 | + return Option({ | |
133 | + //the number of threads with each peer | |
134 | + notifications: notifications(author), | |
135 | + imageEl: api.about.html.avatar(author), | |
136 | + label: api.about.obs.name(author), | |
137 | + selected: location.feed === author, | |
138 | + location: Object.assign({}, msg, { feed: author }) // TODO make obs? | |
139 | + }) | |
140 | + } | |
141 | + }) | |
142 | + | |
143 | + function updateRecentMsgCache (soFar, newMsg) { | |
144 | + soFar.transaction(() => { | |
145 | + const { author, timestamp } = newMsg.value | |
146 | + const index = indexOf(soFar, (msg) => author === resolve(msg).value.author) | |
147 | + var object = Value() | |
148 | + | |
149 | + if (index >= 0) { | |
150 | + // reference already exists, lets use this instead! | |
151 | + const existingMsg = soFar.get(index) | |
152 | + | |
153 | + if (resolve(existingMsg).value.timestamp > timestamp) return | |
154 | + // but abort if the existing reference is newer | |
155 | + | |
156 | + object = existingMsg | |
157 | + soFar.deleteAt(index) | |
158 | + } | |
159 | + | |
160 | + object.set(newMsg) | |
161 | + | |
162 | + const justOlderPosition = indexOf(soFar, (msg) => timestamp > resolve(msg).value.timestamp) | |
163 | + if (justOlderPosition > -1) { | |
164 | + soFar.insert(object, justOlderPosition) | |
165 | + } else { | |
166 | + soFar.push(object) | |
167 | + } | |
168 | + }) | |
169 | + } | |
170 | + | |
171 | + } | |
172 | + | |
173 | + function getUnreadMsgsCache (key) { | |
174 | + var cache = unreadMsgsCache.get(key) | |
175 | + if (!cache) { | |
176 | + cache = Set () | |
177 | + unreadMsgsCache.put(key, cache) | |
178 | + } | |
179 | + return cache | |
180 | + } | |
181 | + | |
182 | + function notifications (key) { | |
183 | + return computed(getUnreadMsgsCache(key), cache => cache.length) | |
184 | + } | |
185 | + | |
186 | + function LevelTwoSideNav () { | |
187 | + const { key, value, feed: targetUser, page } = location | |
188 | + const root = get(value, 'content.root', key) | |
189 | + if (!targetUser) return | |
190 | + if (page === 'userShow') return | |
191 | + | |
192 | + | |
193 | + const prepend = Option({ | |
194 | + selected: page === 'threadNew', | |
195 | + location: {page: 'threadNew', feed: targetUser}, | |
196 | + label: h('Button', strings.threadNew.action.new), | |
197 | + }) | |
198 | + | |
199 | + var userLastMsgCache = usersLastMsgCache.get(targetUser) | |
200 | + if (!userLastMsgCache) { | |
201 | + userLastMsgCache = MutantArray() | |
202 | + usersLastMsgCache.put(targetUser, userLastMsgCache) | |
203 | + } | |
204 | + | |
205 | + return api.app.html.scroller({ | |
206 | + classList: [ 'level', '-two' ], | |
207 | + prepend, | |
208 | + stream: api.feed.pull.private, | |
209 | + filter: () => pull( | |
210 | + pull.filter(msg => !msg.value.content.root), | |
211 | + pull.filter(msg => msg.value.content.type === 'post'), | |
212 | + pull.filter(msg => msg.value.content.recps), | |
213 | + pull.filter(msg => msg.value.content.recps | |
214 | + .map(recp => typeof recp === 'object' ? recp.link : recp) | |
215 | + .some(recp => recp === targetUser) | |
216 | + ) | |
217 | + ), | |
218 | + store: userLastMsgCache, | |
219 | + updateTop: updateLastMsgCache, | |
220 | + updateBottom: updateLastMsgCache, | |
221 | + render: (rootMsgObs) => { | |
222 | + const rootMsg = resolve(rootMsgObs) | |
223 | + return Option({ | |
224 | + notifications: notifications(rootMsg.key), | |
225 | + label: api.message.html.subject(rootMsg), | |
226 | + selected: rootMsg.key === root, | |
227 | + location: Object.assign(rootMsg, { feed: targetUser }), | |
228 | + }) | |
229 | + } | |
230 | + }) | |
231 | + | |
232 | + function updateLastMsgCache (soFar, newMsg) { | |
233 | + soFar.transaction(() => { | |
234 | + const { timestamp } = newMsg.value | |
235 | + const index = indexOf(soFar, (msg) => timestamp === resolve(msg).value.timestamp) | |
236 | + | |
237 | + if (index >= 0) return | |
238 | + // if reference already exists, abort | |
239 | + | |
240 | + var object = Value(newMsg) | |
241 | + | |
242 | + const justOlderPosition = indexOf(soFar, (msg) => timestamp > resolve(msg).value.timestamp) | |
243 | + if (justOlderPosition > -1) { | |
244 | + soFar.insert(object, justOlderPosition) | |
245 | + } else { | |
246 | + soFar.push(object) | |
247 | + } | |
248 | + }) | |
249 | + } | |
250 | + } | |
251 | + | |
252 | + function Option ({ notifications = 0, imageEl, label, location, selected }) { | |
253 | + const className = selected ? '-selected' : '' | |
254 | + function goToLocation (e) { | |
255 | + e.preventDefault() | |
256 | + e.stopPropagation() | |
257 | + api.history.sync.push(resolve(location)) | |
258 | + } | |
259 | + | |
260 | + if (!imageEl) { | |
261 | + return h('Option', { className, 'ev-click': goToLocation }, [ | |
262 | + when(notifications, h('div.spacer', h('div.alert', notifications))), | |
263 | + h('div.label', label) | |
264 | + ]) | |
265 | + } | |
266 | + | |
267 | + return h('Option', { className }, [ | |
268 | + h('div.circle', [ | |
269 | + when(notifications, h('div.alert', notifications)), | |
270 | + imageEl | |
271 | + ]), | |
272 | + h('div.label', { 'ev-click': goToLocation }, label) | |
273 | + ]) | |
274 | + } | |
275 | + | |
276 | + function privateMsgFilter () { | |
277 | + return pull( | |
278 | + pull.filter(msg => msg.value.content.type === 'post'), | |
279 | + pull.filter(msg => msg.value.author != myKey), | |
280 | + pull.filter(msg => msg.value.content.recps) | |
281 | + ) | |
282 | + } | |
283 | + } | |
284 | +} | |
285 | + | |
286 | +function indexOf (array, fn) { | |
287 | + for (var i = 0; i < array.getLength(); i++) { | |
288 | + if (fn(array.get(i))) { | |
289 | + return i | |
290 | + } | |
291 | + } | |
292 | + return -1 | |
293 | +} | |
294 | + | |
295 | + |
app/html/sideNav/sideNavDiscovery.mcss | ||
---|---|---|
@@ -1,0 +1,109 @@ | ||
1 | +SideNav -discovery { | |
2 | + div.level { | |
3 | + section { | |
4 | + } | |
5 | + | |
6 | + -one {} | |
7 | + -two { | |
8 | + section { | |
9 | + div.Option { | |
10 | + padding: 0 1rem | |
11 | + } | |
12 | + } | |
13 | + } | |
14 | + } | |
15 | +} | |
16 | + | |
17 | +Option { | |
18 | + min-width: 8rem | |
19 | + padding: .5rem 1rem | |
20 | + display: flex | |
21 | + align-items: center | |
22 | + | |
23 | + :hover { | |
24 | + cursor: pointer | |
25 | + $backgroundSelected | |
26 | + } | |
27 | + | |
28 | + -selected { | |
29 | + $backgroundSelected | |
30 | + } | |
31 | + | |
32 | + div.spacer { | |
33 | + display: flex | |
34 | + align-self: center | |
35 | + div.alert { | |
36 | + position: relative | |
37 | + width: 1.2rem | |
38 | + height: 1.2rem | |
39 | + border-radius: 1rem | |
40 | + top: -.2rem | |
41 | + left: -.2rem | |
42 | + | |
43 | + background-color: red | |
44 | + color: #fff | |
45 | + font-size: .8rem | |
46 | + | |
47 | + display: flex | |
48 | + justify-content: center | |
49 | + align-items: center | |
50 | + align-self: flex-start | |
51 | + } | |
52 | + } | |
53 | + | |
54 | + div.circle { | |
55 | + width: 3.6rem | |
56 | + position: relative | |
57 | + | |
58 | + div.alert { | |
59 | + position: absolute | |
60 | + width: 1.2rem | |
61 | + height: 1.2rem | |
62 | + border-radius: 1rem | |
63 | + top: -.2rem | |
64 | + left: -.2rem | |
65 | + | |
66 | + background-color: red | |
67 | + color: #fff | |
68 | + font-size: .8rem | |
69 | + | |
70 | + display: flex | |
71 | + justify-content: center | |
72 | + align-items: center | |
73 | + } | |
74 | + | |
75 | + | |
76 | + a img { | |
77 | + | |
78 | + } | |
79 | + | |
80 | + i { | |
81 | + $circleSmall | |
82 | + $colorPrimary | |
83 | + font-size: 1.3rem | |
84 | + display: flex | |
85 | + justify-content: center | |
86 | + align-items: center | |
87 | + } | |
88 | + } | |
89 | + | |
90 | + div.label { | |
91 | + $markdownSmall | |
92 | + (a) { | |
93 | + $colorFontBasic | |
94 | + :hover { text-decoration: none } | |
95 | + } | |
96 | + | |
97 | + flex-grow: 1 | |
98 | + | |
99 | + display: flex | |
100 | + align-items: center | |
101 | + min-height: 3rem | |
102 | + | |
103 | + div.Button { | |
104 | + flex-grow: 1 | |
105 | + } | |
106 | + } | |
107 | +} | |
108 | + | |
109 | + |
app/index.js | ||
---|---|---|
@@ -4,15 +4,17 @@ | ||
4 | 4 | }, |
5 | 5 | html: { |
6 | 6 | app: require('./html/app'), |
7 | 7 | comments: require('./html/comments'), |
8 | - context: require('./html/context'), | |
9 | 8 | header: require('./html/header'), |
10 | 9 | thread: require('./html/thread'), |
11 | 10 | link: require('./html/link'), |
12 | 11 | blogCard: require('./html/blogCard'), |
13 | 12 | blogNav: require('./html/blogNav'), |
14 | 13 | scroller: require('./html/scroller'), |
14 | + sideNav: { | |
15 | + discovery: require('./html/sideNav/sideNavDiscovery'), | |
16 | + } | |
15 | 17 | }, |
16 | 18 | page: { |
17 | 19 | blogIndex: require('./page/blogIndex'), |
18 | 20 | blogNew: require('./page/blogNew'), |
app/page/blogIndex.js | ||
---|---|---|
@@ -4,9 +4,9 @@ | ||
4 | 4 | |
5 | 5 | exports.gives = nest('app.page.blogIndex') |
6 | 6 | |
7 | 7 | exports.needs = nest({ |
8 | - 'app.html.context': 'first', | |
8 | + 'app.html.sideNav': 'first', | |
9 | 9 | 'app.html.blogCard': 'first', |
10 | 10 | 'app.html.blogNav': 'first', |
11 | 11 | 'app.html.scroller': 'first', |
12 | 12 | // 'feed.pull.public': 'first', |
@@ -37,17 +37,17 @@ | ||
37 | 37 | pull.filter(msg => !msg.value.content.root), // show only root messages |
38 | 38 | pull.filter(msg => !api.message.sync.isBlocked(msg)) |
39 | 39 | ), |
40 | 40 | // FUTURE : if we need better perf, we can add a persistent cache. At the moment this page is fast enough though. |
41 | - // See implementation of app.html.context for example | |
41 | + // See implementation of app.html.sideNav for example | |
42 | 42 | // store: recentMsgCache, |
43 | 43 | // updateTop: updateRecentMsgCache, |
44 | 44 | // updateBottom: updateRecentMsgCache, |
45 | 45 | render |
46 | 46 | }) |
47 | 47 | |
48 | 48 | return h('Page -blogIndex', {title: strings.home}, [ |
49 | - api.app.html.context(location), | |
49 | + api.app.html.sideNav(location), | |
50 | 50 | blogs |
51 | 51 | ]) |
52 | 52 | }) |
53 | 53 |
app/page/blogNew.js | ||
---|---|---|
@@ -6,9 +6,9 @@ | ||
6 | 6 | |
7 | 7 | exports.gives = nest('app.page.blogNew') |
8 | 8 | |
9 | 9 | exports.needs = nest({ |
10 | - 'app.html.context': 'first', | |
10 | + 'app.html.sideNav': 'first', | |
11 | 11 | 'channel.async.suggest': 'first', |
12 | 12 | 'history.sync.push': 'first', |
13 | 13 | 'message.html.compose': 'first', |
14 | 14 | 'translations.sync.strings': 'first', |
@@ -59,9 +59,9 @@ | ||
59 | 59 | placeholder: strings.channel |
60 | 60 | }) |
61 | 61 | |
62 | 62 | var page = h('Page -blogNew', [ |
63 | - api.app.html.context(location), | |
63 | + api.app.html.sideNav(location), | |
64 | 64 | h('div.content', [ |
65 | 65 | h('div.container', [ |
66 | 66 | h('div.field -channel', [ |
67 | 67 | h('div.label', strings.channel), |
app/page/blogSearch.js | ||
---|---|---|
@@ -4,9 +4,9 @@ | ||
4 | 4 | |
5 | 5 | exports.gives = nest('app.page.blogSearch') |
6 | 6 | |
7 | 7 | exports.needs = nest({ |
8 | - 'app.html.context': 'first', | |
8 | + 'app.html.sideNav': 'first', | |
9 | 9 | 'app.html.blogCard': 'first', |
10 | 10 | 'app.html.blogNav': 'first', |
11 | 11 | 'app.html.scroller': 'first', |
12 | 12 | 'channel.obs.recent': 'first', |
@@ -76,17 +76,17 @@ | ||
76 | 76 | }), |
77 | 77 | pull.filter(msg => !msg.value.content.root) // show only root messages |
78 | 78 | ), |
79 | 79 | // FUTURE : if we need better perf, we can add a persistent cache. At the moment this page is fast enough though. |
80 | - // See implementation of app.html.context for example | |
80 | + // See implementation of app.html.sideNav for example | |
81 | 81 | // store: recentMsgCache, |
82 | 82 | // updateTop: updateRecentMsgCache, |
83 | 83 | // updateBottom: updateRecentMsgCache, |
84 | 84 | render |
85 | 85 | }) |
86 | 86 | |
87 | 87 | return h('Page -blogSearch', {title: strings.home}, [ |
88 | - api.app.html.context(location), | |
88 | + api.app.html.sideNav(location), | |
89 | 89 | blogs |
90 | 90 | ]) |
91 | 91 | } |
92 | 92 |
app/page/blogShow.js | ||
---|---|---|
@@ -10,9 +10,9 @@ | ||
10 | 10 | 'about.html.avatar': 'first', |
11 | 11 | 'about.obs.name': 'first', |
12 | 12 | 'app.html.blogNav': 'first', |
13 | 13 | 'app.html.comments': 'first', |
14 | - 'app.html.context': 'first', | |
14 | + 'app.html.sideNav': 'first', | |
15 | 15 | 'contact.html.follow': 'first', |
16 | 16 | 'message.html.channel': 'first', |
17 | 17 | 'message.html.likes': 'first', |
18 | 18 | 'message.html.markdown': 'first', |
@@ -39,9 +39,9 @@ | ||
39 | 39 | |
40 | 40 | const { timeago, channel, markdown, compose } = api.message.html |
41 | 41 | |
42 | 42 | return h('Page -blogShow', [ |
43 | - api.app.html.context({ page: 'discover' }), // HACK to highlight discover | |
43 | + api.app.html.sideNav({ page: 'discover' }), // HACK to highlight discover | |
44 | 44 | h('div.content', [ |
45 | 45 | h('section.top', [ |
46 | 46 | api.app.html.blogNav(location) |
47 | 47 | ]), |
app/page/threadNew.js | ||
---|---|---|
@@ -5,9 +5,9 @@ | ||
5 | 5 | |
6 | 6 | exports.needs = nest({ |
7 | 7 | 'about.html.image': 'first', |
8 | 8 | 'about.obs.name': 'first', |
9 | - 'app.html.context': 'first', | |
9 | + 'app.html.sideNav': 'first', | |
10 | 10 | 'app.html.thread': 'first', |
11 | 11 | 'history.sync.push': 'first', |
12 | 12 | 'keys.sync.id': 'first', |
13 | 13 | 'message.html.compose': 'first', |
@@ -44,9 +44,9 @@ | ||
44 | 44 | (err, msg) => api.history.sync.push(err ? err : Object.assign(msg, { feed })) |
45 | 45 | ) |
46 | 46 | |
47 | 47 | return h('Page -threadNew', {title: strings.threadNew.pageTitle}, [ |
48 | - api.app.html.context(location), | |
48 | + api.app.html.sideNav(location), | |
49 | 49 | h('div.content', [ |
50 | 50 | h('div.container', [ |
51 | 51 | h('div.field -to', [ |
52 | 52 | h('div.label', strings.threadNew.field.to), |
app/page/threadShow.js | ||
---|---|---|
@@ -5,9 +5,9 @@ | ||
5 | 5 | |
6 | 6 | exports.gives = nest('app.page.threadShow') |
7 | 7 | |
8 | 8 | exports.needs = nest({ |
9 | - 'app.html.context': 'first', | |
9 | + 'app.html.sideNav': 'first', | |
10 | 10 | 'app.html.thread': 'first', |
11 | 11 | 'message.html.compose': 'first', |
12 | 12 | 'unread.sync.markRead': 'first', |
13 | 13 | 'feed.obs.thread': 'first' |
@@ -34,9 +34,9 @@ | ||
34 | 34 | } |
35 | 35 | const composer = api.message.html.compose({ meta, shrink: false }) |
36 | 36 | |
37 | 37 | return h('Page -threadShow', [ |
38 | - api.app.html.context(location), | |
38 | + api.app.html.sideNav(location), | |
39 | 39 | h('div.content', [ |
40 | 40 | when(thread.subject, h('h1', thread.subject)), |
41 | 41 | api.app.html.thread(thread), |
42 | 42 | composer |
app/page/userShow.js | ||
---|---|---|
@@ -9,9 +9,9 @@ | ||
9 | 9 | exports.needs = nest({ |
10 | 10 | 'about.html.avatar': 'first', |
11 | 11 | 'about.obs.name': 'first', |
12 | 12 | 'about.obs.description': 'first', |
13 | - 'app.html.context': 'first', | |
13 | + 'app.html.sideNav': 'first', | |
14 | 14 | 'app.html.link': 'first', |
15 | 15 | 'app.html.blogCard': 'first', |
16 | 16 | 'app.html.blogNav': 'first', |
17 | 17 | 'app.html.scroller': 'first', |
@@ -79,9 +79,9 @@ | ||
79 | 79 | const store = MutantArray() |
80 | 80 | // store(console.log) |
81 | 81 | |
82 | 82 | return h('Page -userShow', [ |
83 | - api.app.html.context(location), | |
83 | + api.app.html.sideNav(location), | |
84 | 84 | api.app.html.scroller({ |
85 | 85 | classList: ['content'], |
86 | 86 | prepend, |
87 | 87 | // stream: api.feed.pull.profile(feed), |
Built with git-ssb-web