git ssb

16+

Dominic / patchbay



Tree: 7a1861362de764504e02614b8c8bc5837fdc089d

Files: 7a1861362de764504e02614b8c8bc5837fdc089d / message / html / compose.js

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

Built with git-ssb-web