git ssb

2+

mixmix / ticktack



Tree: 7458998a2a59e0edaf11b6ce83867a72f96577d6

Files: 7458998a2a59e0edaf11b6ce83867a72f96577d6 / message / html / compose.js

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

Built with git-ssb-web