git ssb

16+

Dominic / patchbay



Tree: 7c8d8e05082b5e9832a5dd9f0fab079a44a3e9e5

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

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

Built with git-ssb-web