@seamly/web-ui 18.3.0-beta.1 → 19.0.0-beta.2

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 (106) hide show
  1. package/build/dist/lib/index.debug.js +348 -73
  2. package/build/dist/lib/index.debug.min.js +1 -1
  3. package/build/dist/lib/index.debug.min.js.LICENSE.txt +108 -8
  4. package/build/dist/lib/index.js +5247 -5187
  5. package/build/dist/lib/index.min.js +1 -1
  6. package/build/dist/lib/index.min.js.LICENSE.txt +1 -1
  7. package/build/dist/lib/standalone.js +2334 -2225
  8. package/build/dist/lib/standalone.min.js +1 -1
  9. package/build/dist/lib/standalone.min.js.LICENSE.txt +1 -1
  10. package/build/dist/lib/style-guide.js +1480 -796
  11. package/build/dist/lib/style-guide.min.js +1 -1
  12. package/build/dist/lib/styles.css +1 -1
  13. package/package.json +27 -28
  14. package/src/javascripts/api/index.js +25 -40
  15. package/src/javascripts/api/producer.js +3 -6
  16. package/src/javascripts/config.js +3 -3
  17. package/src/javascripts/domains/app/actions.js +24 -6
  18. package/src/javascripts/domains/app/hooks.js +6 -0
  19. package/src/javascripts/domains/app/index.js +3 -0
  20. package/src/javascripts/domains/app/reducer.js +16 -0
  21. package/src/javascripts/domains/app/selectors.js +8 -0
  22. package/src/javascripts/domains/app/utils.js +4 -0
  23. package/src/javascripts/domains/config/actions.js +1 -3
  24. package/src/javascripts/domains/config/middleware.js +0 -4
  25. package/src/javascripts/domains/config/reducer.js +2 -13
  26. package/src/javascripts/domains/config/selectors.js +3 -3
  27. package/src/javascripts/domains/config/utils.js +4 -0
  28. package/src/javascripts/domains/forms/actions.js +1 -3
  29. package/src/javascripts/domains/forms/reducer.js +1 -2
  30. package/src/javascripts/domains/forms/selectors.js +2 -2
  31. package/src/javascripts/domains/forms/utils.js +5 -0
  32. package/src/javascripts/domains/i18n/actions.js +35 -0
  33. package/src/javascripts/domains/i18n/hooks.js +38 -0
  34. package/src/javascripts/domains/i18n/index.js +5 -84
  35. package/src/javascripts/domains/i18n/reducer.js +58 -0
  36. package/src/javascripts/domains/i18n/selectors.js +15 -0
  37. package/src/javascripts/domains/i18n/utils.js +9 -0
  38. package/src/javascripts/domains/interrupt/actions.js +1 -3
  39. package/src/javascripts/domains/interrupt/reducer.js +1 -2
  40. package/src/javascripts/domains/interrupt/selectors.js +3 -2
  41. package/src/javascripts/domains/interrupt/utils.js +4 -0
  42. package/src/javascripts/domains/redux/hooks.js +1 -0
  43. package/src/javascripts/domains/store/index.js +7 -1
  44. package/src/javascripts/domains/translations/actions.js +1 -3
  45. package/src/javascripts/domains/translations/components/chat-status.js +1 -1
  46. package/src/javascripts/domains/translations/components/options-dialog/form.js +11 -6
  47. package/src/javascripts/domains/translations/index.js +1 -0
  48. package/src/javascripts/domains/translations/middleware.js +43 -0
  49. package/src/javascripts/domains/translations/reducer.js +2 -9
  50. package/src/javascripts/domains/translations/selectors.js +2 -2
  51. package/src/javascripts/domains/translations/utils.js +4 -0
  52. package/src/javascripts/index.js +3 -0
  53. package/src/javascripts/lib/engine/index.js +1 -0
  54. package/src/javascripts/lib/mutex.js +30 -0
  55. package/src/javascripts/lib/redux-helpers/index.js +11 -8
  56. package/src/javascripts/style-guide/components/app.js +7 -2
  57. package/src/javascripts/style-guide/components/static-core.js +9 -3
  58. package/src/javascripts/style-guide/states.js +8 -8
  59. package/src/javascripts/style-guide/style-guide-engine.js +14 -11
  60. package/src/javascripts/ui/components/conversation/event/divider/variants/new-translation.js +1 -1
  61. package/src/javascripts/ui/components/conversation/event/upload.js +2 -2
  62. package/src/javascripts/ui/components/core/seamly-activity-monitor.js +2 -0
  63. package/src/javascripts/ui/components/core/seamly-event-subscriber.js +2 -0
  64. package/src/javascripts/ui/components/core/seamly-instance-functions-loader.js +1 -7
  65. package/src/javascripts/ui/components/core/seamly-new-notifications.js +5 -6
  66. package/src/javascripts/ui/components/core/seamly-read-state.js +6 -4
  67. package/src/javascripts/ui/components/entry/text-entry/hooks.js +6 -4
  68. package/src/javascripts/ui/components/entry/text-entry/text-entry-form.js +10 -3
  69. package/src/javascripts/ui/components/entry/upload/file-upload-form.js +6 -3
  70. package/src/javascripts/ui/components/entry/upload/index.js +8 -3
  71. package/src/javascripts/ui/components/faq/faq.js +2 -2
  72. package/src/javascripts/ui/components/layout/app-frame.js +11 -8
  73. package/src/javascripts/ui/components/layout/interrupt.js +6 -2
  74. package/src/javascripts/ui/components/warnings/resume-conversation-prompt.js +1 -1
  75. package/src/javascripts/ui/components/widgets/upload-progress.js +1 -1
  76. package/src/javascripts/ui/hooks/seamly-api-hooks.js +0 -6
  77. package/src/javascripts/ui/hooks/seamly-entry-hooks.js +17 -21
  78. package/src/javascripts/ui/hooks/seamly-hooks.js +0 -1
  79. package/src/javascripts/ui/hooks/use-seamly-commands.js +5 -6
  80. package/src/javascripts/ui/hooks/use-seamly-visibility.js +3 -5
  81. package/src/javascripts/ui/hooks/use-single-file-upload.js +4 -1
  82. package/src/javascripts/ui/utils/general-utils.js +6 -13
  83. package/src/stylesheets/1-settings/_config.scss +2 -1
  84. package/src/stylesheets/3-app/_app.scss +3 -4
  85. package/src/stylesheets/5-components/_faq.scss +3 -8
  86. package/src/stylesheets/5-components/_modal.scss +3 -3
  87. package/webpack/config.package.js +0 -18
  88. package/webpack/config.site.js +6 -0
  89. package/webpack/defaults.js +0 -3
  90. package/CHANGELOG.md +0 -572
  91. package/build/dist/translations/de-informal.js +0 -274
  92. package/build/dist/translations/de-informal.min.js +0 -1
  93. package/build/dist/translations/en.js +0 -274
  94. package/build/dist/translations/en.min.js +0 -1
  95. package/build/dist/translations/es-informal.js +0 -280
  96. package/build/dist/translations/es-informal.min.js +0 -1
  97. package/build/dist/translations/nl-formal.js +0 -274
  98. package/build/dist/translations/nl-formal.min.js +0 -1
  99. package/build/dist/translations/nl-informal.js +0 -274
  100. package/build/dist/translations/nl-informal.min.js +0 -1
  101. package/src/javascripts/lib/i18n.js +0 -46
  102. package/translations/de-informal.js +0 -235
  103. package/translations/en.js +0 -232
  104. package/translations/es-informal.js +0 -241
  105. package/translations/nl-formal.js +0 -228
  106. package/translations/nl-informal.js +0 -228
