git ssb

16+

Dominic / patchbay



Tree: 69a136809ca4b726c56731b35fb8c951b3d4260c

Files: 69a136809ca4b726c56731b35fb8c951b3d4260c / message / html / compose.js

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

Built with git-ssb-web