@defra/flood-webchat 0.0.1-beta.54 → 0.0.1-beta.55
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/docs/assets/create-a-new-release.png +0 -0
- package/docs/assets/example-pre-release.png +0 -0
- package/docs/assets/example-release.png +0 -0
- package/docs/development-guide.md +5 -0
- package/docs/how-to-publish.md +41 -0
- package/jest.config.mjs +29 -0
- package/main.scss +4 -4
- package/package.json +2 -2
- package/src/client/components/availability/availability.jsx +130 -0
- package/src/client/components/chat/chat.jsx +233 -0
- package/src/client/components/chat/chat.scss +0 -1
- package/src/client/components/errorSummary/error-summary.jsx +34 -0
- package/src/client/components/live-region.jsx +55 -0
- package/src/client/components/message/message.jsx +21 -0
- package/src/client/components/panel/panel-footer.jsx +9 -0
- package/src/client/components/panel/panel-header.jsx +75 -0
- package/src/client/components/panel/panel.jsx +163 -0
- package/src/client/components/screens/end-chat.jsx +77 -0
- package/src/client/components/screens/feedback.jsx +65 -0
- package/src/client/components/screens/pre-chat.jsx +62 -0
- package/src/client/components/screens/request-chat.jsx +183 -0
- package/src/client/components/screens/settings.jsx +97 -0
- package/src/client/components/screens/unavailable.jsx +23 -0
- package/src/client/components/skip-link.jsx +42 -0
- package/src/client/hooks/useFocusedElements.js +80 -0
- package/src/client/hooks/useTextareaAutosize.js +13 -0
- package/src/client/index.jsx +33 -0
- package/src/client/lib/agent-status-headline.js +17 -0
- package/src/client/lib/check-availability.js +8 -0
- package/src/client/lib/classnames.js +1 -0
- package/src/client/lib/history.js +13 -0
- package/src/client/lib/message-notification.js +30 -0
- package/src/client/lib/transform-messages.js +47 -0
- package/src/client/store/AppProvider.jsx +183 -0
- package/src/client/store/actions-map.js +152 -0
- package/src/client/store/constants.js +3 -0
- package/src/client/store/reducer.js +31 -0
- package/src/client/store/useApp.js +5 -0
- package/src/client/store/useChatSdk.js +37 -0
- package/src/server/index.js +60 -0
- package/src/server/lib/client.js +88 -0
- package/src/server/lib/utils.js +23 -0
- package/webpack.config.mjs +30 -0
- package/webpack.prod.mjs +7 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# How to Publish
|
|
2
|
+
|
|
3
|
+
This repo implements automated publishing via GitHub actions triggered by creating a release in GitHub.
|
|
4
|
+
Releases should respect [the semantic versioning specification v2.0.0](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## A Pre-Release
|
|
7
|
+
1) Go to the [GitHub repo homepage](https://github.com/DEFRA/flood-webchat) for this project.
|
|
8
|
+
2) Click "Create a new release"
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
3) Select your Target branch, by default it will be the "main" branch.
|
|
13
|
+
4) Click "Choose a Tag", enter a version tag with the appropriate semantic version bump and suffixed with a pre-release identifier and click "Create new tag". _(for example if the latest published version was `v1.2.3` and you wish to publish a pre-release from your "cool-update"
|
|
14
|
+
branch, name the release: `v1.2.4-cool-update.1`)_
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
5) Use your version name as a title, and add appropriate release notes in the description field.
|
|
19
|
+
6) Tick the "Set as a pre-release" checkbox.
|
|
20
|
+
7) Click "Publish release"
|
|
21
|
+
8) A GitHub Action should then be triggered automatically, publishing your pre-release to npm.
|
|
22
|
+
9) You can install your pre-release into a consuming project by running `npm i @defra/flood-webchat@<version-number>`, e.g. `npm i @defra/flood-webchat@1.2.4-cool-update.1`.
|
|
23
|
+
|
|
24
|
+
_NOTE: pre-release identifiers must match the pattern `^-[0-9A-Za-z-].*$`_
|
|
25
|
+
|
|
26
|
+
## A Release
|
|
27
|
+
1) Go to the [GitHub repo homepage](https://github.com/DEFRA/flood-webchat) for this project.
|
|
28
|
+
2) Click "Create a new release"
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
3) Ensure that your Target branch is the "main" branch (this is the default).
|
|
33
|
+
4) Click "Choose a Tag", enter a version tag with the appropriate semantic version bump.
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
5) Use your version name as a title, and add appropriate release notes in the description field.
|
|
38
|
+
6) Ensure that the "Set as a pre-release" checkbox is **NOT** checked.
|
|
39
|
+
7) Click "Publish release"
|
|
40
|
+
8) A GitHub Action should then be triggered automatically, publishing your release to npm.
|
|
41
|
+
9) You can install your release into a consuming project by running `npm i @defra/flood-webchat@<version-number>`, e.g. `npm i @defra/flood-webchat@1.2.4`.
|
package/jest.config.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For a detailed explanation regarding each configuration property, visit:
|
|
3
|
+
* https://jestjs.io/docs/configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {import('jest').Config} */
|
|
7
|
+
const config = {
|
|
8
|
+
collectCoverage: true,
|
|
9
|
+
coverageDirectory: 'coverage',
|
|
10
|
+
transform: {
|
|
11
|
+
'\\.jsx?$': 'babel-jest',
|
|
12
|
+
'\\.mjs$': 'babel-jest'
|
|
13
|
+
},
|
|
14
|
+
projects: [
|
|
15
|
+
{
|
|
16
|
+
displayName: 'client-side',
|
|
17
|
+
testEnvironment: 'jsdom',
|
|
18
|
+
testMatch: ['**/__tests__/client/**/*.?(m)js?(x)'],
|
|
19
|
+
transformIgnorePatterns: []
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
displayName: 'server-side',
|
|
23
|
+
testEnvironment: 'node',
|
|
24
|
+
testMatch: ['**/__tests__/server/**/*.?(m)js?(x)']
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default config
|
package/main.scss
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
// Required GOVUK Styles
|
|
3
|
-
@import "govuk-frontend/govuk/core/typography";
|
|
4
|
-
@import "govuk-frontend/govuk/core/links";
|
|
5
|
-
@import "govuk-frontend/govuk/utilities/visually-hidden";
|
|
6
|
-
@import "govuk-frontend/govuk/components/button/button";
|
|
3
|
+
@import "govuk-frontend/dist/govuk/core/typography";
|
|
4
|
+
@import "govuk-frontend/dist/govuk/core/links";
|
|
5
|
+
@import "govuk-frontend/dist/govuk/utilities/visually-hidden";
|
|
6
|
+
@import "govuk-frontend/dist/govuk/components/button/button";
|
|
7
7
|
|
|
8
8
|
// (ITCSS)
|
|
9
9
|
@import "./src/client/scss/settings";
|
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.55",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"browser": "dist/client.js",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"webpack-node-externals": "^3.0.0"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
|
-
"govuk-frontend": "^4.0
|
|
60
|
+
"govuk-frontend": "^5.4.0"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@nice-devone/nice-cxone-chat-web-sdk": "^1.3.0",
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { classnames } from '../../lib/classnames'
|
|
4
|
+
import { Panel } from '../panel/panel.jsx'
|
|
5
|
+
import { SkipLink } from '../skip-link.jsx'
|
|
6
|
+
|
|
7
|
+
import { useApp } from '../../store/useApp'
|
|
8
|
+
import { historyPushState, historyReplaceState } from '../../lib/history.js'
|
|
9
|
+
import { LiveRegion } from '../live-region.jsx'
|
|
10
|
+
|
|
11
|
+
export function Availability () {
|
|
12
|
+
const { availability, isChatOpen, setChatVisibility, unseenCount, threadId, setUnseenCount, setInstigatorId, setLiveRegionText } = useApp()
|
|
13
|
+
|
|
14
|
+
const buttonRef = useRef()
|
|
15
|
+
|
|
16
|
+
const showUnseenCount = unseenCount > 0 && !isChatOpen
|
|
17
|
+
|
|
18
|
+
const messageText = unseenCount > 1 ? 'new messages' : 'new message'
|
|
19
|
+
|
|
20
|
+
const onClick = e => {
|
|
21
|
+
e.preventDefault()
|
|
22
|
+
setUnseenCount(0)
|
|
23
|
+
setChatVisibility(!isChatOpen)
|
|
24
|
+
setInstigatorId(e.target.id)
|
|
25
|
+
|
|
26
|
+
if (!isChatOpen) {
|
|
27
|
+
historyPushState()
|
|
28
|
+
} else {
|
|
29
|
+
historyReplaceState()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const onKeyDown = event => {
|
|
34
|
+
if (event.key === ' ') {
|
|
35
|
+
event.preventDefault()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const onKeyUp = event => {
|
|
40
|
+
if (event.key === ' ') {
|
|
41
|
+
setUnseenCount(0)
|
|
42
|
+
setChatVisibility(!isChatOpen)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (showUnseenCount) {
|
|
48
|
+
setLiveRegionText(`Floodline webchat - ${unseenCount} ${messageText}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
setLiveRegionText()
|
|
53
|
+
}
|
|
54
|
+
}, [unseenCount, isChatOpen])
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const onScroll = () => {
|
|
58
|
+
if (!threadId) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (availability !== 'AVAILABLE') {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const rect = buttonRef.current.parentElement.getBoundingClientRect()
|
|
67
|
+
|
|
68
|
+
const isBelowFold = rect.top + 35 > (window.innerHeight || document.documentElement.clientHeight)
|
|
69
|
+
const isFixed = !isChatOpen && isBelowFold
|
|
70
|
+
|
|
71
|
+
document.documentElement.classList.toggle('wc-u-scroll-padding', isFixed)
|
|
72
|
+
document.body.classList.toggle('wc-u-scroll-padding', isFixed)
|
|
73
|
+
buttonRef.current.classList.toggle('wc-availability--fixed', isFixed)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
document.addEventListener('scroll', onScroll)
|
|
77
|
+
onScroll()
|
|
78
|
+
|
|
79
|
+
return () => {
|
|
80
|
+
document.removeEventListener('scroll', onScroll)
|
|
81
|
+
}
|
|
82
|
+
}, [threadId, isChatOpen, availability])
|
|
83
|
+
|
|
84
|
+
switch (availability) {
|
|
85
|
+
case 'AVAILABLE':
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
<div
|
|
89
|
+
className={classnames('wc-availability', isChatOpen && 'wc-open')}
|
|
90
|
+
ref={buttonRef}
|
|
91
|
+
>
|
|
92
|
+
<div className='wc-availability__inner'>
|
|
93
|
+
<a
|
|
94
|
+
id='webchat-start-chat-link'
|
|
95
|
+
className='wc-availability__link'
|
|
96
|
+
href='#webchat'
|
|
97
|
+
draggable='false'
|
|
98
|
+
onClick={onClick}
|
|
99
|
+
onKeyUp={onKeyUp}
|
|
100
|
+
onKeyDown={onKeyDown}
|
|
101
|
+
role='button'
|
|
102
|
+
data-module='govuk-button'
|
|
103
|
+
>
|
|
104
|
+
{!threadId ? <>Start chat</> : <>Show chat</>}
|
|
105
|
+
{showUnseenCount
|
|
106
|
+
? (
|
|
107
|
+
<span className='wc-availability__unseen'>
|
|
108
|
+
{unseenCount}
|
|
109
|
+
<span className='govuk-visually-hidden'> {messageText}</span>
|
|
110
|
+
</span>
|
|
111
|
+
)
|
|
112
|
+
: null}
|
|
113
|
+
</a>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
{!isChatOpen ? <LiveRegion /> : <Panel />}
|
|
117
|
+
<SkipLink />
|
|
118
|
+
</>
|
|
119
|
+
)
|
|
120
|
+
case 'EXISTING':
|
|
121
|
+
case 'UNAVAILABLE':
|
|
122
|
+
return (
|
|
123
|
+
<p className='govuk-body'>When it is available, a 'start chat' link will appear.</p>
|
|
124
|
+
)
|
|
125
|
+
default:
|
|
126
|
+
return (
|
|
127
|
+
<p className='govuk-body'>Checking availability</p>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { PanelHeader } from '../panel/panel-header.jsx'
|
|
4
|
+
import { PanelFooter } from '../panel/panel-footer.jsx'
|
|
5
|
+
import { Message } from '../message/message.jsx'
|
|
6
|
+
|
|
7
|
+
import { useApp } from '../../store/useApp.js'
|
|
8
|
+
import { useTextareaAutosize } from '../../hooks/useTextareaAutosize.js'
|
|
9
|
+
import { formatTranscript } from '../../lib/transform-messages.js'
|
|
10
|
+
import { agentStatusHeadline } from '../../lib/agent-status-headline.js'
|
|
11
|
+
|
|
12
|
+
export function Chat ({ onEndChatScreen, onSettingsScreen }) {
|
|
13
|
+
const { availability, thread, messages, agent, agentStatus, isAgentTyping, isChatOpen, settings, isKeyboard, setLiveRegionText } = useApp()
|
|
14
|
+
|
|
15
|
+
const [userMessage, setUserMessage] = useState('')
|
|
16
|
+
const [focusVisibleWithin, setFocusVisibleWithin] = useState(false)
|
|
17
|
+
|
|
18
|
+
const messageRef = useRef()
|
|
19
|
+
|
|
20
|
+
useTextareaAutosize(messageRef.current, userMessage)
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (settings.scroll && messages.length !== 0) {
|
|
24
|
+
const chatBody = document.querySelector('.wc-body')
|
|
25
|
+
chatBody.scrollTop = chatBody.scrollHeight
|
|
26
|
+
}
|
|
27
|
+
}, [messages, isAgentTyping])
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const label = document.querySelector('.wc-form__label')
|
|
31
|
+
|
|
32
|
+
if (userMessage.length === 0) {
|
|
33
|
+
label.classList.remove('govuk-visually-hidden')
|
|
34
|
+
} else {
|
|
35
|
+
label.classList.add('govuk-visually-hidden')
|
|
36
|
+
}
|
|
37
|
+
}, [userMessage])
|
|
38
|
+
|
|
39
|
+
const onChange = e => {
|
|
40
|
+
setUserMessage(e.target?.value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const agentName = agent?.nickname || agent?.firstName
|
|
44
|
+
|
|
45
|
+
const connectionHeadlineText = agentStatusHeadline(availability, agentStatus, agentName)
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (connectionHeadlineText) {
|
|
49
|
+
setLiveRegionText(`Floodline webchat - ${connectionHeadlineText}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
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 (agentStatus !== 'closed' && lastAgentMessage?.direction === 'outbound') {
|
|
71
|
+
setLiveRegionText(`${agentName} said: ${lastAgentMessage.text}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
setLiveRegionText()
|
|
76
|
+
}
|
|
77
|
+
}, [messages])
|
|
78
|
+
|
|
79
|
+
const sendMessage = () => {
|
|
80
|
+
if (messageRef.current.value.length === 0 || agentStatus === 'closed') {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const message = messageRef.current.value.trim()
|
|
86
|
+
thread.sendTextMessage(message)
|
|
87
|
+
setLiveRegionText(`You said: ${message}`)
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.log('[Chat Error] sendMessage', err)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setUserMessage('')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const handleSubmit = e => {
|
|
96
|
+
e.preventDefault()
|
|
97
|
+
sendMessage()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const saveChat = () => {
|
|
101
|
+
const transcript = formatTranscript(messages)
|
|
102
|
+
|
|
103
|
+
const saveChatLink = document.querySelector('#transcript-download')
|
|
104
|
+
|
|
105
|
+
saveChatLink.setAttribute('href', `data:text/plain;charset=utf-8,${transcript}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const handleKeyPress = e => {
|
|
109
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
110
|
+
switch (e.target.id) {
|
|
111
|
+
case 'text-area':
|
|
112
|
+
e.preventDefault()
|
|
113
|
+
sendMessage()
|
|
114
|
+
break
|
|
115
|
+
case 'end-chat':
|
|
116
|
+
onEndChatScreen(e)
|
|
117
|
+
break
|
|
118
|
+
case 'wc-settings':
|
|
119
|
+
onSettingsScreen(e)
|
|
120
|
+
break
|
|
121
|
+
case 'transcript-download':
|
|
122
|
+
saveChat()
|
|
123
|
+
break
|
|
124
|
+
default:
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (e.key === ' ') {
|
|
129
|
+
if (e.target.id === 'end-chat') {
|
|
130
|
+
onEndChatScreen(e)
|
|
131
|
+
}
|
|
132
|
+
if (e.target.id === 'wc-settings') {
|
|
133
|
+
onSettingsScreen(e)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<>
|
|
140
|
+
<PanelHeader />
|
|
141
|
+
|
|
142
|
+
<div className='wc-status'>
|
|
143
|
+
<p className='wc-status__availability'>{connectionHeadlineText}</p>
|
|
144
|
+
<a
|
|
145
|
+
id='end-chat'
|
|
146
|
+
className='wc-status__link'
|
|
147
|
+
href='#'
|
|
148
|
+
data-module='govuk-button'
|
|
149
|
+
role='button'
|
|
150
|
+
onClick={onEndChatScreen}
|
|
151
|
+
onKeyDown={handleKeyPress}
|
|
152
|
+
>
|
|
153
|
+
End chat
|
|
154
|
+
</a>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div className='wc-body' tabIndex='0'>
|
|
158
|
+
<ul className='wc-chat'>
|
|
159
|
+
{messages.length
|
|
160
|
+
? messages.map((msg, index) => <Message key={msg.id} message={msg} previousMessage={messages[index - 1]} />)
|
|
161
|
+
: null}
|
|
162
|
+
{isAgentTyping
|
|
163
|
+
? (
|
|
164
|
+
<li className='wc-chat__message outbound'>
|
|
165
|
+
<div className='wc-chat__from'>{agentName} is typing</div>
|
|
166
|
+
<div className='wc-chat__text outbound'>
|
|
167
|
+
<svg width='28' height='16' x='0px' y='0px' viewBox='0 0 28 16'>
|
|
168
|
+
<circle stroke='none' cx='3' cy='8' r='3' fill='currentColor' />
|
|
169
|
+
<circle stroke='none' cx='14' cy='8' r='3' fill='currentColor' />
|
|
170
|
+
<circle stroke='none' cx='25' cy='8' r='3' fill='currentColor' />
|
|
171
|
+
</svg>
|
|
172
|
+
</div>
|
|
173
|
+
</li>
|
|
174
|
+
)
|
|
175
|
+
: null}
|
|
176
|
+
</ul>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<PanelFooter>
|
|
180
|
+
<form className={`wc-form${focusVisibleWithin ? ' wc-focus-within' : ''}`} noValidate onSubmit={handleSubmit}>
|
|
181
|
+
<label className='govuk-label wc-form__label' htmlFor='wc-form-textarea'>
|
|
182
|
+
Your message<span className='govuk-visually-hidden'> (enter key submits)</span>
|
|
183
|
+
</label>
|
|
184
|
+
|
|
185
|
+
<textarea
|
|
186
|
+
ref={messageRef}
|
|
187
|
+
rows='1'
|
|
188
|
+
aria-required='true'
|
|
189
|
+
className='wc-form__textarea'
|
|
190
|
+
id='text-area'
|
|
191
|
+
name='message'
|
|
192
|
+
onChange={onChange}
|
|
193
|
+
onKeyDown={handleKeyPress}
|
|
194
|
+
onFocus={() => { setFocusVisibleWithin(isKeyboard) }}
|
|
195
|
+
onBlur={() => { setFocusVisibleWithin(false) }}
|
|
196
|
+
value={userMessage}
|
|
197
|
+
/>
|
|
198
|
+
|
|
199
|
+
<input
|
|
200
|
+
type='submit'
|
|
201
|
+
className='wc-form__button govuk-button'
|
|
202
|
+
value='Send'
|
|
203
|
+
data-prevent-double-click='true'
|
|
204
|
+
/>
|
|
205
|
+
</form>
|
|
206
|
+
|
|
207
|
+
<div className='wc-footer__settings'>
|
|
208
|
+
<a
|
|
209
|
+
href='#'
|
|
210
|
+
id='wc-settings'
|
|
211
|
+
className='wc-footer__settings-link'
|
|
212
|
+
data-module='govuk-button'
|
|
213
|
+
onKeyDown={handleKeyPress}
|
|
214
|
+
onClick={onSettingsScreen}
|
|
215
|
+
>
|
|
216
|
+
Settings
|
|
217
|
+
</a>
|
|
218
|
+
<a
|
|
219
|
+
href='#'
|
|
220
|
+
id='transcript-download'
|
|
221
|
+
className='wc-footer__settings-link'
|
|
222
|
+
data-module='govuk-button'
|
|
223
|
+
download='floodline-webchat-transcript.txt'
|
|
224
|
+
onKeyDown={handleKeyPress}
|
|
225
|
+
onClick={saveChat}
|
|
226
|
+
>
|
|
227
|
+
Save chat
|
|
228
|
+
</a>
|
|
229
|
+
</div>
|
|
230
|
+
</PanelFooter>
|
|
231
|
+
</>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
export const ErrorSummary = ({ errors }) => {
|
|
4
|
+
const errs = Object.keys(errors)
|
|
5
|
+
|
|
6
|
+
const goToInput = e => {
|
|
7
|
+
e.preventDefault()
|
|
8
|
+
const key = e.target.getAttribute('data-key')
|
|
9
|
+
document.querySelector(`#wc-${key}`).focus()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (errs.length === 0) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className='govuk-error-summary govuk-!-static-margin-bottom-7' data-module='govuk-error-summary' tabIndex='-1'>
|
|
18
|
+
<div role='alert'>
|
|
19
|
+
<h2 id='wc-error' className='wc-error-summary__title govuk-error-summary__title'>
|
|
20
|
+
There is a problem
|
|
21
|
+
</h2>
|
|
22
|
+
<div className='govuk-error-summary__body'>
|
|
23
|
+
<ul className='govuk-list govuk-error-summary__list wc-error-summary__list'>
|
|
24
|
+
{errs.map(key => (
|
|
25
|
+
<li key={key}>
|
|
26
|
+
<a href='#' data-key={key} onClick={goToInput}>{errors[key]}</a>
|
|
27
|
+
</li>
|
|
28
|
+
))}
|
|
29
|
+
</ul>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -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
|
+
const DEBOUNCE_MILLISECONDS = 2000
|
|
6
|
+
|
|
7
|
+
export const LiveRegion = () => {
|
|
8
|
+
const { agentStatus, liveRegionText, setLiveRegionText } = useApp()
|
|
9
|
+
|
|
10
|
+
const [textA, setTextA] = useState()
|
|
11
|
+
const [textB, setTextB] = useState()
|
|
12
|
+
|
|
13
|
+
const isSessionEnded = agentStatus === 'closed' || agentStatus === null
|
|
14
|
+
|
|
15
|
+
const clearText = () => {
|
|
16
|
+
setTextA()
|
|
17
|
+
setTextB()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const setText = useCallback(debounce(text => {
|
|
21
|
+
if (textA) {
|
|
22
|
+
setTextB(text)
|
|
23
|
+
} else {
|
|
24
|
+
setTextA(text)
|
|
25
|
+
}
|
|
26
|
+
}, DEBOUNCE_MILLISECONDS), [textA, textB])
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
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' role='status' aria-atomic='true'>
|
|
48
|
+
{textA}
|
|
49
|
+
</div>
|
|
50
|
+
<div className='wc-live' role='status' aria-atomic='true'>
|
|
51
|
+
{textB}
|
|
52
|
+
</div>
|
|
53
|
+
</>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { classnames } from '../../lib/classnames'
|
|
3
|
+
import { formatMessage } from '../../lib/transform-messages'
|
|
4
|
+
|
|
5
|
+
export function Message ({ message, previousMessage }) {
|
|
6
|
+
const outbound = message.direction === 'outbound'
|
|
7
|
+
|
|
8
|
+
const messageOwnerSameAsPrevious = message?.direction === previousMessage?.direction
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<li className={classnames('wc-chat__message', outbound ? 'outbound' : 'inbound')}>
|
|
12
|
+
<div className='wc-chat__from'>
|
|
13
|
+
{messageOwnerSameAsPrevious ? null : <span className={classnames(outbound ? 'outbound' : 'inbound')}>{outbound ? message.assignee : 'You'}</span>}
|
|
14
|
+
<span className='govuk-visually-hidden'>:</span>
|
|
15
|
+
</div>
|
|
16
|
+
<div className={classnames('wc-chat__text', outbound ? 'outbound' : 'inbound')}>
|
|
17
|
+
{formatMessage(message.text)}
|
|
18
|
+
</div>
|
|
19
|
+
</li>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useApp } from '../../store/useApp.js'
|
|
3
|
+
import { historyReplaceState } from '../../lib/history.js'
|
|
4
|
+
|
|
5
|
+
export function PanelHeader () {
|
|
6
|
+
const { thread, threadId, setChatVisibility, setUnseenCount, isMobile } = useApp()
|
|
7
|
+
|
|
8
|
+
const onClose = e => {
|
|
9
|
+
e.preventDefault()
|
|
10
|
+
setChatVisibility(false)
|
|
11
|
+
historyReplaceState()
|
|
12
|
+
|
|
13
|
+
if (threadId) {
|
|
14
|
+
thread?.lastMessageSeen()
|
|
15
|
+
setUnseenCount(0)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const BackButtonComponent = (
|
|
20
|
+
<button className='wc-header__back' aria-label='Close the webchat' onClick={onClose}>
|
|
21
|
+
<svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
|
|
22
|
+
<path d='M4.828,11L12.314,18.485L10.899,19.899L1,10L10.899,0.101L12.314,1.515L4.828,9L19,9L19,11L4.828,11Z' fill='currentColor' />
|
|
23
|
+
</svg>
|
|
24
|
+
</button>
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const CloseButtonComponent = (
|
|
28
|
+
<button className='wc-header__close' aria-label='Close the webchat' onClick={onClose}>
|
|
29
|
+
<svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
|
|
30
|
+
<path d='M10,8.6L15.6,3L17,4.4L11.4,10L17,15.6L15.6,17L10,11.4L4.4,17L3,15.6L8.6,10L3,4.4L4.4,3L10,8.6Z' fill='currentColor' />
|
|
31
|
+
</svg>
|
|
32
|
+
</button>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const MinimiseButtonComponent = (
|
|
36
|
+
<button className='wc-header__hide' aria-label='Minimise the webchat' onClick={onClose}>
|
|
37
|
+
<svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
|
|
38
|
+
<path d='M10 14.4l-7-7L4.4 6l5.6 5.6L15.6 6 17 7.4l-7 7z' fill='currentColor' />
|
|
39
|
+
</svg>
|
|
40
|
+
</button>
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const isMobileAndHasHistory = isMobile && window.history.state
|
|
44
|
+
const isMobileAndNoHistory = isMobile && !window.history.state
|
|
45
|
+
|
|
46
|
+
let RightButtonComponent = CloseButtonComponent
|
|
47
|
+
|
|
48
|
+
if (threadId) {
|
|
49
|
+
RightButtonComponent = MinimiseButtonComponent
|
|
50
|
+
|
|
51
|
+
if (isMobileAndNoHistory) {
|
|
52
|
+
RightButtonComponent = MinimiseButtonComponent
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!threadId && isMobileAndNoHistory) {
|
|
57
|
+
RightButtonComponent = CloseButtonComponent
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isMobileAndHasHistory) {
|
|
61
|
+
RightButtonComponent = null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className='wc-header'>
|
|
66
|
+
{isMobileAndHasHistory ? BackButtonComponent : null}
|
|
67
|
+
|
|
68
|
+
<h2 id='wc-header' className='wc-header__title'>
|
|
69
|
+
Floodline webchat
|
|
70
|
+
</h2>
|
|
71
|
+
|
|
72
|
+
{RightButtonComponent}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|