git ssb

16+

Dominic / patchbay



Tree: fdca1d25ea142fa395482e66593db66d33f6589c

Files: fdca1d25ea142fa395482e66593db66d33f6589c / app / page / thread.js

4770 bytesRaw
1const { h, Struct, computed, map, resolve, onceTrue } = require('mutant')
2const nest = require('depnest')
3const get = require('lodash/get')
4const { isFeed } = require('ssb-ref')
5
6exports.gives = nest('app.page.thread')
7
8exports.needs = nest({
9 'about.html.avatar': 'first',
10 'app.html.scroller': 'first',
11 'app.html.tabs': 'first',
12 'app.sync.locationId': 'first',
13 'contact.obs.following': 'first',
14 'feed.obs.thread': 'first',
15 'keys.sync.id': 'first',
16 'message.html.compose': 'first',
17 'message.html.render': 'first',
18 'message.async.name': 'first',
19 'sbot.async.run': 'first'
20})
21
22exports.create = function (api) {
23 return nest('app.page.thread', threadPage)
24
25 function threadPage (location) {
26 const root = get(location, 'value.content.root', location.key)
27 const msg = location.key
28 if (msg !== root) scrollDownToMessage(msg)
29
30 const { messages, isPrivate, rootId, lastId, channel, recps } = api.feed.obs.thread(root)
31 const composer = api.message.html.compose({
32 meta: Struct({
33 type: 'post',
34 root: rootId,
35 branch: lastId,
36 channel,
37 recps
38 }),
39 location,
40 feedIdsInThread: computed(messages, msgs => msgs.map(m => m.value.author)),
41 placeholder: 'Write a reply',
42 shrink: false
43 })
44 onceTrue(channel, ch => {
45 const channelInput = composer.querySelector('input')
46 channelInput.value = `#${ch}`
47 channelInput.disabled = true
48 })
49
50 const content = map(messages, m => {
51 const message = api.message.html.render(resolve(m), { pageId: root })
52 markReadWhenVisible(message)
53 return message
54 }, { comparer })
55
56 const { container } = api.app.html.scroller({ prepend: Header({ isPrivate, recps }), content, append: composer })
57 container.classList.add('Thread')
58 container.title = msg
59 api.message.async.name(root, (err, name) => {
60 if (err) throw err
61 container.title = name
62 })
63
64 container.scrollDownToMessage = scrollDownToMessage
65 container.addQuote = composer.addQuote
66 return container
67
68 function scrollDownToMessage (id) {
69 const locationId = api.app.sync.locationId(location)
70 const tabs = api.app.html.tabs()
71 locateKey()
72
73 function locateKey () {
74 // wait till we're on the right page
75 if (tabs.currentPage().id !== locationId) return setTimeout(locateKey, 200)
76
77 if (!tabs.currentPage().scroll) return setTimeout(locateKey, 200)
78
79 tabs.currentPage().scroll('first')
80 const msg = tabs.currentPage().querySelector(`[data-id='${id}']`)
81 if (!msg) return setTimeout(locateKey, 200)
82
83 ;(msg.scrollIntoViewIfNeeded || msg.scrollIntoView).call(msg)
84 msg.focus()
85 }
86 }
87 }
88
89 // only for private threads
90 function Header ({ isPrivate, recps }) {
91 return computed(isPrivate, isPrivate => {
92 if (!isPrivate) return
93
94 const myId = api.keys.sync.id()
95 const ImFollowing = api.contact.obs.following(myId)
96
97 return computed([recps, ImFollowing], (recps, ImFollowing) => {
98 recps = recps.map(r => isFeed(r) ? r : r.link)
99
100 const strangers = recps
101 .filter(r => !Array.from(ImFollowing).includes(r))
102 .filter(r => r !== myId)
103
104 return [
105 h('section.recipients', recps.map(r => {
106 const className = strangers.includes(r) ? 'warning' : ''
107 return h('div', { className }, api.about.html.avatar(r))
108 })),
109 strangers.length
110 ? h('section.info -warning', 'There is a person in this thread you do not follow (bordered in red). If you think you know this person it might be worth checking their profile to confirm they are who they say they are.')
111 : h('section.info', 'These are the other participants in this thread. Once a private thread is started you cannot add people to it.')
112 ]
113 })
114 })
115 }
116
117 // UnreadFeature (search codebase for this if extracting)
118 //
119 // TODO
120 // - extract this into a global depject module?
121 // - garbage collect observation?
122
123 var observer
124 function markReadWhenVisible (el) {
125 if (!observer) {
126 observer = new IntersectionObserver((entries, observer) => {
127 entries
128 .filter(e => e.isIntersecting && e.target.dataset && e.target.dataset.key)
129 .forEach(e => {
130 markRead(e.target.dataset.key, e.target)
131 })
132 }, { threshold: 0.35 })
133 }
134
135 observer.observe(el)
136 }
137
138 function markRead (key, el) {
139 api.sbot.async.run(server => server.unread.markRead(key, (err, data) => {
140 if (err) console.error(err)
141
142 if (el) setTimeout(() => el.classList.add('-read'), 2000)
143 }))
144 }
145}
146
147function comparer (a, b) {
148 return get(resolve(a), 'key') === get(resolve(b), 'key')
149}
150

Built with git-ssb-web