git ssb

16+

Dominic / patchbay



Tree: 06d5f9edff08206becab379b2f89c78d0d066da5

Files: 06d5f9edff08206becab379b2f89c78d0d066da5 / message / html / compose.js

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

Built with git-ssb-web