@hellotext/hellotext 1.8.4 → 1.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/__tests__/core/configuration/webchat_test.js +163 -0
  2. package/__tests__/hellotext_test.js +97 -101
  3. package/dist/hellotext.js +1 -1
  4. package/docs/webchat.md +101 -0
  5. package/jest.config.js +1 -0
  6. package/jest.setup.js +9 -0
  7. package/lib/api/index.js +6 -0
  8. package/lib/api/web_chat/messages.js +74 -0
  9. package/lib/api/web_chats.js +53 -0
  10. package/lib/api/webchat/messages.js +85 -0
  11. package/lib/api/webchats.js +53 -0
  12. package/lib/builders/logo_builder.js +2 -1
  13. package/lib/channels/application_channel.js +68 -0
  14. package/lib/channels/web_chat_channel.js +97 -0
  15. package/lib/channels/webchat_channel.js +112 -0
  16. package/lib/controllers/mixins/usePopover.js +61 -0
  17. package/lib/controllers/web_chat/pagination_controller.js +81 -0
  18. package/lib/controllers/webchat/emoji_picker_controller.js +134 -0
  19. package/lib/controllers/webchat_controller.js +446 -0
  20. package/lib/core/configuration/web_chat.js +168 -0
  21. package/lib/core/configuration/webchat.js +185 -0
  22. package/lib/core/configuration.js +6 -1
  23. package/lib/core/event.js +1 -1
  24. package/lib/hellotext.js +18 -9
  25. package/lib/index.js +4 -0
  26. package/lib/models/business.js +9 -1
  27. package/lib/models/index.js +7 -0
  28. package/lib/models/web_chat.js +51 -0
  29. package/lib/models/webchat.js +51 -0
  30. package/package.json +8 -7
  31. package/src/api/index.js +5 -0
  32. package/src/api/webchat/messages.js +55 -0
  33. package/src/api/webchats.js +30 -0
  34. package/src/builders/logo_builder.js +8 -4
  35. package/src/channels/application_channel.js +45 -0
  36. package/src/channels/webchat_channel.js +77 -0
  37. package/src/controllers/mixins/usePopover.js +50 -0
  38. package/src/controllers/webchat/emoji_picker_controller.js +82 -0
  39. package/src/controllers/webchat_controller.js +448 -0
  40. package/src/core/configuration/webchat.js +176 -0
  41. package/src/core/configuration.js +5 -0
  42. package/src/core/event.js +10 -1
  43. package/src/hellotext.js +7 -2
  44. package/src/index.js +5 -0
  45. package/src/models/business.js +11 -1
  46. package/src/models/index.js +1 -0
  47. package/src/models/webchat.js +28 -0
  48. package/styles/index.css +4 -0
  49. package/webpack.config.js +1 -1
