git ssb

1+

Daan Patchwork / patchwork



Tree: 3451510316992d414ec76ba5b29681fe359b7428

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

10372 bytesRaw
1const { h, when, watch, Proxy, Struct, Array: MutantArray, Value, computed, onceTrue } = require('mutant')
2const nest = require('depnest')
3const ref = require('ssb-ref')
4const AnchorHook = require('../../../../anchor-hook')
5const sort = require('ssb-sort')
6const pull = require('pull-stream')
7const isBlog = require('scuttle-blog/isBlog')
8const Blog = require('scuttle-blog')
9const _ = require('lodash')
10const getRoot = require('../../../../message/sync/root')
11
12exports.needs = nest({
13 'keys.sync.id': 'first',
14 'sbot.pull.stream': 'first',
15 'message.obs.name': 'first',
16 'message.html.render': 'first',
17 'message.html.compose': 'first',
18 'message.html.missing': 'first',
19 'profile.html.person': 'first',
20 'sbot.async.get': 'first',
21 'intl.sync.i18n': 'first',
22 'sbot.obs.connection': 'first'
23})
24
25exports.gives = nest('page.html.render')
26
27exports.create = function (api) {
28 const i18n = api.intl.sync.i18n
29 return nest('page.html.render', function (id) {
30 if (!ref.isMsgLink(id)) return
31
32 const link = ref.parseLink(id)
33 const unbox = link.query && link.query.unbox
34 id = link.link
35
36 const loader = h('div', { className: 'Loading -large' })
37
38 const result = Proxy(loader)
39 const anchor = Value()
40 const participants = Proxy([])
41 const messageRefs = MutantArray()
42
43 const yourId = api.keys.sync.id()
44
45 const meta = Struct({
46 type: 'post',
47 root: Proxy(link.link),
48 fork: Proxy(undefined),
49 branch: Proxy(link.link),
50 reply: Proxy(undefined),
51 channel: Value(undefined),
52 recps: Value(undefined)
53 })
54
55 const isRecipient = computed(meta.recps, recps => {
56 if (recps == null) return true // not a private message
57 return normalizedRecps(recps).includes(yourId)
58 }, { idle: true })
59
60 const compose = api.message.html.compose({
61 meta,
62 draftKey: id,
63 isPrivate: when(meta.recps, true),
64 shrink: false,
65 participants,
66 hooks: [
67 AnchorHook('reply', anchor, (el) => el.focus())
68 ],
69 placeholder: when(meta.recps,
70 i18n('Write a private reply'),
71 when(meta.fork, i18n('Write a public reply in sub-thread (fork)'), i18n('Write a public reply'))
72 )
73 })
74
75 get(id, { unbox }, (err, rootMessage) => {
76 if (err) {
77 return result.set(h('PageHeading', [
78 h('h1', i18n('Cannot load thread'))
79 ]))
80 }
81
82 if (!rootMessage) {
83 return result.set(h('PageHeading', [
84 h('h1', i18n('Cannot display message.'))
85 ]))
86 }
87
88 const content = rootMessage.value.content
89 messageRefs.push(getMessageRef(rootMessage))
90
91 // Apply the recps of the original root message to all replies. What happens in private stays in private!
92 meta.recps.set(content.recps)
93
94 if (Array.isArray(content.recps)) {
95 // use private recps if available
96 participants.set(uniq(normalizedRecps(content.recps)))
97 } else {
98 // otherwise message authors
99 participants.set(computed(messageRefs, messages => {
100 return uniq(messages.map(msg => msg && msg.value && msg.value.author))
101 }, { idle: true }))
102 }
103
104 const root = getRoot(rootMessage) || id
105 const isFork = id !== root
106
107 meta.channel.set(content.channel)
108 meta.root.set(id)
109
110 // if we are viewing a message with a root directly, then direct replies fork the original thread
111 meta.fork.set(isFork ? root : undefined)
112
113 // track message author for resolving missing messages and reply mentions
114 meta.reply.set(computed(messageRefs, messages => {
115 const result = {}
116 const first = messages[0]
117 const last = messages[messages.length - 1]
118
119 if (first && first.value) {
120 result[messages[0].key] = messages[0].value.author
121 }
122
123 if (last && last !== first && last.value) {
124 result[last.key] = last.value.author
125 }
126
127 return result
128 }, { idle: true }))
129
130 // set message heads
131 meta.branch.set(computed(messageRefs, messages => {
132 let branches = sort.heads(messages)
133 if (branches.length <= 1) {
134 branches = branches[0]
135 }
136 return branches
137 }, { idle: true }))
138
139 const rootMessageElement = api.message.html.render(rootMessage, {
140 forkedFrom: rootMessage.root,
141 pageId: rootMessage.key,
142 hooks: [UnreadClassHook(anchor, rootMessage.key)],
143 includeForks: false,
144 includeReferences: true
145 })
146
147 // handle display unknown message types as root
148 if (!rootMessageElement) {
149 result.set(h('Thread', [
150 isFork ? h('a.full', { href: root, anchor: id }, [i18n('View parent thread')]) : null,
151 h('div.messages', [
152 api.message.html.render(rootMessage, {
153 renderUnknown: true
154 })
155 ])
156 ]))
157
158 return
159 }
160
161 const messagesContainer = h('div.messages', [rootMessageElement])
162
163 const container = h('Thread', [
164 isFork ? h('a.full', { href: root, anchor: id }, [i18n('View parent thread')]) : null,
165 messagesContainer,
166 when(isRecipient, compose)
167 ])
168
169 let sync = false
170 pull(
171 api.sbot.pull.stream(sbot => sbot.patchwork.thread.sorted({
172 live: true,
173 old: true,
174 dest: rootMessage.key,
175 useBlocksFrom: rootMessage.value.author,
176 types: ['post', 'about']
177 })),
178 pull.drain(msg => {
179 if (msg.sync) {
180 // actually add container to DOM when we get sync on thread
181 sync = true
182 result.set(container)
183 } else {
184 let element
185 if (_.get(msg, 'value.meta.blockedBy.role') === 'threadAuthor') {
186 element = h('Message', [
187 h('a.backlink', {
188 href: msg.key
189 }, [
190 h('strong', [
191 api.profile.html.person(msg.value.author),
192 i18n(' replied but is blocked by '),
193 api.profile.html.person(msg.value.meta.blockedBy.id),
194 ':'
195 ]), ' ',
196 api.message.obs.name(msg.key)
197 ])
198 ])
199 } else {
200 element = api.message.html.render(msg, {
201 hooks: [UnreadClassHook(anchor, msg.key)],
202 includeForks: msg.key !== id,
203 includeReferences: true
204 })
205 }
206
207 // mark messages as new if added in realtime
208 if (sync && element && element.classList) {
209 element.classList.add('-new')
210 setTimeout(() => {
211 // remove the new class after 30 seconds
212 element.classList.remove('-new')
213 }, 30 * 1e3)
214 }
215
216 messageRefs.push(getMessageRef(msg))
217 messagesContainer.append(h('div', {
218 hooks: [AnchorHook(msg.key, anchor, showContext)]
219 }, [
220 msg.key !== id ? api.message.html.missing(first(msg.value.content.branch), msg, rootMessage) : null,
221 element
222 ]))
223
224 if (document.activeElement && document.activeElement.nodeName === 'TEXTAREA') {
225 // ensure compose box remains on screen even after a post is added
226 document.activeElement.scrollIntoViewIfNeeded()
227 }
228 }
229 })
230 )
231 })
232
233 const view = h('div', { className: 'SplitView' }, [
234 h('div.main', {
235 intersectionBindingViewport: { rootMargin: '1000px' }
236 }, [
237 result
238 ])
239 ])
240
241 view.setAnchor = function (value) {
242 anchor.set(value)
243 }
244
245 return view
246 })
247
248 function get (id, { unbox }, cb) {
249 api.sbot.async.get({ id, private: true, unbox }, (err, value) => {
250 if (err) return cb(err)
251 const msg = { key: id, value }
252
253 const me = api.keys.sync.id()
254 onceTrue(api.sbot.obs.connection, sbot => {
255 sbot.patchwork.contacts.isBlocking({ source: me, dest: value.author }, (err, blocking) => {
256 if (err) return cb(err)
257 if (blocking) {
258 // Returning null to render 'Cannot display message.' if we've
259 // blocked the person
260 cb(null, null)
261 } else if (isBlog(msg)) {
262 Blog(api.sbot.obs.connection).async.get(msg, (err, result) => {
263 if (err) return cb(err)
264 msg.body = result.body
265 cb(null, msg)
266 })
267 } else {
268 cb(null, msg)
269 }
270 })
271 })
272 })
273 }
274}
275
276function showContext (element) {
277 const scrollParent = getScrollParent(element)
278 if (scrollParent) {
279 // ensure context is visible
280 scrollParent.scrollTop = Math.max(0, scrollParent.scrollTop - 100)
281 }
282}
283
284function getScrollParent (element) {
285 while (element.parentNode) {
286 if (element.parentNode.scrollTop > 10 && isScroller(element.parentNode)) {
287 return element.parentNode
288 } else {
289 element = element.parentNode
290 }
291 }
292}
293
294function isScroller (element) {
295 const value = window.getComputedStyle(element)['overflow-y']
296 return (value === 'auto' || value === 'scroll')
297}
298
299function first (array) {
300 if (Array.isArray(array)) {
301 return array[0]
302 } else {
303 return array
304 }
305}
306
307function getMessageRef (msg) {
308 // only store structure meta data, not full message content to ease memory usage
309 if (msg.value && msg.value.content) {
310 return {
311 key: msg.key,
312 value: {
313 author: msg.value.author,
314 content: {
315 root: msg.value.content.root,
316 branch: msg.value.content.branch
317 }
318 }
319 }
320 }
321}
322
323function UnreadClassHook (anchor, msgId) {
324 return function (element) {
325 return watch(anchor, (current) => {
326 if (current && current.unread && current.unread.includes(msgId)) {
327 element.classList.add('-unread')
328 } else {
329 element.classList.remove('-unread')
330 }
331 })
332 }
333}
334
335function normalizedRecps (recps) {
336 return Array.isArray(recps) && recps.map(recp => {
337 if (recp == null) return null
338 if (typeof recp === 'string') {
339 return recp
340 }
341 // if recp is mentions object
342 if (typeof recp === 'object') {
343 return recp.link
344 }
345 return null
346 })
347}
348
349function uniq (array) {
350 return Array.from(new Set(array))
351}
352

Built with git-ssb-web