git ssb

16+

Dominic / patchbay



Tree: 557f4b880701243b85fff447aa30939faf97a997

Files: 557f4b880701243b85fff447aa30939faf97a997 / message / html / compose.js

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

Built with git-ssb-web