@defra/flood-webchat 0.0.1-alpha.2 → 0.0.1-alpha.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/README.md +73 -1
- package/babel.config.cjs +3 -0
- package/dist/templates.js +554 -0
- package/docs/development-guide.md +2 -1
- package/main.scss +743 -2
- package/package.json +14 -4
- package/src/client/index.js +12 -2
- package/src/client/lib/availability.js +75 -0
- package/src/client/lib/config.js +17 -0
- package/src/client/lib/keyboard.js +149 -0
- package/src/client/lib/notification.js +62 -0
- package/src/client/lib/nunjucks.js +3 -0
- package/src/client/lib/panel.js +293 -0
- package/src/client/lib/provider.js +11 -0
- package/src/client/lib/skiplink.js +27 -0
- package/src/client/lib/state.js +82 -0
- package/src/client/lib/transcript.js +34 -0
- package/src/client/lib/utils.js +190 -0
- package/src/client/lib/webchat.js +1073 -0
- package/src/server/client.js +72 -0
- package/src/server/index.js +45 -2
- package/src/server/utils.js +19 -0
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
import { ChatEvent, ChatSdk, EnvironmentName } from '@nice-devone/nice-cxone-chat-web-sdk'
|
|
2
|
+
import State from './state.js'
|
|
3
|
+
import Skiplink from './skiplink.js'
|
|
4
|
+
import Availability from './availability.js'
|
|
5
|
+
import Panel from './panel.js'
|
|
6
|
+
import Notification from './notification.js'
|
|
7
|
+
import Keyboard from './keyboard.js'
|
|
8
|
+
import Transcript from './transcript.js'
|
|
9
|
+
import Config from './config.js'
|
|
10
|
+
import Utils from './utils.js'
|
|
11
|
+
|
|
12
|
+
/** Class representing flood webchat. */
|
|
13
|
+
class WebChat {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} id
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {string} options.brandId
|
|
18
|
+
* @param {string} options.channelId
|
|
19
|
+
* @param {string} options.environmentName
|
|
20
|
+
* @param {string} options.availabilityEndpoint
|
|
21
|
+
**/
|
|
22
|
+
constructor (id, options) {
|
|
23
|
+
this.id = id
|
|
24
|
+
this.brandId = options.brandId
|
|
25
|
+
this.channelId = options.channelId
|
|
26
|
+
this.availabilityEndpoint = options.availabilityEndpoint
|
|
27
|
+
this.environment = EnvironmentName[options.environmentName]
|
|
28
|
+
|
|
29
|
+
// Initialise state
|
|
30
|
+
this.state = new State(this._openChat.bind(this), this._closeChat.bind(this))
|
|
31
|
+
|
|
32
|
+
// Initialise availability
|
|
33
|
+
this.availability = new Availability(id, this._openChat.bind(this))
|
|
34
|
+
|
|
35
|
+
// Initialise panel
|
|
36
|
+
this.panel = new Panel()
|
|
37
|
+
|
|
38
|
+
// Initialise skiplink
|
|
39
|
+
this.skiplink = new Skiplink()
|
|
40
|
+
|
|
41
|
+
// Initialise notification
|
|
42
|
+
this.notification = new Notification()
|
|
43
|
+
|
|
44
|
+
// Reinstate html visiblity (avoid refresh flicker)
|
|
45
|
+
if (document.body.classList.contains('wc-hidden')) {
|
|
46
|
+
document.body.classList.remove('wc-hidden')
|
|
47
|
+
document.body.classList.add('wc-body')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Initialise keyboard interface
|
|
51
|
+
const state = this.state
|
|
52
|
+
Keyboard.init(state)
|
|
53
|
+
|
|
54
|
+
// Render availability content
|
|
55
|
+
this.availability.update(state)
|
|
56
|
+
|
|
57
|
+
// Render panel if #webchat exists in url
|
|
58
|
+
if (state.isOpen) {
|
|
59
|
+
const panel = this.panel
|
|
60
|
+
panel.create(state, this._addDomEvents.bind(this))
|
|
61
|
+
panel.update(state)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Conditionally add skiplink
|
|
65
|
+
this.skiplink.toggle(state.view === 'OPEN' || state.view === 'END')
|
|
66
|
+
|
|
67
|
+
// Attach sticky footer scroll event
|
|
68
|
+
document.addEventListener('scroll', e => {
|
|
69
|
+
this.availability.scroll(state)
|
|
70
|
+
})
|
|
71
|
+
this.availability.scroll(state)
|
|
72
|
+
|
|
73
|
+
// Attach custom 'ready' event listener
|
|
74
|
+
this.livechatReady = new CustomEvent('livechatReady', {})
|
|
75
|
+
document.addEventListener('livechatReady', this._handleReadyEvent.bind(this))
|
|
76
|
+
|
|
77
|
+
// Conditiopnally recover thread
|
|
78
|
+
if (state.threadId) {
|
|
79
|
+
this._recoverThread()
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Ready to check availability
|
|
84
|
+
document.dispatchEvent(this.livechatReady)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async _authorise () {
|
|
88
|
+
console.log('_authorise')
|
|
89
|
+
|
|
90
|
+
// New SDK instance
|
|
91
|
+
const sdk = new ChatSdk({
|
|
92
|
+
brandId: this.brandId,
|
|
93
|
+
channelId: this.channelId,
|
|
94
|
+
environment: this.environment,
|
|
95
|
+
customerId: window.localStorage.getItem('CUSTOMER_ID') || ''
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Authorise and set customerId
|
|
99
|
+
const response = await sdk.authorize()
|
|
100
|
+
const customerId = response?.consumerIdentity.idOnExternalPlatform
|
|
101
|
+
window.localStorage.setItem('CUSTOMER_ID', customerId || '')
|
|
102
|
+
this.customerId = customerId
|
|
103
|
+
|
|
104
|
+
// How do we know they have been authorised?
|
|
105
|
+
const state = this.state
|
|
106
|
+
state.isAuthorised = true
|
|
107
|
+
|
|
108
|
+
// Confirm availability from SDK
|
|
109
|
+
const isOnline = response && response.channel.availability.status === 'online'
|
|
110
|
+
state.availability = isOnline ? state.availability : 'UNAVAILABLE'
|
|
111
|
+
|
|
112
|
+
// Add sdk event listeners
|
|
113
|
+
|
|
114
|
+
// v1.3.0
|
|
115
|
+
// sdk.onChatEvent(ChatEvent.CONSUMER_AUTHORIZED, this._handleConsumerAuthorizedEvent.bind(this))
|
|
116
|
+
sdk.onChatEvent(ChatEvent.LIVECHAT_RECOVERED, this._handleLivechatRecoveredEvent.bind(this))
|
|
117
|
+
sdk.onChatEvent(ChatEvent.MESSAGE_CREATED, this._handleMessageCreatedEvent.bind(this))
|
|
118
|
+
sdk.onChatEvent(ChatEvent.AGENT_TYPING_STARTED, this._handleAgentTypingEvent.bind(this))
|
|
119
|
+
sdk.onChatEvent(ChatEvent.AGENT_TYPING_ENDED, this._handleAgentTypingEvent.bind(this))
|
|
120
|
+
sdk.onChatEvent(ChatEvent.MESSAGE_SEEN_BY_END_USER, this._handleMessageSeenByEndUserEvent.bind(this))
|
|
121
|
+
sdk.onChatEvent(ChatEvent.ASSIGNED_AGENT_CHANGED, this._handleAssignedAgentChangedEvent.bind(this))
|
|
122
|
+
sdk.onChatEvent(ChatEvent.CONTACT_CREATED, this._handleContactCreatedEvent.bind(this))
|
|
123
|
+
sdk.onChatEvent(ChatEvent.CONTACT_STATUS_CHANGED, this._handleContactStatusChangedEvent.bind(this))
|
|
124
|
+
sdk.onChatEvent('SetConsumerContactCustomFields', this._handleChatEvent.bind(this))
|
|
125
|
+
|
|
126
|
+
// v1.2.0
|
|
127
|
+
sdk.onChatEvent(ChatEvent.CASE_INBOX_ASSIGNEE_CHANGED, this._handleAssignedAgentChangedEvent.bind(this))
|
|
128
|
+
// sdk.onChatEvent(ChatEvent.CASE_CREATED, this._handleContactCreatedEvent.bind(this))
|
|
129
|
+
// sdk.onChatEvent(ChatEvent.CASE_STATUS_CHANGED, this._handleContactStatusChangedEvent.bind(this))
|
|
130
|
+
|
|
131
|
+
this.sdk = sdk
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async _recoverThread () {
|
|
135
|
+
console.log('_recoverThread')
|
|
136
|
+
|
|
137
|
+
// Recover thread
|
|
138
|
+
try {
|
|
139
|
+
await this._authorise()
|
|
140
|
+
await this._getThread()
|
|
141
|
+
await this.thread.recover()
|
|
142
|
+
} catch (err) {
|
|
143
|
+
// Address issue with no thread but we still have the thread id
|
|
144
|
+
console.log(err)
|
|
145
|
+
window.localStorage.removeItem('THREAD_ID')
|
|
146
|
+
// Reset view
|
|
147
|
+
this.state.view = 'PRECHAT'
|
|
148
|
+
// Dispatch ready event
|
|
149
|
+
document.dispatchEvent(this.livechatReady)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async _getThread () {
|
|
154
|
+
// Get thread
|
|
155
|
+
let threadId = window.localStorage.getItem('THREAD_ID')
|
|
156
|
+
if (!threadId) {
|
|
157
|
+
// Generate id
|
|
158
|
+
const random = Math.floor(Math.random() * 1000).toString()
|
|
159
|
+
const time = (new Date()).getTime()
|
|
160
|
+
threadId = `${time}${random}`
|
|
161
|
+
window.localStorage.setItem('THREAD_ID', threadId)
|
|
162
|
+
}
|
|
163
|
+
const thread = await this.sdk.getThread(threadId)
|
|
164
|
+
this.thread = thread
|
|
165
|
+
this.state.threadId = threadId
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async _updateAvailability () {
|
|
169
|
+
const state = this.state
|
|
170
|
+
const previousAvailability = state.availability
|
|
171
|
+
state.availability = await Utils.getAvailability(this.availabilityEndpoint)
|
|
172
|
+
const availability = this.availability
|
|
173
|
+
availability.update(state)
|
|
174
|
+
availability.scroll(state)
|
|
175
|
+
|
|
176
|
+
// Alert assistive technology
|
|
177
|
+
const isNewlyAvailable = previousAvailability === 'UNAVAILABLE' && state.availability === 'AVAILABLE'
|
|
178
|
+
if (!state.isOpen && isNewlyAvailable) {
|
|
179
|
+
this._alertAT('Webchat now available')
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async _startChat () {
|
|
184
|
+
console.log('_startChat')
|
|
185
|
+
|
|
186
|
+
// Check availability
|
|
187
|
+
await this._updateAvailability()
|
|
188
|
+
const state = this.state
|
|
189
|
+
if (state.availability !== 'AVAILABLE') {
|
|
190
|
+
this._unavailable()
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Authorise user
|
|
195
|
+
if (!state.isAuthorised) {
|
|
196
|
+
await this._authorise()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ***Bug: Set userName populates last name from string after last space
|
|
200
|
+
this.sdk.getCustomer().setName(state.name)
|
|
201
|
+
|
|
202
|
+
// Start chat
|
|
203
|
+
try {
|
|
204
|
+
await this._getThread()
|
|
205
|
+
this.thread.startChat(state.question || 'Begin conversation')
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.log(err)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Update panel attributes
|
|
211
|
+
const panel = this.panel
|
|
212
|
+
panel.setAttributes(state)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async _confirmEndChat () {
|
|
216
|
+
console.log('_confirmEndChat')
|
|
217
|
+
|
|
218
|
+
// Close thread
|
|
219
|
+
window.localStorage.removeItem('THREAD_ID')
|
|
220
|
+
const state = this.state
|
|
221
|
+
state.threadId = null
|
|
222
|
+
|
|
223
|
+
// Clear name and initial question
|
|
224
|
+
state.messages = []
|
|
225
|
+
state.name = null
|
|
226
|
+
state.question = null
|
|
227
|
+
|
|
228
|
+
// Show feedback view
|
|
229
|
+
this._feedback()
|
|
230
|
+
|
|
231
|
+
// Clear timeout
|
|
232
|
+
this._resetTimeout()
|
|
233
|
+
|
|
234
|
+
// End chat if still open
|
|
235
|
+
const status = state.status
|
|
236
|
+
if (status && status !== 'closed') {
|
|
237
|
+
this.thread.endChat()
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_addDomEvents () {
|
|
242
|
+
// Button events
|
|
243
|
+
const container = this.panel.container
|
|
244
|
+
container.addEventListener('click', e => {
|
|
245
|
+
if (e.target.hasAttribute('data-wc-back-btn')) {
|
|
246
|
+
e.preventDefault()
|
|
247
|
+
this._closeChat(e)
|
|
248
|
+
}
|
|
249
|
+
if (e.target.hasAttribute('data-wc-hide-btn')) {
|
|
250
|
+
e.preventDefault()
|
|
251
|
+
this._closeChat(e)
|
|
252
|
+
}
|
|
253
|
+
if (e.target.hasAttribute('data-wc-close-btn')) {
|
|
254
|
+
e.preventDefault()
|
|
255
|
+
this._closeChat(e)
|
|
256
|
+
}
|
|
257
|
+
if (e.target.hasAttribute('data-wc-start-btn')) {
|
|
258
|
+
e.preventDefault()
|
|
259
|
+
this._start(e)
|
|
260
|
+
}
|
|
261
|
+
if (e.target.hasAttribute('data-wc-prechat-back-btn')) {
|
|
262
|
+
e.preventDefault()
|
|
263
|
+
this._prechat(e)
|
|
264
|
+
}
|
|
265
|
+
if (e.target.hasAttribute('data-wc-start-back-btn')) {
|
|
266
|
+
e.preventDefault()
|
|
267
|
+
this._start(e)
|
|
268
|
+
}
|
|
269
|
+
if (e.target.hasAttribute('data-wc-request-chat-btn')) {
|
|
270
|
+
e.preventDefault()
|
|
271
|
+
this._validatePrechat(this._startChat.bind(this))
|
|
272
|
+
}
|
|
273
|
+
if (e.target.hasAttribute('data-wc-end-btn')) {
|
|
274
|
+
e.preventDefault()
|
|
275
|
+
this._endChat()
|
|
276
|
+
}
|
|
277
|
+
if (e.target.hasAttribute('data-wc-resume-btn')) {
|
|
278
|
+
e.preventDefault()
|
|
279
|
+
this._resumeChat()
|
|
280
|
+
}
|
|
281
|
+
if (e.target.hasAttribute('data-wc-confirm-end-btn')) {
|
|
282
|
+
e.preventDefault()
|
|
283
|
+
this._confirmEndChat()
|
|
284
|
+
}
|
|
285
|
+
if (e.target.hasAttribute('data-wc-feedback-btn')) {
|
|
286
|
+
e.preventDefault()
|
|
287
|
+
this._feedback()
|
|
288
|
+
}
|
|
289
|
+
if (e.target.hasAttribute('data-wc-submit-feedback-btn')) {
|
|
290
|
+
e.preventDefault()
|
|
291
|
+
this._validateFeedback(this._submitFeedback.bind(this))
|
|
292
|
+
}
|
|
293
|
+
if (e.target.hasAttribute('data-wc-settings-btn')) {
|
|
294
|
+
e.preventDefault()
|
|
295
|
+
this._settings()
|
|
296
|
+
}
|
|
297
|
+
if (e.target.hasAttribute('data-wc-transcript-btn')) {
|
|
298
|
+
e.preventDefault()
|
|
299
|
+
this._download()
|
|
300
|
+
}
|
|
301
|
+
if (e.target.hasAttribute('data-wc-save-settings-btn')) {
|
|
302
|
+
e.preventDefault()
|
|
303
|
+
this._saveSettings(e.target)
|
|
304
|
+
}
|
|
305
|
+
if (e.target.hasAttribute('data-wc-cancel-settings-btn')) {
|
|
306
|
+
e.preventDefault()
|
|
307
|
+
this._resumeChat()
|
|
308
|
+
}
|
|
309
|
+
if (e.target.hasAttribute('data-wc-error-summary-link')) {
|
|
310
|
+
e.preventDefault()
|
|
311
|
+
const id = e.target.href.slice(e.target.href.indexOf('#') + 1)
|
|
312
|
+
document.getElementById(id).focus()
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
container.addEventListener('keydown', e => {
|
|
316
|
+
// Send keystroke event
|
|
317
|
+
if (e.target.hasAttribute('data-wc-textbox')) {
|
|
318
|
+
this._handleSendKeystrokeEvent()
|
|
319
|
+
}
|
|
320
|
+
// Prevent scroll chaining
|
|
321
|
+
if (e.target.hasAttribute('data-wc-body')) {
|
|
322
|
+
const m = e.target
|
|
323
|
+
const isBottom = m.scrollTop === (m.scrollHeight - m.offsetHeight)
|
|
324
|
+
const isTop = m.scrollTop <= 0
|
|
325
|
+
if ((e.key === 'ArrowUp' && isTop) || (e.key === 'ArrowDown' && isBottom)) {
|
|
326
|
+
e.preventDefault()
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
// Reset timeout event
|
|
331
|
+
container.addEventListener('keyup', e => {
|
|
332
|
+
if (e.target.hasAttribute('data-wc-textbox') && this.timeout) {
|
|
333
|
+
this._resetTimeout()
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
// Form submit
|
|
337
|
+
container.addEventListener('submit', e => {
|
|
338
|
+
// Start chat form
|
|
339
|
+
if (e.target.hasAttribute('data-wc-start-form')) {
|
|
340
|
+
e.preventDefault()
|
|
341
|
+
this._validatePrechat(this._startChat.bind(this))
|
|
342
|
+
}
|
|
343
|
+
// Send message form
|
|
344
|
+
if (e.target.hasAttribute('data-wc-message-form')) {
|
|
345
|
+
e.preventDefault()
|
|
346
|
+
this._sendMessage()
|
|
347
|
+
}
|
|
348
|
+
}, true)
|
|
349
|
+
// Close dialog
|
|
350
|
+
container.addEventListener('keyup', e => {
|
|
351
|
+
if (this.state.isOpen && (e.key === 'Escape' || e.key === 'Esc')) {
|
|
352
|
+
this._closeChat(e)
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_validatePrechat (successCb) {
|
|
358
|
+
const name = document.getElementById('name')
|
|
359
|
+
const question = document.getElementById('question')
|
|
360
|
+
const state = this.state
|
|
361
|
+
state.name = name.value
|
|
362
|
+
state.question = question ? question.value : ''
|
|
363
|
+
|
|
364
|
+
// Validation error
|
|
365
|
+
const isErrorName = state.name.length <= 0
|
|
366
|
+
const isErrorQuestion = state.question.length <= 0 || state.question.length > 500
|
|
367
|
+
|
|
368
|
+
if (isErrorName || isErrorQuestion) {
|
|
369
|
+
const error = {
|
|
370
|
+
nameEmpty: isErrorName,
|
|
371
|
+
questionEmpty: state.question.length <= 0,
|
|
372
|
+
questionExceeded: state.question.length > 500
|
|
373
|
+
}
|
|
374
|
+
const panel = this.panel
|
|
375
|
+
panel.update(state, error)
|
|
376
|
+
|
|
377
|
+
// Move focus to error summary
|
|
378
|
+
const summary = panel.container.querySelector('[data-wc-error-summary]')
|
|
379
|
+
summary.focus()
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
successCb()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_validateFeedback (successCb) {
|
|
387
|
+
const panel = this.panel
|
|
388
|
+
const satisfaction = panel.container.querySelector('input[name="satisfaction"]:checked')
|
|
389
|
+
|
|
390
|
+
// Missing satisfaction selection
|
|
391
|
+
if (!satisfaction) {
|
|
392
|
+
const error = {
|
|
393
|
+
satisfactionEmpty: true
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Update panel
|
|
397
|
+
const state = this.state
|
|
398
|
+
state.view = 'FEEDBACK'
|
|
399
|
+
panel.update(state, error)
|
|
400
|
+
|
|
401
|
+
// Move focus to error summary
|
|
402
|
+
const summary = panel.container.querySelector('[data-wc-error-summary]')
|
|
403
|
+
summary.focus()
|
|
404
|
+
|
|
405
|
+
// Reset view
|
|
406
|
+
state.view = 'PRECHAT'
|
|
407
|
+
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Update SDK
|
|
412
|
+
const thread = this.thread
|
|
413
|
+
const improvements = panel.container.querySelector('textarea[name="improvements"]')
|
|
414
|
+
thread.setCustomField('rating', satisfaction.value)
|
|
415
|
+
thread.setCustomField('comment', improvements.value)
|
|
416
|
+
|
|
417
|
+
successCb()
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
_openChat (e, instigatorId) {
|
|
421
|
+
console.log('_openChat', instigatorId)
|
|
422
|
+
|
|
423
|
+
const state = this.state
|
|
424
|
+
|
|
425
|
+
// Return if already open
|
|
426
|
+
if (state.isOpen) {
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
state.instigatorId = instigatorId
|
|
431
|
+
state.isOpen = true
|
|
432
|
+
|
|
433
|
+
// Reset timeout
|
|
434
|
+
if (this.timeout) {
|
|
435
|
+
this._resetTimeout()
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Conditionally push new state to history
|
|
439
|
+
const isBtn = e instanceof window.PointerEvent || e instanceof window.MouseEvent || e instanceof window.KeyboardEvent
|
|
440
|
+
if (isBtn) {
|
|
441
|
+
e.preventDefault()
|
|
442
|
+
state.pushState('PRECHAT')
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Create panel
|
|
446
|
+
const panel = this.panel
|
|
447
|
+
if (!panel.container) {
|
|
448
|
+
// Create panel
|
|
449
|
+
panel.create(state, this._addDomEvents.bind(this))
|
|
450
|
+
panel.update(state)
|
|
451
|
+
|
|
452
|
+
// Mark messages as seen
|
|
453
|
+
const thread = this.thread
|
|
454
|
+
if (thread && state.view === 'OPEN') {
|
|
455
|
+
thread.lastMessageSeen()
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Hide sticky availability
|
|
460
|
+
this.availability.scroll(state)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
_closeChat (e) {
|
|
464
|
+
console.log('_closeChat')
|
|
465
|
+
|
|
466
|
+
const state = this.state
|
|
467
|
+
|
|
468
|
+
// History back
|
|
469
|
+
const isBtn = e instanceof window.PointerEvent || e instanceof window.MouseEvent || e instanceof window.KeyboardEvent
|
|
470
|
+
if (isBtn && state.isBack) {
|
|
471
|
+
state.back()
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
state.isOpen = false
|
|
476
|
+
|
|
477
|
+
// Reset timeout
|
|
478
|
+
if (this.timeout) {
|
|
479
|
+
this._resetTimeout()
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const panel = this.panel
|
|
483
|
+
const instigatorId = state.instigatorId
|
|
484
|
+
|
|
485
|
+
// Toggle skiplink
|
|
486
|
+
const hasSkip = state.view === 'OPEN' || state.view === 'END'
|
|
487
|
+
this.skiplink.toggle(hasSkip)
|
|
488
|
+
|
|
489
|
+
// Remove panel
|
|
490
|
+
if (panel.container) {
|
|
491
|
+
panel.setAttributes(state)
|
|
492
|
+
panel.container = panel.container.remove()
|
|
493
|
+
state.replaceState()
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Update availability content
|
|
497
|
+
this.availability.update(state)
|
|
498
|
+
this.availability.scroll(state)
|
|
499
|
+
|
|
500
|
+
Keyboard.toggleInert()
|
|
501
|
+
|
|
502
|
+
// Move focus back to instigator
|
|
503
|
+
if (instigatorId) {
|
|
504
|
+
const instigator = document.getElementById(instigatorId)
|
|
505
|
+
if (instigator) {
|
|
506
|
+
document.getElementById(instigatorId).focus()
|
|
507
|
+
}
|
|
508
|
+
delete state.instigatorId
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
_timeoutChat () {
|
|
513
|
+
console.log('_timeoutChat')
|
|
514
|
+
|
|
515
|
+
// Close thread
|
|
516
|
+
window.localStorage.removeItem('THREAD_ID')
|
|
517
|
+
|
|
518
|
+
// Show timeout view
|
|
519
|
+
const state = this.state
|
|
520
|
+
state.view = 'TIMEOUT'
|
|
521
|
+
state.messages = []
|
|
522
|
+
this.panel.update(state)
|
|
523
|
+
|
|
524
|
+
// Clear timeout
|
|
525
|
+
this._resetTimeout()
|
|
526
|
+
|
|
527
|
+
// End thread if still open
|
|
528
|
+
const status = state.status
|
|
529
|
+
if (status && status !== 'closed') {
|
|
530
|
+
// *** Bug: Promise is void, why?
|
|
531
|
+
this.thread.endChat()
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Dont persist view, set to prechat
|
|
535
|
+
state.view = 'PRECHAT'
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
_prechat () {
|
|
539
|
+
const state = this.state
|
|
540
|
+
state.view = 'PRECHAT'
|
|
541
|
+
console.log('_prechat')
|
|
542
|
+
const panel = this.panel
|
|
543
|
+
panel.update(state)
|
|
544
|
+
panel.container.focus()
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
_start () {
|
|
548
|
+
const state = this.state
|
|
549
|
+
state.view = 'START'
|
|
550
|
+
console.log('_start')
|
|
551
|
+
const panel = this.panel
|
|
552
|
+
panel.update(state)
|
|
553
|
+
panel.container.focus()
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
_unavailable () {
|
|
557
|
+
const state = this.state
|
|
558
|
+
state.view = 'UNAVAILABLE'
|
|
559
|
+
console.log('_unavailable')
|
|
560
|
+
const panel = this.panel
|
|
561
|
+
panel.update(state)
|
|
562
|
+
panel.container.focus()
|
|
563
|
+
|
|
564
|
+
// Dont persist view, set to prechat
|
|
565
|
+
state.view = 'PRECHAT'
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
_endChat () {
|
|
569
|
+
const state = this.state
|
|
570
|
+
state.view = 'END'
|
|
571
|
+
console.log('_endChat')
|
|
572
|
+
this.panel.update(state)
|
|
573
|
+
const btn = document.querySelector('[data-wc-confirm-end-btn]')
|
|
574
|
+
btn.focus()
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
_resumeChat () {
|
|
578
|
+
const state = this.state
|
|
579
|
+
state.view = 'OPEN'
|
|
580
|
+
console.log('_resumeChat')
|
|
581
|
+
const panel = this.panel
|
|
582
|
+
panel.update(state)
|
|
583
|
+
panel.container.focus()
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
_feedback () {
|
|
587
|
+
console.log('_feedback')
|
|
588
|
+
const state = this.state
|
|
589
|
+
state.view = 'FEEDBACK'
|
|
590
|
+
this.panel.update(state)
|
|
591
|
+
const btn = document.querySelector('[data-wc-submit-feedback-btn]')
|
|
592
|
+
btn.focus()
|
|
593
|
+
|
|
594
|
+
// Dont persist view, set to prechat
|
|
595
|
+
state.view = 'PRECHAT'
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
_submitFeedback () {
|
|
599
|
+
console.log('_submitFeedback')
|
|
600
|
+
const state = this.state
|
|
601
|
+
state.view = 'FINISH'
|
|
602
|
+
this.panel.update(state)
|
|
603
|
+
const btn = document.querySelector('[role="button"][data-wc-close-btn]')
|
|
604
|
+
btn.focus()
|
|
605
|
+
|
|
606
|
+
// Dont persist view, set to prechat
|
|
607
|
+
state.view = 'PRECHAT'
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
_settings () {
|
|
611
|
+
const state = this.state
|
|
612
|
+
state.view = 'SETTINGS'
|
|
613
|
+
console.log('_settings')
|
|
614
|
+
const panel = this.panel
|
|
615
|
+
panel.update(state)
|
|
616
|
+
panel.container.focus()
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
_saveSettings () {
|
|
620
|
+
// Toggle settings
|
|
621
|
+
const state = this.state
|
|
622
|
+
const panel = this.panel
|
|
623
|
+
const audio = panel.container.querySelector('#audio')
|
|
624
|
+
const scroll = panel.container.querySelector('#scroll')
|
|
625
|
+
state.hasAudio = audio.checked
|
|
626
|
+
state.isScroll = scroll.checked
|
|
627
|
+
|
|
628
|
+
// Update local storage
|
|
629
|
+
window.localStorage.setItem('SETTINGS', [state.hasAudio, state.isScroll])
|
|
630
|
+
|
|
631
|
+
// Return to open view
|
|
632
|
+
state.view = 'OPEN'
|
|
633
|
+
console.log('_resumeChat')
|
|
634
|
+
panel.update(state)
|
|
635
|
+
panel.container.focus()
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
_mergeMessages (batch) {
|
|
639
|
+
// Create array of message objects
|
|
640
|
+
const messages = []
|
|
641
|
+
for (let i = 0; i < batch.length; i++) {
|
|
642
|
+
messages.push({
|
|
643
|
+
id: batch[i].id,
|
|
644
|
+
text: Utils.parseMessage(batch[i].messageContent.text),
|
|
645
|
+
user: batch[i].authorEndUserIdentity ? batch[i].authorEndUserIdentity.fullName.trim() : null,
|
|
646
|
+
assignee: batch[i].authorUser ? batch[i].authorUser.firstName : null,
|
|
647
|
+
date: Utils.formatDate(new Date(batch[i].createdAt)),
|
|
648
|
+
createdAt: new Date(batch[i].createdAt),
|
|
649
|
+
direction: batch[i].direction
|
|
650
|
+
})
|
|
651
|
+
}
|
|
652
|
+
messages.reverse()
|
|
653
|
+
|
|
654
|
+
// Merge with existing messafes
|
|
655
|
+
this.state.messages = messages.concat(this.state.messages)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
_sendMessage (e) {
|
|
659
|
+
console.log('_sendMessage')
|
|
660
|
+
|
|
661
|
+
// Do we need to check here?
|
|
662
|
+
const message = document.getElementById('message')
|
|
663
|
+
|
|
664
|
+
if (!(message && message.value.length)) {
|
|
665
|
+
return
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// *** Bug: Some times this results in inconsitent data error?
|
|
669
|
+
try {
|
|
670
|
+
this.thread.sendTextMessage(message.value.trim())
|
|
671
|
+
} catch (err) {
|
|
672
|
+
console.log(err)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
_resetTimeout () {
|
|
677
|
+
// Return if not set
|
|
678
|
+
if (Config.timeout <= 0) {
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Clear existing timeout
|
|
683
|
+
clearTimeout(this.timeout)
|
|
684
|
+
clearInterval(this.countdown)
|
|
685
|
+
|
|
686
|
+
// We don't have an open thread
|
|
687
|
+
const state = this.state
|
|
688
|
+
const status = state.status
|
|
689
|
+
const view = state.view
|
|
690
|
+
if (!status || status === 'closed' || view === 'TIMEOUT') {
|
|
691
|
+
console.log('Timeout stopped...')
|
|
692
|
+
return
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Remove timeout cancel button
|
|
696
|
+
if (view === 'OPEN') {
|
|
697
|
+
this.panel.toggleTimeout(false)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Restart timeout
|
|
701
|
+
console.log('Timeout started...')
|
|
702
|
+
this.timeout = setTimeout(this._handleTimeout.bind(this), Config.timeout * 1000)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
_download () {
|
|
706
|
+
console.log('download')
|
|
707
|
+
|
|
708
|
+
// Need to test for accessibility of this approach
|
|
709
|
+
const messages = this.state.messages
|
|
710
|
+
const transcript = new Transcript(messages)
|
|
711
|
+
const data = transcript.data
|
|
712
|
+
const anchor = document.createElement('a')
|
|
713
|
+
anchor.className = 'govuk-visually-hidden'
|
|
714
|
+
anchor.setAttribute('href', data)
|
|
715
|
+
anchor.setAttribute('download', 'transcript.txt')
|
|
716
|
+
document.body.appendChild(anchor)
|
|
717
|
+
anchor.click()
|
|
718
|
+
document.body.removeChild(anchor)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
_alertAT (text, politeness = 'polite') {
|
|
722
|
+
// Get reference to live element
|
|
723
|
+
const el = document.querySelector('[data-wc-panel-live], [data-wc-availability-live]')
|
|
724
|
+
el.setAttribute('aria-live', politeness)
|
|
725
|
+
el.innerHTML = `<p>${text}</p>`
|
|
726
|
+
setTimeout(() => { el.innerHTML = '' }, 1000)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
//
|
|
730
|
+
// Event handlers
|
|
731
|
+
//
|
|
732
|
+
|
|
733
|
+
// _handleConsumerAuthorizedEvent (e) {
|
|
734
|
+
// console.log('_handleConsumerAuthorizedEvent')
|
|
735
|
+
// }
|
|
736
|
+
|
|
737
|
+
async _handleLivechatRecoveredEvent (e) {
|
|
738
|
+
console.log('_handleLivechatRecoveredEvent')
|
|
739
|
+
|
|
740
|
+
// Set thread status
|
|
741
|
+
const state = this.state
|
|
742
|
+
const status = e.detail.data.contact.status
|
|
743
|
+
state.status = status
|
|
744
|
+
|
|
745
|
+
// Calculate elapsed time
|
|
746
|
+
let messages = e.detail.data.messages
|
|
747
|
+
const timeout = Config.timeout
|
|
748
|
+
const countdown = Config.countdown
|
|
749
|
+
const latestDatetime = new Date(messages[0].createdAt)
|
|
750
|
+
const elapsed = Math.abs((new Date()) - latestDatetime) / 1000
|
|
751
|
+
const isExpired = timeout > 0 && (timeout + countdown) - elapsed <= 0
|
|
752
|
+
|
|
753
|
+
// End chat if elapsed time outside allowance
|
|
754
|
+
if (isExpired) {
|
|
755
|
+
window.localStorage.removeItem('THREAD_ID')
|
|
756
|
+
state.view = 'TIMEOUT'
|
|
757
|
+
if (state.status !== 'closed') {
|
|
758
|
+
// *** Bug: Promise is void, why?
|
|
759
|
+
this.thread.endChat()
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Ready
|
|
763
|
+
document.dispatchEvent(this.livechatReady)
|
|
764
|
+
return
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Set assignee and unseen message count
|
|
768
|
+
state.view = 'OPEN'
|
|
769
|
+
const assignee = e.detail.data.inboxAssignee
|
|
770
|
+
state.assignee = assignee ? assignee.nickname || assignee.firstName : null
|
|
771
|
+
const unseen = e.detail.data.thread.unseenByEndUserMessagesCount || 0
|
|
772
|
+
state.unseen = unseen
|
|
773
|
+
|
|
774
|
+
// Conditionally mark messages as seen
|
|
775
|
+
if (state.isOpen) {
|
|
776
|
+
this.thread.lastMessageSeen()
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Recursively merge messages with previous messages
|
|
780
|
+
while (messages.length) {
|
|
781
|
+
this._mergeMessages(messages)
|
|
782
|
+
try {
|
|
783
|
+
const response = await this.thread.loadMoreMessages()
|
|
784
|
+
messages = response.data.messages
|
|
785
|
+
} catch (err) {
|
|
786
|
+
console.log(err)
|
|
787
|
+
messages = []
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
console.log('All messages loaded')
|
|
791
|
+
|
|
792
|
+
// Remove duplicates?? LoadMoreMessages doesnt always get a unique set?
|
|
793
|
+
state.messages = [...new Map(state.messages.map(m => [m.id, m])).values()]
|
|
794
|
+
|
|
795
|
+
// Sort on date
|
|
796
|
+
state.messages = Utils.sortMessages(state.messages)
|
|
797
|
+
|
|
798
|
+
// Add html to message objects
|
|
799
|
+
state.messages = Utils.addMessagesHtml(state.messages)
|
|
800
|
+
|
|
801
|
+
// Ready
|
|
802
|
+
document.dispatchEvent(this.livechatReady)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
_handleReadyEvent (e) {
|
|
806
|
+
console.log('_handleReadyEvent')
|
|
807
|
+
|
|
808
|
+
// Start/reset timeout
|
|
809
|
+
this._resetTimeout()
|
|
810
|
+
|
|
811
|
+
// Poll availability
|
|
812
|
+
let isInit = true
|
|
813
|
+
const interval = Config.poll > 0 ? Config.poll * 1000 : 0
|
|
814
|
+
const state = this.state
|
|
815
|
+
const panel = this.panel
|
|
816
|
+
const availability = this.availability
|
|
817
|
+
const availabilityEndPoint = this.availabilityEndpoint
|
|
818
|
+
|
|
819
|
+
Utils.poll({
|
|
820
|
+
fn: async () => {
|
|
821
|
+
console.log('Polling availability')
|
|
822
|
+
state.availability = await Utils.getAvailability(availabilityEndPoint)
|
|
823
|
+
|
|
824
|
+
// Set view
|
|
825
|
+
if (state.view !== 'OPEN') {
|
|
826
|
+
state.view = state.availability === 'AVAILABLE' ? 'PRECHAT' : 'UNAVAILABLE'
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Update panel on page load only
|
|
830
|
+
if (isInit) {
|
|
831
|
+
panel.update(state)
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Update availability on each poll
|
|
835
|
+
availability.update(state)
|
|
836
|
+
availability.scroll(state)
|
|
837
|
+
|
|
838
|
+
// Remove init flag
|
|
839
|
+
isInit = false
|
|
840
|
+
},
|
|
841
|
+
interval
|
|
842
|
+
})
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
_handleContactStatusChangedEvent (e) {
|
|
846
|
+
console.log('_handleContactStatusChangedEvent', this.state.view)
|
|
847
|
+
|
|
848
|
+
const state = this.state
|
|
849
|
+
state.status = e.detail.data.case.status
|
|
850
|
+
const panel = this.panel
|
|
851
|
+
|
|
852
|
+
// ***Todo: Need a more robust way to determine who instigated this
|
|
853
|
+
const isEndedByAdviser = state.messages.length
|
|
854
|
+
|
|
855
|
+
// Currently only responding to a closed case
|
|
856
|
+
if (state.status === 'closed') {
|
|
857
|
+
// Instigated by adviser
|
|
858
|
+
if (isEndedByAdviser && state.view === 'OPEN') {
|
|
859
|
+
panel.updateHeader(state)
|
|
860
|
+
|
|
861
|
+
// Alert assistive technology
|
|
862
|
+
const el = document.querySelector('[data-wc-status]')
|
|
863
|
+
const text = el ? el.innerHTML : ''
|
|
864
|
+
this._alertAT(text)
|
|
865
|
+
|
|
866
|
+
// Start/reset timeout
|
|
867
|
+
this._resetTimeout()
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Instigated by end user
|
|
871
|
+
if (!isEndedByAdviser) {
|
|
872
|
+
this._updateAvailability()
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
_handleAssignedAgentChangedEvent (e) {
|
|
878
|
+
console.log('_handleAssignedAgentChangedEvent')
|
|
879
|
+
|
|
880
|
+
const assignee = e.detail.data.inboxAssignee
|
|
881
|
+
const state = this.state
|
|
882
|
+
state.assignee = assignee ? assignee.nickname || assignee.firstName : null
|
|
883
|
+
|
|
884
|
+
// ***Limitation: Adviser availability doesn't fire an event in the SDK
|
|
885
|
+
if (assignee && !['AVAILABLE', 'EXISTING'].includes(state.availability)) {
|
|
886
|
+
state.availability = 'EXISTING'
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Update header
|
|
890
|
+
if (state.view === 'OPEN') {
|
|
891
|
+
const panel = this.panel
|
|
892
|
+
panel.updateHeader(state)
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Alert assistive technology
|
|
896
|
+
const el = document.querySelector('[data-wc-status]')
|
|
897
|
+
const text = el ? el.innerHTML : ''
|
|
898
|
+
this._alertAT(text)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
_handleContactCreatedEvent (e) {
|
|
902
|
+
console.log('_handleContactCreatedEvent')
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
_handleMessageCreatedEvent (e) {
|
|
906
|
+
console.log('_handleMessageCreatedEvent')
|
|
907
|
+
|
|
908
|
+
const state = this.state
|
|
909
|
+
const response = e.detail.data.message
|
|
910
|
+
const assignee = response.authorUser ? response.authorUser.firstName : null
|
|
911
|
+
const user = response.authorEndUserIdentity ? response.authorEndUserIdentity.fullName.trim() : null
|
|
912
|
+
const direction = response.direction.toLowerCase()
|
|
913
|
+
state.status = e.detail.data.case.status
|
|
914
|
+
state.assignee = assignee
|
|
915
|
+
|
|
916
|
+
// Update messages array
|
|
917
|
+
const message = {
|
|
918
|
+
id: response.id,
|
|
919
|
+
text: Utils.parseMessage(response.messageContent.text),
|
|
920
|
+
user,
|
|
921
|
+
assignee,
|
|
922
|
+
date: Utils.formatDate(new Date(response.createdAt)),
|
|
923
|
+
createdAt: new Date(response.createdAt),
|
|
924
|
+
direction
|
|
925
|
+
}
|
|
926
|
+
state.messages.push(message)
|
|
927
|
+
|
|
928
|
+
// Add html to messages
|
|
929
|
+
state.messages = Utils.addMessagesHtml(state.messages)
|
|
930
|
+
|
|
931
|
+
// Update unseen count
|
|
932
|
+
if (direction === 'outbound' && !state.isOpen) {
|
|
933
|
+
state.unseen += 1
|
|
934
|
+
this.availability.update(state)
|
|
935
|
+
this.availability.scroll(state)
|
|
936
|
+
|
|
937
|
+
// Alert assistive technology
|
|
938
|
+
const text = `${state.unseen} new message${state.unseen > 1 ? 's' : ''}`
|
|
939
|
+
this._alertAT(text)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Clear input
|
|
943
|
+
const textbox = document.querySelector('[data-wc-textbox]')
|
|
944
|
+
if (textbox && direction === 'inbound') {
|
|
945
|
+
textbox.value = ''
|
|
946
|
+
textbox.style.height = 'auto'
|
|
947
|
+
const event = new Event('change')
|
|
948
|
+
textbox.dispatchEvent(event)
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Update messages
|
|
952
|
+
const panel = this.panel
|
|
953
|
+
if (state.view === 'START') {
|
|
954
|
+
// Update panel if new question
|
|
955
|
+
state.view = 'OPEN'
|
|
956
|
+
panel.update(state)
|
|
957
|
+
|
|
958
|
+
// Set focus to panel
|
|
959
|
+
panel.container.focus()
|
|
960
|
+
} else if (state.view === 'OPEN' && state.isOpen) {
|
|
961
|
+
// Add message if existing thread
|
|
962
|
+
panel.addMessage(state, message)
|
|
963
|
+
|
|
964
|
+
// Mark as seen
|
|
965
|
+
if (this.thread) {
|
|
966
|
+
this.thread.lastMessageSeen()
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Alert assistive technology
|
|
970
|
+
const author = message.direction === 'outbound' ? message.assignee : 'You'
|
|
971
|
+
const text = `${author} said: ${message.text}`
|
|
972
|
+
this._alertAT(text)
|
|
973
|
+
|
|
974
|
+
// Set focus to message field
|
|
975
|
+
const el = document.getElementById('message')
|
|
976
|
+
if (el && direction === 'inbound') {
|
|
977
|
+
el.focus()
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Play notification sound
|
|
982
|
+
if (state.hasAudio && direction === 'outbound') {
|
|
983
|
+
const notification = this.notification
|
|
984
|
+
notification.playSound()
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Start/reset timeout
|
|
988
|
+
this._resetTimeout()
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
_handleAgentTypingEvent (e) {
|
|
992
|
+
console.log('_handleAgentTypingEvent')
|
|
993
|
+
|
|
994
|
+
// Event may fire when list is not available
|
|
995
|
+
const list = document.querySelector('[data-wc-list]')
|
|
996
|
+
if (!list) {
|
|
997
|
+
return
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// Reset timeout
|
|
1001
|
+
this._resetTimeout()
|
|
1002
|
+
|
|
1003
|
+
// Toggle agent typing
|
|
1004
|
+
const state = this.state
|
|
1005
|
+
const panel = this.panel
|
|
1006
|
+
const isTyping = e.type === 'AgentTypingStarted'
|
|
1007
|
+
state.assignee = e.detail.data.user.firstName
|
|
1008
|
+
panel.toggleAgentTyping(state, isTyping)
|
|
1009
|
+
|
|
1010
|
+
// ***Limitation: Adviser availability doesn't fire an event in the SDK
|
|
1011
|
+
// ***Bug: CaseInboxAssigneeChanged/AssignedAgentChanged not always firing
|
|
1012
|
+
if (!state.assignee || !['AVAILABLE', 'EXISTING'].includes(state.availability)) {
|
|
1013
|
+
state.availability = 'EXISTING'
|
|
1014
|
+
panel.updateHeader(state)
|
|
1015
|
+
|
|
1016
|
+
// Alert assistive technology
|
|
1017
|
+
const el = document.querySelector('[data-wc-status]')
|
|
1018
|
+
const text = el ? el.innerHTML : ''
|
|
1019
|
+
this._alertAT(text)
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Alert assistive technology
|
|
1023
|
+
if (isTyping) {
|
|
1024
|
+
this._alertAT(`${state.assignee} is typing`)
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
_handleMessageSeenByEndUserEvent (e) {
|
|
1029
|
+
console.log('_handleMessageSeenByEndUserEvent')
|
|
1030
|
+
|
|
1031
|
+
// Clear unseen count
|
|
1032
|
+
const state = this.state
|
|
1033
|
+
state.unseen = 0
|
|
1034
|
+
this.availability.update(state)
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
_handleTimeout (e) {
|
|
1038
|
+
console.log('Countdown starting...')
|
|
1039
|
+
|
|
1040
|
+
// Show timeout notification
|
|
1041
|
+
const state = this.state
|
|
1042
|
+
const panel = this.panel
|
|
1043
|
+
if (state.view === 'OPEN') {
|
|
1044
|
+
panel.toggleTimeout(state, true)
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Set countdown
|
|
1048
|
+
const element = panel.container.querySelector('[data-wc-countdown]')
|
|
1049
|
+
this.countdown = Utils.setCountdown(element, () => {
|
|
1050
|
+
console.log('Count down ended')
|
|
1051
|
+
if (panel.container) {
|
|
1052
|
+
this._timeoutChat()
|
|
1053
|
+
} else {
|
|
1054
|
+
console.log('Chat has timedout')
|
|
1055
|
+
}
|
|
1056
|
+
})
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
_handleSendKeystrokeEvent () {
|
|
1060
|
+
console.log('_handleSendKeystrokeEvent')
|
|
1061
|
+
|
|
1062
|
+
this.thread.keystroke(1000)
|
|
1063
|
+
setTimeout(() => {
|
|
1064
|
+
this.thread.stopTyping()
|
|
1065
|
+
}, 1000)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
_handleChatEvent (e) {
|
|
1069
|
+
console.log('Chat event: ', e)
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
export default WebChat
|