git ssb

2+

mixmix / ticktack



Tree: fdd450031b20bc486a2acf1b6d9d691da4ce0cb2

Files: fdd450031b20bc486a2acf1b6d9d691da4ce0cb2 / message / html / compose.js

5700 bytesRaw
1const nest = require('depnest')
2const { h, when, send, resolve, Value, computed, map } = require('mutant')
3const assign = require('lodash/assign')
4const isEmpty = require('lodash/isEmpty')
5const ssbMentions = require('ssb-mentions')
6const addSuggest = require('suggest-box')
7
8exports.gives = nest('message.html.compose')
9
10exports.needs = nest({
11 'about.async.suggest': 'first',
12 'blob.html.input': 'first',
13 'channel.async.suggest': 'first',
14 'emoji.async.suggest': 'first',
15 'emoji.sync.names': 'first',
16 'emoji.sync.url': 'first',
17 'message.async.publish': 'first',
18 'message.html.markdown': 'first',
19 // 'message.html.confirm': 'first'
20 'translations.sync.strings': 'first'
21})
22
23exports.create = function (api) {
24 return nest('message.html.compose', compose)
25
26 function compose (options, cb) {
27 var {
28 meta, // required
29 feedIdsInThread = [],
30 placeholder,
31 shrink = true,
32 canAttach = true, canPreview = true,
33 prepublish
34 } = options
35
36 const strings = api.translations.sync.strings()
37 const getUserSuggestions = api.about.async.suggest()
38 const getChannelSuggestions = api.channel.async.suggest()
39 const getEmojiSuggestions = api.emoji.async.suggest()
40
41 placeholder = placeholder || strings.writeMessage
42
43 var files = []
44 var filesById = {}
45 var textAreaFocused = Value(false)
46 var focused = textAreaFocused
47 var hasContent = Value(false)
48
49 var blurTimeout = null
50
51 var expanded = computed([shrink, focused, hasContent], (shrink, focused, hasContent) => {
52 if (!shrink || hasContent) return true
53
54 return focused
55 })
56
57 var textRaw = Value('')
58 var textArea = h('textarea', {
59 'ev-input': () => textRaw.set(textArea.value),
60 'ev-blur': () => {
61 clearTimeout(blurTimeout)
62 blurTimeout = setTimeout(() => textAreaFocused.set(false), 200)
63 },
64 'ev-focus': send(textAreaFocused.set, true),
65 placeholder
66 })
67 textRaw(text => hasContent.set(!!text))
68
69 textArea.publish = publish // TODO: fix - clunky api for the keyboard shortcut to target
70
71 var fileInput
72 if (!meta.recps) {
73 fileInput = api.blob.html.input(file => {
74 files.push(file)
75 filesById[file.link] = file
76
77 var imgPrefix = file.type.match(/^image/) ? '!' : ''
78 var spacer = imgPrefix ? '\n' : ' '
79 var insertLink = spacer + imgPrefix + '[' + file.name + ']' + '(' + file.link + ')' + spacer
80
81 var pos = textArea.selectionStart
82 var newText = textRaw().slice(0, pos) + insertLink + textRaw().slice(pos)
83 textArea.value = newText
84 textRaw.set(newText)
85
86 console.log('added:', file)
87 })
88
89 fileInput.onclick = () => hasContent.set(true)
90 }
91 // if fileInput is null, send button moves to the left side
92 // and we don't want that.
93 else { fileInput = h('input', { style: {visibility: 'hidden'} }) }
94
95 function PreviewSetup (strings) {
96 var showPreview = Value(false)
97 var previewBtn = h('Button',
98 {
99 className: when(showPreview, '-strong', '-subtle'),
100 'ev-click': () => showPreview.set(!showPreview())
101 },
102 when(showPreview, strings.blogNew.actions.edit, strings.blogNew.actions.preview)
103 )
104 return { previewBtn, showPreview }
105 }
106 var { previewBtn, showPreview } = PreviewSetup(strings)
107 var preview = computed(textRaw, text => api.message.html.markdown(text))
108
109 var publishBtn = h('Button -primary', { 'ev-click': publish }, strings.sendMessage)
110
111 var actions = h('section.actions', [
112 canAttach ? fileInput : '',
113 canPreview ? previewBtn : '',
114 publishBtn
115 ])
116
117 var composer = h('Compose', {
118 classList: when(expanded, '-expanded', '-contracted')
119 }, [
120 when(showPreview, preview, textArea),
121 actions
122 ])
123
124 addSuggest(textArea, (inputText, cb) => {
125 const char = inputText[0]
126 const wordFragment = inputText.slice(1)
127
128 if (char === '@') cb(null, getUserSuggestions(wordFragment, feedIdsInThread))
129 if (char === '#') cb(null, getChannelSuggestions(wordFragment))
130 if (char === ':') cb(null, getEmojiSuggestions(wordFragment))
131 }, {cls: 'PatchSuggest'})
132
133 return composer
134
135 // scoped
136
137 function publish () {
138 if (publishBtn.disabled) return
139
140 const text = resolve(textRaw)
141 if (isEmpty(text)) return
142
143 publishBtn.disabled = true
144
145 const mentions = ssbMentions(text).map(mention => {
146 // merge markdown-detected mention with file info
147 var file = filesById[mention.link]
148 if (file) {
149 if (file.type) mention.type = file.type
150 if (file.size) mention.size = file.size
151 }
152 return mention
153 })
154
155 var content = assign({}, resolve(meta), {
156 text,
157 mentions
158 })
159 for (var k in content) { content[k] = resolve(content[k]) }
160
161 if (!content.channel) delete content.channel
162 if (!mentions.length) delete content.mentions
163 if (content.recps && content.recps.length === 0) delete content.recps
164
165 if (typeof prepublish === 'function') {
166 prepublish(content, function (err, content) {
167 if (err) handleErr(err)
168 else api.message.async.publish(content, done)
169 })
170 } else { api.message.async.publish(content, done) }
171
172 function done (err, msg) {
173 publishBtn.disabled = false
174 if (err) handleErr(err)
175 else if (msg) {
176 textRaw.set('')
177 textArea.value = ''
178 }
179 if (cb) cb(err, msg)
180 }
181
182 function handleErr (err) {
183 publishBtn.disabled = false
184 if (cb) cb(err)
185 else throw err
186 }
187 }
188 }
189}
190

Built with git-ssb-web