git ssb

16+

Dominic / patchbay



Tree: c4c384106bcf50acf4c0fcb3f7b842ad68f25bcb

Files: c4c384106bcf50acf4c0fcb3f7b842ad68f25bcb / app / page / thread.js

5376 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 let root = get(location, 'value.content.root') || get(location, 'value.content.about') || location.key
27 if (location.value && location.value.unbox) // direct link with unbox key
28 root = location.key
29 const msg = location.key
30 if (msg !== root) scrollDownToMessage(msg)
31
32 const { messages, isPrivate, rootId, lastId, channel, recps } = api.feed.obs.thread(root)
33 const composer = api.message.html.compose({
34 meta: Struct({
35 type: 'post',
36 root: rootId,
37 branch: lastId,
38 channel,
39 recps
40 }),
41 location,
42 feedIdsInThread: computed(messages, msgs => msgs.map(m => m.value.author)),
43 placeholder: 'Write a reply',
44 shrink: false
45 })
46
47 onceTrue(channel, ch => {
48 const channelInput = composer.querySelector('input')
49 channelInput.value = `#${ch}`
50 channelInput.disabled = true
51 })
52
53 const content = map(messages, m => {
54 let msg = resolve(m)
55 if (msg.key == location.key && location.value && location.value.unbox) // we have an unbox key, so message is already unboxed
56 msg = location
57 const message = api.message.html.render(msg, { pageId: root })
58 markReadWhenVisible(message)
59 return message
60 }, { comparer })
61
62 const { container } = api.app.html.scroller({ prepend: Header({ isPrivate, recps }), content, append: composer })
63 container.classList.add('Thread')
64 container.title = root
65 api.message.async.name(root, (err, name) => {
66 if (err) throw err
67 container.title = name
68
69 // TODO tidy this up
70 // over-ride message.async.name OR create message.async.subject
71 onceTrue(messages, msgs => {
72 const subject = get(msgs, ' [0].value.content.subject')
73 if (!subject) return
74 container.title = subject
75 })
76 })
77
78 container.scrollDownToMessage = scrollDownToMessage
79 container.addQuote = composer.addQuote
80 return container
81
82 function scrollDownToMessage (id) {
83 const locationId = api.app.sync.locationId(location)
84 const tabs = api.app.html.tabs()
85 locateKey()
86
87 function locateKey () {
88 // wait till we're on the right page
89 if (tabs.currentPage().id !== locationId) return setTimeout(locateKey, 200)
90
91 if (!tabs.currentPage().keyboardScroll) return setTimeout(locateKey, 200)
92
93 tabs.currentPage().keyboardScroll('first')
94 const msg = tabs.currentPage().querySelector(`[data-id='${id}']`)
95 if (!msg) return setTimeout(locateKey, 200)
96
97 ;(msg.scrollIntoViewIfNeeded || msg.scrollIntoView).call(msg)
98 msg.focus()
99 }
100 }
101 }
102
103 // only for private threads
104 function Header ({ isPrivate, recps }) {
105 return computed(isPrivate, isPrivate => {
106 if (!isPrivate) return
107
108 const myId = api.keys.sync.id()
109 const ImFollowing = api.contact.obs.following(myId)
110
111 return computed([recps, ImFollowing], (recps, ImFollowing) => {
112 recps = recps.map(r => isFeed(r) ? r : r.link)
113
114 const strangers = recps
115 .filter(r => !Array.from(ImFollowing).includes(r))
116 .filter(r => r !== myId)
117
118 return [
119 h('section.recipients', recps.map(r => {
120 const className = strangers.includes(r) ? 'warning' : ''
121 return h('div', { className }, api.about.html.avatar(r))
122 })),
123 strangers.length
124 ? 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.')
125 : h('section.info', 'These are the other participants in this thread. Once a private thread is started you cannot add people to it.')
126 ]
127 })
128 })
129 }
130
131 // UnreadFeature (search codebase for this if extracting)
132 //
133 // TODO
134 // - extract this into a global depject module?
135 // - garbage collect observation?
136
137 var observer
138 function markReadWhenVisible (el) {
139 if (!observer) {
140 observer = new IntersectionObserver((entries, observer) => {
141 entries
142 .filter(e => e.isIntersecting && e.target.dataset && e.target.dataset.key)
143 .forEach(e => {
144 markRead(e.target.dataset.key, e.target)
145 })
146 }, { threshold: 0.35 })
147 }
148
149 observer.observe(el)
150 }
151
152 function markRead (key, el) {
153 api.sbot.async.run(server => server.unread.markRead(key, (err, data) => {
154 if (err) console.error(err)
155
156 if (el) setTimeout(() => el.classList.add('-read'), 2000)
157 }))
158 }
159}
160
161function comparer (a, b) {
162 return get(resolve(a), 'key') === get(resolve(b), 'key')
163}
164

Built with git-ssb-web