@@ -0,0 +1,45 @@
1
+ class ApplicationChannel {
2
+ static webSocket;
3
+
4
+ send({ command, identifier }) {
5
+ const data = {
6
+ command,
7
+ identifier: JSON.stringify(identifier)
8
+ }
9
+
10
+ if (this.webSocket.readyState === WebSocket.OPEN) {
11
+ this.webSocket.send(JSON.stringify(data))
12
+ } else {
13
+ this.webSocket.addEventListener('open', () => {
14
+ this.webSocket.send(JSON.stringify(data))
15
+ })
16
+ }
17
+ }
18
+
19
+ onMessage(callback) {
20
+ this.webSocket.addEventListener('message', (event) => {
21
+ const data = JSON.parse(event.data)
22
+ const { type, message } = data
23
+
24
+ if (this.ignoredEvents.includes(type)) {
25
+ return;
26
+ }
27
+
28
+ callback(message)
29
+ })
30
+ }
31
+
32
+ get webSocket() {
33
+ if (!ApplicationChannel.webSocket) {
34
+ return ApplicationChannel.webSocket = new WebSocket("ws://localhost:3000/cable")
35
+ }
36
+
37
+ return ApplicationChannel.webSocket
38
+ }
39
+
40
+ get ignoredEvents() {
41
+ return ['ping', 'confirm_subscription', 'welcome']
42
+ }
43
+ }
44
+
45
+ export default ApplicationChannel
@@ -0,0 +1,77 @@
1
+ import ApplicationChannel from './application_channel'
2
+
3
+ class WebchatChannel extends ApplicationChannel {
4
+ constructor(id, session, conversation) {
5
+ super()
6
+
7
+ this.id = id
8
+ this.session = session
9
+ this.conversation = conversation
10
+
11
+ this.subscribe()
12
+ }
13
+
14
+ subscribe() {
15
+ const params = {
16
+ channel: "WebchatChannel",
17
+ id: this.id,
18
+ session: this.session,
19
+ conversation: this.conversation,
20
+ }
21
+
22
+ this.send( { command: 'subscribe', identifier: params })
23
+ }
24
+
25
+ unsubscribe() {
26
+ const params = {
27
+ channel: "WebchatChannel",
28
+ id: this.id,
29
+ session: this.session,
30
+ conversation: this.conversation,
31
+ }
32
+
33
+ this.send({ command: 'unsubscribe', identifier: params })
34
+ }
35
+
36
+ onMessage(callback) {
37
+ super.onMessage((message) => {
38
+ if(message.type !== 'message') return
39
+ callback(message)
40
+ })
41
+ }
42
+
43
+ onConversationAssignment(callback) {
44
+ super.onMessage((message) => {
45
+ if(message.type === 'conversation.assigned') {
46
+ callback(message)
47
+ }
48
+ })
49
+ }
50
+
51
+ onAgentOnline(callback) {
52
+ super.onMessage((message) => {
53
+ if(message.type === 'agent_is_online') {
54
+ callback(message)
55
+ }
56
+ })
57
+ }
58
+
59
+ onReaction(callback) {
60
+ super.onMessage((message) => {
61
+ if(message.type === 'reaction.create' || message.type === 'reaction.destroy') {
62
+ callback(message)
63
+ }
64
+ })
65
+ }
66
+
67
+ updateSubscriptionWith(conversation) {
68
+ this.unsubscribe()
69
+
70
+ setTimeout(() => {
71
+ this.conversation = conversation
72
+ this.subscribe()
73
+ }, 1000)
74
+ }
75
+ }
76
+
77
+ export default WebchatChannel
@@ -0,0 +1,50 @@
1
+ import { autoUpdate, computePosition } from '@floating-ui/dom'
2
+ import Hellotext from "../../hellotext";
3
+
4
+ export const usePopover = (controller) => {
5
+ Object.assign(controller, {
6
+ show() {
7
+ this.openValue = true
8
+ },
9
+ hide() {
10
+ this.openValue = false
11
+ },
12
+ toggle() {
13
+ this.openValue = !this.openValue
14
+ },
15
+ setupFloatingUI({ trigger, popover }) {
16
+ this.floatingUICleanup = autoUpdate(trigger, popover, () => {
17
+ computePosition(trigger, popover, {
18
+ placement: this.placementValue,
19
+ middleware: this.middlewares,
20
+ }).then(({x, y}) => {
21
+ const newStyle = {
22
+ left: `${x}px`,
23
+ top: `${y}px`
24
+ }
25
+
26
+ Object.assign(popover.style, newStyle)
27
+ });
28
+ })
29
+ },
30
+ openValueChanged() {
31
+ if(this.disabledValue) return
32
+
33
+ if(this.openValue) {
34
+ this.popoverTarget.showPopover()
35
+ this.popoverTarget.setAttribute("aria-expanded", "true")
36
+
37
+ if(this['onPopoverOpened']) {
38
+ this.onPopoverOpened()
39
+ }
40
+ } else {
41
+ this.popoverTarget.hidePopover()
42
+ this.popoverTarget.removeAttribute("aria-expanded")
43
+
44
+ if(this['onPopoverClosed']) {
45
+ this.onPopoverClosed()
46
+ }
47
+ }
48
+ }
49
+ })
50
+ }
@@ -0,0 +1,82 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import { computePosition, autoUpdate, shift, autoPlacement, offset } from '@floating-ui/dom'
3
+
4
+ import { Picker } from 'emoji-mart'
5
+
6
+ import { usePopover } from '../mixins/usePopover'
7
+
8
+ export default class extends Controller {
9
+ static targets = [
10
+ 'button',
11
+ 'popover',
12
+ ]
13
+
14
+ static values = {
15
+ placement: { type: String, default: "bottom-end" },
16
+ open: { type: Boolean, default: false },
17
+ autoPlacement: { type: Boolean, default: false },
18
+ disabled: { type: Boolean, default: false },
19
+ size: { type: Number, default: 24 },
20
+ perLine: { type: Number, default: 9 }
21
+ }
22
+
23
+ initialize() {
24
+ this.onEmojiSelect = this.onEmojiSelect.bind(this)
25
+ super.initialize()
26
+ }
27
+
28
+ connect() {
29
+ usePopover(this)
30
+
31
+ this.setupFloatingUI({ trigger: this.buttonTarget, popover: this.popoverTarget })
32
+ this.popoverTarget.appendChild(this.pickerObject)
33
+
34
+ super.connect()
35
+ }
36
+
37
+ disconnect() {
38
+ this.floatingUICleanup()
39
+ super.disconnect()
40
+ }
41
+
42
+ onEmojiSelect(emoji) {
43
+ this.dispatch('selected', {
44
+ detail: emoji.native,
45
+ })
46
+
47
+ this.hide()
48
+ }
49
+
50
+ onClickOutside(event) {
51
+ if (this.openValue && event.target.nodeType && this.element.contains(event.target) === false) {
52
+ this.openValue = false
53
+ }
54
+ }
55
+
56
+ get pickerObject() {
57
+ return new Picker({
58
+ onEmojiSelect: this.onEmojiSelect,
59
+ theme: 'light',
60
+ dynamicWidth: true,
61
+ previewPosition: 'none',
62
+ skinTonePosition: 'none',
63
+ emojiSize: this.sizeValue,
64
+ perLine: this.perLineValue,
65
+ data: async () => {
66
+ const response = await fetch(
67
+ 'https://cdn.jsdelivr.net/npm/@emoji-mart/data',
68
+ )
69
+
70
+ return response.json()
71
+ }
72
+ })
73
+ }
74
+
75
+ get middlewares() {
76
+ return [
77
+ offset(5),
78
+ shift({ padding: 24 }),
79
+ autoPlacement({ allowedPlacements: ['top', 'bottom' ]}),
80
+ ]
81
+ }
82
+ }
@@ -0,0 +1,448 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import { shift, flip, offset } from '@floating-ui/dom'
3
+
4
+ import WebchatMessagesAPI from '../api/webchat/messages'
5
+ import WebchatChannel from '../channels/webchat_channel'
6
+ import Hellotext from "../hellotext"
7
+
8
+ import { Webchat as WebchatConfiguration, behaviors } from '../core/configuration/webchat'
9
+
10
+ import { usePopover } from './mixins/usePopover'
11
+ import { LogoBuilder } from '../builders/logo_builder'
12
+
13
+ export default class extends Controller {
14
+ static values = {
15
+ id: String,
16
+ conversationId: String,
17
+ media: Object,
18
+ fileSizeErrorMessage: String,
19
+ placement: { type: String, default: "bottom-end" },
20
+ open: { type: Boolean, default: false },
21
+ autoPlacement: { type: Boolean, default: false },
22
+ disabled: { type: Boolean, default: false },
23
+ nextPage: { type: Number, default: undefined },
24
+ }
25
+
26
+ static targets = [
27
+ 'trigger',
28
+ 'popover',
29
+ 'input',
30
+ 'attachmentInput',
31
+ 'attachmentButton',
32
+ 'errorMessageContainer',
33
+ 'attachmentTemplate',
34
+ 'attachmentContainer',
35
+ 'attachment',
36
+ 'messageTemplate',
37
+ 'messagesContainer',
38
+ 'title',
39
+ 'onlineStatus',
40
+ 'attachmentImage',
41
+ 'footer',
42
+ 'toolbar',
43
+ 'message',
44
+ 'unreadCounter',
45
+ ]
46
+
47
+ initialize() {
48
+ this.messagesAPI = new WebchatMessagesAPI(this.idValue)
49
+ this.webChatChannel = new WebchatChannel(this.idValue, Hellotext.session, this.conversationIdValue)
50
+ this.files = []
51
+
52
+ this.onMessageReceived = this.onMessageReceived.bind(this)
53
+ this.onConversationAssignment = this.onConversationAssignment.bind(this)
54
+ this.onAgentOnline = this.onAgentOnline.bind(this)
55
+ this.onMessageReaction = this.onMessageReaction.bind(this)
56
+
57
+ this.onScroll = this.onScroll.bind(this)
58
+
59
+ super.initialize()
60
+ }
61
+
62
+ connect() {
63
+ usePopover(this)
64
+
65
+ this.popoverTarget.classList.add(...WebchatConfiguration.classes)
66
+ this.triggerTarget.classList.add(...WebchatConfiguration.triggerClasses)
67
+
68
+ this.setupFloatingUI({ trigger: this.triggerTarget, popover: this.popoverTarget })
69
+
70
+ this.webChatChannel.onMessage(this.onMessageReceived)
71
+ this.webChatChannel.onConversationAssignment(this.onConversationAssignment)
72
+
73
+ this.webChatChannel.onAgentOnline(this.onAgentOnline)
74
+ this.webChatChannel.onReaction(this.onMessageReaction)
75
+
76
+ this.messagesContainerTarget.addEventListener('scroll', this.onScroll)
77
+
78
+ if (!Hellotext.business.features.white_label) {
79
+ this.toolbarTarget.appendChild(LogoBuilder.build())
80
+ }
81
+
82
+ if(localStorage.getItem(`hellotext--webchat--${this.idValue}`) === 'opened') {
83
+ this.openValue = true
84
+ }
85
+
86
+ Hellotext.eventEmitter.dispatch('webchat:mounted')
87
+ super.connect()
88
+ }
89
+
90
+ disconnect() {
91
+ this.messagesContainerTarget.removeEventListener('scroll', this.onScroll)
92
+ this.floatingUICleanup()
93
+
94
+ super.disconnect()
95
+ }
96
+
97
+ async onScroll() {
98
+ if(this.messagesContainerTarget.scrollTop > 300 || !this.nextPageValue || this.fetchingNextPage) return
99
+
100
+ this.fetchingNextPage = true
101
+ const response = await this.messagesAPI.index({ page: this.nextPageValue, session: Hellotext.session })
102
+
103
+ const { next: nextPage, messages } = await response.json()
104
+
105
+ this.nextPageValue = nextPage
106
+ this.oldScrollHeight = this.messagesContainerTarget.scrollHeight
107
+
108
+ messages.forEach(message => {
109
+ const { body, attachments } = message
110
+
111
+ const div = document.createElement('div')
112
+ div.innerHTML = body
113
+
114
+ const element = this.messageTemplateTarget.cloneNode(true)
115
+
116
+ element.setAttribute('data-hellotext--webchat-target', 'message')
117
+ element.style.removeProperty('display')
118
+
119
+ element.querySelector('[data-body]').innerHTML = div.innerHTML
120
+
121
+ if(message.state === 'received') {
122
+ element.classList.add('received')
123
+ } else {
124
+ element.classList.remove('received')
125
+ }
126
+
127
+ if(attachments) {
128
+ attachments.forEach(attachmentUrl => {
129
+ const image = this.attachmentImageTarget.cloneNode(true)
130
+
131
+ image.removeAttribute('data-hellotext--webchat-target')
132
+
133
+ image.src = attachmentUrl
134
+ image.style.display = 'block'
135
+
136
+ element.querySelector('[data-attachment-container]').appendChild(image)
137
+ })
138
+ }
139
+
140
+ element.setAttribute('data-body', body)
141
+ this.messagesContainerTarget.prepend(element)
142
+ })
143
+
144
+ this.messagesContainerTarget.scroll({
145
+ top: this.messagesContainerTarget.scrollHeight - this.oldScrollHeight,
146
+ behavior: 'instant',
147
+ })
148
+
149
+ this.fetchingNextPage = false
150
+ }
151
+
152
+ onClickOutside(event) {
153
+ if (WebchatConfiguration.behaviour === behaviors.POPOVER && this.openValue && event.target.nodeType && this.element.contains(event.target) === false) {
154
+ this.openValue = false
155
+ }
156
+ }
157
+
158
+ onPopoverOpened() {
159
+ this.inputTarget.focus()
160
+
161
+ if(!this.scrolled) {
162
+ this.messagesContainerTarget.scroll({
163
+ top: this.messagesContainerTarget.scrollHeight,
164
+ behavior: 'instant',
165
+ })
166
+
167
+ this.scrolled = true
168
+ }
169
+
170
+ Hellotext.eventEmitter.dispatch('webchat:opened')
171
+
172
+ localStorage.setItem(`hellotext--webchat--${this.idValue}`, 'opened')
173
+
174
+ if(this.unreadCounterTarget.style.display === 'none') return
175
+
176
+ this.unreadCounterTarget.style.display = 'none'
177
+ this.unreadCounterTarget.innerText = '0'
178
+
179
+ this.messagesAPI.markAsSeen()
180
+ }
181
+
182
+ onPopoverClosed() {
183
+ Hellotext.eventEmitter.dispatch('webchat:closed')
184
+
185
+ setTimeout(() => {
186
+ this.inputTarget.value = ""
187
+ })
188
+
189
+ localStorage.setItem(`hellotext--webchat--${this.idValue}`, 'closed')
190
+ }
191
+
192
+ onMessageReaction(message) {
193
+ const { message: messageId, reaction, type } = message
194
+ const messageElement = this.messageTargets.find(element => element.dataset.id === messageId)
195
+
196
+ const reactionsContainer = messageElement.querySelector('[data-reactions]')
197
+
198
+ if(type === 'reaction.destroy') {
199
+ const reactionElement = reactionsContainer.querySelector(`[data-id="${reaction.id}"]`)
200
+ return reactionElement.remove()
201
+ }
202
+
203
+ if(reactionsContainer.querySelector(`[data-id="${reaction.id}"]`)) {
204
+ const reactionElement = reactionsContainer.querySelector(`[data-id="${reaction.id}"]`)
205
+ reactionElement.innerText = reaction.emoji
206
+ } else {
207
+ const reactionElement = document.createElement('span')
208
+
209
+ reactionElement.innerText = reaction.emoji
210
+ reactionElement.setAttribute('data-id', reaction.id)
211
+
212
+ reactionsContainer.appendChild(reactionElement)
213
+ }
214
+ }
215
+
216
+ onMessageReceived(message) {
217
+ const { body, attachments } = message
218
+
219
+ const div = document.createElement('div')
220
+ div.innerHTML = body
221
+
222
+ const element = this.messageTemplateTarget.cloneNode(true)
223
+ element.style.display = 'flex'
224
+
225
+ element.querySelector('[data-body]').innerHTML = div.innerHTML
226
+ element.setAttribute('data-hellotext--webchat-target', 'message')
227
+
228
+ if(attachments) {
229
+ attachments.forEach(attachmentUrl => {
230
+ const image = this.attachmentImageTarget.cloneNode(true)
231
+ image.src = attachmentUrl
232
+ image.style.display = 'block'
233
+
234
+ element.querySelector('[data-attachment-container]').appendChild(image)
235
+ })
236
+ }
237
+
238
+ this.messagesContainerTarget.appendChild(element)
239
+
240
+ Hellotext.eventEmitter.dispatch('webchat:message:received', {
241
+ ...message,
242
+ body: element.querySelector('[data-body]').innerText,
243
+ })
244
+
245
+ element.scrollIntoView({ behavior: 'smooth' })
246
+ this.setOfflineTimeout()
247
+
248
+ if(this.openValue) return
249
+
250
+ this.unreadCounterTarget.style.display = 'flex'
251
+
252
+ const unreadCount = (parseInt(this.unreadCounterTarget.innerText) || 0) + 1
253
+ this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount
254
+ }
255
+
256
+ onConversationAssignment(conversation) {
257
+ const { to: user } = conversation
258
+
259
+ this.titleTarget.innerText = user.name
260
+
261
+ if(user.online) {
262
+ this.onlineStatusTarget.style.display = 'flex'
263
+ } else {
264
+ this.onlineStatusTarget.style.display = 'none'
265
+ }
266
+ }
267
+
268
+ onAgentOnline() {
269
+ this.onlineStatusTarget.style.display = 'flex'
270
+ this.setOfflineTimeout()
271
+ }
272
+
273
+ async sendMessage() {
274
+ const formData = new FormData()
275
+
276
+ const message = {
277
+ body: this.inputTarget.value,
278
+ attachments: this.files
279
+ }
280
+
281
+ formData.append('message[body]', this.inputTarget.value)
282
+
283
+ this.files.forEach(file => {
284
+ formData.append('message[attachments][]', file)
285
+ })
286
+
287
+ formData.append('session', Hellotext.session)
288
+
289
+ const element = this.messageTemplateTarget.cloneNode(true)
290
+
291
+ element.classList.add('received')
292
+ element.style.removeProperty('display')
293
+
294
+ element.setAttribute('data-hellotext--webchat-target', 'message')
295
+ element.querySelector('[data-body]').innerText = this.inputTarget.value
296
+
297
+ const attachments = this.attachmentContainerTarget.querySelectorAll('img')
298
+
299
+ if(attachments.length > 0) {
300
+ attachments.forEach(attachment => {
301
+ element.querySelector('[data-attachment-container]').appendChild(attachment)
302
+ })
303
+ }
304
+
305
+ this.messagesContainerTarget.appendChild(element)
306
+ element.scrollIntoView({ behavior: 'smooth' })
307
+
308
+ this.inputTarget.value = ""
309
+ this.files = []
310
+ this.attachmentContainerTarget.innerHTML = ""
311
+ this.attachmentContainerTarget.classList.add("hidden")
312
+
313
+ this.inputTarget.focus()
314
+
315
+ const response = await this.messagesAPI.create(formData)
316
+
317
+ if(response.failed) {
318
+ return element.classList.add('failed')
319
+ }
320
+
321
+ const data = await response.json()
322
+ element.setAttribute('data-id', data.id)
323
+ message.id = data.id
324
+
325
+ Hellotext.eventEmitter.dispatch('webchat:message:sent', message)
326
+
327
+ if(data.conversation !== this.conversationIdValue) {
328
+ this.conversationIdValue = data.conversation
329
+ this.webChatChannel.updateSubscriptionWith(this.conversationIdValue)
330
+ }
331
+ }
332
+
333
+ openAttachment() {
334
+ this.attachmentInputTarget.click()
335
+ }
336
+
337
+ onFileInputChange() {
338
+ this.errorMessageContainerTarget.classList.add('hidden')
339
+
340
+ this.files = Array.from(this.attachmentInputTarget.files)
341
+
342
+ const fileMaxSizeTooMuch = this.files.find(file => {
343
+ const type = file.type.split("/")[0]
344
+
345
+ if(['image', 'video', 'audio'].includes(type)) {
346
+ return this.mediaValue[type].max_size < file.size
347
+ } else {
348
+ return this.mediaValue.document.max_size < file.size
349
+ }
350
+ })
351
+
352
+ if(fileMaxSizeTooMuch) {
353
+ const type = fileMaxSizeTooMuch.type.split("/")[0]
354
+ const mediaType = ['image', 'audio', 'video'].includes(type) ? type : 'document'
355
+
356
+ this.errorMessageContainerTarget.innerText = this.fileSizeErrorMessageValue.replace('%{limit}', this.byteToMegabyte(this.mediaValue[mediaType].max_size))
357
+ return
358
+ }
359
+
360
+ this.errorMessageContainerTarget.innerText = ""
361
+ this.files.forEach(file => this.createAttachmentElement(file))
362
+ this.inputTarget.focus()
363
+ }
364
+
365
+ createAttachmentElement(file) {
366
+ const element = this.attachmentElement()
367
+
368
+ this.attachmentContainerTarget.classList.remove('hidden')
369
+
370
+ element.setAttribute('data-name', file.name)
371
+
372
+ if(file.type.startsWith("image/")) {
373
+ const thumbnail = this.attachmentImageTarget.cloneNode(true)
374
+
375
+ thumbnail.src = URL.createObjectURL(file)
376
+ thumbnail.style.display = 'block'
377
+
378
+ element.appendChild(thumbnail)
379
+
380
+ this.attachmentContainerTarget.appendChild(element)
381
+ this.attachmentContainerTarget.style.display = 'flex'
382
+ } else {
383
+ element.querySelector("main").classList.add(...this.widthClasses, "h-20", "rounded-md", "bg-gray-200", "p-1")
384
+ element.querySelector("p[data-attachment-name]").innerText = file.name
385
+ }
386
+ }
387
+
388
+ removeAttachment({ currentTarget }) {
389
+ const attachment = currentTarget.closest("[data-hellotext--webchat-target='attachment']")
390
+
391
+ this.files = this.files.filter(file => file.name !== attachment.dataset.name)
392
+
393
+ attachment.remove()
394
+ this.inputTarget.focus()
395
+ }
396
+
397
+ attachmentTargetDisconnected() {
398
+ if(this.attachmentTargets.length === 0) {
399
+ this.attachmentContainerTarget.innerHTML = ""
400
+ this.attachmentContainerTarget.style.display = 'none'
401
+ }
402
+ }
403
+
404
+ attachmentElement() {
405
+ const element = this.attachmentTemplateTarget.cloneNode(true)
406
+ element.removeAttribute("hidden")
407
+ element.style.display = 'flex'
408
+
409
+ element.setAttribute("data-hellotext--webchat-target", "attachment")
410
+
411
+ return element
412
+ }
413
+
414
+ onEmojiSelect({ detail: emoji }) {
415
+ const value = this.inputTarget.value
416
+ const start = this.inputTarget.selectionStart
417
+ const end = this.inputTarget.selectionEnd
418
+
419
+ this.inputTarget.value = value.slice(0, start) + emoji + value.slice(end)
420
+
421
+ this.inputTarget.selectionStart = this.inputTarget.selectionEnd = start + emoji.length
422
+ this.inputTarget.focus()
423
+ }
424
+
425
+ setOfflineTimeout() {
426
+ clearTimeout(this.offlineTimeout)
427
+
428
+ this.offlineTimeout = setTimeout(() => {
429
+ this.onlineStatusTarget.style.display = 'none'
430
+ }, this.fiveMinutes)
431
+ }
432
+
433
+ byteToMegabyte(bytes) {
434
+ return Math.ceil(bytes / 1024 / 1024)
435
+ }
436
+
437
+ get fiveMinutes() {
438
+ return 300000
439
+ }
440
+
441
+ get middlewares() {
442
+ return [
443
+ offset(5),
444
+ shift({ padding: 24 }),
445
+ flip(),
446
+ ]
447
+ }
448
+ }