git ssb

16+

Dominic / patchbay



Tree: f1d9a256d5c365f61e080c2fe9a7a0ba1d32b619

Files: f1d9a256d5c365f61e080c2fe9a7a0ba1d32b619 / app / page / thread.js

5068 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') || get(location, 'value.content.about') || 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 = root
59 api.message.async.name(root, (err, name) => {
60 if (err) throw err
61 container.title = name
62
63 // TODO tidy this up
64 // over-ride message.async.name OR create message.async.subject
65 onceTrue(messages, msgs => {
66 const { subject } = msgs[0].value.content
67 if (!subject) return
68 container.title = subject
69 })
70 })
71
72 container.scrollDownToMessage = scrollDownToMessage
73 container.addQuote = composer.addQuote
74 return container
75
76 function scrollDownToMessage (id) {
77 const locationId = api.app.sync.locationId(location)
78 const tabs = api.app.html.tabs()
79 locateKey()
80
81 function locateKey () {
82 // wait till we're on the right page
83 if (tabs.currentPage().id !== locationId) return setTimeout(locateKey, 200)
84
85 if (!tabs.currentPage().scroll) return setTimeout(locateKey, 200)
86
87 tabs.currentPage().scroll('first')
88 const msg = tabs.currentPage().querySelector(`[data-id='${id}']`)
89 if (!msg) return setTimeout(locateKey, 200)
90
91 ;(msg.scrollIntoViewIfNeeded || msg.scrollIntoView).call(msg)
92 msg.focus()
93 }
94 }
95 }
96
97 // only for private threads
98 function Header ({ isPrivate, recps }) {
99 return computed(isPrivate, isPrivate => {
100 if (!isPrivate) return
101
102 const myId = api.keys.sync.id()
103 const ImFollowing = api.contact.obs.following(myId)
104
105 return computed([recps, ImFollowing], (recps, ImFollowing) => {
106 recps = recps.map(r => isFeed(r) ? r : r.link)
107
108 const strangers = recps
109 .filter(r => !Array.from(ImFollowing).includes(r))
110 .filter(r => r !== myId)
111
112 return [
113 h('section.recipients', recps.map(r => {
114 const className = strangers.includes(r) ? 'warning' : ''
115 return h('div', { className }, api.about.html.avatar(r))
116 })),
117 strangers.length
118 ? 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.')
119 : h('section.info', 'These are the other participants in this thread. Once a private thread is started you cannot add people to it.')
120 ]
121 })
122 })
123 }
124
125 // UnreadFeature (search codebase for this if extracting)
126 //
127 // TODO
128 // - extract this into a global depject module?
129 // - garbage collect observation?
130
131 var observer
132 function markReadWhenVisible (el) {
133 if (!observer) {
134 observer = new IntersectionObserver((entries, observer) => {
135 entries
136 .filter(e => e.isIntersecting && e.target.dataset && e.target.dataset.key)
137 .forEach(e => {
138 markRead(e.target.dataset.key, e.target)
139 })
140 }, { threshold: 0.35 })
141 }
142
143 observer.observe(el)
144 }
145
146 function markRead (key, el) {
147 api.sbot.async.run(server => server.unread.markRead(key, (err, data) => {
148 if (err) console.error(err)
149
150 if (el) setTimeout(() => el.classList.add('-read'), 2000)
151 }))
152 }
153}
154
155function comparer (a, b) {
156 return get(resolve(a), 'key') === get(resolve(b), 'key')
157}
158

Built with git-ssb-web