git ssb

16+

Dominic / patchbay



Tree: 822a744863931f6f173393bae8f44e31debafb99

Files: 822a744863931f6f173393bae8f44e31debafb99 / message / html / compose.js

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

Built with git-ssb-web