git ssb

16+

Dominic / patchbay



Tree: 53d03c145268665aa93e60777b4153a8a0c4dca5

Files: 53d03c145268665aa93e60777b4153a8a0c4dca5 / message / html / compose.js

5627 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 fileInput = api.blob.html.input(file => {
69 files.push(file)
70 filesById[file.link] = file
71
72 var embed = file.type.match(/^image/) ? '!' : ''
73 var spacer = embed ? '\n' : ' '
74 var insertLink = spacer + embed + '[' + file.name + ']' + '(' + file.link + ')' + spacer
75
76 var pos = textArea.selectionStart
77 textArea.value = textArea.value.slice(0, pos) + insertLink + textArea.value.slice(pos)
78
79 console.log('added:', file)
80 })
81
82 fileInput.onclick = () => hasContent.set(true)
83
84 var publishBtn = h('button', { 'ev-click': publish }, 'Publish')
85
86 var actions = h('section.actions', [
87 fileInput,
88 publishBtn
89 ])
90
91 var composer = h('Compose', {
92 classList: when(expanded, '-expanded', '-contracted')
93 }, [
94 channelInput,
95 textArea,
96 actions
97 ])
98
99 addSuggest(channelInput, (inputText, cb) => {
100 if (inputText[0] === '#') {
101 cb(null, getChannelSuggestions(inputText.slice(1)))
102 }
103 }, {cls: 'SuggestBox'})
104 channelInput.addEventListener('suggestselect', ev => {
105 channelInput.value = ev.detail.id // HACK : this over-rides the markdown value
106 })
107
108 addSuggest(textArea, (inputText, cb) => {
109 if (inputText[0] === '@') {
110 cb(null, getProfileSuggestions(inputText.slice(1)))
111 } else if (inputText[0] === '#') {
112 cb(null, getChannelSuggestions(inputText.slice(1)))
113 } else if (inputText[0] === ':') {
114 // suggest emojis
115 var word = inputText.slice(1)
116 if (word[word.length - 1] === ':') {
117 word = word.slice(0, -1)
118 }
119 // TODO: when no emoji typed, list some default ones
120 cb(null, api.emoji.sync.names().filter(function (name) {
121 return name.slice(0, word.length) === word
122 }).slice(0, 100).map(function (emoji) {
123 return {
124 image: api.emoji.sync.url(emoji),
125 title: emoji,
126 subtitle: emoji,
127 value: ':' + emoji + ':'
128 }
129 }))
130 }
131 }, {cls: 'SuggestBox'})
132
133 return composer
134
135 // scoped
136
137 function publish () {
138 publishBtn.disabled = true
139
140 const channel = channelInput.value.startsWith('#')
141 ? channelInput.value.substr(1).trim()
142 : channelInput.value.trim()
143 const mentions = ssbMentions(textArea.value).map(mention => {
144 // merge markdown-detected mention with file info
145 var file = filesById[mention.link]
146 if (file) {
147 if (file.type) mention.type = file.type
148 if (file.size) mention.size = file.size
149 }
150 return mention
151 })
152
153 meta = extend(resolve(meta), {
154 text: textArea.value,
155 channel,
156 mentions
157 })
158
159 if (!channel) delete meta.channel
160 if (!mentions.length) delete meta.mentions
161 if (meta.recps && meta.recps.length === 0) delete meta.recps
162
163 try {
164 if (typeof prepublish === 'function') {
165 meta = prepublish(meta)
166 }
167 } catch (err) {
168 publishBtn.disabled = false
169 if (cb) cb(err)
170 else throw err
171 }
172
173 return api.message.html.confirm(meta, done)
174
175 function done (err, msg) {
176 publishBtn.disabled = false
177 if (err) throw err
178 else if (msg) textArea.value = ''
179 if (cb) cb(err, msg)
180 }
181 }
182 }
183}
184
185

Built with git-ssb-web