git ssb

10+

Matt McKegg / patchwork



Tree: 7cda75eab5c68ce985a6dd1154a45acc301b29ec

Files: 7cda75eab5c68ce985a6dd1154a45acc301b29ec / modules / page / html / render / public.js

9846 bytesRaw
1var nest = require('depnest')
2var extend = require('xtend')
3var pull = require('pull-stream')
4var { h, send, when, computed, map, onceTrue } = require('mutant')
5
6exports.needs = nest({
7 sbot: {
8 obs: {
9 connectedPeers: 'first',
10 localPeers: 'first',
11 connection: 'first'
12 }
13 },
14 'sbot.pull.stream': 'first',
15 'feed.pull.public': 'first',
16 'about.html.image': 'first',
17 'about.obs.name': 'first',
18 'invite.sheet': 'first',
19
20 'message.html.compose': 'first',
21 'message.async.publish': 'first',
22 'message.sync.root': 'first',
23 'progress.html.peer': 'first',
24
25 'feed.html.rollup': 'first',
26 'profile.obs.recentlyUpdated': 'first',
27 'contact.obs.following': 'first',
28 'contact.obs.blocking': 'first',
29 'channel.obs': {
30 subscribed: 'first',
31 recent: 'first'
32 },
33 'channel.sync.normalize': 'first',
34 'keys.sync.id': 'first',
35 'settings.obs.get': 'first',
36 'intl.sync.i18n': 'first'
37})
38
39exports.gives = nest({
40 'page.html.render': true
41})
42
43exports.create = function (api) {
44 const i18n = api.intl.sync.i18n
45 return nest('page.html.render', page)
46
47 function page (path) {
48 if (path !== '/public') return // "/" is a sigil for "page"
49
50 var id = api.keys.sync.id()
51 var following = api.contact.obs.following(id)
52 var blocking = api.contact.obs.blocking(id)
53 var subscribedChannels = api.channel.obs.subscribed(id)
54 var recentChannels = api.channel.obs.recent()
55 var loading = computed([subscribedChannels.sync, recentChannels.sync], (...args) => !args.every(Boolean))
56 var channels = computed(recentChannels, items => items.slice(0, 8), {comparer: arrayEq})
57 var connectedPeers = api.sbot.obs.connectedPeers()
58 var localPeers = api.sbot.obs.localPeers()
59 var connectedPubs = computed([connectedPeers, localPeers], (c, l) => c.filter(x => !l.includes(x)))
60
61 var prepend = [
62 api.message.html.compose({ meta: { type: 'post' }, placeholder: i18n('Write a public message') })
63 ]
64
65 var lastMessage = null
66
67 var getStream = (opts) => {
68 if (!opts.lt) {
69 // HACK: reset the isReplacementMessage check
70 lastMessage = null
71 }
72 if (opts.lt != null && !opts.lt.marker) {
73 // if an lt has been specified that is not a marker, assume stream is finished
74 return pull.empty()
75 } else {
76 return api.sbot.pull.stream(sbot => sbot.patchwork.roots(extend(opts, {
77 ids: [id],
78 onlySubscribedChannels: filters() && filters().onlySubscribed
79 })))
80 }
81 }
82
83 var filters = api.settings.obs.get('filters')
84 var feedView = api.feed.html.rollup(getStream, {
85 prepend,
86 prefiltered: true, // we've already filtered out the roots we don't want to include
87 updateStream: api.sbot.pull.stream(sbot => sbot.patchwork.latest({ids: [id]})),
88 bumpFilter: function (msg) {
89 // this needs to match the logic in sbot/roots so that we display the
90 // correct bump explainations
91 if (msg.value && msg.value.content && typeof msg.value.content === 'object') {
92 var type = msg.value.content.type
93 if (type === 'vote') return false
94
95 var author = msg.value.author
96
97 if (id === author || following().includes(author)) {
98 return true
99 } else if (matchesSubscribedChannel(msg)) {
100 return 'matches-channel'
101 }
102 }
103 },
104 rootFilter: function (msg) {
105 // skip messages that are directly replaced by the previous message
106 // e.g. follow / unfollow in quick succession
107 // THIS IS A TOTAL HACK!!! SHOULD BE REPLACED WITH A PROPER ROLLUP!
108 var isOutdated = isReplacementMessage(msg, lastMessage)
109 if (checkFeedFilter(msg) && !isOutdated) {
110 lastMessage = msg
111 return true
112 }
113 },
114 compactFilter: function (msg, root) {
115 if (!root && api.message.sync.root(msg)) {
116 // msg has a root, but is being displayed as root (fork)
117 return true
118 }
119 },
120 waitFor: computed([
121 following.sync,
122 subscribedChannels.sync
123 ], (...x) => x.every(Boolean))
124 })
125
126 // call reload whenever filters changes (equivalent to the refresh from inside rollup)
127 filters(feedView.reload)
128
129 var result = h('div.SplitView', [
130 h('div.side', [
131 getSidebar()
132 ]),
133 h('div.main', feedView)
134 ])
135
136 result.pendingUpdates = feedView.pendingUpdates
137 result.reload = function () {
138 feedView.reload()
139 }
140
141 return result
142
143 function checkFeedFilter (root) {
144 if (filters()) {
145 if (filters().following && getType(root) === 'contact') return false
146 }
147 return true
148 }
149
150 function matchesSubscribedChannel (msg) {
151 if (msg.filterResult) {
152 return msg.filterResult.matchesChannel || msg.filterResult.matchingTags.length
153 } else {
154 var channel = api.channel.sync.normalize(msg.value.content.channel)
155 var tagged = checkTag(msg.value.content.mentions)
156 var isSubscribed = channel ? subscribedChannels().has(channel) : false
157 return isSubscribed || tagged
158 }
159 }
160
161 function checkTag (mentions) {
162 if (Array.isArray(mentions)) {
163 return mentions.some((mention) => {
164 if (mention && typeof mention.link === 'string' && mention.link.startsWith('#')) {
165 var channel = api.channel.sync.normalize(mention.link.slice(1))
166 return channel ? subscribedChannels().has(channel) : false
167 }
168 })
169 }
170 }
171
172 function getSidebar () {
173 var whoToFollow = computed([api.profile.obs.recentlyUpdated(), following, blocking, localPeers], (recent, ...ignoreFeeds) => {
174 return recent.filter(x => x !== id && !ignoreFeeds.some(f => f.includes(x))).slice(0, 10)
175 })
176 return [
177 h('button -pub -full', {
178 'ev-click': api.invite.sheet
179 }, i18n('+ Join Pub')),
180 when(loading, [ h('Loading') ], [
181 when(computed(channels, x => x.length), h('h2', i18n('Active Channels'))),
182 h('div', {
183 classList: 'ChannelList',
184 hidden: loading
185 }, [
186 map(channels, (channel) => {
187 var subscribed = subscribedChannels.has(channel)
188 return h('a.channel', {
189 href: `#${channel}`,
190 classList: [
191 when(subscribed, '-subscribed')
192 ]
193 }, [
194 h('span.name', '#' + channel),
195 when(subscribed,
196 h('a.-unsubscribe', {
197 'ev-click': send(unsubscribe, channel)
198 }, i18n('Unsubscribe')),
199 h('a.-subscribe', {
200 'ev-click': send(subscribe, channel)
201 }, i18n('Subscribe'))
202 )
203 ])
204 }, {maxTime: 5}),
205 h('a.channel -more', {href: '/channels'}, i18n('More Channels...'))
206 ])
207 ]),
208
209 PeerList(localPeers, i18n('Local')),
210 PeerList(connectedPubs, i18n('Connected Pubs')),
211
212 when(computed(whoToFollow, x => x.length), h('h2', i18n('Who to follow'))),
213 when(following.sync,
214 h('div', {
215 classList: 'ProfileList'
216 }, [
217 map(whoToFollow, (id) => {
218 return h('a.profile', {
219 href: id
220 }, [
221 h('div.avatar', [api.about.html.image(id)]),
222 h('div.main', [
223 h('div.name', [ api.about.obs.name(id) ])
224 ])
225 ])
226 })
227 ])
228 )
229 ]
230 }
231
232 function PeerList (ids, title) {
233 return [
234 when(computed(ids, x => x.length), h('h2', title)),
235 h('div', {
236 classList: 'ProfileList'
237 }, [
238 map(ids, (id) => {
239 var connected = computed([connectedPeers, id], (peers, id) => peers.includes(id))
240 return h('a.profile', {
241 classList: [
242 when(connected, '-connected')
243 ],
244 href: id
245 }, [
246 h('div.avatar', [api.about.html.image(id)]),
247 h('div.main', [
248 h('div.name', [ api.about.obs.name(id) ])
249 ]),
250 h('div.progress', [
251 api.progress.html.peer(id)
252 ]),
253 h('div.controls', [
254 h('a.disconnect', {href: '#disconnect', 'ev-click': send(disconnect, id), title: i18n('Force Disconnect')}, ['x'])
255 ])
256 ])
257 })
258 ])
259 ]
260 }
261
262 function subscribe (id) {
263 api.message.async.publish({
264 type: 'channel',
265 channel: id,
266 subscribed: true
267 })
268 }
269
270 function unsubscribe (id) {
271 api.message.async.publish({
272 type: 'channel',
273 channel: id,
274 subscribed: false
275 })
276 }
277
278 function disconnect (id) {
279 onceTrue(api.sbot.obs.connection, (sbot) => {
280 sbot.patchwork.disconnect(id)
281 })
282 }
283 }
284}
285
286function getType (msg) {
287 return msg && msg.value && msg.value.content && msg.value.content.type
288}
289
290function hasChannel (msg) {
291 return getType(msg) !== 'channel' && msg && msg.value && msg.value.content && !!msg.value.content.channel
292}
293
294function arrayEq (a, b) {
295 if (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a !== b) {
296 return a.every((value, i) => value === b[i])
297 }
298}
299
300function isReplacementMessage (msgA, msgB) {
301 if (msgA && msgB && msgA.value.content && msgB.value.content && msgA.value.content.type === msgB.value.content.type) {
302 if (msgA.key === msgB.key) return false
303 var type = msgA.value.content.type
304 if (type === 'contact') {
305 return msgA.value.author === msgB.value.author && msgA.value.content.contact === msgB.value.content.contact
306 }
307 }
308}
309

Built with git-ssb-web