@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/dist/client.js +189 -31
- package/dist/client.js.map +1 -1
- package/package.json +2 -1
- package/src/client/components/availability/availability.jsx +20 -2
- package/src/client/components/chat/chat.jsx +35 -14
- package/src/client/components/live-region.jsx +55 -0
- package/src/client/components/panel/panel.jsx +2 -0
- package/src/client/components/screens/end-chat.jsx +2 -0
- package/src/client/lib/agent-status-headline.js +19 -0
- package/src/client/scss/_settings.scss +21 -0
- package/src/client/store/AppProvider.jsx +7 -1
- package/src/client/store/actions-map.js +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/flood-webchat",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
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
|
|
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
|
-
|
|
45
|
+
const connectionHeadlineText = agentStatusHeadline(availability, agentStatus, agentName)
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
connectionHeadlineText
|
|
48
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
64
|
+
const setMessages = (state, payload) => {
|
|
65
65
|
return {
|
|
66
66
|
...state,
|
|
67
|
-
messages:
|
|
67
|
+
messages: transformMessages(payload).reverse()
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
const
|
|
71
|
+
const setMessage = (state, payload) => {
|
|
72
72
|
return {
|
|
73
73
|
...state,
|
|
74
|
-
|
|
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
|
}
|