@defra/flood-webchat 0.0.1-alpha.9 → 0.0.1-beta.2

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