git ssb

16+

Dominic / patchbay



Tree: 1e573fd55bd3d16562c5727c0fbc36d3b65328ee

Files: 1e573fd55bd3d16562c5727c0fbc36d3b65328ee / message / html / compose.js

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

Built with git-ssb-web