git ssb

2+

mixmix / ticktack



Tree: dc5f4dc6989f09141fae9235f7690c01ebdc9e74

Files: dc5f4dc6989f09141fae9235f7690c01ebdc9e74 / message / html / compose.js

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

Built with git-ssb-web