@defra/flood-webchat 0.0.1-beta.16 → 0.0.1-beta.18

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.
@@ -6,13 +6,14 @@ import { RequestChat } from '../screens/request-chat.jsx'
6
6
  import { Chat } from '../chat/chat.jsx'
7
7
  import { Unavailable } from '../screens/unavailable.jsx'
8
8
  import { EndChat } from '../screens/end-chat.jsx'
9
+ import { Settings } from '../screens/settings.jsx'
9
10
 
10
11
  import { useApp } from '../../store/useApp.js'
11
12
  import { useChatSdk } from '../../store/useChatSdk.js'
12
13
  import { useFocusedElements } from '../../hooks/useFocusedElements.js'
13
14
 
14
15
  export function Panel () {
15
- const { sdk, availability, thread, threadId, setThread, setThreadId, setChatVisibility, setMessages } = useApp()
16
+ const { sdk, availability, thread, threadId, setThread, setThreadId, setChatVisibility, setMessages, setUnseenCount } = useApp()
16
17
  const { fetchThread, fetchMessages } = useChatSdk(sdk)
17
18
 
18
19
  const [screen, setScreen] = useState(threadId ? 2 : 0)
@@ -51,7 +52,13 @@ export function Panel () {
51
52
  }
52
53
 
53
54
  if (threadId) {
54
- (thread) ? setScreen(2) : recover()
55
+ if (thread) {
56
+ thread.lastMessageSeen()
57
+ setUnseenCount(0)
58
+ setScreen(2)
59
+ } else {
60
+ recover()
61
+ }
55
62
  }
56
63
  }, [thread, threadId])
57
64
 
