git ssb

10+

Matt McKegg / patchwork



Tree: 24269db9f3d8f25ca1b4996019e7c798f5ee96b8

Files: 24269db9f3d8f25ca1b4996019e7c798f5ee96b8 / modules / page / html / render / public.js

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

Built with git-ssb-web