git ssb

0+

alanz / patchwork



forked from Matt McKegg / patchwork

Tree: b8dcb5e1ae317132d4e8498b59e5279fcf92490f

Files: b8dcb5e1ae317132d4e8498b59e5279fcf92490f / modules / page / html / render / public.js

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

Built with git-ssb-web