git ssb

16+

Dominic / patchbay



Tree: 645ea1809f02b239e03e301a0c96fe35bb731f01

Files: 645ea1809f02b239e03e301a0c96fe35bb731f01 / message / html / compose.js

6340 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 'blob.html.input': 'first',
12 'channel.async.suggest': 'first',
13 'emoji.sync': {
14 names: 'first',
15 url: 'first'
16 },
17 'message.html.confirm': 'first'
18})
19
20exports.create = function (api) {
21 return nest({ 'message.html.compose': compose })
22
23 function compose ({ shrink = true, meta, prepublish, placeholder = 'Write a message' }, cb) {
24 var files = []
25 var filesById = {}
26 var channelInputFocused = Value(false)
27 var textAreaFocused = Value(false)
28 var focused = computed([channelInputFocused, textAreaFocused], (a, b) => a || b)
29 var hasContent = Value(false)
30 var getProfileSuggestions = api.about.async.suggest()
31 var getChannelSuggestions = api.channel.async.suggest()
32
33 var blurTimeout = null
34
35 var expanded = computed([shrink, focused, hasContent], (shrink, focused, hasContent) => {
36 if (!shrink || hasContent) return true
37
38 return focused
39 })
40
41 var channelInput = h('input.channel', {
42 'ev-input': () => hasContent.set(!!channelInput.value),
43 'ev-keyup': ev => {
44 ev.target.value = ev.target.value.replace(/^#*([\w@%&])/, '#$1')
45 },
46 'ev-blur': () => {
47 clearTimeout(blurTimeout)
48 blurTimeout = setTimeout(() => channelInputFocused.set(false), 200)
49 },
50 'ev-focus': send(channelInputFocused.set, true),
51 placeholder: '#channel (optional)',
52 value: computed(meta.channel, ch => ch ? '#' + ch : null),
53 disabled: when(meta.channel, true),
54 title: when(meta.channel, 'Reply is in same channel as original message')
55 })
56
57 var textArea = h('textarea', {
58 'ev-input': () => hasContent.set(!!textArea.value),
59 'ev-blur': () => {
60 clearTimeout(blurTimeout)
61 blurTimeout = setTimeout(() => textAreaFocused.set(false), 200)
62 },
63 'ev-focus': send(textAreaFocused.set, true),
64 placeholder
65 })
66 textArea.publish = publish // TODO: fix - clunky api for the keyboard shortcut to target
67
68 var warningMessage = Value(null)
69 var warning = h('section.warning',
70 { className: when(warningMessage, '-open', '-closed') },
71 [
72 h('div.warning', warningMessage),
73 h('div.close', { 'ev-click': () => warningMessage.set(null) }, 'x')
74 ]
75 )
76 var fileInput = api.blob.html.input(file => {
77
78 const megabytes = file.size / 1024 / 1024
79 if (megabytes >= 5) {
80 const rounded = Math.floor(megabytes*100)/100
81 warningMessage.set([
82 h('i.fa.fa-exclamation-triangle'),
83 h('strong', file.name),
84 ` is ${rounded}MB - the current limit is 5MB`
85 ])
86 return
87 }
88
89 files.push(file)
90 filesById[file.link] = file
91
92 const pos = textArea.selectionStart
93 const embed = file.type.match(/^image/) ? '!' : ''
94 const spacer = embed ? '\n' : ' '
95 const insertLink = spacer + embed + '[' + file.name + ']' + '(' + file.link + ')' + spacer
96
97 textArea.value = textArea.value.slice(0, pos) + insertLink + textArea.value.slice(pos)
98
99 console.log('added:', file)
100 })
101
102 fileInput.onclick = () => hasContent.set(true)
103
104 var publishBtn = h('button', { 'ev-click': publish }, 'Publish')
105
106 var actions = h('section.actions', [
107 fileInput,
108 publishBtn
109 ])
110
111 var composer = h('Compose', {
112 classList: when(expanded, '-expanded', '-contracted')
113 }, [
114 channelInput,
115 textArea,
116 warning,
117 actions
118 ])
119
120 addSuggest(channelInput, (inputText, cb) => {
121 if (inputText[0] === '#') {
122 cb(null, getChannelSuggestions(inputText.slice(1)))
123 }
124 }, {cls: 'SuggestBox'})
125 channelInput.addEventListener('suggestselect', ev => {
126 channelInput.value = ev.detail.id // HACK : this over-rides the markdown value
127 })
128
129 addSuggest(textArea, (inputText, cb) => {
130 if (inputText[0] === '@') {
131 cb(null, getProfileSuggestions(inputText.slice(1)))
132 } else if (inputText[0] === '#') {
133 cb(null, getChannelSuggestions(inputText.slice(1)))
134 } else if (inputText[0] === ':') {
135 // suggest emojis
136 var word = inputText.slice(1)
137 if (word[word.length - 1] === ':') {
138 word = word.slice(0, -1)
139 }
140 word = word.toLowerCase()
141 // TODO: when no emoji typed, list some default ones
142
143 const suggestions = api.emoji.sync.names()
144 .filter(name => ~name.indexOf(word))
145 .slice(0, 100).map(emoji => {
146 return {
147 image: api.emoji.sync.url(emoji),
148 title: emoji,
149 subtitle: emoji,
150 value: ':' + emoji + ':'
151 }
152 })
153
154 cb(null, suggestions)
155 }
156 }, {cls: 'SuggestBox'})
157
158 return composer
159
160 // scoped
161
162 function publish () {
163 publishBtn.disabled = true
164
165 const channel = channelInput.value.startsWith('#')
166 ? channelInput.value.substr(1).trim()
167 : channelInput.value.trim()
168 const mentions = ssbMentions(textArea.value).map(mention => {
169 // merge markdown-detected mention with file info
170 var file = filesById[mention.link]
171 if (file) {
172 if (file.type) mention.type = file.type
173 if (file.size) mention.size = file.size
174 }
175 return mention
176 })
177
178 var content = extend(resolve(meta), {
179 text: textArea.value,
180 channel,
181 mentions
182 })
183
184 if (!channel) delete content.channel
185 if (!mentions.length) delete content.mentions
186 if (content.recps && content.recps.length === 0) delete content.recps
187
188 try {
189 if (typeof prepublish === 'function') {
190 content = prepublish(content)
191 }
192 } catch (err) {
193 publishBtn.disabled = false
194 if (cb) cb(err)
195 else throw err
196 }
197
198 return api.message.html.confirm(content, done)
199
200 function done (err, msg) {
201 publishBtn.disabled = false
202 if (err) throw err
203 else if (msg) textArea.value = ''
204 if (cb) cb(err, msg)
205 }
206 }
207 }
208}
209

Built with git-ssb-web