@defra/flood-webchat 0.0.1-beta.37 → 0.0.1-beta.38-draft

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/flood-webchat",
3
- "version": "0.0.1-beta.37",
3
+ "version": "0.0.1-beta.38-draft",
4
4
  "description": "",
5
5
  "main": "dist/server.js",
6
6
  "browser": "dist/client.js",
@@ -62,6 +62,7 @@
62
62
  "@nice-devone/nice-cxone-chat-web-sdk": "^1.3.0",
63
63
  "axios": "^1.5.0",
64
64
  "jwt-decode": "^3.1.2",
65
+ "lodash.debounce": "^4.0.8",
65
66
  "luxon": "^3.4.3",
66
67
  "querystring": "^0.2.1",
67
68
  "react": "^18.2.0",
@@ -6,9 +6,10 @@ import { SkipLink } from '../skip-link.jsx'
6
6
 
7
7
  import { useApp } from '../../store/useApp'
8
8
  import { historyPushState, historyReplaceState } from '../../lib/history.js'
9
+ import { LiveRegion } from '../live-region.jsx'
9
10
 
10
11
  export function Availability () {
11
- const { availability, isChatOpen, setChatVisibility, unseenCount, threadId, setUnseenCount, setInstigatorId } = useApp()
12
+ const { availability, isChatOpen, setChatVisibility, unseenCount, threadId, setUnseenCount, setInstigatorId, setLiveRegionText } = useApp()
12
13
 
13
14
  const buttonRef = useRef()
14
15
 
@@ -38,6 +39,16 @@ export function Availability () {
38
39
  }
39
40
  }
40
41
 
