git ssb

10+

Matt McKegg / patchwork



Tree: 593dfd760dee299082f615e02f2747f1c6f660ec

Files: 593dfd760dee299082f615e02f2747f1c6f660ec / modules / page / html / render / public.js

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

Built with git-ssb-web