git ssb

10+

Matt McKegg / patchwork



Tree: 8de1edb8454d207982be32e1b3b5a6e3497f19ce

Files: 8de1edb8454d207982be32e1b3b5a6e3497f19ce / modules / page / html / render / public.js

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

Built with git-ssb-web