git ssb

16+

Dominic / patchbay



Tree: 488cf0abd647acf310d8d60be778063b0adf8db5

Files: 488cf0abd647acf310d8d60be778063b0adf8db5 / message / html / compose.js

7698 bytesRaw
1const { h, when, send, resolve, Value, Array: MutantArray, computed } = require('mutant')
2const nest = require('depnest')
3const ssbMentions = require('ssb-mentions')
4const extend = require('xtend')
5const addSuggest = require('suggest-box')
6const blobFiles = require('ssb-blob-files')
7
8exports.gives = nest('message.html.compose')
9
10exports.needs = nest({
11 'about.async.suggest': 'first',
12 'channel.async.suggest': 'first',
13 'emoji.async.suggest': 'first',
14 'meme.async.suggest': 'first',
15 'message.html.confirm': 'first',
16 'drafts.sync.get': 'first',
17 'drafts.sync.set': 'first',
18 'drafts.sync.remove': 'first',
19 'settings.obs.get': 'first',
20 'sbot.obs.connection': 'first'
21})
22
23exports.create = function (api) {
24 return nest({ 'message.html.compose': compose })
25
26 function compose (options, cb) {
27 const {
28 meta,
29 location,
30 feedIdsInThread = [],
31 prepublish,
32 placeholder = 'Write a message',
33 shrink = true
34 } = options
35
36 if (typeof resolve(meta) !== 'object') throw new Error('Compose needs meta data about what sort of message composer you are making')
37 if (!location) throw new Error('Compose expects a unique location so it can save drafts of messages')
38
39 var files = []
40 var filesById = {}
41 var channelInputFocused = Value(false)
42 var textAreaFocused = Value(false)
43 var focused = computed([channelInputFocused, textAreaFocused], (a, b) => a || b)
44 var hasContent = Value(false)
45
46 var blurTimeout = null
47
48 var expanded = computed([shrink, focused, hasContent], (shrink, focused, hasContent) => {
49 if (!shrink || hasContent) return true
50
51 return focused
52 })
53
54 var channelInput = h('input.channel', {
55 'ev-input': () => hasContent.set(!!channelInput.value),
56 'ev-keyup': ev => {
57 ev.target.value = ev.target.value.replace(/^#*([\w@%&])/, '#$1')
58 },
59 'ev-blur': () => {
60 clearTimeout(blurTimeout)
61 blurTimeout = setTimeout(() => channelInputFocused.set(false), 200)
62 },
63 'ev-focus': send(channelInputFocused.set, true),
64 placeholder: '#channel (optional)',
65 value: computed(meta.channel, ch => ch ? '#' + ch : null),
66 disabled: meta.channel ? true : undefined,
67 title: meta.channel ? 'Reply is in same channel as original message' : undefined
68 })
69
70 var draftPerstTimeout = null
71 var draftLocation = location
72 var textArea = h('textarea', {
73 'ev-input': () => {
74 hasContent.set(!!textArea.value)
75 clearTimeout(draftPerstTimeout)
76 draftPerstTimeout = setTimeout(() => {
77 api.drafts.sync.set(draftLocation, textArea.value)
78 }, 200)
79 },
80 'ev-blur': () => {
81 clearTimeout(blurTimeout)
82 blurTimeout = setTimeout(() => textAreaFocused.set(false), 200)
83 },
84 'ev-focus': send(textAreaFocused.set, true),
85 placeholder
86 })
87 textArea.publish = publish // TODO: fix - clunky api for the keyboard shortcut to target
88
89 // load draft
90 let draft = api.drafts.sync.get(draftLocation)
91 if (typeof draft === 'string') {
92 textArea.value = draft
93 hasContent.set(true)
94 }
95
96 var isPrivate = location.page === 'private' ||
97 (location.key && !location.value) ||
98 (location.value && location.value.private)
99
100 var warningMessages = MutantArray([])
101 var warning = computed(warningMessages, msgs => {
102 if (!msgs.length) return
103
104 return h('section.warnings', msgs.map((m, i) => {
105 return h('div.warning', [
106 h('i.fa.fa-exclamation-triangle'),
107 h('div.message', m),
108 h('i.fa.fa-times', { 'ev-click': () => warningMessages.deleteAt(i) })
109 ])
110 }))
111 })
112
113 var fileInput = h('input', {
114 type: 'file',
115 // accept,
116 attributes: { multiple: true },
117 'ev-click': () => hasContent.set(true),
118 'ev-change': (ev) => {
119 warningMessages.set([])
120
121 const files = ev.target.files
122 const opts = {
123 stripExif: api.settings.obs.get('patchbay.removeExif', true),
124 isPrivate
125 }
126 blobFiles(files, api.sbot.obs.connection, opts, afterBlobed)
127 }
128 })
129 function afterBlobed (err, result) {
130 if (err) {
131 console.error(err)
132 warningMessages.push(err.message)
133 return
134 }
135
136 files.push(result)
137 filesById[result.link] = result
138
139 const pos = textArea.selectionStart
140 const embed = result.type.match(/^image/) ? '!' : ''
141 const spacer = embed ? '\n' : ' '
142 const insertLink = spacer + embed + '[' + result.name + ']' + '(' + result.link + ')' + spacer
143
144 textArea.value = textArea.value.slice(0, pos) + insertLink + textArea.value.slice(pos)
145
146 console.log('added:', result)
147 }
148
149 var publishBtn = h('button', { 'ev-click': publish }, isPrivate ? 'Reply' : 'Publish')
150
151 var actions = h('section.actions', [
152 fileInput,
153 publishBtn
154 ])
155
156 var composer = h('Compose', {
157 classList: when(expanded, '-expanded', '-contracted')
158 }, [
159 channelInput,
160 textArea,
161 warning,
162 actions
163 ])
164
165 composer.addQuote = function (data) {
166 try {
167 if (typeof data.content.text === 'string') {
168 var text = data.content.text
169 textArea.value += '> ' + text.replace(/\r\n|\r|\n/g, '\n> ') + '\r\n\n'
170 hasContent.set(!!textArea.value)
171 }
172 } catch (err) {
173 // object not have text or content
174 }
175 }
176
177 if (location.action === 'quote') { composer.addQuote(location.value) }
178
179 addSuggest(channelInput, (inputText, cb) => {
180 if (inputText[0] === '#') {
181 api.channel.async.suggest(inputText.slice(1), cb)
182 }
183 }, { cls: 'PatchSuggest' })
184 channelInput.addEventListener('suggestselect', ev => {
185 channelInput.value = ev.detail.id // HACK : this over-rides the markdown value
186 })
187
188 addSuggest(textArea, (inputText, cb) => {
189 const char = inputText[0]
190 const wordFragment = inputText.slice(1)
191
192 if (char === '@') api.about.async.suggest(wordFragment, feedIdsInThread, cb)
193 if (char === '#') api.channel.async.suggest(wordFragment, cb)
194 if (char === ':') api.emoji.async.suggest(wordFragment, cb)
195 if (char === '&') api.meme.async.suggest(wordFragment, cb)
196 }, { cls: 'PatchSuggest' })
197
198 return composer
199
200 // scoped
201
202 function publish () {
203 publishBtn.disabled = true
204
205 const channel = channelInput.value.startsWith('#')
206 ? channelInput.value.substr(1).trim()
207 : channelInput.value.trim()
208 const mentions = ssbMentions(textArea.value).map(mention => {
209 // merge markdown-detected mention with file info
210 var file = filesById[mention.link]
211 if (file) {
212 if (file.type) mention.type = file.type
213 if (file.size) mention.size = file.size
214 }
215 return mention
216 })
217
218 var content = extend(resolve(meta), {
219 text: textArea.value,
220 channel,
221 mentions
222 })
223
224 if (!channel) delete content.channel
225 if (!mentions.length) delete content.mentions
226 if (content.recps && content.recps.length === 0) delete content.recps
227
228 try {
229 if (typeof prepublish === 'function') {
230 content = prepublish(content)
231 }
232 } catch (err) {
233 publishBtn.disabled = false
234 if (cb) cb(err)
235 else throw err
236 }
237
238 return api.message.html.confirm(content, done)
239
240 function done (err, msg) {
241 publishBtn.disabled = false
242 if (err) throw err
243 else if (msg) {
244 textArea.value = ''
245 api.drafts.sync.remove(draftLocation)
246 }
247 if (cb) cb(err, msg)
248 }
249 }
250 }
251}
252

Built with git-ssb-web