git ssb

1+

Daan Patchwork / patchwork



Tree: 3451510316992d414ec76ba5b29681fe359b7428

Files: 3451510316992d414ec76ba5b29681fe359b7428 / lib / depject / page / html / render / profile.js

15344 bytesRaw
1const nest = require('depnest')
2const ref = require('ssb-ref')
3const { h, when, computed, map, send, dictToCollection, resolve, onceTrue, Value } = require('mutant')
4const displaySheet = require('../../../../sheet/display')
5const timestamp = require('../../../../message/html/timestamp')
6
7exports.needs = nest({
8 'about.obs': {
9 name: 'first',
10 description: 'first',
11 names: 'first',
12 images: 'first',
13 color: 'first'
14 },
15 'blob.sync.url': 'first',
16 'blob.html.input': 'first',
17 'message.async.publish': 'first',
18 'message.html.markdown': 'first',
19 'about.html.image': 'first',
20 'feed.html.rollup': 'first',
21 'sbot.pull.resumeStream': 'first',
22 'sbot.pull.stream': 'first',
23 'sbot.async.publish': 'first',
24 'sbot.async.getLatest': 'first',
25 'sbot.obs.connection': 'first',
26 'keys.sync.id': 'first',
27 'profile.sheet.edit': 'first',
28 'app.navigate': 'first',
29 'profile.obs.contact': 'first',
30 'profile.obs.recentlyUpdated': 'first',
31 'contact.html.followToggle': 'first',
32 'intl.sync.i18n': 'first',
33 'intl.sync.i18n_n': 'first',
34 'sheet.profiles': 'first'
35})
36exports.gives = nest('page.html.render')
37
38exports.create = function (api) {
39 const i18n = api.intl.sync.i18n
40 const plural = api.intl.sync.i18n_n
41 return nest('page.html.render', function profile (id) {
42 if (!ref.isFeed(id)) return
43 const yourId = api.keys.sync.id()
44 const name = api.about.obs.name(id)
45 const description = api.about.obs.description(id)
46 const contact = api.profile.obs.contact(id)
47 const recent = api.profile.obs.recentlyUpdated()
48 const isYou = id === yourId
49 const lastActivity = Value()
50 api.sbot.async.getLatest(id, (err, val) => {
51 if (err) {
52 console.dir(err)
53 } else {
54 if (val) {
55 lastActivity.set(val)
56 }
57 }
58 })
59
60 onceTrue(api.sbot.obs.connection, sbot => {
61 // request a once off replicate of this feed
62 // this is so we can break through the "fog of war", and discover profiles by visiting their keys
63 // ... that is, if any pubs we know have their data!
64 if (contact.blockingFriendsCount() === 0) {
65 sbot.replicate.request(id)
66 }
67 })
68
69 // HACK: if requesting this feed has suddenly downloaded a bunch of posts, then refresh view immediately
70 setTimeout(() => {
71 if (feedView.pendingUpdates() > 0) {
72 feedView.reload()
73 }
74 }, 1000)
75
76 const friends = computed([contact.following, contact.followers], (following, followers) => {
77 return Array.from(following).filter(follow => followers.includes(follow))
78 })
79
80 const following = computed([contact.following, friends], (following, friends) => {
81 return following.filter(follow => !friends.includes(follow))
82 })
83
84 const followers = computed([contact.followers, friends], (followers, friends) => {
85 return followers.filter(follower => !friends.includes(follower))
86 })
87
88 // only include names/images assigned by people you follow or people they follow
89 const names = computed([api.about.obs.names(id), contact.yourFollowing, contact.following, yourId, id], filterByValues)
90 const images = computed([api.about.obs.images(id), contact.yourFollowing, contact.following, yourId, id], filterByValues)
91
92 const namePicker = h('div', { className: 'Picker' }, [
93 map(dictToCollection(names), (item) => {
94 const isSelf = computed(item.value, (ids) => ids.includes(id))
95 const isAssigned = computed(item.value, (ids) => ids.includes(yourId))
96 return h('a.name', {
97 'ev-click': () => {
98 if (!isAssigned()) {
99 assignName(id, resolve(item.key))
100 }
101 },
102 href: '#',
103 classList: [
104 when(isSelf, '-self'),
105 when(isAssigned, '-assigned')
106 ],
107 title: nameList(when(isSelf, i18n('Self Assigned'), i18n('Assigned By')), item.value)
108 }, [
109 item.key
110 ])
111 }),
112 isYou
113 ? null
114 : h('a -add', {
115 'ev-click': () => {
116 rename(id)
117 },
118 href: '#'
119 }, ['+'])
120 ])
121
122 const imagePicker = h('div', { className: 'Picker' }, [
123 map(dictToCollection(images), (item) => {
124 const isSelf = computed(item.value, (ids) => ids.includes(id))
125 const isAssigned = computed(item.value, (ids) => ids.includes(yourId))
126 return h('a.name', {
127 'ev-click': () => {
128 if (!isAssigned()) {
129 assignImage(id, resolve(item.key))
130 }
131 },
132 href: '#',
133 classList: [
134 when(isSelf, '-self'),
135 when(isAssigned, '-assigned')
136 ],
137 title: nameList(when(isSelf, i18n('Self Assigned'), i18n('Assigned By')), item.value)
138 }, [
139 h('img', {
140 className: 'Avatar',
141 style: { 'background-color': api.about.obs.color(id) },
142 src: computed(item.key, api.blob.sync.url)
143 })
144 ])
145 }),
146 isYou
147 ? null
148 : h('span.add', [
149 api.blob.html.input((err, file) => {
150 if (err) return
151 assignImage(id, file.link)
152 }, {
153 accept: 'image/*',
154 resize: { width: 500, height: 500 }
155 })
156 ])
157 ])
158
159 const prepend = h('header', { className: 'ProfileHeader' }, [
160 h('div.image', [
161 api.about.html.image(id),
162 h('div.lastActivity',
163 computed(lastActivity,
164 msg => `${i18n('Last activity')}: ${timestamp(msg, false)}`))]),
165 h('div.main', [
166 h('div.title', [
167 h('h1', [name]),
168 h('div.meta', [
169 when(id === yourId, [
170 h('button', { 'ev-click': api.profile.sheet.edit }, i18n('Edit Your Profile'))
171 ], api.contact.html.followToggle(id))
172 ])
173 ]),
174 h('section -publicKey', [
175 h('pre', { title: i18n('Public key for this profile') }, id)
176 ]),
177
178 when(contact.hidden, [
179 h('section -blocked', {
180 classList: [when(contact.youBlock, null, '-ignore')]
181 }, [
182 h('h1', [when(contact.youBlock,
183 ['โ›”๏ธ ', i18n('You have chosen to publicly block this person.')],
184 ['๐Ÿ‘ค ', i18n('You have chosen to privately ignore this person.')]
185 )]),
186 h('p', i18n('No new messages will be downloaded. Existing messages will be hidden.'))
187 ])
188 ]),
189 when(contact.notFollowing, [
190 when(contact.blockingFriendsCount, h('section -blockWarning', [
191 h('a', {
192 href: '#',
193 'ev-click': send(displayBlockingFriends, contact.blockingFriends)
194 }, [
195 'โš ๏ธ ', computed(['This person is blocked by %s of your friends.', contact.blockingFriendsCount], plural)
196 ])
197 ])),
198
199 when(contact.noIncoming,
200 h('section -distanceWarning', [
201 h('h1', i18n('You don\'t follow anyone who follows this person')),
202 h('p', i18n('You might not be seeing their latest messages. You could try joining a pub that they are a member of.')),
203 when(contact.hasOutgoing,
204 h('p', i18n('However, since they follow someone that follows you, they should be able to see your posts.')),
205 h('p', i18n('They might not be able to see your posts either.'))
206 )
207 ]),
208 when(contact.noOutgoing,
209 h('section -distanceWarning', [
210 h('h1', i18n('This person does not follow anyone that follows you')),
211 h('p', i18n('They might not receive your private messages or replies. You could try joining a pub that they are a member of.')),
212 h('p', i18n('However, since you follow someone that follows them, you should be able to see their latest posts.'))
213 ]),
214 when(contact.mutualFriendsCount,
215 h('section -mutualFriends', [
216 h('a', {
217 href: '#',
218 title: nameList(i18n('Mutual Friends'), contact.mutualFriends),
219 'ev-click': send(displayMutualFriends, contact.mutualFriends)
220 }, [
221 '๐Ÿ‘ฅ ', computed(['You share %s mutual friends with this person.', contact.mutualFriendsCount], plural)
222 ])
223 ]),
224 h('section -mutualFriends', [
225 h('a', {
226 href: '#',
227 title: nameList(i18n('Followed by'), contact.incomingVia),
228 'ev-click': send(displayFollowedBy, contact.incomingVia)
229 }, [
230 '๐Ÿ‘ฅ ', computed(['You follow %s people that follow this person.', contact.incomingViaCount], plural)
231 ])
232 ])
233 )
234 )
235 )
236 ]),
237
238 h('section -description', [
239 computed(description, (text) => {
240 if (typeof text === 'string') {
241 return api.message.html.markdown(text)
242 }
243 })
244 ]),
245 h('section', [namePicker, imagePicker])
246 ])
247 ])
248
249 const getStream = api.sbot.pull.resumeStream((sbot, opts) => {
250 return sbot.patchwork.profile.roots(opts)
251 }, { limit: 40, reverse: true, id })
252
253 const feedView = api.feed.html.rollup(getStream, {
254 prepend,
255 hidden: contact.hidden,
256 compactFilter: (msg) => msg.value.author !== id, // show root context messages smaller
257 ungroupFilter: (msg) => msg.value.author !== id,
258 updateStream: api.sbot.pull.stream(sbot => sbot.patchwork.profile.latest({ id }))
259 })
260
261 const container = h('div', { className: 'SplitView' }, [
262 h('div.main', [
263 feedView
264 ]),
265 h('div.side.-right', [
266 h('button PrivateMessageButton', { 'ev-click': () => api.app.navigate('/private', { compose: { to: id } }) }, i18n('Send Private Message')),
267 when(contact.sync,
268 h('div', [
269 renderContactBlock(i18n('Friends'), onlyRecent(friends, 10), contact.yourFollowing, friends),
270 renderContactBlock(i18n('Followers'), onlyFollowing(followers, 10), contact.yourFollowing, followers),
271 renderContactBlock(i18n('Following'), onlyRecent(following, 10), contact.yourFollowing, following),
272 renderContactBlock(i18n('Blocked by'), contact.blockingFriends, contact.yourFollowing)
273 ]),
274 h('div', { className: 'Loading' })
275 )
276 ])
277 ])
278
279 container.pendingUpdates = feedView.pendingUpdates
280 container.reload = feedView.reload
281 return container
282
283 // scoped
284
285 function onlyFollowing (ids, max) {
286 return computed([recent, ids, contact.yourFollowing], (a, b, c) => {
287 let result = a.filter(x => b.includes(x) && c.includes(x))
288 if (result.length === 0 && a.length) {
289 // fallback to just recent
290 result = a.filter(x => b.includes(x))
291 }
292 if (max) {
293 return result.slice(0, max)
294 } else {
295 return result
296 }
297 })
298 }
299
300 function onlyRecent (ids, max) {
301 return computed([recent, ids], (a, b) => {
302 const result = a.filter(x => b.includes(x))
303 if (max) {
304 return result.slice(0, max)
305 } else {
306 return result
307 }
308 })
309 }
310 })
311
312 function displayFollowedBy (profiles) {
313 api.sheet.profiles(profiles, i18n('Followed by'))
314 }
315
316 function displayMutualFriends (profiles) {
317 api.sheet.profiles(profiles, i18n('Mutual Friends'))
318 }
319
320 function displayBlockingFriends (profiles) {
321 api.sheet.profiles(profiles, i18n('Blocked by'))
322 }
323
324 function renderContactBlock (title, profiles, yourFollowing, fullList) {
325 const moreCount = computed([profiles, fullList], (a, b) => a && b && a.length < b.length && b.length - a.length)
326 return [
327 when(computed([profiles, fullList], (a, b) => a.length || (b && b.length)), h('h2', title)),
328 h('div', {
329 classList: 'ProfileList'
330 }, [
331 map(profiles, (id) => {
332 const following = computed(yourFollowing, f => f.includes(id))
333 return h('a.profile', {
334 href: id,
335 classList: [
336 when(following, '-following')
337 ]
338 }, [
339 h('div.avatar', [api.about.html.image(id)]),
340 h('div.main', [
341 h('div.name', [api.about.obs.name(id)])
342 ])
343 ])
344 }, {
345 maxTime: 5,
346 nextTick: true
347 }),
348 when(moreCount,
349 h('a.profile -more', {
350 href: '#',
351 'ev-click': function () {
352 api.sheet.profiles(fullList, title)
353 }
354 }, [
355 h('div.main', [
356 h('div.name', computed(moreCount, count => {
357 return count && plural('View %s more', count)
358 }))
359 ])
360 ])
361 )
362 ])
363 ]
364 }
365
366 function assignImage (id, image) {
367 api.message.async.publish({
368 type: 'about',
369 about: id,
370 image
371 })
372 }
373
374 function assignName (id, name) {
375 api.message.async.publish({
376 type: 'about',
377 about: id,
378 name
379 })
380 }
381
382 function rename (id) {
383 displaySheet(close => {
384 const currentName = api.about.obs.name(id)
385 const input = h('input', {
386 style: { 'font-size': '150%' },
387 value: currentName()
388 })
389 setTimeout(() => {
390 input.focus()
391 input.select()
392 }, 5)
393 return {
394 content: h('div', {
395 style: {
396 padding: '20px',
397 'text-align': 'center'
398 }
399 }, [
400 h('h2', {
401 style: {
402 'font-weight': 'normal'
403 }
404 }, [i18n('What would you like to call '), h('strong', [currentName]), '?']),
405 h('h3', {
406 style: {
407 'font-weight': 'normal'
408 }
409 }, [i18n('Names you assign here will be publicly visible to others.')]),
410 input
411 ]),
412 footer: [
413 h('button -save', {
414 'ev-click': () => {
415 if (input.value.trim() && input.value !== currentName()) {
416 // no confirm
417 api.sbot.async.publish({
418 type: 'about',
419 about: id,
420 name: input.value.trim()
421 })
422 }
423 close()
424 }
425 }, i18n('Confirm')),
426 h('button -cancel', {
427 'ev-click': close
428 }, i18n('Cancel'))
429 ]
430 }
431 })
432 }
433
434 function nameList (prefix, ids) {
435 const items = map(ids, api.about.obs.name)
436 return computed([prefix, items], (prefix, names) => {
437 return (prefix ? (prefix + '\n') : '') + names.map((n) => `- ${n}`).join('\n')
438 })
439 }
440}
441
442function filterByValues (attributes, ...matchValues) {
443 return Object.keys(attributes).reduce((result, key) => {
444 const values = attributes[key].filter(value => {
445 return matchValues.some(matchValue => {
446 if (Array.isArray(matchValue)) {
447 return matchValue.includes(value)
448 } else {
449 return matchValue === value
450 }
451 })
452 })
453 if (values.length) {
454 result[key] = values
455 }
456 return result
457 }, {})
458}
459

Built with git-ssb-web