42
+ useEffect(() => {
43
+ if (!isChatOpen && unseenCount > 0) {
44
+ setLiveRegionText(`Floodline Webchat - ${unseenCount} new messages`)
45
+ }
46
+
47
+ return () => {
48
+ setLiveRegionText()
49
+ }
50
+ }, [unseenCount, isChatOpen])
51
+
41
52
  useEffect(() => {
42
53
  const onScroll = () => {
43
54
  if (!threadId) {
@@ -93,7 +104,14 @@ export function Availability () {
93
104
  data-module='govuk-button'
94
105
  >
95
106
  {TextComponent}
96
- {unseenCount > 0 && !isChatOpen ? <span className='wc-availability__unseen'>{unseenCount}</span> : null}
107
+ {unseenCount > 0 && !isChatOpen
108
+ ? (
109
+ <>
110
+ <span className='wc-availability__unseen'>{unseenCount}</span>
111
+ <LiveRegion />
112
+ </>
113
+ )
114
+ : null}
97
115
  </a>
98
116
  </div>
99
117
  </div>
@@ -7,9 +7,10 @@ import { Message } from '../message/message.jsx'
7
7
  import { useApp } from '../../store/useApp.js'
8
8
  import { useTextareaAutosize } from '../../hooks/useTextareaAutosize.js'
9
9
  import { formatTranscript } from '../../lib/transform-messages.js'
10
+ import { agentStatusHeadline } from '../../lib/agent-status-headline.js'
10
11
 
11
12
  export function Chat ({ onEndChatScreen, onSettingsScreen }) {
12
- const { availability, thread, messages, agent, agentStatus, isAgentTyping, settings, isKeyboard } = useApp()
13
+ const { availability, thread, messages, agent, agentStatus, isAgentTyping, isChatOpen, settings, isKeyboard, setLiveRegionText } = useApp()
13
14
 
14
15
  const [userMessage, setUserMessage] = useState('')
15
16
  const [focusVisibleWithin, setFocusVisibleWithin] = useState(false)
@@ -41,21 +42,39 @@ export function Chat ({ onEndChatScreen, onSettingsScreen }) {
41
42
 
42
43
  const agentName = agent?.nickname || agent?.firstName
43
44
 
44
- let connectionHeadlineText = 'Connecting to Floodline'
45
+ const connectionHeadlineText = agentStatusHeadline(availability, agentStatus, agentName)
45
46
 
46
- if (agentStatus === 'closed') {
47
- connectionHeadlineText = agentName ? `${agentName} ended the session` : 'Session ended by advisor'
48
- } else if (agentStatus) {
49
- if (agentName) {
50
- connectionHeadlineText = `You are speaking with ${agentName}`
51
- } else {
52
- connectionHeadlineText = 'No advisers currently available'
47
+ useEffect(() => {
48
+ if (connectionHeadlineText) {
49
+ setLiveRegionText(`Floodline Webchat - ${connectionHeadlineText}`)
53
50
  }
54
- }
55
51
 
56
- if (availability === 'UNAVAILABLE') {
57
- connectionHeadlineText = 'Webchat is not currently available'
58
- }
52
+ return () => {
53
+ setLiveRegionText()
54
+ }
55
+ }, [connectionHeadlineText])
56
+
57
+ useEffect(() => {
58
+ if (isAgentTyping && isChatOpen) {
59
+ setLiveRegionText(`${agentName} is typing...`)
60
+ }
61
+
62
+ return () => {
63
+ setLiveRegionText()
64
+ }
65
+ }, [isAgentTyping, isChatOpen])
66
+
67
+ useEffect(() => {
68
+ const lastAgentMessage = messages[messages.length - 1]
69
+
70
+ if (lastAgentMessage?.direction === 'outbound') {
71
+ setLiveRegionText(`${agentName} said: ${lastAgentMessage.text}`)
72
+ }
73
+
74
+ return () => {
75
+ setLiveRegionText()
76
+ }
77
+ }, [messages])
59
78
 
60
79
  const sendMessage = () => {
61
80
  if (messageRef.current.value.length === 0) {
@@ -63,7 +82,9 @@ export function Chat ({ onEndChatScreen, onSettingsScreen }) {
63
82
  }
64
83
 
65
84
  try {
66
- thread.sendTextMessage(messageRef.current.value.trim())
85
+ const message = messageRef.current.value.trim()
86
+ thread.sendTextMessage(message)
87
+ setLiveRegionText(`You said: ${message}`)
67
88
  } catch (err) {
68
89
  console.log('[Chat Error] sendMessage', err)
69
90
  }
@@ -0,0 +1,55 @@
1
+ import React, { useEffect, useState, useCallback } from 'react'
2
+ import debounce from 'lodash.debounce'
3
+ import { useApp } from '../store/useApp'
4
+
5
+ export const LiveRegion = () => {
6
+ const { agentStatus, liveRegionText, setLiveRegionText } = useApp()
7
+
8
+ const [textA, setTextA] = useState()
9
+ const [textB, setTextB] = useState()
10
+
11
+ const isSessionEnded = agentStatus === 'closed' || agentStatus === null
12
+
13
+ const clearText = () => {
14
+ setTextA()
15
+ setTextB()
16
+ }
17
+
18
+ const setText = useCallback(debounce(text => {
19
+ if (textA) {
20
+ setTextB(text)
21
+ } else {
22
+ setTextA(text)
23
+ }
24
+ }, 2000), [textA, textB])
25
+
26
+ useEffect(() => {
27
+ console.log('liveRegionText', liveRegionText)
28
+
29
+ if (liveRegionText) {
30
+ clearText()
31
+ setText(liveRegionText)
32
+ }
33
+
34
+ if (isSessionEnded) {
35
+ clearText()
36
+ setLiveRegionText()
37
+ }
38
+
39
+ return () => {
40
+ clearText()
41
+ setLiveRegionText()
42
+ }
43
+ }, [liveRegionText])
44
+
45
+ return (
46
+ <>
47
+ <div className='wc-live' aria-live='polite' aria-atomic='true'>
48
+ {textA}
49
+ </div>
50
+ <div className='wc-live' aria-live='polite' aria-atomic='true'>
51
+ {textB}
52
+ </div>
53
+ </>
54
+ )
55
+ }
@@ -9,6 +9,7 @@ import { Unavailable } from '../screens/unavailable.jsx'
9
9
  import { EndChat } from '../screens/end-chat.jsx'
10
10
  import { Settings } from '../screens/settings.jsx'
11
11
  import { Feedback } from '../screens/feedback.jsx'
12
+ import { LiveRegion } from '../live-region.jsx'
12
13
 
13
14
  import { useApp } from '../../store/useApp.js'
14
15
  import { useChatSdk } from '../../store/useChatSdk.js'
@@ -136,6 +137,7 @@ export function Panel () {
136
137
  <div className='wc-panel__inner'>
137
138
  {ScreenComponent}
138
139
  </div>
140
+ <LiveRegion />
139
141
  </div>
140
142
  )
141
143
 
@@ -1,6 +1,7 @@
1
1
  import React, { useEffect } from 'react'
2
2
  import { PanelHeader } from '../panel/panel-header.jsx'
3
3
  import { useApp } from '../../store/useApp.js'
4
+ import { historyReplaceState } from '../../lib/history.js'
4
5
 
5
6
  export function EndChat ({ onChatScreen, onEndChatConfirm }) {
6
7
  const { setCustomerId, setThreadId, setMessages, agentStatus, thread, setUnseenCount, isKeyboard } = useApp()
@@ -19,6 +20,7 @@ export function EndChat ({ onChatScreen, onEndChatConfirm }) {
19
20
  thread.endChat()
20
21
  }
21
22
 
23
+ historyReplaceState()
22
24
  onEndChatConfirm(e)
23
25
  }
24
26
 
@@ -0,0 +1,19 @@
1
+ export const agentStatusHeadline = (availability, agentStatus, agentName) => {
2
+ let connectionHeadlineText = 'Connecting to Floodline'
3
+
4
+ if (agentStatus === 'closed') {
5
+ connectionHeadlineText = agentName ? `${agentName} ended the session` : 'Session ended by advisor'
6
+ } else if (agentStatus) {
7
+ if (agentName) {
8
+ connectionHeadlineText = `You are speaking with ${agentName}`
9
+ } else {
10
+ connectionHeadlineText = 'No advisers currently available'
11
+ }
12
+ }
13
+
14
+ if (availability === 'UNAVAILABLE') {
15
+ connectionHeadlineText = 'Webchat is not currently available'
16
+ }
17
+
18
+ return connectionHeadlineText
19
+ }
@@ -73,3 +73,24 @@
73
73
  line-height: 1.25;
74
74
  margin: 0 0 25px;
75
75
  }
76
+
77
+ // stylelint-disable declaration-no-important
78
+ .wc-live {
79
+ position: absolute !important;
80
+ width: 1px !important;
81
+ height: 1px !important;
82
+ margin: 0 !important;
83
+ padding: 0 !important;
84
+ overflow: hidden !important;
85
+ clip: rect(0 0 0 0) !important;
86
+ -webkit-clip-path: inset(50%) !important;
87
+ clip-path: inset(50%) !important;
88
+ border: 0 !important;
89
+ white-space: nowrap !important;
90
+
91
+ &::before,
92
+ &::after {
93
+ content: ' ';
94
+ }
95
+ }
96
+ // stylelint-enable declaration-no-important
@@ -27,12 +27,13 @@ export const AppProvider = ({ sdk, availability, options, children }) => {
27
27
  dispatch({ type: 'SET_AGENT_STATUS', payload: e.detail.data.case.status })
28
28
  }
29
29
 
30
- const onAgentTypingStarted = () => {
30
+ const onAgentTypingStarted = e => {
31
31
  dispatch({ type: 'SET_AGENT_TYPING', payload: true })
32
32
  }
33
33
 
34
34
  const onAgentTypingEnded = () => {
35
35
  dispatch({ type: 'SET_AGENT_TYPING', payload: false })
36
+ dispatch({ type: 'SET_LIVE_REGION_TEXT' })
36
37
  }
37
38
 
38
39
  const onMessageCreated = e => {
@@ -149,6 +150,10 @@ export const AppProvider = ({ sdk, availability, options, children }) => {
149
150
  dispatch({ type: 'SET_INSTIGATOR_ID', payload: id })
150
151
  }
151
152
 
153
+ const setLiveRegionText = text => {
154
+ dispatch({ type: 'SET_LIVE_REGION_TEXT', payload: text })
155
+ }
156
+
152
157
  /**
153
158
  * Application-wide state and state functions
154
159
  */
@@ -163,6 +168,7 @@ export const AppProvider = ({ sdk, availability, options, children }) => {
163
168
  setUnseenCount,
164
169
  setChatVisibility,
165
170
  setInstigatorId,
171
+ setLiveRegionText,
166
172
  onLiveChatRecovered,
167
173
  onAssignedAgentChanged,
168
174
  onAgentTypingStarted,
@@ -61,17 +61,18 @@ const setThread = (state, payload) => {
61
61
  }
62
62
  }
63
63
 
64
- const setMessage = (state, payload) => {
64
+ const setMessages = (state, payload) => {
65
65
  return {
66
66
  ...state,
67
- messages: [...state.messages, transformMessage(payload)]
67
+ messages: transformMessages(payload).reverse()
68
68
  }
69
69
  }
70
70
 
71
- const setMessages = (state, payload) => {
71
+ const setMessage = (state, payload) => {
72
72
  return {
73
73
  ...state,
74
- messages: transformMessages(payload).reverse()
74
+ message: transformMessage(payload),
75
+ messages: [...state.messages, transformMessage(payload)]
75
76
  }
76
77
  }
77
78
 
@@ -110,6 +111,13 @@ const setInstigatorId = (state, payload) => {
110
111
  }
111
112
  }
112
113
 
114
+ const setLiveRegionText = (state, payload) => {
115
+ return {
116
+ ...state,
117
+ liveRegionText: payload
118
+ }
119
+ }
120
+
113
121
  const toggleIsMobile = (state, payload) => {
114
122
  return {
115
123
  ...state,
@@ -138,6 +146,7 @@ export const actionsMap = {
138
146
  SET_AGENT_STATUS: setAgentStatus,
139
147
  SET_UNSEEN_COUNT: setUnseenCount,
140
148
  SET_INSTIGATOR_ID: setInstigatorId,
149
+ SET_LIVE_REGION_TEXT: setLiveRegionText,
141
150
  TOGGLE_IS_MOBILE: toggleIsMobile,
142
151
  TOGGLE_IS_KEYBOARD: toggleIsKeyboard
143
152
  }