git ssb

10+

Matt McKegg / patchwork



Tree: 2c560265d95bf0384c48f982264559ea3cb3e3a4

Files: 2c560265d95bf0384c48f982264559ea3cb3e3a4 / modules / message / html / compose.js

6802 bytesRaw
1var h = require('mutant/h')
2var when = require('mutant/when')
3var send = require('mutant/send')
4var resolve = require('mutant/resolve')
5var Value = require('mutant/value')
6var computed = require('mutant/computed')
7var nest = require('depnest')
8var mentions = require('ssb-mentions')
9var extend = require('xtend')
10var addSuggest = require('suggest-box')
11var emoji = require('emojilib')
12var ref = require('ssb-ref')
13
14exports.needs = nest({
15 'blob.html.input': 'first',
16 'profile.async.suggest': 'first',
17 'channel.async.suggest': 'first',
18 'message.async.publish': 'first',
19 'emoji.sync.names': 'first',
20 'emoji.sync.url': 'first',
21 'intl.sync.i18n': 'first'
22})
23
24exports.gives = nest('message.html.compose')
25
26exports.create = function (api) {
27 const i18n = api.intl.sync.i18n
28 return nest('message.html.compose', function ({shrink = true, isPrivate, participants, meta, hooks, prepublish, placeholder = 'Write a message'}, cb) {
29 var files = []
30 var filesById = {}
31 var focused = Value(false)
32 var hasContent = Value(false)
33 var publishing = Value(false)
34 var getProfileSuggestions = api.profile.async.suggest()
35 var getChannelSuggestions = api.channel.async.suggest()
36
37 var blurTimeout = null
38
39 var expanded = computed([shrink, focused, hasContent], (shrink, focused, hasContent) => {
40 if (!shrink || hasContent) {
41 return true
42 } else {
43 return focused
44 }
45 })
46
47 var textArea = h('textarea', {
48 'ev-input': function () {
49 hasContent.set(!!textArea.value)
50 },
51 'ev-blur': () => {
52 clearTimeout(blurTimeout)
53 blurTimeout = setTimeout(() => focused.set(false), 200)
54 },
55 'ev-focus': send(focused.set, true),
56 disabled: publishing,
57 placeholder
58 })
59
60 var warningMessage = Value(null)
61 var warning = h('section.warning',
62 { className: when(warningMessage, '-open', '-closed') },
63 [
64 h('div.warning', warningMessage),
65 h('div.close', { 'ev-click': () => warningMessage.set(null) }, 'x')
66 ]
67 )
68 var fileInput = api.blob.html.input(file => {
69 const megabytes = file.size / 1024 / 1024
70 if (megabytes >= 5) {
71 const rounded = Math.floor(megabytes * 100) / 100
72 warningMessage.set([
73 h('i.fa.fa-exclamation-triangle'),
74 h('strong', file.name),
75 ` is ${rounded}MB - the current limit is 5MB`
76 ])
77 return
78 }
79
80 files.push(file)
81
82 var parsed = ref.parseLink(file.link)
83 filesById[parsed.link] = file
84
85 var embed = isEmbeddable(file.type) ? '!' : ''
86 var pos = textArea.selectionStart
87 var before = textArea.value.slice(0, pos)
88 var after = textArea.value.slice(pos)
89
90 var spacer = embed ? '\n' : ' '
91 if (before && !before.endsWith(spacer)) before += spacer
92 if (!after.startsWith(spacer)) after = spacer + after
93
94 var embedPrefix = getEmbedPrefix(file.type)
95
96 textArea.value = `${before}${embed}[${embedPrefix}${file.name}](${file.link})${after}`
97 console.log('added:', file)
98 }, {
99 private: isPrivate
100 })
101
102 fileInput.onclick = function () {
103 hasContent.set(true)
104 }
105
106 var publishBtn = h('button', {
107 'ev-click': publish,
108 classList: [
109 when(isPrivate, '-private')
110 ],
111 disabled: publishing
112 }, when(publishing,
113 i18n('Publishing...'),
114 when(isPrivate, i18n('Preview & Publish Privately'), i18n('Preview & Publish'))
115 ))
116
117 var actions = h('section.actions', [
118 fileInput,
119 publishBtn
120 ])
121
122 var composer = h('Compose', {
123 hooks,
124 classList: [
125 when(expanded, '-expanded', '-contracted')
126 ]
127 }, [
128 textArea,
129 warning,
130 actions
131 ])
132
133 composer.focus = function () {
134 textArea.focus()
135 }
136
137 composer.setText = function (value) {
138 textArea.value = value
139 hasContent.set(!!textArea.value)
140 }
141
142 addSuggest(textArea, (inputText, cb) => {
143 if (inputText[0] === '@') {
144 cb(null, getProfileSuggestions(inputText.slice(1), resolve(participants)))
145 } else if (inputText[0] === '#') {
146 cb(null, getChannelSuggestions(inputText.slice(1)))
147 } else if (inputText[0] === ':') {
148 // suggest emojis
149 var word = inputText.slice(1)
150 if (word[word.length - 1] === ':') {
151 word = word.slice(0, -1)
152 }
153 cb(null, suggestEmoji(word).slice(0, 100).map(function (emoji) {
154 return {
155 image: api.emoji.sync.url(emoji),
156 title: emoji,
157 subtitle: emoji,
158 value: ':' + emoji + ':'
159 }
160 }))
161 }
162 }, {cls: 'SuggestBox'})
163
164 return composer
165
166 // scoped
167
168 function publish () {
169 if (!textArea.value) {
170 return
171 }
172 publishing.set(true)
173
174 var content = extend(resolve(meta), {
175 text: textArea.value,
176 mentions: mentions(textArea.value).map(mention => {
177 // merge markdown-detected mention with file info
178 var file = filesById[mention.link]
179 if (file) {
180 if (file.type) mention.type = file.type
181 if (file.size) mention.size = file.size
182 }
183 return mention
184 })
185 })
186
187 try {
188 if (typeof prepublish === 'function') {
189 content = prepublish(content)
190 }
191 } catch (err) {
192 return done(err)
193 }
194
195 return api.message.async.publish(content, done)
196
197 function done (err, msg) {
198 publishing.set(false)
199 if (err) {
200 if (cb) cb(err)
201 else {
202 showDialog({
203 type: 'error',
204 title: i18n('Error'),
205 buttons: [i18n('OK')],
206 message: i18n('An error occured while publishing your message.'),
207 detail: err.message
208 })
209 }
210 } else {
211 if (msg) textArea.value = ''
212 if (cb) cb(null, msg)
213 }
214 }
215 }
216 })
217
218 function suggestEmoji (prefix) {
219 var availableEmoji = api.emoji.sync.names()
220 return emoji.ordered.filter(key => {
221 if (!availableEmoji.includes(key)) return false
222 return key.startsWith(prefix) || key.includes('_' + prefix) || emoji.lib[key].keywords.some(word => word.startsWith(prefix) || word.startsWith(':' + prefix))
223 })
224 }
225}
226
227function showDialog (opts) {
228 var electron = require('electron')
229 electron.remote.dialog.showMessageBox(electron.remote.getCurrentWindow(), opts)
230}
231
232function isEmbeddable (type) {
233 return type.startsWith('image/') || type.startsWith('audio/') || type.startsWith('video/')
234}
235
236function getEmbedPrefix (type) {
237 if (typeof type === 'string') {
238 if (type.startsWith('audio/')) return 'audio:'
239 if (type.startsWith('video/')) return 'video:'
240 }
241 return ''
242}
243

Built with git-ssb-web