git ssb

1+

Daan Patchwork / patchwork



Tree: 455af00799601cb57c6ea54edad5e85c516b799f

Files: 455af00799601cb57c6ea54edad5e85c516b799f / lib / depject / message / html / compose.js

9167 bytesRaw
1const h = require('mutant/h')
2const when = require('mutant/when')
3const resolve = require('mutant/resolve')
4const Value = require('mutant/value')
5const computed = require('mutant/computed')
6const nest = require('depnest')
7const mentions = require('ssb-mentions')
8const extend = require('xtend')
9const ref = require('ssb-ref')
10const blobFiles = require('ssb-blob-files')
11
12exports.needs = nest({
13 'blob.html.input': 'first',
14 'suggest.hook': 'first',
15
16 'message.async.publish': 'first',
17 'sbot.obs.connection': 'first',
18 'intl.sync.i18n': 'first'
19})
20
21exports.gives = nest('message.html.compose')
22
23exports.create = function (api) {
24 const i18n = api.intl.sync.i18n
25 return nest('message.html.compose', function ({ shrink = true, isPrivate, participants, meta, hooks, prepublish, placeholder = 'Write a message', draftKey }, cb) {
26 const files = []
27 const filesById = {}
28 const focused = Value(false)
29 const hasContent = Value(false)
30 const publishing = Value(false)
31
32 let blurTimeout = null
33 let saveTimer = null
34
35 const expanded = computed([shrink, focused, hasContent], (shrink, focused, hasContent) => {
36 if (!shrink || hasContent) {
37 return true
38 } else {
39 return focused
40 }
41 })
42
43 const textArea = h('textarea', {
44 hooks: [api.suggest.hook({ participants })],
45 'ev-dragover': onDragOver,
46 'ev-drop': onDrop,
47 'ev-input': function () {
48 refreshHasContent()
49 queueSave()
50 },
51 'ev-blur': () => {
52 clearTimeout(blurTimeout)
53 blurTimeout = setTimeout(() => focused.set(false), 200)
54 },
55 'ev-focus': () => {
56 clearTimeout(blurTimeout)
57 focused.set(true)
58 },
59 'ev-paste': ev => {
60 const files = ev.clipboardData && ev.clipboardData.files
61 if (!files || !files.length) return
62 attachFiles(files)
63 },
64 'ev-keydown': ev => {
65 if (ev.key === 'Enter' && (ev.ctrlKey || ev.metaKey)) {
66 publish()
67 ev.preventDefault()
68 }
69 },
70 disabled: publishing,
71 placeholder
72 })
73
74 const contentWarningInput = h('input.contentWarning', {
75 placeholder: 'Write a content warning (optional)',
76 'ev-input': function () {
77 refreshHasContent()
78 queueSave()
79 },
80 'ev-blur': () => {
81 clearTimeout(blurTimeout)
82 blurTimeout = setTimeout(() => focused.set(false), 200)
83 },
84 'ev-focus': () => {
85 clearTimeout(blurTimeout)
86 focused.set(true)
87 }
88 })
89
90 if (draftKey) {
91 const draft = window.localStorage[`patchwork.drafts.${draftKey}`]
92 if (draft) {
93 textArea.value = draft
94 }
95 const draftCW = window.localStorage[`patchwork.drafts.contentWarning.${draftKey}`]
96 if (draftCW) {
97 contentWarningInput.value = draftCW
98 }
99 refreshHasContent()
100 }
101
102 const warningMessage = Value(null)
103 const warning = h('section.warning',
104 { className: when(warningMessage, '-open', '-closed') },
105 [
106 h('div.warning', warningMessage),
107 h('div.close', { 'ev-click': () => warningMessage.set(null) }, 'x')
108 ]
109 )
110 const fileInput = api.blob.html.input(afterAttach, {
111 private: isPrivate,
112 multiple: true
113 })
114
115 fileInput.onclick = function () {
116 hasContent.set(true)
117 }
118
119 const clearButton = h('button -clear', {
120 'ev-click': clear
121 }, [
122 i18n('Clear Draft')
123 ])
124
125 const publishBtn = h('button', {
126 'ev-click': publish,
127 classList: [
128 when(isPrivate, '-private')
129 ],
130 disabled: publishing
131 }, when(publishing,
132 i18n('Publishing...'),
133 when(isPrivate, i18n('Preview & Publish Privately'), i18n('Preview & Publish'))
134 ))
135
136 const actions = h('section.actions', [
137 fileInput,
138 contentWarningInput,
139 h('div', [
140 when(hasContent, clearButton),
141 publishBtn
142 ])
143 ])
144
145 const composer = h('Compose', {
146 hooks,
147 classList: [
148 when(expanded, '-expanded', '-contracted')
149 ]
150 }, [
151 textArea,
152 warning,
153 actions
154 ])
155
156 composer.focus = function () {
157 textArea.focus()
158 }
159
160 composer.setText = function (value) {
161 textArea.value = value
162 refreshHasContent()
163 }
164
165 return composer
166
167 // scoped
168
169 function clear () {
170 if (!window.confirm(i18n('Are you certain you want to clear your draft?'))) {
171 return
172 }
173 textArea.value = ''
174 contentWarningInput.value = ''
175 refreshHasContent()
176 save()
177 }
178
179 function refreshHasContent () {
180 hasContent.set(!!textArea.value || !!contentWarningInput.value)
181 }
182
183 function queueSave () {
184 saveTimer = setTimeout(save, 1000)
185 }
186
187 function save () {
188 clearTimeout(saveTimer)
189 if (draftKey) {
190 if (!textArea.value) {
191 delete window.localStorage[`patchwork.drafts.${draftKey}`]
192 } else {
193 window.localStorage[`patchwork.drafts.${draftKey}`] = textArea.value
194 }
195 if (!contentWarningInput.value) {
196 delete window.localStorage[`patchwork.drafts.contentWarning.${draftKey}`]
197 } else {
198 window.localStorage[`patchwork.drafts.contentWarning.${draftKey}`] = contentWarningInput.value
199 }
200 }
201 }
202
203 function onDragOver (ev) {
204 ev.dataTransfer.dropEffect = 'copy'
205 ev.preventDefault()
206 return false
207 }
208
209 function onDrop (ev) {
210 ev.preventDefault()
211
212 const files = ev.dataTransfer && ev.dataTransfer.files
213 if (!files || !files.length) return
214
215 ev.dataTransfer.dropEffect = 'copy'
216 attachFiles(files)
217 return false
218 }
219
220 function attachFiles (files) {
221 blobFiles(files, api.sbot.obs.connection, {
222 stripExif: true,
223 isPrivate: resolve(isPrivate)
224 }, afterAttach)
225 }
226
227 function afterAttach (err, file) {
228 if (err) {
229 if (err instanceof blobFiles.MaxSizeError) {
230 warningMessage.set([
231 // TODO: handle localised error messages (https://github.com/ssbc/ssb-blob-files/issues/3)
232 '⚠️ ', i18n('{{name}} ({{size}}) is larger than the allowed limit of {{max_size}}', {
233 name: err.fileName,
234 size: humanSize(err.fileSize),
235 max_size: humanSize(err.maxFileSize)
236 })
237 ])
238 }
239 return
240 }
241
242 files.push(file)
243
244 const parsed = ref.parseLink(file.link)
245 filesById[parsed.link] = file
246
247 const embed = isEmbeddable(file.type) ? '!' : ''
248 const pos = textArea.selectionStart
249 let before = textArea.value.slice(0, pos)
250 let after = textArea.value.slice(pos)
251
252 const spacer = embed ? '\n' : ' '
253 if (before && !before.endsWith(spacer)) before += spacer
254 if (!after.startsWith(spacer)) after = spacer + after
255
256 const embedPrefix = getEmbedPrefix(file.type)
257
258 textArea.value = `${before}${embed}[${embedPrefix}${file.name}](${file.link})${after}`
259 console.log('added:', file)
260 }
261
262 function publish () {
263 if (!textArea.value) {
264 return
265 }
266 publishing.set(true)
267
268 let content = extend(resolve(meta), {
269 text: textArea.value,
270 mentions: mentions(textArea.value).map(mention => {
271 // merge markdown-detected mention with file info
272 const file = filesById[mention.link]
273 if (file) {
274 if (file.type) mention.type = file.type
275 if (file.size) mention.size = file.size
276 }
277 return mention
278 })
279 })
280
281 const cw = contentWarningInput.value.trim()
282 if (cw.length > 0) {
283 content.contentWarning = cw
284 }
285
286 try {
287 if (typeof prepublish === 'function') {
288 content = prepublish(content)
289 }
290 } catch (err) {
291 return done(err)
292 }
293
294 return api.message.async.publish(content, done)
295
296 function done (err, msg) {
297 publishing.set(false)
298 if (err) {
299 if (cb) cb(err)
300 else {
301 showDialog({
302 type: 'error',
303 title: i18n('Error'),
304 buttons: [i18n('OK')],
305 message: i18n('An error occurred while publishing your message.'),
306 detail: err.message
307 })
308 }
309 } else {
310 if (msg) {
311 textArea.value = ''
312 contentWarningInput.value = ''
313 refreshHasContent()
314 save()
315 } else {
316 textArea.focus()
317 }
318 if (cb) cb(null, msg)
319 }
320 }
321 }
322 })
323}
324
325function showDialog (opts) {
326 const electron = require('electron')
327 electron.remote.dialog.showMessageBox(electron.remote.getCurrentWindow(), opts)
328}
329
330function isEmbeddable (type) {
331 return type.startsWith('image/') || type.startsWith('audio/') || type.startsWith('video/')
332}
333
334function getEmbedPrefix (type) {
335 if (typeof type === 'string') {
336 if (type.startsWith('audio/')) return 'audio:'
337 if (type.startsWith('video/')) return 'video:'
338 }
339 return ''
340}
341
342function humanSize (size) {
343 return (Math.ceil(size / (1024 * 1024) * 10) / 10) + ' MB'
344}
345

Built with git-ssb-web