@defra/flood-webchat 0.0.1-beta.1 → 0.0.1-beta.11

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 (34) hide show
  1. package/README.md +83 -1
  2. package/dist/client.js +1316 -214
  3. package/dist/client.js.map +1 -1
  4. package/dist/server.js +181 -240
  5. package/dist/server.js.map +1 -1
  6. package/main.scss +4 -3
  7. package/package.json +9 -3
  8. package/src/client/components/availability/availability.jsx +43 -46
  9. package/src/client/components/chat/chat.jsx +143 -0
  10. package/src/client/components/chat/chat.scss +134 -0
  11. package/src/client/components/message/message.jsx +20 -0
  12. package/src/client/components/panel/panel-footer.jsx +9 -0
  13. package/src/client/components/panel/panel-header.jsx +39 -0
  14. package/src/client/components/panel/panel.jsx +113 -0
  15. package/src/client/components/panel/panel.scss +72 -0
  16. package/src/client/components/screens/end-chat.jsx +17 -0
  17. package/src/client/components/screens/pre-chat.jsx +55 -0
  18. package/src/client/components/screens/request-chat.jsx +170 -0
  19. package/src/client/components/screens/unavailable.jsx +23 -0
  20. package/src/client/hooks/useFocusedElements.js +71 -0
  21. package/src/client/hooks/useTextareaAutosize.js +13 -0
  22. package/src/client/index.jsx +15 -1
  23. package/src/client/lib/check-availability.js +1 -0
  24. package/src/client/lib/transform-messages.js +14 -0
  25. package/src/client/store/AppProvider.jsx +114 -0
  26. package/src/client/store/actions-map.js +97 -0
  27. package/src/client/store/reducer.js +29 -0
  28. package/src/client/store/useApp.js +5 -0
  29. package/src/client/store/useChatSdk.js +37 -0
  30. package/src/server/index.js +15 -6
  31. package/src/server/lib/client.js +31 -35
  32. package/src/server/lib/utils.js +18 -9
  33. package/src/client/lib/external-stores.js +0 -4
  34. package/src/client/lib/external-sync-store.js +0 -42