@@ -42,14 +42,6 @@ export function createThunk(type, thunkCreator) {
42
42
  return fn
43
43
  }
44
44
 
45
- export function createDomain(domain) {
46
- return {
47
- createAction: prefixType(domain, createAction, DOMAIN_DELIMITER),
48
- createActions: prefixType(domain, createActions, DOMAIN_DELIMITER),
49
- createThunk: prefixType(domain, createThunk, DOMAIN_DELIMITER),
50
- }
51
- }
52
-
53
45
  export function createReducer(domain, handlers = {}, defaultState) {
54
46
  const reducer = (state, action) => {
55
47
  if (state === undefined) {
@@ -62,3 +54,14 @@ export function createReducer(domain, handlers = {}, defaultState) {
62
54
  reducer.toString = () => domain
63
55
  return reducer
64
56
  }
57
+
58
+ export function createDomain(domain) {
59
+ return {
60
+ createAction: prefixType(domain, createAction, DOMAIN_DELIMITER),
61
+ createActions: prefixType(domain, createActions, DOMAIN_DELIMITER),
62
+ createThunk: prefixType(domain, createThunk, DOMAIN_DELIMITER),
63
+ createReducer: (handlers, defaultState) =>
64
+ createReducer(domain, handlers, defaultState),
65
+ selectState: (state) => state[domain],
66
+ }
67
+ }
@@ -4,7 +4,12 @@ import StyleGuideView from './view'
4
4
  import StyleGuideLinks from './links'
5
5
  import { getStateObj } from '../states'
6
6
 
7
- const StyleGuideApp = ({ config, styleGuideConfig, headingLevel = 2 }) => {
7
+ const StyleGuideApp = ({
8
+ config,
9
+ styleGuideConfig,
10
+ translations,
11
+ headingLevel = 2,
12
+ }) => {
8
13
  const [staticState, setStaticState] = useState(null)
9
14
  const [selectedStateDescription, setSelectedStateDescription] = useState('')
10
15
  const [showStyleGuide, setShowStyleGuide] = useState(true)
@@ -138,7 +143,7 @@ const StyleGuideApp = ({ config, styleGuideConfig, headingLevel = 2 }) => {
138
143
  {showStyleGuide && (
139
144
  <StyleGuideView
140
145
  customComponents={styleGuideConfig.customComponents}
141
- translations={config.translations}
146
+ translations={translations}
142
147
  state={staticState}
143
148
  />
144
149
  )}
@@ -9,8 +9,12 @@ import {
9
9
  import stateReducer from '../../domains/store/state-reducer'
10
10
  import { Reducer as formReducer } from '../../domains/forms'
11
11
  import { Reducer as translationsReducer } from '../../domains/translations'
12
- import { Reducer as i18nReducer } from '../../domains/i18n'
12
+ import {
13
+ Reducer as i18nReducer,
14
+ Actions as I18nActions,
15
+ } from '../../domains/i18n'
13
16
  import { Reducer as interruptReducer } from '../../domains/interrupt'
17
+ import { Reducer as appReducer } from '../../domains/app'
14
18
  import {
15
19
  Reducer as configReducer,
16
20
  Actions as ConfigActions,
@@ -24,7 +28,7 @@ const bareApi = {
24
28
  store: { get: () => {}, set: () => {} },
25
29
  }
26
30
 
27
- const SeamlyTestCore = ({ state, children }) => {
31
+ const SeamlyTestCore = ({ state, translations, children }) => {
28
32
  const liveMsgRef = useRef(() => {})
29
33
  const eventBusRef = useRef({ emit: () => {} })
30
34
 
@@ -38,6 +42,7 @@ const SeamlyTestCore = ({ state, children }) => {
38
42
  const newStore = createReduxStore({
39
43
  reducers: {
40
44
  state: stateReducer,
45
+ [String(appReducer)]: appReducer,
41
46
  [String(configReducer)]: configReducer,
42
47
  [String(formReducer)]: formReducer,
43
48
  [String(translationsReducer)]: translationsReducer,
@@ -51,8 +56,9 @@ const SeamlyTestCore = ({ state, children }) => {
51
56
  },
52
57
  })
53
58
  newStore.dispatch(ConfigActions.initialize(configSlice || {}))
59
+ newStore.dispatch(I18nActions.setLocaleResolve('en-GB', translations))
54
60
  return newStore
55
- }, [state])
61
+ }, [state, translations])
56
62
 
57
63
  return (
58
64
  state && (
@@ -479,7 +479,7 @@ const imageMessage = {
479
479
  description: 'Plaatje',
480
480
  isZoomable: false,
481
481
  type: 'image',
482
- url: 'https://via.placeholder.com/150',
482
+ url: 'https://developers.seamly.ai/clients/web-ui/static/photos/image-square-small.jpg',
483
483
  },
484
484
  fromClient: false,
485
485
  fromHistory: true,
@@ -504,7 +504,7 @@ const imageMessageWithLightbox = {
504
504
  description: 'Plaatje',
505
505
  isZoomable: true,
506
506
  type: 'image',
507
- url: 'https://via.placeholder.com/150',
507
+ url: 'https://developers.seamly.ai/clients/web-ui/static/photos/image-portrait.jpg',
508
508
  },
509
509
  fromClient: false,
510
510
  fromHistory: true,
@@ -654,7 +654,7 @@ const fileDownloadPayload = {
654
654
  contentType: 'image/jpg',
655
655
  filename: 'placeholder.jpg',
656
656
  filesize: 991078,
657
- url: 'https://via.placeholder.com/150',
657
+ url: 'https://developers.seamly.ai/clients/web-ui/static/photos/image-square-small.jpg',
658
658
  },
659
659
  }
660
660
 
@@ -796,7 +796,7 @@ const cardAskText = {
796
796
  description:
797
797
  'Pizza Margherita is a **typical Neapolitan pizza**.\n\nIt is made with San Marzano tomatoes, mozzarella cheese, fresh basil, salt, and extra-virgin olive oil.',
798
798
  image:
799
- 'https://via.placeholder.com/400x200/dee3e5/6a7f8c?text=Margherita',
799
+ 'https://developers.seamly.ai/clients/web-ui/static/photos/card-square.jpg',
800
800
  title: 'Pizza Margherita',
801
801
  },
802
802
  },
@@ -815,7 +815,7 @@ const cardNavigate = {
815
815
  buttonText: 'Order now!',
816
816
  description: 'Pizza Margherita is a **typical Neapolitan pizza**.',
817
817
  image:
818
- 'https://via.placeholder.com/400x200/dee3e5/6a7f8c?text=Margherita',
818
+ 'https://developers.seamly.ai/clients/web-ui/static/photos/card-landscape.jpg',
819
819
  title: 'Pizza Margherita',
820
820
  },
821
821
  },
@@ -832,7 +832,7 @@ const cardTopic = {
832
832
  },
833
833
  buttonText: 'Set topic! (title & description optional)',
834
834
  image:
835
- 'https://via.placeholder.com/400x200/dee3e5/6a7f8c?text=Margherita',
835
+ 'https://developers.seamly.ai/clients/web-ui/static/photos/card-portrait.jpg',
836
836
  },
837
837
  },
838
838
  }
@@ -1182,9 +1182,9 @@ const standardState = {
1182
1182
  showDisclaimer: true,
1183
1183
  },
1184
1184
  },
1185
- cobrowserBar: {
1185
+ chatStatusBar: {
1186
1186
  category: categoryKeys.features,
1187
- headingText: `Cobrowse bar`,
1187
+ headingText: `Chat status bar`,
1188
1188
  description: '',
1189
1189
  ...baseState,
1190
1190
  options: {
@@ -1,6 +1,5 @@
1
1
  import { render } from 'preact'
2
- import { Engine } from '@seamly/web-ui'
3
- import en from '@seamly/web-ui/translations/en'
2
+ import { API, Engine } from '@seamly/web-ui'
4
3
  import StyleGuideApp from './components/app'
5
4
 
6
5
  class SeamlyStyleGuideInstance extends Engine {
@@ -9,31 +8,35 @@ class SeamlyStyleGuideInstance extends Engine {
9
8
  this.styleGuideConfig = styleGuideConfig || {}
10
9
  }
11
10
 
12
- render() {
11
+ async render() {
13
12
  const restComponents = {
14
13
  ...(this.config.customComponents || {}),
15
14
  view: undefined,
16
15
  }
17
16
 
17
+ const api = new API({
18
+ namespace: this.config.namespace,
19
+ config: this.config.api,
20
+ })
21
+ api.URLS = {
22
+ translations: `/client/${this.config.api.key}/translations/{version}/{locale}.json`,
23
+ }
24
+ const translations = await api.getTranslations(
25
+ this.config.context.locale || 'en-GB',
26
+ )
27
+
18
28
  const renderConfig = {
19
29
  ...this.config,
20
30
  customComponents: Object.keys(restComponents).length
21
31
  ? restComponents
22
32
  : undefined,
23
- translations: this.config.translations || {
24
- ...en,
25
- disclaimer: {
26
- ...en.disclaimer,
27
- content:
28
- 'This chat session will be saved to help us improve our service delivery. <a href="https://seamly.ai/">More information</a>',
29
- },
30
- },
31
33
  }
32
34
 
33
35
  render(
34
36
  <StyleGuideApp
35
37
  config={renderConfig}
36
38
  styleGuideConfig={this.styleGuideConfig}
39
+ translations={translations}
37
40
  />,
38
41
  this.parentElement,
39
42
  )
@@ -25,7 +25,7 @@ const NewTranslationDivider = ({ event }) => {
25
25
  translationEnabled
26
26
  ? 'translations.divider.startText'
27
27
  : 'translations.divider.stopText',
28
- languageName,
28
+ { language: languageName },
29
29
  )}
30
30
  </p>
31
31
  {translationEnabled ? (
@@ -23,8 +23,8 @@ const Upload = ({ event, ...props }) => {
23
23
  const srText = useMemo(
24
24
  () =>
25
25
  url
26
- ? t('fileUpload.srFileDownloadText', filename)
27
- : t('fileUpload.srFileUploadedText', filename),
26
+ ? t('fileUpload.srFileDownloadText', { fileName: filename })
27
+ : t('fileUpload.srFileUploadedText', { fileName: filename }),
28
28
  [url, filename, t],
29
29
  )
30
30
 
@@ -5,6 +5,7 @@ import {
5
5
  useSeamlyIdleDetachCountdown,
6
6
  } from '../../hooks/seamly-hooks'
7
7
  import { activitySendDelay } from '../../../config'
8
+ import { className } from '../../../lib/css'
8
9
 
9
10
  const SeamlyActivityMonitor = ({ children }) => {
10
11
  const prevSendTimestamp = useRef(0)
@@ -35,6 +36,7 @@ const SeamlyActivityMonitor = ({ children }) => {
35
36
  // be fired inside the container on the initial focus event.
36
37
  return (
37
38
  <div
39
+ className={className('activity-monitor')}
38
40
  tabIndex="-1"
39
41
  onMouseDown={onActivityHandler}
40
42
  onKeyUp={onActivityHandler}
@@ -13,6 +13,7 @@ import SeamlyGeneralError from '../../../api/errors/seamly-general-error'
13
13
  import SeamlySessionExpiredError from '../../../api/errors/seamly-session-expired-error'
14
14
  import SeamlyOfflineError from '../../../api/errors/seamly-offline-error'
15
15
  import { Actions as InterruptActions } from '../../../domains/interrupt'
16
+ import { Actions as AppActions } from '../../../domains/app'
16
17
 
17
18
  const {
18
19
  ADD_EVENT,
@@ -92,6 +93,7 @@ const SeamlyEventSubscriber = ({ eventBus }) => {
92
93
  dispatch({ type: INIT_RESUME_CONVERSATION_PROMPT })
93
94
  break
94
95
  case 'user_first_response':
96
+ dispatch(AppActions.setHasResponded(true))
95
97
  eventBus.emit('system.userFirstResponse', payload.body)
96
98
  break
97
99
  }
@@ -59,13 +59,7 @@ const SeamlyInstanceFunctionsLoader = () => {
59
59
  },
60
60
  [api?.send],
61
61
  )
62
- useSeamlyInstanceFunction(
63
- 'setLocale',
64
- (locale) => {
65
- sendContext({ locale })
66
- },
67
- [api?.send],
68
- )
62
+
69
63
  useSeamlyInstanceFunction(
70
64
  'setVariables',
71
65
  (variables) => {
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, useCallback } from 'preact/hooks'
1
+ import { useEffect, useRef, useMemo } from 'preact/hooks'
2
2
  import { useI18n } from '../../../domains/i18n'
3
3
  import {
4
4
  useEvents,
@@ -20,8 +20,8 @@ const SeamlyNewNotifications = () => {
20
20
  const prevIsOpen = useRef(null)
21
21
  const debounceFunc = useRef(null)
22
22
 
23
- const notifyUnread = useCallback(
24
- debounce((eventArray) => {
23
+ const notifyUnread = useMemo(() => {
24
+ return debounce((eventArray) => {
25
25
  const serverEventCount = eventArray.filter(
26
26
  ({ payload }) => !payload.fromClient && !payload.fromHistory,
27
27
  ).length
@@ -33,9 +33,8 @@ const SeamlyNewNotifications = () => {
33
33
  )
34
34
  previousServerEventCount.current = serverEventCount
35
35
  }
36
- }, newMessageScreenReaderWait),
37
- [sendPolite],
38
- )
36
+ }, newMessageScreenReaderWait)
37
+ }, [sendPolite, t])
39
38
 
40
39
  useEffect(() => {
41
40
  if (events.length > previousEventCount.current) {
@@ -1,4 +1,4 @@
1
- import { useEffect, useCallback, useRef } from 'preact/hooks'
1
+ import { useEffect, useRef, useMemo } from 'preact/hooks'
2
2
 
3
3
  import {
4
4
  useEvents,
@@ -28,9 +28,11 @@ const SeamlyReadState = () => {
28
28
  const { sendAction } = useSeamlyCommands()
29
29
  const unreadCount = useSeamlyUnreadCount()
30
30
  const { sendPolite } = useLiveRegion()
31
- const sendLive = useCallback(debounce(sendPolite, unreadScreenReaderWait), [
32
- sendPolite,
33
- ])
31
+ const sendLive = useMemo(
32
+ () => debounce(sendPolite, unreadScreenReaderWait),
33
+
34
+ [sendPolite],
35
+ )
34
36
  const prevIsVisible = useRef(null)
35
37
  const cancelSend = useRef(null)
36
38
 
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useMemo } from 'preact/hooks'
1
+ import { useEffect, useMemo } from 'preact/hooks'
2
2
  import { useI18n } from '../../../../domains/i18n'
3
3
  import { useLiveRegion } from '../../../hooks/live-region-hooks'
4
4
  import { useEntryTextLimit } from '../../../hooks/seamly-state-hooks'
@@ -14,14 +14,16 @@ export function useCharacterLimit(controlName) {
14
14
  const { sendAssertive } = useLiveRegion()
15
15
  const { hasLimit, limit } = useEntryTextLimit()
16
16
 
17
- const debouncedSendAssertive = useCallback(
18
- debounce(sendAssertive, maxCharacterSrDebounceDelay),
17
+ const debouncedSendAssertive = useMemo(
18
+ () => debounce(sendAssertive, maxCharacterSrDebounceDelay),
19
19
  [sendAssertive],
20
20
  )
21
21
  const validateLimit = useMemo(() => {
22
22
  return debounce((_reachedCharacterWarning, _remainingChars) => {
23
23
  if (_reachedCharacterWarning) {
24
- debouncedSendAssertive(t('input.srCharacterLimitText', _remainingChars))
24
+ debouncedSendAssertive(
25
+ t('input.srCharacterLimitText', { limit: _remainingChars }),
26
+ )
25
27
  }
26
28
  }, maxCharacterSrDebounceDelay)
27
29
  }, [debouncedSendAssertive, t])
@@ -33,18 +33,25 @@ export default function TextEntryForm({ controlName, skipLinkId }) {
33
33
 
34
34
  const handleFocus = useCallback(() => {
35
35
  if (reachedCharacterWarning) {
36
- sendAssertive(t('input.srCharacterLimitText', remainingChars))
36
+ sendAssertive(t('input.srCharacterLimitText', { limit: remainingChars }))
37
37
  }
38
38
 
39
39
  emitEvent('ui.inputFocus')
40
40
  }, [t, sendAssertive, reachedCharacterWarning, remainingChars, emitEvent])
41
41
  const placeholder = useMemo(
42
42
  () =>
43
- t('input.inputPlaceholder', hasCharacterLimit ? characterLimit : null),
43
+ t('input.inputPlaceholder', {
44
+ hasLimit: hasCharacterLimit,
45
+ limit: hasCharacterLimit ? characterLimit : null,
46
+ }),
44
47
  [t, hasCharacterLimit, characterLimit],
45
48
  )
46
49
  const labelText = useMemo(
47
- () => t('input.inputLabel', hasCharacterLimit ? characterLimit : null),
50
+ () =>
51
+ t('input.inputLabel', {
52
+ hasLimit: hasCharacterLimit,
53
+ limit: hasCharacterLimit ? characterLimit : null,
54
+ }),
48
55
  [t, hasCharacterLimit, characterLimit],
49
56
  )
50
57
  // When the input holds a value, the component should be blocked from switching
@@ -15,8 +15,8 @@ export default function FileInputForm({
15
15
  }) {
16
16
  const { t } = useI18n()
17
17
  const [{ value: fileList }] = useFormControl(controlName)
18
- const selectedFileName =
19
- fileList && fileList.length > 0 ? fileList[0].name : ''
18
+ const hasFile = fileList && fileList.length > 0
19
+ const selectedFileName = hasFile ? fileList[0].name : ''
20
20
 
21
21
  return (
22
22
  <Form className={className('input', 'input--file')}>
@@ -25,7 +25,10 @@ export default function FileInputForm({
25
25
  id={skiplinkId}
26
26
  accept={accept}
27
27
  labelText={t('fileUpload.labelText')}
28
- outputText={t('fileUpload.selectedText', selectedFileName)}
28
+ outputText={t('fileUpload.selectedText', {
29
+ hasFile,
30
+ filename: selectedFileName,
31
+ })}
29
32
  contentHint={contentHint}
30
33
  />
31
34
  <div className={className('upload__button-container')}>
@@ -40,12 +40,14 @@ const Upload = () => {
40
40
 
41
41
  const hasError = false
42
42
 
43
- const { selectedFileName, uploadHandle, hasServerError, progress } =
43
+ const { hasFile, selectedFileName, uploadHandle, hasServerError, progress } =
44
44
  useSingleFileUpload(formName, fileInputName)
45
45
  const notificationId = useGeneratedId()
46
46
  const prevIsComplete = useRef(true)
47
47
 
48
- const contentHintText = t('fileUpload.contentHint', formatBytes(maxSize))
48
+ const contentHintText = t('fileUpload.contentHint', {
49
+ size: formatBytes(maxSize),
50
+ })
49
51
  const prevContentHintText = useRef('')
50
52
  const containerRef = useRef(null)
51
53
 
@@ -177,7 +179,10 @@ const Upload = () => {
177
179
  contentHint={contentHintText}
178
180
  isComplete={isComplete}
179
181
  isUploading={isUploading}
180
- outputText={t('fileUpload.selectedText', selectedFileName)}
182
+ outputText={t('fileUpload.selectedText', {
183
+ hasFile,
184
+ filename: selectedFileName,
185
+ })}
181
186
  onClickCancel={handleOnClickCancel}
182
187
  />
183
188
  )}
@@ -10,7 +10,6 @@ import {
10
10
  } from '../../hooks/seamly-state-hooks'
11
11
  import { useGeneratedId } from '../../hooks/utility-hooks'
12
12
  import { useSkiplinkTargetFocusing } from '../../hooks/focus-helper-hooks'
13
- import { useSeamlyHasUserResponded } from '../../hooks/seamly-api-hooks'
14
13
  import { useLiveRegion } from '../../hooks/live-region-hooks'
15
14
  import useSeamlyIdleDetachCountdown from '../../hooks/use-seamly-idle-detach-countdown'
16
15
  import useSeamlyResumeConversationPrompt from '../../hooks/use-seamly-resume-conversation-prompt'
@@ -20,6 +19,7 @@ import InOutTransition, {
20
19
  } from '../widgets/in-out-transition'
21
20
  import { useTranslatedEventData } from '../../../domains/translations'
22
21
  import { useInterrupt } from '../../../domains/interrupt'
22
+ import { useUserHasResponded } from '../../../domains/app'
23
23
 
24
24
  const Faq = () => {
25
25
  const { t } = useI18n()
@@ -55,7 +55,7 @@ const Faq = () => {
55
55
  const prevHasFaqs = useRef(false)
56
56
 
57
57
  const { isInline } = useSeamlyLayoutMode()
58
- const hasResponded = useSeamlyHasUserResponded()
58
+ const hasResponded = useUserHasResponded()
59
59
  const hideForWindow = !isInline && hasResponded
60
60
  const prevHideForWindow = useRef(hideForWindow)
61
61
 
@@ -1,25 +1,25 @@
1
- import { useState, useEffect, useCallback } from 'preact/hooks'
1
+ import { useCallback, useMemo } from 'preact/hooks'
2
2
  import { className } from '../../../lib/css'
3
3
  import {
4
4
  useSeamlyAppContainerClassNames,
5
5
  useSeamlyVisibility,
6
6
  useSeamlyLayoutMode,
7
- useSeamlyHasUserResponded,
8
7
  useSeamlyContainerElement,
9
8
  } from '../../hooks/seamly-hooks'
10
9
  import Faq from '../faq/faq'
11
10
  import { visibilityStates } from '../../utils/seamly-utils'
12
11
  import { useConfig } from '../../../domains/config'
12
+ import { useUserHasResponded } from '../../../domains/app'
13
+ import { useI18n } from '../../../domains/i18n'
13
14
 
14
15
  const AppFrame = ({ children }) => {
15
16
  const [, setSeamlyContainerElement] = useSeamlyContainerElement()
16
17
  const { isOpen, isVisible, setVisibility } = useSeamlyVisibility()
17
- const { context, zIndex, showFaq } = useConfig()
18
+ const { zIndex, showFaq } = useConfig()
18
19
  const { isModal, isInline } = useSeamlyLayoutMode()
19
20
  const appContainerClassNames = useSeamlyAppContainerClassNames()
20
- const userResponded = useSeamlyHasUserResponded()
21
- const [blockLang, setBlockLang] = useState(undefined)
22
- const { locale } = context || {}
21
+ const userResponded = useUserHasResponded()
22
+ const { locale } = useI18n()
23
23
 
24
24
  const containerElementRef = useCallback(
25
25
  (container) => {
@@ -28,13 +28,16 @@ const AppFrame = ({ children }) => {
28
28
  [setSeamlyContainerElement],
29
29
  )
30
30
 
31
- useEffect(() => {
31
+ const blockLang = useMemo(() => {
32
32
  if (locale) {
33
33
  const htmlElementLang = document
34
34
  .querySelector('html')
35
35
  .getAttribute('lang')
36
- if (!htmlElementLang || htmlElementLang !== locale) setBlockLang(locale)
36
+ if (htmlElementLang !== locale) {
37
+ return locale
38
+ }
37
39
  }
40
+ return undefined
38
41
  }, [locale])
39
42
 
40
43
  const classNames = ['app', ...appContainerClassNames]
@@ -25,14 +25,18 @@ const Interrupt = ({
25
25
  useEffect(() => {
26
26
  if (isExpiredError) {
27
27
  seamlyCommands[action]()
28
- } else if (srText) {
28
+ }
29
+ }, [action, seamlyCommands, isExpiredError])
30
+
31
+ useEffect(() => {
32
+ if (!isExpiredError && srText) {
29
33
  // Wait for live regions to stabilise in case this occurs
30
34
  // at an initial render
31
35
  setTimeout(() => {
32
36
  sendPolite(srText)
33
37
  }, 200)
34
38
  }
35
- }, [sendPolite, seamlyCommands, srText, isExpiredError, action])
39
+ }, [sendPolite, srText, isExpiredError])
36
40
 
37
41
  const onClickHandler = () => {
38
42
  seamlyCommands[action]()
@@ -20,7 +20,7 @@ const ResumeConversationPrompt = () => {
20
20
  return (
21
21
  <Prompt
22
22
  baseClassName="prompt"
23
- title={t('resumeConversationPrompt.title', currentAgentName)}
23
+ title={t('resumeConversationPrompt.title', { name: currentAgentName })}
24
24
  >
25
25
  <div className={className('prompt__options')}>
26
26
  <button
@@ -25,7 +25,7 @@ const UploadProgress = () => {
25
25
  role="progressbar"
26
26
  aria-valuemin="0"
27
27
  aria-valuemax="100"
28
- aria-label={t('fileUpload.srProgressLabel', name)}
28
+ aria-label={t('fileUpload.srProgressLabel', { fileName: name })}
29
29
  max="100"
30
30
  aria-valuenow={progress}
31
31
  value={progress}
@@ -22,9 +22,3 @@ export const useSeamlyHasConversation = () => {
22
22
  const url = useSeamlyConversationUrl()
23
23
  return !!url
24
24
  }
25
-
26
- export const useSeamlyHasUserResponded = () => {
27
- const { get } = useSeamlyObjectStore()
28
-
29
- return get ? !!get('userResponded') : false
30
- }
@@ -4,7 +4,7 @@ import { useSeamlyStateContext } from './seamly-state-hooks'
4
4
  import useSeamlyDispatchContext from './use-seamly-dispatch'
5
5
  import { useSeamlyOptions } from './seamly-option-hooks'
6
6
  import useSeamlyCommands from './use-seamly-commands'
7
- import { useConfig } from '../../domains/config'
7
+ import { typingTimeout } from '../../config'
8
8
 
9
9
  const {
10
10
  SET_BLOCK_AUTO_ENTRY_SWITCH,
@@ -14,19 +14,18 @@ const {
14
14
 
15
15
  export const useSeamlyTyping = () => {
16
16
  const { sendAction } = useSeamlyCommands()
17
- const { typing: typingConfig } = useConfig()
18
17
  const { features } = useSeamlyOptions()
19
18
  const { typingPeekahead } = features || {}
20
- const typingTimeout = useRef(null)
21
- const sendEndTypingTimeout = useRef(null)
19
+ const typingTimerId = useRef(null)
20
+ const sendEndTypingTimerId = useRef(null)
22
21
  const isTyping = useRef(false)
23
- const typingInterval = useRef(null)
22
+ const typingIntervalId = useRef(null)
24
23
 
25
24
  useEffect(() => {
26
25
  return () => {
27
- clearInterval(typingInterval.current)
28
- clearTimeout(typingTimeout.current)
29
- clearTimeout(sendEndTypingTimeout.current)
26
+ clearInterval(typingIntervalId.current)
27
+ clearTimeout(typingTimerId.current)
28
+ clearTimeout(sendEndTypingTimerId.current)
30
29
  }
31
30
  }, [])
32
31
 
@@ -48,33 +47,30 @@ export const useSeamlyTyping = () => {
48
47
  if ((e.code && e.code === 'Enter') || e.keyCode === 13) {
49
48
  return
50
49
  }
51
- if (!typingConfig) {
52
- return
53
- }
54
50
 
55
51
  isTyping.current = true
56
- if (!typingInterval.current) {
52
+ if (!typingIntervalId.current) {
57
53
  sendTypingState(true, e.target.value)
58
- typingInterval.current = setInterval(() => {
54
+ typingIntervalId.current = setInterval(() => {
59
55
  if (!isTyping.current) {
60
- clearInterval(typingInterval.current)
61
- typingInterval.current = null
56
+ clearInterval(typingIntervalId.current)
57
+ typingIntervalId.current = null
62
58
  } else if (typingPeekahead && typingPeekahead.enabled) {
63
59
  sendTypingState(true, e.target.value)
64
60
  }
65
- }, typingConfig.timeout)
61
+ }, typingTimeout)
66
62
  }
67
63
 
68
- clearTimeout(typingTimeout.current)
69
- clearTimeout(sendEndTypingTimeout.current)
64
+ clearTimeout(typingTimerId.current)
65
+ clearTimeout(sendEndTypingTimerId.current)
70
66
 
71
- typingTimeout.current = setTimeout(() => {
67
+ typingTimerId.current = setTimeout(() => {
72
68
  isTyping.current = false
73
69
  }, 300)
74
70
 
75
- sendEndTypingTimeout.current = setTimeout(() => {
71
+ sendEndTypingTimerId.current = setTimeout(() => {
76
72
  sendTypingState(false, e.target.value)
77
- }, typingConfig.timeout)
73
+ }, typingTimeout)
78
74
  }
79
75
  }
80
76
 
@@ -24,7 +24,6 @@ export {
24
24
  export {
25
25
  useSeamlyApiContext,
26
26
  useSeamlyConversationUrl,
27
- useSeamlyHasUserResponded,
28
27
  } from './seamly-api-hooks'
29
28
  export { default as useSeamlyDispatchContext } from './use-seamly-dispatch'
30
29
  export {