git ssb

16+

Dominic / patchbay



Tree: b8e8bccb7f0d43f6b02e0f1a5e8f4a2be24ab15e

Files: b8e8bccb7f0d43f6b02e0f1a5e8f4a2be24ab15e / app / page / thread.js

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

Built with git-ssb-web