@hellotext/hellotext 2.1.3 → 2.1.4
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.
- package/dist/hellotext.js +1 -1
- package/lib/channels/application_channel.cjs +9 -6
- package/lib/channels/application_channel.js +9 -6
- package/lib/channels/webchat_channel.cjs +42 -17
- package/lib/channels/webchat_channel.js +42 -17
- package/lib/controllers/webchat_controller.cjs +141 -42
- package/lib/controllers/webchat_controller.js +142 -43
- package/lib/core/configuration.cjs +1 -0
- package/lib/core/configuration.js +1 -0
- package/lib/models/query.cjs +1 -2
- package/lib/models/query.js +1 -2
- package/lib/models/session.cjs +1 -1
- package/lib/models/session.js +1 -1
- package/package.json +1 -1
- package/src/channels/application_channel.js +12 -9
- package/src/channels/webchat_channel.js +33 -19
- package/src/controllers/webchat_controller.js +133 -34
- package/src/core/configuration.js +1 -0
- package/src/models/query.js +1 -3
- package/src/models/session.js +1 -1
|
@@ -23,6 +23,10 @@ export default class extends Controller {
|
|
|
23
23
|
disabled: { type: Boolean, default: false },
|
|
24
24
|
nextPage: { type: Number, default: undefined },
|
|
25
25
|
fullScreenThreshold: { type: Number, default: 1024 },
|
|
26
|
+
typingIndicatorKeepAlive: { type: Number, default: 30000 }, // 30 seconds
|
|
27
|
+
offset: { type: Number, default: 24 },
|
|
28
|
+
padding: { type: Number, default: 24 },
|
|
29
|
+
optimisticTypingIndicatorWait: { type: Number, default: 1000 }, // 1 second
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
static classes = ['fadeOut']
|
|
@@ -40,12 +44,13 @@ export default class extends Controller {
|
|
|
40
44
|
'messageTemplate',
|
|
41
45
|
'messagesContainer',
|
|
42
46
|
'title',
|
|
43
|
-
'onlineStatus',
|
|
44
47
|
'attachmentImage',
|
|
45
48
|
'footer',
|
|
46
49
|
'toolbar',
|
|
47
50
|
'message',
|
|
48
51
|
'unreadCounter',
|
|
52
|
+
'typingIndicator',
|
|
53
|
+
'typingIndicatorTemplate',
|
|
49
54
|
]
|
|
50
55
|
|
|
51
56
|
initialize() {
|
|
@@ -60,9 +65,8 @@ export default class extends Controller {
|
|
|
60
65
|
this.files = []
|
|
61
66
|
|
|
62
67
|
this.onMessageReceived = this.onMessageReceived.bind(this)
|
|
63
|
-
this.onConversationAssignment = this.onConversationAssignment.bind(this)
|
|
64
|
-
this.onAgentOnline = this.onAgentOnline.bind(this)
|
|
65
68
|
this.onMessageReaction = this.onMessageReaction.bind(this)
|
|
69
|
+
this.onTypingStart = this.onTypingStart.bind(this)
|
|
66
70
|
|
|
67
71
|
this.onScroll = this.onScroll.bind(this)
|
|
68
72
|
|
|
@@ -81,9 +85,8 @@ export default class extends Controller {
|
|
|
81
85
|
this.setupFloatingUI({ trigger: this.triggerTarget, popover: this.popoverTarget })
|
|
82
86
|
|
|
83
87
|
this.webChatChannel.onMessage(this.onMessageReceived)
|
|
84
|
-
this.webChatChannel.
|
|
88
|
+
this.webChatChannel.onTypingStart(this.onTypingStart)
|
|
85
89
|
|
|
86
|
-
this.webChatChannel.onAgentOnline(this.onAgentOnline)
|
|
87
90
|
this.webChatChannel.onReaction(this.onMessageReaction)
|
|
88
91
|
|
|
89
92
|
this.messagesContainerTarget.addEventListener('scroll', this.onScroll)
|
|
@@ -106,12 +109,94 @@ export default class extends Controller {
|
|
|
106
109
|
this.broadcastChannel.removeEventListener('message', this.onOutboundMessageSent)
|
|
107
110
|
this.messagesContainerTarget.removeEventListener('scroll', this.onScroll)
|
|
108
111
|
|
|
112
|
+
// Clean up typing indicator timeouts
|
|
113
|
+
this.clearTypingIndicator()
|
|
114
|
+
|
|
109
115
|
this.broadcastChannel.close()
|
|
110
116
|
this.floatingUICleanup()
|
|
111
117
|
|
|
112
118
|
super.disconnect()
|
|
113
119
|
}
|
|
114
120
|
|
|
121
|
+
onTypingStart() {
|
|
122
|
+
if (this.typingIndicatorVisible) {
|
|
123
|
+
return this.resetTypingIndicatorTimer()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.showTypingIndicator()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
showOptimisticTypingIndicator() {
|
|
130
|
+
if (this.typingIndicatorVisible) return
|
|
131
|
+
|
|
132
|
+
this.showTypingIndicator()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
showTypingIndicator() {
|
|
136
|
+
this.clearTypingIndicator()
|
|
137
|
+
|
|
138
|
+
this.typingIndicatorVisible = true
|
|
139
|
+
const indicator = this.typingIndicatorTemplateTarget.cloneNode(true)
|
|
140
|
+
|
|
141
|
+
indicator.setAttribute('data-hellotext--webchat-target', 'typingIndicator')
|
|
142
|
+
indicator.style.display = 'flex'
|
|
143
|
+
|
|
144
|
+
this.messagesContainerTarget.appendChild(indicator)
|
|
145
|
+
|
|
146
|
+
requestAnimationFrame(() => {
|
|
147
|
+
this.messagesContainerTarget.scroll({
|
|
148
|
+
top: this.messagesContainerTarget.scrollHeight,
|
|
149
|
+
behavior: 'instant',
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const timeout = this.typingIndicatorKeepAliveValue
|
|
154
|
+
|
|
155
|
+
this.incomingTypingIndicatorTimeout = setTimeout(() => {
|
|
156
|
+
this.clearTypingIndicator()
|
|
157
|
+
}, timeout)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resetTypingIndicatorTimer() {
|
|
161
|
+
if (!this.typingIndicatorVisible) return
|
|
162
|
+
|
|
163
|
+
// Clear existing timeout
|
|
164
|
+
clearTimeout(this.incomingTypingIndicatorTimeout)
|
|
165
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
166
|
+
|
|
167
|
+
// Use unified timeout for all typing indicators
|
|
168
|
+
const timeout = this.typingIndicatorKeepAliveValue
|
|
169
|
+
|
|
170
|
+
this.incomingTypingIndicatorTimeout = setTimeout(() => {
|
|
171
|
+
this.clearTypingIndicator()
|
|
172
|
+
}, timeout)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
clearTypingIndicator() {
|
|
176
|
+
if (this.hasTypingIndicatorTarget) {
|
|
177
|
+
this.typingIndicatorTarget.remove()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.typingIndicatorVisible = false
|
|
181
|
+
|
|
182
|
+
clearTimeout(this.incomingTypingIndicatorTimeout)
|
|
183
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
onMessageInputChange() {
|
|
187
|
+
this.resizeInput()
|
|
188
|
+
clearTimeout(this.typingIndicatorTimeout)
|
|
189
|
+
|
|
190
|
+
if (!this.hasSentTypingIndicator) {
|
|
191
|
+
this.webChatChannel.startTypingIndicator()
|
|
192
|
+
this.hasSentTypingIndicator = true
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.typingIndicatorTimeout = setTimeout(() => {
|
|
196
|
+
this.hasSentTypingIndicator = false
|
|
197
|
+
}, 3000)
|
|
198
|
+
}
|
|
199
|
+
|
|
115
200
|
onOutboundMessageSent(event) {
|
|
116
201
|
const { data } = event
|
|
117
202
|
|
|
@@ -120,7 +205,13 @@ export default class extends Controller {
|
|
|
120
205
|
const element = new DOMParser().parseFromString(data.element, 'text/html').body
|
|
121
206
|
.firstElementChild
|
|
122
207
|
|
|
123
|
-
|
|
208
|
+
// Insert message before typing indicator if one exists
|
|
209
|
+
if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
|
|
210
|
+
this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget)
|
|
211
|
+
} else {
|
|
212
|
+
this.messagesContainerTarget.appendChild(element)
|
|
213
|
+
}
|
|
214
|
+
|
|
124
215
|
element.scrollIntoView({ behavior: 'instant' })
|
|
125
216
|
},
|
|
126
217
|
'message:failed': data => {
|
|
@@ -295,6 +386,11 @@ export default class extends Controller {
|
|
|
295
386
|
})
|
|
296
387
|
}
|
|
297
388
|
|
|
389
|
+
// Clear any typing indicator when message is received
|
|
390
|
+
if (this.typingIndicatorVisible) {
|
|
391
|
+
this.clearTypingIndicator()
|
|
392
|
+
}
|
|
393
|
+
|
|
298
394
|
this.messagesContainerTarget.appendChild(element)
|
|
299
395
|
|
|
300
396
|
Hellotext.eventEmitter.dispatch('webchat:message:received', {
|
|
@@ -303,7 +399,6 @@ export default class extends Controller {
|
|
|
303
399
|
})
|
|
304
400
|
|
|
305
401
|
element.scrollIntoView({ behavior: 'smooth' })
|
|
306
|
-
this.setOfflineTimeout()
|
|
307
402
|
|
|
308
403
|
if (this.openValue) {
|
|
309
404
|
this.messagesAPI.markAsSeen(id)
|
|
@@ -316,24 +411,20 @@ export default class extends Controller {
|
|
|
316
411
|
this.unreadCounterTarget.innerText = unreadCount > 99 ? '99+' : unreadCount
|
|
317
412
|
}
|
|
318
413
|
|
|
319
|
-
|
|
320
|
-
const
|
|
414
|
+
resizeInput() {
|
|
415
|
+
const maxHeight = 96
|
|
321
416
|
|
|
322
|
-
|
|
417
|
+
// Temporarily reset height to its natural content size
|
|
418
|
+
this.inputTarget.style.height = 'auto'
|
|
323
419
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
this.onlineStatusTarget.style.display = 'none'
|
|
328
|
-
}
|
|
329
|
-
}
|
|
420
|
+
// Set the height to the scrollHeight, which is the minimum height
|
|
421
|
+
// the element needs to fit its content without a scrollbar.
|
|
422
|
+
const scrollHeight = this.inputTarget.scrollHeight
|
|
330
423
|
|
|
331
|
-
|
|
332
|
-
this.onlineStatusTarget.style.display = 'flex'
|
|
333
|
-
this.setOfflineTimeout()
|
|
424
|
+
this.inputTarget.style.height = `${Math.min(scrollHeight, maxHeight)}px`
|
|
334
425
|
}
|
|
335
426
|
|
|
336
|
-
async sendMessage() {
|
|
427
|
+
async sendMessage(e) {
|
|
337
428
|
const formData = new FormData()
|
|
338
429
|
|
|
339
430
|
const message = {
|
|
@@ -342,6 +433,10 @@ export default class extends Controller {
|
|
|
342
433
|
}
|
|
343
434
|
|
|
344
435
|
if (this.inputTarget.value.trim().length === 0 && this.files.length === 0) {
|
|
436
|
+
if (e && e.target) {
|
|
437
|
+
e.preventDefault()
|
|
438
|
+
}
|
|
439
|
+
|
|
345
440
|
return
|
|
346
441
|
}
|
|
347
442
|
|
|
@@ -381,7 +476,13 @@ export default class extends Controller {
|
|
|
381
476
|
})
|
|
382
477
|
}
|
|
383
478
|
|
|
384
|
-
|
|
479
|
+
// Insert message before typing indicator if one exists
|
|
480
|
+
if (this.typingIndicatorVisible && this.hasTypingIndicatorTarget) {
|
|
481
|
+
this.messagesContainerTarget.insertBefore(element, this.typingIndicatorTarget)
|
|
482
|
+
} else {
|
|
483
|
+
this.messagesContainerTarget.appendChild(element)
|
|
484
|
+
}
|
|
485
|
+
|
|
385
486
|
element.scrollIntoView({ behavior: 'smooth' })
|
|
386
487
|
|
|
387
488
|
this.broadcastChannel.postMessage({
|
|
@@ -390,6 +491,7 @@ export default class extends Controller {
|
|
|
390
491
|
})
|
|
391
492
|
|
|
392
493
|
this.inputTarget.value = ''
|
|
494
|
+
this.resizeInput()
|
|
393
495
|
this.files = []
|
|
394
496
|
|
|
395
497
|
this.attachmentInputTarget.value = ''
|
|
@@ -422,6 +524,15 @@ export default class extends Controller {
|
|
|
422
524
|
this.webChatChannel.updateSubscriptionWith(this.conversationIdValue)
|
|
423
525
|
}
|
|
424
526
|
|
|
527
|
+
if (this.typingIndicatorVisible) {
|
|
528
|
+
this.resetTypingIndicatorTimer()
|
|
529
|
+
} else {
|
|
530
|
+
clearTimeout(this.optimisticTypingTimeout)
|
|
531
|
+
this.optimisticTypingTimeout = setTimeout(() => {
|
|
532
|
+
this.showOptimisticTypingIndicator()
|
|
533
|
+
}, this.optimisticTypingIndicatorWaitValue)
|
|
534
|
+
}
|
|
535
|
+
|
|
425
536
|
this.attachmentContainerTarget.style.display = ''
|
|
426
537
|
}
|
|
427
538
|
|
|
@@ -534,24 +645,12 @@ export default class extends Controller {
|
|
|
534
645
|
this.inputTarget.focus()
|
|
535
646
|
}
|
|
536
647
|
|
|
537
|
-
setOfflineTimeout() {
|
|
538
|
-
clearTimeout(this.offlineTimeout)
|
|
539
|
-
|
|
540
|
-
this.offlineTimeout = setTimeout(() => {
|
|
541
|
-
this.onlineStatusTarget.style.display = 'none'
|
|
542
|
-
}, this.fiveMinutes)
|
|
543
|
-
}
|
|
544
|
-
|
|
545
648
|
byteToMegabyte(bytes) {
|
|
546
649
|
return Math.ceil(bytes / 1024 / 1024)
|
|
547
650
|
}
|
|
548
651
|
|
|
549
|
-
get fiveMinutes() {
|
|
550
|
-
return 300000
|
|
551
|
-
}
|
|
552
|
-
|
|
553
652
|
get middlewares() {
|
|
554
|
-
return [offset(
|
|
653
|
+
return [offset(this.offsetValue), shift({ padding: this.paddingValue }), flip()]
|
|
555
654
|
}
|
|
556
655
|
|
|
557
656
|
get shouldOpenOnMount() {
|
package/src/models/query.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Cookies } from './cookies'
|
|
2
|
-
|
|
3
1
|
class Query {
|
|
4
2
|
static get inPreviewMode() {
|
|
5
3
|
return new this().inPreviewMode
|
|
@@ -22,7 +20,7 @@ class Query {
|
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
get session() {
|
|
25
|
-
return this.get('session')
|
|
23
|
+
return this.get('session')
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
toHellotextParam(param) {
|
package/src/models/session.js
CHANGED
|
@@ -18,7 +18,7 @@ class Session {
|
|
|
18
18
|
static initialize() {
|
|
19
19
|
this.#query = new Query()
|
|
20
20
|
|
|
21
|
-
this.session =
|
|
21
|
+
this.session = this.#query.session || Configuration.session || Cookies.get('hello_session')
|
|
22
22
|
|
|
23
23
|
if (!this.session && Configuration.autoGenerateSession) {
|
|
24
24
|
this.session = crypto.randomUUID()
|