Files: 1ff52dc468c28cdb7ae8aacd80a6a310a42e91b3 / app / html / context.js
6476 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 | 'keys.sync.id': 'first', |
17 | 'history.sync.push': 'first', |
18 | 'message.html.subject': 'first', |
19 | 'sbot.obs.localPeers': 'first', |
20 | 'translations.sync.strings': 'first', |
21 | }) |
22 | |
23 | exports.create = (api) => { |
24 | var recentMsgCache = MutantArray() |
25 | var userMsgCache = Dict() // { id: [ msgs ] } |
26 | |
27 | return nest('app.html.context', context) |
28 | |
29 | function context (location) { |
30 | const strings = api.translations.sync.strings() |
31 | const myKey = api.keys.sync.id() |
32 | |
33 | var nearby = api.sbot.obs.localPeers() |
34 | |
35 | return h('Context -feed', [ |
36 | LevelOneContext(), |
37 | LevelTwoContext() |
38 | ]) |
39 | |
40 | function LevelOneContext () { |
41 | function isDiscoverContext (loc) { |
42 | const PAGES_UNDER_DISCOVER = ['blogIndex', 'blogShow', 'home'] |
43 | |
44 | return PAGES_UNDER_DISCOVER.includes(location.page) |
45 | || get(location, 'value.private') === undefined |
46 | } |
47 | |
48 | const prepend = [ |
49 | // Nearby |
50 | computed(nearby, n => !isEmpty(n) ? h('header', strings.peopleNearby) : null), |
51 | map(nearby, feedId => Option({ |
52 | notifications: Math.random() > 0.7 ? Math.floor(Math.random()*9+1) : 0, // TODO |
53 | imageEl: api.about.html.avatar(feedId, 'small'), |
54 | label: api.about.obs.name(feedId), |
55 | selected: location.feed === feedId, |
56 | location: computed(recentMsgCache, recent => { |
57 | const lastMsg = recent[feedId] |
58 | return lastMsg |
59 | ? Object.assign(lastMsg, { feed: feedId }) |
60 | : { page: 'threadNew', feed: feedId } |
61 | }), |
62 | }), { comparer: (a, b) => a === b }), |
63 | |
64 | // --------------------- |
65 | computed(nearby, n => !isEmpty(n) ? h('hr') : null), |
66 | |
67 | // Discover |
68 | Option({ |
69 | notifications: Math.floor(Math.random()*5+1), |
70 | imageEl: h('i.fa.fa-binoculars'), |
71 | label: strings.blogIndex.title, |
72 | selected: isDiscoverContext(location), |
73 | location: { page: 'blogIndex' }, |
74 | }) |
75 | ] |
76 | |
77 | return api.app.html.scroller({ |
78 | classList: [ 'level', '-one' ], |
79 | prepend, |
80 | stream: api.feed.pull.private, |
81 | filter: () => pull( |
82 | pull.filter(msg => msg.value.content.type === 'post'), // TODO is this the best way to protect against votes? |
83 | pull.filter(msg => msg.value.author != myKey), |
84 | pull.filter(msg => msg.value.content.recps) |
85 | ), |
86 | store: recentMsgCache, |
87 | updateTop: updateRecentMsgCache, |
88 | updateBottom: updateRecentMsgCache, |
89 | render: (msgObs) => { |
90 | const msg = resolve(msgObs) |
91 | const { author } = msg.value |
92 | if (nearby.has(author)) return |
93 | |
94 | return Option({ |
95 | notifications: Math.random() > 0.7 ? Math.floor(Math.random()*9+1) : 0, // TODO |
96 | imageEl: api.about.html.avatar(author), |
97 | label: api.about.obs.name(author), |
98 | selected: location.feed === author, |
99 | location: Object.assign({}, msg, { feed: author }) // TODO make obs? |
100 | }) |
101 | } |
102 | }) |
103 | |
104 | function updateRecentMsgCache (soFar, newMsg) { |
105 | soFar.transaction(function () { |
106 | const { author, timestamp } = newMsg.value |
107 | const index = indexOf(soFar, (msg) => author === resolve(msg).value.author) |
108 | var object = Value() |
109 | |
110 | if (index >= 0) { |
111 | // reference already exists, lets use this instead! |
112 | const existingMsg = soFar.get(index) |
113 | |
114 | if (resolve(existingMsg).value.timestamp > timestamp) return |
115 | // but abort if the existing reference is newer |
116 | |
117 | object = existingMsg |
118 | soFar.deleteAt(index) |
119 | } |
120 | |
121 | object.set(newMsg) |
122 | |
123 | const justOlderPosition = indexOf(soFar, (msg) => timestamp > resolve(msg).value.timestamp) |
124 | if (justOlderPosition > -1) { |
125 | soFar.insert(object, justOlderPosition) |
126 | } else { |
127 | soFar.push(object) |
128 | } |
129 | }) |
130 | } |
131 | |
132 | function indexOf (array, fn) { |
133 | for (var i = 0; i < array.getLength(); i++) { |
134 | if (fn(array.get(i))) { |
135 | return i |
136 | } |
137 | } |
138 | return -1 |
139 | } |
140 | } |
141 | |
142 | function LevelTwoContext () { |
143 | const { key, value, feed: targetUser, page } = location |
144 | const root = get(value, 'content.root', key) |
145 | if (!targetUser) return |
146 | |
147 | var threads = MutantArray() |
148 | |
149 | const prepend = Option({ |
150 | selected: page === 'threadNew', |
151 | location: {page: 'threadNew', feed: targetUser}, |
152 | label: h('Button', strings.threadNew.action.new), |
153 | }) |
154 | |
155 | return api.app.html.scroller({ |
156 | classList: [ 'level', '-two' ], |
157 | prepend, |
158 | stream: api.feed.pull.private, |
159 | filter: () => pull( |
160 | pull.filter(msg => msg.value.content.recps), |
161 | pull.filter(msg => msg.value.content.recps |
162 | .map(recp => typeof recp === 'object' ? recp.link : recp) |
163 | .some(recp => recp === targetUser) |
164 | ), |
165 | api.feed.pull.rollup() // TODO - not technically a filter ...? |
166 | ), |
167 | render: (thread) => { |
168 | return Option({ |
169 | label: api.message.html.subject(thread), |
170 | selected: thread.key === root, |
171 | location: Object.assign(thread, { feed: targetUser }), |
172 | }) |
173 | } |
174 | }) |
175 | } |
176 | |
177 | function Option ({ notifications = 0, imageEl, label, location, selected }) { |
178 | const className = selected ? '-selected' : '' |
179 | const goToLocation = (e) => { |
180 | e.preventDefault() |
181 | e.stopPropagation() |
182 | api.history.sync.push(resolve(location)) |
183 | } |
184 | |
185 | if (!imageEl) { |
186 | return h('Option', { className, 'ev-click': goToLocation }, [ |
187 | h('div.label', label) |
188 | ]) |
189 | } |
190 | |
191 | return h('Option', { className }, [ |
192 | h('div.circle', [ |
193 | when(notifications, h('div.alert', notifications)), |
194 | imageEl |
195 | ]), |
196 | h('div.label', { 'ev-click': goToLocation }, label) |
197 | ]) |
198 | } |
199 | } |
200 | } |
201 | |
202 |
Built with git-ssb-web