@@ -70,6 +77,7 @@ export function Panel () {
70
77
  const onBack = handleScreenChange(screen - 1)
71
78
  const onEndChat = handleScreenChange(3)
72
79
  const onResume = handleScreenChange(2)
80
+ const onSettings = handleScreenChange(5)
73
81
 
74
82
  const onEndChatConfirm = e => {
75
83
  e.preventDefault()
@@ -86,11 +94,14 @@ export function Panel () {
86
94
  ScreenComponent = <RequestChat onBack={onBack} />
87
95
  break
88
96
  case 2:
89
- ScreenComponent = <Chat setScreen={setScreen} onEndChat={onEndChat} />
97
+ ScreenComponent = <Chat onSettings={onSettings} onEndChat={onEndChat} />
90
98
  break
91
99
  case 3:
92
100
  ScreenComponent = <EndChat onResume={onResume} onEndChatConfirm={onEndChatConfirm} />
93
101
  break
102
+ case 5:
103
+ ScreenComponent = <Settings onCancel={onResume} />
104
+ break
94
105
  default:
95
106
  ScreenComponent = <PreChat onForward={onForward} />
96
107
  }
@@ -100,7 +111,7 @@ export function Panel () {
100
111
  }
101
112
 
102
113
  const Component = (
103
- <div id='wc-panel' className='wc-panel' role='dialog' tabIndex='-1' aria-modal='true' aria-labelledby='wc-header-title'>
114
+ <div id='wc-panel' role='dialog' className='wc-panel' tabIndex='-1' aria-modal='true' aria-labelledby='wc-header-title'>
104
115
  {ScreenComponent}
105
116
  </div>
106
117
  )
@@ -0,0 +1,67 @@
1
+ import React, { useState } from 'react'
2
+ import { PanelHeader } from '../panel/panel-header.jsx'
3
+
4
+ import { useApp } from '../../store/useApp.js'
5
+
6
+ export function Settings ({ onCancel }) {
7
+ const { settings, setSettings } = useApp()
8
+
9
+ const [optionAudio, setOptionAudio] = useState(settings.audio)
10
+ const [optionScroll, setOptionScroll] = useState(settings.scroll)
11
+
12
+ const onSave = e => {
13
+ e.preventDefault()
14
+
15
+ setSettings({ audio: optionAudio, scroll: optionScroll })
16
+ onCancel(e)
17
+ }
18
+
19
+ return (
20
+ <>
21
+ <PanelHeader />
22
+
23
+ <div className='wc-body'>
24
+ <fieldset className='govuk-fieldset govuk-!-margin-bottom-4'>
25
+ <legend className='govuk-fieldset__legend govuk-fieldset__legend'>
26
+ <h3 id='wc-subtitle' className='govuk-heading-s'>Change settings</h3>
27
+ </legend>
28
+ <div className='govuk-checkboxes govuk-checkboxes--small' data-module='govuk-checkboxes'>
29
+ <div className='govuk-checkboxes__item'>
30
+ <input
31
+ id='audio'
32
+ name='audio'
33
+ type='checkbox'
34
+ value='audio'
35
+ className='govuk-checkboxes__input'
36
+ defaultChecked={optionAudio}
37
+ onChange={() => setOptionAudio(!optionAudio)}
38
+ />
39
+ <label className='govuk-label govuk-checkboxes__label govuk-!-font-size-16' htmlFor='audio'>
40
+ Play a sound when receiving a new message
41
+ </label>
42
+ </div>
43
+ <div className='govuk-checkboxes__item'>
44
+ <input
45
+ id='scroll'
46
+ name='scroll'
47
+ type='checkbox'
48
+ value='scroll'
49
+ className='govuk-checkboxes__input'
50
+ defaultChecked={optionScroll}
51
+ onChange={() => setOptionScroll(!optionScroll)}
52
+ />
53
+ <label className='govuk-label govuk-checkboxes__label govuk-!-font-size-16' htmlFor='scroll'>
54
+ Scroll automatically to a new message
55
+ </label>
56
+ </div>
57
+ </div>
58
+ </fieldset>
59
+
60
+ <div className='govuk-button-group'>
61
+ <a id='settings-save' href='#' className='govuk-button govuk-!-font-size-16' data-module='govuk-button' onClick={onSave}>Save</a>
62
+ <a id='settings-cancel' href='#' className='govuk-link govuk-!-font-size-16' data-module='govuk-button' onClick={onCancel}>Cancel</a>
63
+ </div>
64
+ </div>
65
+ </>
66
+ )
67
+ }
@@ -4,7 +4,7 @@ import { ChatSdk } from '@nice-devone/nice-cxone-chat-web-sdk'
4
4
  import { Availability } from './components/availability/availability.jsx'
5
5
  import { checkAvailability } from './lib/check-availability'
6
6
  import { AppProvider } from './store/AppProvider.jsx'
7
- import { CUSTOMER_ID_STORAGE_KEY } from './store/reducer.js'
7
+ import { CUSTOMER_ID_STORAGE_KEY } from './store/constants.js'
8
8
 
9
9
  export async function init (container, options) {
10
10
  const sdk = new ChatSdk({
@@ -23,8 +23,8 @@ export async function init (container, options) {
23
23
  availability = 'UNAVAILABLE'
24
24
  }
25
25
  root.render(
26
- <AppProvider sdk={sdk} availability={availability}>
27
- <Availability availability={availability} />
26
+ <AppProvider sdk={sdk} availability={availability} options={options}>
27
+ <Availability />
28
28
  </AppProvider>
29
29
  )
30
30
  }
@@ -0,0 +1,36 @@
1
+ export const messageNotification = audioUrl => {
2
+ let buffer
3
+
4
+ const context = new (window.AudioContext || window.webkitAudioContext)()
5
+
6
+ if (context.state === 'suspended') {
7
+ const events = ['touchstart', 'touchend', 'mousedown', 'wheel', 'keydown', 'click']
8
+
9
+ const unlock = _e => {
10
+ events.forEach(event => {
11
+ document.body.removeEventListener(event, unlock)
12
+ })
13
+
14
+ context.resume()
15
+ }
16
+
17
+ events.forEach(event => {
18
+ document.body.addEventListener(event, unlock, false)
19
+ })
20
+ }
21
+
22
+ fetch(audioUrl)
23
+ .then(response => response.arrayBuffer())
24
+ .then(data => context.decodeAudioData(data))
25
+ .then(decodedData => {
26
+ buffer = decodedData
27
+ })
28
+ .catch(console.error)
29
+
30
+ return () => {
31
+ const source = context.createBufferSource()
32
+ source.buffer = buffer
33
+ source.connect(context.destination)
34
+ source.start()
35
+ }
36
+ }
@@ -1,19 +1,25 @@
1
1
  import React, { createContext, useEffect, useReducer, useMemo } from 'react'
2
2
  import { ChatEvent } from '@nice-devone/nice-cxone-chat-web-sdk'
3
3
 
4
- import { initialState, reducer, CUSTOMER_ID_STORAGE_KEY, THREAD_ID_STORAGE_KEY } from './reducer.js'
4
+ import { messageNotification } from '../lib/message-notification.js'
5
+
6
+ import { initialState, reducer } from './reducer.js'
7
+ import { CUSTOMER_ID_STORAGE_KEY, THREAD_ID_STORAGE_KEY, SETTINGS_STORAGE_KEY } from './constants.js'
5
8
 
6
9
  export const AppContext = createContext(initialState)
7
10
 
8
- export const AppProvider = ({ sdk, availability, children }) => {
11
+ export const AppProvider = ({ sdk, availability, options, children }) => {
9
12
  const [state, dispatch] = useReducer(reducer, initialState)
10
13
 
14
+ const playSound = messageNotification(options.audioUrl)
15
+
11
16
  /**
12
17
  * SDK event handlers
13
18
  */
14
19
  const onLiveChatRecovered = e => {
15
20
  dispatch({ type: 'SET_AGENT', payload: e.detail.data.inboxAssignee })
16
21
  dispatch({ type: 'SET_AGENT_STATUS', payload: e.detail.data.contact.status })
22
+ dispatch({ type: 'SET_UNSEEN_COUNT', payload: e.detail.data.contact.customerStatistics.unseenMessagesCount })
17
23
  }
18
24
 
19
25
  const onAssignedAgentChanged = e => {
@@ -32,6 +38,13 @@ export const AppProvider = ({ sdk, availability, children }) => {
32
38
  const onMessageCreated = e => {
33
39
  dispatch({ type: 'SET_MESSAGE', payload: e.detail.data.message })
34
40
  dispatch({ type: 'SET_AGENT_STATUS', payload: e.detail.data.case.status })
41
+ dispatch({ type: 'SET_UNSEEN_COUNT', payload: e.detail.data.case.customerStatistics.unseenMessagesCount })
42
+
43
+ const isAudioOn = JSON.parse(window.localStorage.getItem(SETTINGS_STORAGE_KEY)).audio
44
+
45
+ if (isAudioOn && e.detail.data.message.direction === 'outbound') {
46
+ playSound()
47
+ }
35
48
  }
36
49
 
37
50
  const onContactStatusChanged = e => {
@@ -55,6 +68,7 @@ export const AppProvider = ({ sdk, availability, children }) => {
55
68
 
56
69
  setCustomerId(window.localStorage.getItem(CUSTOMER_ID_STORAGE_KEY))
57
70
  setThreadId(window.localStorage.getItem(THREAD_ID_STORAGE_KEY))
71
+ setSettings(JSON.parse(window.localStorage.getItem(SETTINGS_STORAGE_KEY)) || state.settings)
58
72
 
59
73
  if (window.location.hash === '#webchat') {
60
74
  setChatVisibility(true)
@@ -88,16 +102,26 @@ export const AppProvider = ({ sdk, availability, children }) => {
88
102
  dispatch({ type: 'SET_MESSAGES', payload: messages })
89
103
  }
90
104
 
105
+ const setSettings = data => {
106
+ dispatch({ type: 'SET_SETTINGS', payload: data })
107
+ }
108
+
109
+ const setUnseenCount = unseenCount => {
110
+ dispatch({ type: 'SET_UNSEEN_COUNT', payload: unseenCount })
111
+ }
112
+
91
113
  /**
92
114
  * Application-wide state and state functions
93
115
  */
94
116
  const store = useMemo(() => ({
95
117
  ...state,
96
118
  sdk,
119
+ setSettings,
97
120
  setCustomerId,
98
121
  setThreadId,
99
122
  setThread,
100
123
  setMessages,
124
+ setUnseenCount,
101
125
  setChatVisibility,
102
126
  onLiveChatRecovered,
103
127
  onAssignedAgentChanged,
@@ -1,5 +1,18 @@
1
1
  import { transformMessages, transformMessage } from '../lib/transform-messages'
2
- import { CUSTOMER_ID_STORAGE_KEY, THREAD_ID_STORAGE_KEY } from './reducer'
2
+ import { CUSTOMER_ID_STORAGE_KEY, THREAD_ID_STORAGE_KEY, SETTINGS_STORAGE_KEY } from './constants'
3
+
4
+ const setSettings = (state, payload) => {
5
+ if (payload) {
6
+ window.localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(payload))
7
+ } else {
8
+ window.localStorage.removeItem(SETTINGS_STORAGE_KEY)
9
+ }
10
+
11
+ return {
12
+ ...state,
13
+ settings: payload
14
+ }
15
+ }
3
16
 
4
17
  const setCustomerId = (state, payload) => {
5
18
  if (payload) {
@@ -83,9 +96,17 @@ const setAgentStatus = (state, payload) => {
83
96
  }
84
97
  }
85
98
 
99
+ const setUnseenCount = (state, payload) => {
100
+ return {
101
+ ...state,
102
+ unseenCount: payload
103
+ }
104
+ }
105
+
86
106
  export const actionsMap = {
87
107
  SET_CHAT_VISIBILITY: setChatVisibility,
88
108
  SET_AVAILABILITY: setAvailability,
109
+ SET_SETTINGS: setSettings,
89
110
  SET_CUSTOMER_ID: setCustomerId,
90
111
  SET_THREAD_ID: setThreadId,
91
112
  SET_THREAD: setThread,
@@ -93,5 +114,6 @@ export const actionsMap = {
93
114
  SET_MESSAGES: setMessages,
94
115
  SET_AGENT: setAgent,
95
116
  SET_AGENT_TYPING: setAgentIsTyping,
96
- SET_AGENT_STATUS: setAgentStatus
117
+ SET_AGENT_STATUS: setAgentStatus,
118
+ SET_UNSEEN_COUNT: setUnseenCount
97
119
  }
@@ -0,0 +1,3 @@
1
+ export const SETTINGS_STORAGE_KEY = 'webchat_settings'
2
+ export const CUSTOMER_ID_STORAGE_KEY = 'webchat_customer_id'
3
+ export const THREAD_ID_STORAGE_KEY = 'webchat_thread_id'
@@ -1,18 +1,17 @@
1
1
  import { actionsMap } from './actions-map'
2
2
 
3
- export const CUSTOMER_ID_STORAGE_KEY = 'webchat_customer_id'
4
- export const THREAD_ID_STORAGE_KEY = 'webchat_thread_id'
5
-
6
3
  export const initialState = {
7
4
  availability: null,
8
5
  customerId: null,
9
6
  threadId: null,
10
7
  thread: null,
11
8
  messages: [],
9
+ unseenCount: 0,
12
10
  agent: null,
13
11
  agentStatus: null,
14
12
  isAgentTyping: false,
15
- isChatOpen: false
13
+ isChatOpen: false,
14
+ settings: { audio: true, scroll: true }
16
15
  }
17
16
 
18
17
  export const reducer = (state, action) => {