@@ -0,0 +1,39 @@
1
+ import React from 'react'
2
+ import { useApp } from '../../store/useApp.js'
3
+
4
+ export function PanelHeader () {
5
+ const { thread, setChatVisibility } = useApp()
6
+
7
+ const onClose = e => {
8
+ e.preventDefault()
9
+ setChatVisibility(false)
10
+ }
11
+
12
+ let ButtonComponent = (
13
+ <button className='wc-header__close' aria-label='Close the webchat' onClick={onClose}>
14
+ <svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
15
+ <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' />
16
+ </svg>
17
+ </button>
18
+ )
19
+
20
+ if (thread) {
21
+ ButtonComponent = (
22
+ <button className='wc-header__hide' aria-label='Minimise the webchat' onClick={onClose}>
23
+ <svg aria-hidden='true' focusable='false' width='20' height='20' viewBox='0 0 20 20'>
24
+ <path d='M10 14.4l-7-7L4.4 6l5.6 5.6L15.6 6 17 7.4l-7 7z' fill='currentColor' />
25
+ </svg>
26
+ </button>
27
+ )
28
+ }
29
+
30
+ return (
31
+ <div className='wc-header'>
32
+ <h2 id='wc-header-title' className='wc-header__title govuk-heading-s'>
33
+ Floodline Webchat
34
+ </h2>
35
+
36
+ {ButtonComponent}
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,113 @@
1
+ import React, { useEffect, useState, useCallback } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+
4
+ import { PreChat } from '../screens/pre-chat.jsx'
5
+ import { RequestChat } from '../screens/request-chat.jsx'
6
+ import { Chat } from '../chat/chat.jsx'
7
+ import { Unavailable } from '../screens/unavailable.jsx'
8
+ import { EndChat } from '../screens/end-chat.jsx'
9
+
10
+ import { useApp } from '../../store/useApp.js'
11
+ import { useChatSdk } from '../../store/useChatSdk.js'
12
+ import { useFocusedElements } from '../../hooks/useFocusedElements.js'
13
+
14
+ export function Panel () {
15
+ const { sdk, availability, thread, threadId, setThread, setThreadId, setChatVisibility, setMessages } = useApp()
16
+ const { fetchThread, fetchMessages } = useChatSdk(sdk)
17
+
18
+ const [screen, setScreen] = useState(threadId ? 2 : 0)
19
+
20
+ useFocusedElements(screen)
21
+
22
+ /**
23
+ * Initializes the eventListener for pressing the escape key
24
+ */
25
+ useEffect(() => {
26
+ const escapeKeyEvent = document.addEventListener('keydown', onEscapeKey)
27
+
28
+ return () => {
29
+ document.removeEventListener('keydown', escapeKeyEvent)
30
+ }
31
+ }, [])
32
+
33
+ /**
34
+ * Recovers the thread if there is a threadId but no thread loaded in to state
35
+ */
36
+ useEffect(() => {
37
+ const recover = async () => {
38
+ try {
39
+ const fetchedThread = await fetchThread(threadId)
40
+ setThread(fetchedThread)
41
+
42
+ const fetchedMessages = await fetchMessages(fetchedThread, threadId)
43
+ setMessages(fetchedMessages)
44
+ } catch (err) {
45
+ console.log('[Chat Error] fetchThread', err)
46
+
47
+ setThreadId()
48
+ setThread()
49
+ setScreen(0)
50
+ }
51
+ }
52
+
53
+ if (threadId) {
54
+ (thread) ? setScreen(2) : recover()
55
+ }
56
+ }, [thread, threadId])
57
+
58
+ const onEscapeKey = useCallback(e => {
59
+ if (e.key === 'Escape' || e.key === 'Esc') {
60
+ setChatVisibility(false)
61
+ }
62
+ }, [])
63
+
64
+ const handleScreenChange = newScreen => e => {
65
+ e.preventDefault()
66
+ setScreen(newScreen)
67
+ }
68
+
69
+ const onForward = handleScreenChange(screen + 1)
70
+ const onBack = handleScreenChange(screen - 1)
71
+ const onEndChat = handleScreenChange(3)
72
+ const onResume = handleScreenChange(2)
73
+
74
+ const onEndChatConfirm = e => {
75
+ e.preventDefault()
76
+ console.log('confirmed end chat')
77
+ }
78
+
79
+ let ScreenComponent
80
+
81
+ switch (screen) {
82
+ case 0:
83
+ ScreenComponent = <PreChat onForward={onForward} />
84
+ break
85
+ case 1:
86
+ ScreenComponent = <RequestChat onBack={onBack} />
87
+ break
88
+ case 2:
89
+ ScreenComponent = <Chat setScreen={setScreen} onEndChat={onEndChat} />
90
+ break
91
+ case 3:
92
+ ScreenComponent = <EndChat onResume={onResume} onEndChatConfirm={onEndChatConfirm} />
93
+ break
94
+ default:
95
+ ScreenComponent = <PreChat onForward={onForward} />
96
+ }
97
+
98
+ if (availability === 'UNAVAILABLE') {
99
+ ScreenComponent = <Unavailable />
100
+ }
101
+
102
+ const Component = (
103
+ <div id='wc-panel' className='wc-panel' role='dialog' tabIndex='-1' aria-modal='true' aria-labelledby='wc-header-title'>
104
+ {ScreenComponent}
105
+ </div>
106
+ )
107
+
108
+ return (
109
+ <>
110
+ {createPortal(Component, document.body)}
111
+ </>
112
+ )
113
+ }
@@ -0,0 +1,72 @@
1
+ .wc-panel {
2
+ display: flex;
3
+ flex-direction: column;
4
+ position: fixed;
5
+ bottom: 0;
6
+ right: 0;
7
+ top: 0;
8
+ left: 0;
9
+ width: 100%;
10
+ height: 100%;
11
+ background-color: govuk-colour('white');
12
+ border: 1px solid govuk-colour('black');
13
+ z-index: 999;
14
+ outline: none;
15
+
16
+ @include mq ($from: 'tablet') {
17
+ bottom: 0;
18
+ right: 0;
19
+ top: auto;
20
+ left: auto;
21
+ width: 400px;
22
+ height: 540px;
23
+ margin-bottom: 10px;
24
+ margin-right: 10px;
25
+ }
26
+ }
27
+
28
+ .wc-header {
29
+ display: flex;
30
+ flex-direction: row;
31
+ align-items: center;
32
+ background-color: govuk-colour('black');
33
+
34
+ &__link {
35
+ margin: 0 govuk-spacing(2);
36
+ }
37
+
38
+ &__title {
39
+ padding: 0 0 0 govuk-spacing(2);
40
+ color: govuk-colour('white');
41
+ margin-bottom: 0;
42
+ }
43
+
44
+ &__close,
45
+ &__hide {
46
+ display: flex;
47
+ justify-content: center;
48
+ color: govuk-colour('white');
49
+ background: none;
50
+ border: 0;
51
+ padding: govuk-spacing(2);
52
+ margin: 0 0 0 auto;
53
+ cursor: pointer;
54
+
55
+ &:focus {
56
+ @include govuk-focused-text;
57
+ }
58
+ }
59
+ }
60
+
61
+ .wc-body {
62
+ @include govuk-responsive-padding(3);
63
+
64
+ flex: 1;
65
+ position: relative;
66
+ overflow-y: auto;
67
+ overscroll-behavior: contain;
68
+ }
69
+
70
+ .wc-back-link {
71
+ margin: govuk-spacing(1) 0 govuk-spacing(6) 0;
72
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react'
2
+ import { PanelHeader } from '../panel/panel-header.jsx'
3
+
4
+ export function EndChat ({ onResume, onEndChatConfirm }) {
5
+ return (
6
+ <>
7
+ <PanelHeader />
8
+ <div className='wc-body'>
9
+ <h3 className='govuk-heading-s' aria-live='polite'>Are you sure you want to end the chat?</h3>
10
+ <div className='govuk-button-group'>
11
+ <a href='#' className='govuk-button govuk-!-font-size-16' data-module='govuk-button' onClick={onEndChatConfirm}>Yes, end chat</a>
12
+ <a href='#' className='govuk-link govuk-!-font-size-16' onClick={onResume}>No, resume chat</a>
13
+ </div>
14
+ </div>
15
+ </>
16
+ )
17
+ }
@@ -0,0 +1,55 @@
1
+ import React from 'react'
2
+ import { PanelHeader } from '../panel/panel-header.jsx'
3
+
4
+ export function PreChat ({ onForward }) {
5
+ return (
6
+ <>
7
+ <PanelHeader />
8
+
9
+ <div className='wc-body'>
10
+ <h3 className='govuk-heading-s' aria-live='polite'>What you can use webchat for</h3>
11
+ <p className='govuk-body-s'>Webchat lets you talk directly to a Floodline adviser.</p>
12
+
13
+ <p className='govuk-body-s'>You can use webchat to get:</p>
14
+ <ul className='govuk-list govuk-list--bullet govuk-body-s'>
15
+ <li>current flood warnings and alerts in your area</li>
16
+ <li>information on the flood warning service</li>
17
+ <li>advice on what to do before, during and after a flood</li>
18
+ </ul>
19
+
20
+ <p className='govuk-body-s'>
21
+ {
22
+ 'There are other ways to '
23
+ }
24
+ <a className='govuk-link' href='https://www.gov.uk/sign-up-for-flood-warnings'>
25
+ sign up for flood warnings
26
+ </a>
27
+ {
28
+ ' and '
29
+ }
30
+ <a className='govuk-link' href='https://www.fws.environment-agency.gov.uk/app/olr/login'>
31
+ manage your flood warnings account
32
+ </a>.
33
+ </p>
34
+ <p className='govuk-body-s'>Do not use webchat to <a className='govuk-link' href='https://www.gov.uk/report-flood-cause'>report a flood</a>.</p>
35
+
36
+ <button className='govuk-button govuk-!-font-size-16' data-module='govuk-button' onClick={onForward}>Continue</button>
37
+
38
+ <h3 className='govuk-heading-s'>Other flood information</h3>
39
+
40
+ <p className='govuk-body-s'>Call Floodline for all other flood and flood warning information.</p>
41
+ <p className='govuk-body-s'>
42
+ <strong>Floodline helpline</strong>
43
+ <br />
44
+ Telephone: 0345 988 1188
45
+ <br />
46
+ Textphone: 0345 602 6340
47
+ <br />
48
+ Open 24 hours a day, 7 days a week
49
+ <br />
50
+ <a className='govuk-link' href='https://gov.uk/call-charges'>Find out more about call charges</a>
51
+ </p>
52
+ </div>
53
+ </>
54
+ )
55
+ }
@@ -0,0 +1,170 @@
1
+ import React, { useRef, useState, useEffect } from 'react'
2
+ import * as uuid from 'uuid'
3
+
4
+ import { classnames } from '../../lib/classnames.js'
5
+ import { PanelHeader } from '../panel/panel-header.jsx'
6
+ import { useApp } from '../../store/useApp.js'
7
+ import { useChatSdk } from '../../store/useChatSdk.js'
8
+
9
+ const QUESTION_MAX_LENGTH = 500
10
+
11
+ export function RequestChat ({ onBack }) {
12
+ const { sdk, setCustomerId, setThreadId, setThread } = useApp()
13
+ const { fetchCustomerId, fetchThread } = useChatSdk(sdk)
14
+
15
+ const [errors, setErrors] = useState({})
16
+ const [questionLength, setQuestionLength] = useState(0)
17
+
18
+ const nameRef = useRef()
19
+ const questionRef = useRef()
20
+
21
+ useEffect(() => {
22
+ if (Object.keys(errors).length) {
23
+ document.querySelector('.govuk-error-summary')?.focus()
24
+ }
25
+ }, [errors])
26
+
27
+ const onQuestionChange = e => {
28
+ setQuestionLength(e.target.value.length)
29
+ }
30
+
31
+ const onRequestChat = async e => {
32
+ e.preventDefault()
33
+
34
+ const errs = {}
35
+
36
+ if (nameRef.current.value.length === 0) {
37
+ errs.name = 'Enter your name'
38
+ }
39
+
40
+ if (questionRef.current.value.length === 0) {
41
+ errs.question = 'Enter your question'
42
+ }
43
+
44
+ if (questionRef.current.value.length > QUESTION_MAX_LENGTH) {
45
+ errs.question = 'Your question must be 500 characters or less'
46
+ }
47
+
48
+ if (Object.keys(errs).length === 0) {
49
+ try {
50
+ const threadId = uuid.v4()
51
+ const customerId = await fetchCustomerId()
52
+ const thread = await fetchThread(threadId)
53
+
54
+ sdk.getCustomer().setName(nameRef.current.value)
55
+
56
+ await thread.startChat(questionRef.current.value || 'Begin conversation')
57
+
58
+ setCustomerId(customerId)
59
+ setThreadId(threadId)
60
+ setThread(thread)
61
+ } catch (err) {
62
+ console.log('[Request Chat Error]', err)
63
+ }
64
+ }
65
+
66
+ setErrors(errs)
67
+ }
68
+
69
+ const isQuestionLengthValid = QUESTION_MAX_LENGTH >= questionLength
70
+ const questionLengthRemaining = QUESTION_MAX_LENGTH - questionLength
71
+ const questionLengthExceeded = questionLength - QUESTION_MAX_LENGTH
72
+
73
+ let questionHint = `You have ${questionLengthRemaining} characters remaining`
74
+
75
+ if (!isQuestionLengthValid) {
76
+ questionHint = `You have ${questionLengthExceeded} characters too many`
77
+ }
78
+
79
+ return (
80
+ <>
81
+ <PanelHeader />
82
+
83
+ <div className='wc-body'>
84
+ <a href='#' className='wc-back-link govuk-back-link' onClick={onBack}>What you can use webchat for</a>
85
+
86
+ {Object.keys(errors).length > 0
87
+ ? (
88
+ <div className='govuk-error-summary govuk-!-static-margin-bottom-7' data-module='govuk-error-summary' tabIndex='-1'>
89
+ <div role='alert'>
90
+ <h2 id='wc-error' className='govuk-error-summary__title'>
91
+ There is a problem
92
+ </h2>
93
+ <div className='govuk-error-summary__body'>
94
+ <ul className='govuk-list govuk-error-summary__list'>
95
+ {Object.keys(errors).map(key => (
96
+ <li key={key}>
97
+ <a href={`#${key}`}>{errors[key]}</a>
98
+ </li>
99
+ ))}
100
+ </ul>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ )
105
+ : null}
106
+
107
+ <h3 className='govuk-heading-s' aria-live='polite'>Your name and question</h3>
108
+
109
+ <form>
110
+ <div className={classnames('govuk-form-group', errors.name && 'govuk-form-group--error')}>
111
+ <label className='govuk-label' htmlFor='wc-name'>
112
+ Your name
113
+ </label>
114
+ {errors.name && (
115
+ <p className='govuk-error-message'>
116
+ <span className='govuk-visually-hidden'>Error:</span> {errors.name}
117
+ </p>
118
+ )}
119
+ <input
120
+ ref={nameRef}
121
+ className={classnames('govuk-input', errors.name && 'govuk-input--error')}
122
+ id='wc-name'
123
+ name='name'
124
+ type='text'
125
+ data-testid='request-chat-user-name'
126
+ />
127
+ </div>
128
+
129
+ <div className='govuk-character-count' data-module='govuk-character-count' data-maxlength='500'>
130
+ <div className={classnames('govuk-form-group', errors.question && 'govuk-form-group--error')}>
131
+ <label className='govuk-label' htmlFor='wc-question'>
132
+ Your question
133
+ </label>
134
+ {errors.question && (
135
+ <p className='govuk-error-message'>
136
+ <span className='govuk-visually-hidden'>Error:</span>
137
+ {errors.question}
138
+ </p>
139
+ )}
140
+ <textarea
141
+ ref={questionRef}
142
+ id='wc-question'
143
+ name='question'
144
+ rows='5'
145
+ aria-describedby='wc-question-info'
146
+ onChange={onQuestionChange}
147
+ className={classnames('govuk-textarea', 'govuk-js-character-count', !isQuestionLengthValid || errors.question ? 'govuk-textarea--error' : '')}
148
+ data-testid='request-chat-user-question'
149
+ />
150
+ <div id='wc-question-info' className='govuk-hint govuk-character-count__message' style={{ color: `${!isQuestionLengthValid ? '#d4351c' : ''}` }} aria-hidden='true'>
151
+ {questionHint}
152
+ </div>
153
+ <div className='govuk-character-count__sr-status govuk-visually-hidden' aria-live='polite'>
154
+ {questionHint}
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <button
160
+ className='govuk-button govuk-!-margin-top-1 govuk-!-font-size-16'
161
+ data-module='govuk-button'
162
+ onClick={onRequestChat}
163
+ >
164
+ Request chat
165
+ </button>
166
+ </form>
167
+ </div>
168
+ </>
169
+ )
170
+ }
@@ -0,0 +1,23 @@
1
+ import React from 'react'
2
+ import { PanelHeader } from '../panel/panel-header.jsx'
3
+
4
+ export function Unavailable () {
5
+ return (
6
+ <>
7
+ <PanelHeader />
8
+
9
+ <div className='wc-body'>
10
+ <h3 id='wc-subtitle' className='govuk-heading-m'>Webchat is currently not available</h3>
11
+ <p className='govuk-body-s'>Try again later, or call Floodline:</p>
12
+ <p className='govuk-body-s'>
13
+ <strong>Floodline helpline</strong>
14
+ <br />Telephone: 0345 988 1188
15
+ <br />Textphone: 0345 602 6340
16
+ <br />Open 24 hours a day, 7 days a week
17
+ <br /><a className='govuk-link' href='https://gov.uk/call-charges'>Find out more about call charges</a>
18
+ </p>
19
+ <p className='govuk-body-s'>We're running webchat as a trial, it will not always be available.</p>
20
+ </div>
21
+ </>
22
+ )
23
+ }
@@ -0,0 +1,71 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+
3
+ const setAriaHidden = bool => {
4
+ for (const node of document.body.children) {
5
+ if (node.id !== 'wc-panel') {
6
+ (bool) ? node.setAttribute('aria-hidden', 'true') : node.removeAttribute('aria-hidden')
7
+ }
8
+ }
9
+ }
10
+
11
+ export const getFocusableElements = () => {
12
+ const selectors = [
13
+ '#wc-panel a:not([disabled])',
14
+ '#wc-panel button:not([disabled])',
15
+ '#wc-panel select:not([disabled])',
16
+ '#wc-panel input:not([disabled])',
17
+ '#wc-panel textarea:not([disabled])',
18
+ '#wc-panel *[tabindex="0"]:not([disabled])'
19
+ ]
20
+
21
+ const elements = document.body.querySelectorAll(selectors.join(','))
22
+ return Array.from(elements).filter(e => !e.closest('[hidden]') && !e.closest('[aria-hidden="true"]'))
23
+ }
24
+
25
+ const useFocusedElements = screen => {
26
+ const [panelElements, setPanelElements] = useState([])
27
+
28
+ const onKeyDown = useCallback(e => {
29
+ const webchatPanelElement = document.querySelector('#wc-panel')
30
+
31
+ if (e.key === 'Tab') {
32
+ if (webchatPanelElement && !document.activeElement.closest('#wc-panel')) {
33
+ webchatPanelElement.focus()
34
+ }
35
+
36
+ if (e.shiftKey) {
37
+ if (document.activeElement === panelElements[0]) {
38
+ panelElements[panelElements.length - 1].focus()
39
+ e.preventDefault()
40
+ } else if (document.activeElement === document.querySelector('#wc-panel')) {
41
+ panelElements[panelElements.length - 1]?.focus()
42
+ e.preventDefault()
43
+ }
44
+ } else if (document.activeElement === panelElements[panelElements.length - 1]) {
45
+ panelElements[0].focus()
46
+ e.preventDefault()
47
+ }
48
+ }
49
+ }, [panelElements])
50
+
51
+ useEffect(() => {
52
+ setPanelElements(getFocusableElements())
53
+ }, [screen])
54
+
55
+ useEffect(() => {
56
+ setAriaHidden(true)
57
+
58
+ const panelElement = document.querySelector('#wc-panel')
59
+ panelElement.focus()
60
+
61
+ document.addEventListener('keydown', onKeyDown)
62
+
63
+ return () => {
64
+ setAriaHidden()
65
+ document.body.querySelector('.wc-availability__link')?.focus()
66
+ document.removeEventListener('keydown', onKeyDown)
67
+ }
68
+ }, [panelElements])
69
+ }
70
+
71
+ export { useFocusedElements }
@@ -0,0 +1,13 @@
1
+ import { useEffect } from 'react'
2
+
3
+ export const useTextareaAutosize = (textAreaRef, value) => {
4
+ useEffect(() => {
5
+ if (textAreaRef) {
6
+ textAreaRef.style.height = '0px'
7
+
8
+ const scrollHeight = textAreaRef.scrollHeight
9
+
10
+ textAreaRef.style.height = `${scrollHeight}px`
11
+ }
12
+ }, [textAreaRef, value])
13
+ }
@@ -1,9 +1,19 @@
1
1
  import React from 'react'
2
2
  import { createRoot } from 'react-dom/client'
3
+ import { ChatSdk } from '@nice-devone/nice-cxone-chat-web-sdk'
3
4
  import { Availability } from './components/availability/availability.jsx'
4
5
  import { checkAvailability } from './lib/check-availability'
6
+ import { AppProvider } from './store/AppProvider.jsx'
7
+ import { CUSTOMER_ID_STORAGE_KEY } from './store/reducer.js'
5
8
 
6
9
  export async function init (container, options) {
10
+ const sdk = new ChatSdk({
11
+ brandId: options.brandId,
12
+ channelId: options.channelId,
13
+ customerId: window.localStorage.getItem(CUSTOMER_ID_STORAGE_KEY) || '',
14
+ environment: options.environment
15
+ })
16
+
7
17
  const root = createRoot(container)
8
18
  let availability
9
19
  try {
@@ -12,5 +22,9 @@ export async function init (container, options) {
12
22
  } catch (e) {
13
23
  availability = 'UNAVAILABLE'
14
24
  }
15
- root.render(<Availability availability={availability} />)
25
+ root.render(
26
+ <AppProvider sdk={sdk} availability={availability}>
27
+ <Availability availability={availability} />
28
+ </AppProvider>
29
+ )
16
30
  }
@@ -1,6 +1,7 @@
1
1
  export async function checkAvailability (endpoint) {
2
2
  const response = await fetch(endpoint)
3
3
  const data = await response.json()
4
+
4
5
  return {
5
6
  availability: data.availability || 'UNAVAILABLE'
6
7
  }
@@ -0,0 +1,14 @@
1
+ import sanitizeHtml from 'sanitize-html'
2
+
3
+ export const transformMessage = message => {
4
+ return {
5
+ id: message.id,
6
+ text: sanitizeHtml(message.messageContent?.text),
7
+ createdAt: new Date(message.createdAt),
8
+ user: message.authorEndUserIdentity?.fullName?.trim() || null,
9
+ assignee: message.authorUser?.firstName || null,
10
+ direction: message.direction
11
+ }
12
+ }
13
+
14
+ export const transformMessages = messages => messages.map(message => transformMessage(message))