@seamly/web-ui 21.0.8 → 22.0.0-beta.1

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 (163) hide show
  1. package/build/dist/lib/components.js +9295 -7845
  2. package/build/dist/lib/components.js.map +1 -0
  3. package/build/dist/lib/components.min.js +2 -1
  4. package/build/dist/lib/components.min.js.LICENSE.txt +2 -2
  5. package/build/dist/lib/components.min.js.map +1 -0
  6. package/build/dist/lib/config.js +2 -1
  7. package/build/dist/lib/config.js.map +1 -0
  8. package/build/dist/lib/config.min.js +2 -1
  9. package/build/dist/lib/config.min.js.map +1 -0
  10. package/build/dist/lib/contexts.js +2 -1
  11. package/build/dist/lib/contexts.js.map +1 -0
  12. package/build/dist/lib/contexts.min.js +2 -1
  13. package/build/dist/lib/contexts.min.js.map +1 -0
  14. package/build/dist/lib/deprecated-view.css +1 -1
  15. package/build/dist/lib/deprecated-view.js +1 -1
  16. package/build/dist/lib/hooks.js +6839 -5731
  17. package/build/dist/lib/hooks.js.map +1 -0
  18. package/build/dist/lib/hooks.min.js +2 -1
  19. package/build/dist/lib/hooks.min.js.map +1 -0
  20. package/build/dist/lib/index.debug.js +964 -383
  21. package/build/dist/lib/index.debug.js.map +1 -0
  22. package/build/dist/lib/index.debug.min.js +2 -1
  23. package/build/dist/lib/index.debug.min.js.LICENSE.txt +336 -108
  24. package/build/dist/lib/index.debug.min.js.map +1 -0
  25. package/build/dist/lib/index.js +2991 -5659
  26. package/build/dist/lib/index.js.map +1 -0
  27. package/build/dist/lib/index.min.js +2 -1
  28. package/build/dist/lib/index.min.js.LICENSE.txt +2 -2
  29. package/build/dist/lib/index.min.js.map +1 -0
  30. package/build/dist/lib/sounds/beep.mp3 +0 -0
  31. package/build/dist/lib/standalone.js +9454 -12449
  32. package/build/dist/lib/standalone.js.map +1 -0
  33. package/build/dist/lib/standalone.min.js +2 -1
  34. package/build/dist/lib/standalone.min.js.LICENSE.txt +1 -1
  35. package/build/dist/lib/standalone.min.js.map +1 -0
  36. package/build/dist/lib/storage.js +2 -1
  37. package/build/dist/lib/storage.js.map +1 -0
  38. package/build/dist/lib/storage.min.js +2 -1
  39. package/build/dist/lib/storage.min.js.map +1 -0
  40. package/build/dist/lib/style-guide.js +1828 -6015
  41. package/build/dist/lib/style-guide.js.map +1 -0
  42. package/build/dist/lib/style-guide.min.js +2 -1
  43. package/build/dist/lib/style-guide.min.js.LICENSE.txt +2 -2
  44. package/build/dist/lib/style-guide.min.js.map +1 -0
  45. package/build/dist/lib/styles-default-implementation.css +1 -1
  46. package/build/dist/lib/styles-default-implementation.js +1 -1
  47. package/build/dist/lib/styles.css +1 -1
  48. package/build/dist/lib/styles.js +1 -1
  49. package/build/dist/lib/utils.js +11601 -14586
  50. package/build/dist/lib/utils.js.map +1 -0
  51. package/build/dist/lib/utils.min.js +2 -1
  52. package/build/dist/lib/utils.min.js.LICENSE.txt +1 -6
  53. package/build/dist/lib/utils.min.js.map +1 -0
  54. package/package.json +58 -48
  55. package/src/javascripts/api/conversation-connector.ts +2 -0
  56. package/src/javascripts/api/errors/seamly-api-error.ts +15 -0
  57. package/src/javascripts/api/index.ts +168 -94
  58. package/src/javascripts/config.ts +1 -1
  59. package/src/javascripts/config.types.ts +18 -11
  60. package/src/javascripts/domains/config/selectors.ts +1 -1
  61. package/src/javascripts/domains/config/slice.ts +12 -0
  62. package/src/javascripts/domains/forms/forms.types.ts +1 -0
  63. package/src/javascripts/domains/forms/hooks.ts +10 -2
  64. package/src/javascripts/domains/i18n/slice.ts +2 -0
  65. package/src/javascripts/domains/interrupt/hooks.ts +15 -7
  66. package/src/javascripts/domains/interrupt/selectors.ts +4 -0
  67. package/src/javascripts/domains/interrupt/slice.ts +2 -2
  68. package/src/javascripts/domains/store/selectors.ts +23 -10
  69. package/src/javascripts/domains/store/slice.ts +63 -11
  70. package/src/javascripts/domains/store/store.types.ts +39 -1
  71. package/src/javascripts/domains/translations/components/options-button.tsx +1 -4
  72. package/src/javascripts/domains/translations/components/translation-status.tsx +4 -3
  73. package/src/javascripts/domains/translations/hooks.ts +11 -4
  74. package/src/javascripts/domains/translations/slice.ts +2 -0
  75. package/src/javascripts/index.ts +2 -0
  76. package/src/javascripts/lib/url-helpers.ts +24 -0
  77. package/src/javascripts/schema.ts +10 -0
  78. package/src/javascripts/style-guide/states.js +65 -0
  79. package/src/javascripts/ui/components/app-options/index.js +4 -3
  80. package/src/javascripts/ui/components/conversation/conversation.tsx +2 -0
  81. package/src/javascripts/ui/components/conversation/event/chat-scroll/chat-scroll-provider.tsx +2 -0
  82. package/src/javascripts/ui/components/conversation/event/choice-prompt.js +1 -1
  83. package/src/javascripts/ui/components/conversation/event/text.js +1 -1
  84. package/src/javascripts/ui/components/conversation/event/upload.js +50 -9
  85. package/src/javascripts/ui/components/conversation/use-chat-scroll.ts +3 -2
  86. package/src/javascripts/ui/components/core/seamly-event-subscriber.ts +16 -14
  87. package/src/javascripts/ui/components/core/seamly-file-upload.tsx +156 -0
  88. package/src/javascripts/ui/components/core/seamly-instance-functions-loader.js +5 -5
  89. package/src/javascripts/ui/components/entry/abort-transaction-button/abort-transaction-button.tsx +45 -0
  90. package/src/javascripts/ui/components/entry/deprecated-toggle-button.js +4 -3
  91. package/src/javascripts/ui/components/entry/text-entry/hooks.ts +108 -0
  92. package/src/javascripts/ui/components/entry/text-entry/index.js +7 -4
  93. package/src/javascripts/ui/components/entry/text-entry/{text-entry-form.js → text-entry-form.tsx} +8 -22
  94. package/src/javascripts/ui/components/faq/faq.js +5 -4
  95. package/src/javascripts/ui/components/form-controls/{input.js → input.tsx} +13 -2
  96. package/src/javascripts/ui/components/form-controls/{wrapper.js → wrapper.tsx} +8 -4
  97. package/src/javascripts/ui/components/layout/agent-info.js +4 -3
  98. package/src/javascripts/ui/components/layout/chat-frame.js +7 -8
  99. package/src/javascripts/ui/components/layout/deprecated-chat-frame.js +7 -8
  100. package/src/javascripts/ui/components/layout/interrupt.js +6 -15
  101. package/src/javascripts/ui/components/layout/pre-chat-messages.js +4 -3
  102. package/src/javascripts/ui/components/suggestions/index.js +5 -4
  103. package/src/javascripts/ui/components/translation-chat-status/index.tsx +4 -3
  104. package/src/javascripts/ui/components/view/app-view.js +1 -2
  105. package/src/javascripts/ui/components/view/deprecated-view.js +1 -2
  106. package/src/javascripts/ui/components/view/{index.js → index.tsx} +53 -4
  107. package/src/javascripts/ui/components/view/inline-view.js +1 -11
  108. package/src/javascripts/ui/components/view/window-view/{index.js → index.tsx} +15 -11
  109. package/src/javascripts/ui/components/view/window-view/window-open-button.js +4 -3
  110. package/src/javascripts/ui/components/widgets/{in-out-transition.js → in-out-transition.tsx} +67 -28
  111. package/src/javascripts/ui/hooks/sounds/beep.mp3 +0 -0
  112. package/src/javascripts/ui/hooks/use-click-outside.ts +5 -3
  113. package/src/javascripts/ui/hooks/use-notifications.ts +114 -0
  114. package/src/javascripts/ui/hooks/{use-seamly-chat.js → use-seamly-chat.ts} +5 -1
  115. package/src/javascripts/ui/hooks/use-session-expired-command.ts +17 -0
  116. package/src/javascripts/ui/hooks/use-timeout.ts +20 -0
  117. package/src/stylesheets/3-chat/_chat.scss +3 -5
  118. package/src/stylesheets/4-base/_formelements.scss +0 -36
  119. package/src/stylesheets/5-components/_abort-transaction.scss +10 -0
  120. package/src/stylesheets/5-components/_buttons.scss +18 -3
  121. package/src/stylesheets/5-components/_character-limit.scss +2 -2
  122. package/src/stylesheets/5-components/_chat-status.scss +26 -37
  123. package/src/stylesheets/5-components/_choice-prompt.scss +9 -10
  124. package/src/stylesheets/5-components/_conversation.scss +9 -62
  125. package/src/stylesheets/5-components/_disclaimer.scss +11 -3
  126. package/src/stylesheets/5-components/_error.scss +3 -2
  127. package/src/stylesheets/5-components/_idle.scss +3 -8
  128. package/src/stylesheets/5-components/_input.scss +34 -13
  129. package/src/stylesheets/5-components/_interrupt.scss +3 -10
  130. package/src/stylesheets/5-components/_loader.scss +1 -2
  131. package/src/stylesheets/5-components/_message-author.scss +2 -4
  132. package/src/stylesheets/5-components/_message-body.scss +33 -10
  133. package/src/stylesheets/5-components/_message-card.scss +2 -10
  134. package/src/stylesheets/5-components/_message-carousel.scss +4 -4
  135. package/src/stylesheets/5-components/_message-cta.scss +0 -6
  136. package/src/stylesheets/5-components/_message.scss +1 -0
  137. package/src/stylesheets/5-components/_modal.scss +2 -5
  138. package/src/stylesheets/5-components/_options.scss +17 -22
  139. package/src/stylesheets/5-components/_pre-chat-messages.scss +3 -1
  140. package/src/stylesheets/5-components/_prompt.scss +3 -7
  141. package/src/stylesheets/5-components/_skip-link.scss +2 -1
  142. package/src/stylesheets/5-components/_suggestions.scss +2 -2
  143. package/src/stylesheets/5-components/_translation-options.scss +5 -2
  144. package/src/stylesheets/5-components/_unread-messages.scss +33 -0
  145. package/src/stylesheets/5-components/_upload.scss +20 -27
  146. package/src/stylesheets/6-default-implementation/_hover.scss +14 -17
  147. package/src/stylesheets/7-deprecated/1-settings/_config.scss +17 -0
  148. package/src/stylesheets/7-deprecated/3-app/_app.scss +2 -1
  149. package/src/stylesheets/7-deprecated/5-components/_card.scss +1 -0
  150. package/src/stylesheets/7-deprecated/5-components/_chat-status.scss +66 -20
  151. package/src/stylesheets/7-deprecated/5-components/_conversation.scss +1 -4
  152. package/src/stylesheets/7-deprecated/5-components/_input.scss +6 -1
  153. package/src/stylesheets/7-deprecated/5-components/_interrupt.scss +1 -4
  154. package/src/stylesheets/7-deprecated/5-components/_message.scss +49 -12
  155. package/src/stylesheets/7-deprecated/5-components/_translation-options.scss +30 -37
  156. package/src/stylesheets/7-deprecated/5-components/_unread-messages.scss +38 -0
  157. package/src/stylesheets/deprecated-view.scss +1 -0
  158. package/src/stylesheets/styles.scss +2 -0
  159. package/webpack/config.common.js +6 -1
  160. package/webpack/config.package.js +18 -0
  161. package/webpack/defaults.js +1 -1
  162. package/src/javascripts/ui/components/core/seamly-file-upload.js +0 -86
  163. package/src/javascripts/ui/components/entry/text-entry/hooks.js +0 -46
@@ -0,0 +1,108 @@
1
+ import { useEffect, useMemo } from 'preact/hooks'
2
+ import { useDispatch } from 'react-redux'
3
+ import { maxCharacterSrDebounceDelay, maxCharacterWarningLimit } from 'config'
4
+ import { useLiveRegion } from 'ui/hooks/live-region-hooks'
5
+ import {
6
+ useEntryTextLimit,
7
+ useSeamlyStateContext,
8
+ } from 'ui/hooks/seamly-state-hooks'
9
+ import { debounce } from 'ui/utils/general-utils'
10
+ import { ControlState } from 'domains/forms/forms.types'
11
+ import { useFormControl } from 'domains/forms/hooks'
12
+ import { useI18n } from 'domains/i18n/hooks'
13
+ import { clearAbortTransaction } from 'domains/store/slice'
14
+
15
+ export function useCharacterLimit(controlName) {
16
+ const { t } = useI18n()
17
+ const { sendAssertive } = useLiveRegion()
18
+ const { hasLimit, limit } = useEntryTextLimit()
19
+
20
+ const debouncedSendAssertive = useMemo(
21
+ () => debounce(sendAssertive, maxCharacterSrDebounceDelay),
22
+ [sendAssertive],
23
+ )
24
+ const validateLimit = useMemo(() => {
25
+ return debounce((_reachedCharacterWarning, _remainingChars) => {
26
+ if (_reachedCharacterWarning) {
27
+ debouncedSendAssertive(
28
+ t('input.srCharacterLimitText', { limit: _remainingChars }),
29
+ )
30
+ }
31
+ }, maxCharacterSrDebounceDelay)
32
+ }, [debouncedSendAssertive, t])
33
+
34
+ const [{ value }] = useFormControl(controlName)
35
+ const remainingChars = hasLimit && value ? limit - value.length : limit
36
+ const reachedCharacterWarning = hasLimit
37
+ ? remainingChars <= maxCharacterWarningLimit
38
+ : false
39
+ const reachedCharacterLimit = hasLimit ? remainingChars < 0 : false
40
+
41
+ useEffect(() => {
42
+ validateLimit(reachedCharacterWarning, remainingChars)
43
+ }, [reachedCharacterWarning, remainingChars, validateLimit])
44
+
45
+ return {
46
+ hasCharacterLimit: hasLimit,
47
+ characterLimit: limit,
48
+ reachedCharacterWarning,
49
+ reachedCharacterLimit,
50
+ remainingChars,
51
+ }
52
+ }
53
+
54
+ export const useEntryTextTranslation = (controlName: ControlState['name']) => {
55
+ const { hasCharacterLimit, characterLimit } = useCharacterLimit(controlName)
56
+
57
+ const {
58
+ entryMeta: {
59
+ optionsOverride: { text },
60
+ },
61
+ } = useSeamlyStateContext()
62
+
63
+ const { t } = useI18n()
64
+
65
+ const placeholder: string = useMemo(
66
+ () =>
67
+ t('input.inputPlaceholder', {
68
+ hasLimit: hasCharacterLimit,
69
+ text: text?.placeholder || t('input.inputPlaceholderText'),
70
+ limit: hasCharacterLimit ? characterLimit : null,
71
+ }),
72
+ [t, hasCharacterLimit, characterLimit, text?.placeholder],
73
+ )
74
+
75
+ const label: string = useMemo(
76
+ () =>
77
+ t('input.inputLabel', {
78
+ hasLimit: !text?.label ? hasCharacterLimit : false,
79
+ text: text?.label || t('input.inputLabelText'),
80
+ limit: !text?.label && hasCharacterLimit ? characterLimit : null,
81
+ }),
82
+ [t, hasCharacterLimit, characterLimit, text?.label],
83
+ )
84
+
85
+ const labelClass: string = useMemo(
86
+ () => (text?.label ? 'input__label' : 'visually-hidden'),
87
+ [text?.label],
88
+ )
89
+
90
+ return { placeholder, label, labelClass }
91
+ }
92
+
93
+ export const useEntryAbortTransaction = () => {
94
+ const dispatch = useDispatch()
95
+
96
+ const {
97
+ entryMeta: { actions },
98
+ } = useSeamlyStateContext()
99
+
100
+ const clearEntryAbortTransaction = () => {
101
+ dispatch(clearAbortTransaction())
102
+ }
103
+
104
+ return {
105
+ abortTransaction: actions?.abortTransaction,
106
+ clearEntryAbortTransaction,
107
+ }
108
+ }
@@ -9,7 +9,7 @@ import { visibilityStates } from 'domains/visibility/constants'
9
9
  import { useVisibility } from 'domains/visibility/hooks'
10
10
  import TextEntryForm from './text-entry-form'
11
11
 
12
- const controlName = 'textMessageEntry'
12
+ export const textEntryControlName = 'textMessageEntry'
13
13
 
14
14
  export default function TextEntry({ ...props }) {
15
15
  const { isOpen, setVisibility } = useVisibility()
@@ -18,8 +18,8 @@ export default function TextEntry({ ...props }) {
18
18
  const { sendMessage } = useSeamlyCommands()
19
19
  const handleSubmit = useCallback(
20
20
  (values, { updateControlValue }) => {
21
- sendMessage({ body: values[controlName] })
22
- updateControlValue(controlName, '')
21
+ sendMessage({ body: values[textEntryControlName] })
22
+ updateControlValue(textEntryControlName, '')
23
23
  focusSkipLinkTarget()
24
24
 
25
25
  if (!isOpen) {
@@ -36,7 +36,10 @@ export default function TextEntry({ ...props }) {
36
36
  persistData={true}
37
37
  onSubmit={handleSubmit}
38
38
  >
39
- <TextEntryForm controlName={controlName} skipLinkId={skipLinkId} />
39
+ <TextEntryForm
40
+ controlName={textEntryControlName}
41
+ skipLinkId={skipLinkId}
42
+ />
40
43
  </FormProvider>
41
44
  )
42
45
  }
@@ -1,4 +1,4 @@
1
- import { useCallback, useLayoutEffect, useMemo } from 'preact/hooks'
1
+ import { useCallback, useLayoutEffect } from 'preact/hooks'
2
2
  import Form from 'ui/components/form-controls/form'
3
3
  import Input from 'ui/components/form-controls/input'
4
4
  import Icon from 'ui/components/layout/icon'
@@ -7,7 +7,7 @@ import { useLiveRegion, useSeamlyCommands } from 'ui/hooks/seamly-hooks'
7
7
  import { useFormControl } from 'domains/forms/hooks'
8
8
  import { useI18n } from 'domains/i18n/hooks'
9
9
  import { className } from 'lib/css'
10
- import { useCharacterLimit } from './hooks'
10
+ import { useCharacterLimit, useEntryTextTranslation } from './hooks'
11
11
 
12
12
  export default function TextEntryForm({ controlName, skipLinkId }) {
13
13
  const { t } = useI18n()
@@ -15,16 +15,17 @@ export default function TextEntryForm({ controlName, skipLinkId }) {
15
15
  const { emitEvent } = useSeamlyCommands()
16
16
  const handleKeyUp = useSeamlyTyping()
17
17
  const { setBlockAutoEntrySwitch } = useSeamlyEntry()
18
-
18
+ const { placeholder, label, labelClass } =
19
+ useEntryTextTranslation(controlName)
19
20
  // TODO: Standardize the validation on form fields
20
21
  const {
21
22
  hasCharacterLimit,
22
- characterLimit,
23
23
  reachedCharacterWarning,
24
24
  reachedCharacterLimit,
25
25
  remainingChars,
26
26
  } = useCharacterLimit(controlName)
27
27
  const [{ value }] = useFormControl(controlName)
28
+
28
29
  const hasValue = !!value
29
30
 
30
31
  const handleFocus = useCallback(() => {
@@ -34,22 +35,7 @@ export default function TextEntryForm({ controlName, skipLinkId }) {
34
35
 
35
36
  emitEvent('ui.inputFocus')
36
37
  }, [t, sendAssertive, reachedCharacterWarning, remainingChars, emitEvent])
37
- const placeholder = useMemo(
38
- () =>
39
- t('input.inputPlaceholder', {
40
- hasLimit: hasCharacterLimit,
41
- limit: hasCharacterLimit ? characterLimit : null,
42
- }),
43
- [t, hasCharacterLimit, characterLimit],
44
- )
45
- const labelText = useMemo(
46
- () =>
47
- t('input.inputLabel', {
48
- hasLimit: hasCharacterLimit,
49
- limit: hasCharacterLimit ? characterLimit : null,
50
- }),
51
- [t, hasCharacterLimit, characterLimit],
52
- )
38
+
53
39
  // When the input holds a value, the component should be blocked from switching
54
40
  // to file upload form.
55
41
  useLayoutEffect(() => {
@@ -87,8 +73,8 @@ export default function TextEntryForm({ controlName, skipLinkId }) {
87
73
  className={className('input__text')}
88
74
  autocomplete="off"
89
75
  placeholder={placeholder}
90
- labelText={labelText}
91
- labelClass={className('visually-hidden')}
76
+ labelText={label}
77
+ labelClass={className(labelClass)}
92
78
  aria-invalid={hasCharacterLimit ? reachedCharacterLimit : null}
93
79
  onKeyUp={handleKeyUp}
94
80
  onFocus={handleFocus}
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useMemo, useRef } from 'preact/hooks'
2
+ import { useSelector } from 'react-redux'
2
3
  import Icon from 'ui/components/layout/icon'
3
4
  import InOutTransition, {
4
5
  transitionStartStates,
@@ -17,7 +18,7 @@ import { runIfElementContainsOrHasFocus } from 'ui/utils/general-utils'
17
18
  import { actionTypes } from 'ui/utils/seamly-utils'
18
19
  import { useUserHasResponded } from 'domains/app/hooks'
19
20
  import { useI18n } from 'domains/i18n/hooks'
20
- import { useInterrupt } from 'domains/interrupt/hooks'
21
+ import { selectHasError } from 'domains/interrupt/selectors'
21
22
  import { useTranslatedEventData } from 'domains/translations/hooks'
22
23
  import { className } from 'lib/css'
23
24
 
@@ -27,7 +28,7 @@ const Faq = () => {
27
28
  const sectionId = useGeneratedId()
28
29
  const focusSkiplinkTarget = useSkiplinkTargetFocusing()
29
30
  const { sendPolite } = useLiveRegion()
30
- const { hasInterrupt } = useInterrupt()
31
+ const hasError = useSelector(selectHasError)
31
32
  const { hasCountdown, endCountdown } = useSeamlyIdleDetachCountdown()
32
33
  const { hasPrompt, continueChat } = useSeamlyResumeConversationPrompt()
33
34
 
@@ -36,7 +37,7 @@ const Faq = () => {
36
37
  payload: lastFaqEventPayload,
37
38
  })
38
39
  const faqs = useMemo(() => {
39
- const newFaqs = lastFaqEventPayload && !hasInterrupt ? eventBody : []
40
+ const newFaqs = lastFaqEventPayload && !hasError ? eventBody : []
40
41
  const itemBaseClass = `faqs__item`
41
42
  return newFaqs.map(({ categories = [], ...faqRest }) => ({
42
43
  ...faqRest,
@@ -51,7 +52,7 @@ const Faq = () => {
51
52
  ),
52
53
  ],
53
54
  }))
54
- }, [lastFaqEventPayload, hasInterrupt, eventBody])
55
+ }, [lastFaqEventPayload, hasError, eventBody])
55
56
 
56
57
  const prevFaqs = useRef(null)
57
58
  const prevHasFaqs = useRef(false)
@@ -1,16 +1,27 @@
1
+ import { HTMLAttributes } from 'preact/compat'
1
2
  import { useFormContext, useFormControl } from 'domains/forms/hooks'
2
3
  import FormControlWrapper from './wrapper'
3
4
 
5
+ type InputProps = HTMLAttributes<HTMLInputElement> & {
6
+ id: string
7
+ name: string
8
+ type: string
9
+ labelText: string
10
+ labelClass: string
11
+ contentHint?: string
12
+ 'aria-describedby'?: string
13
+ }
14
+
4
15
  function Input({
5
16
  id,
6
17
  name,
7
18
  type,
8
19
  labelText,
9
20
  labelClass,
10
- contentHint,
21
+ contentHint = null,
11
22
  'aria-describedby': ariaDescribedBy,
12
23
  ...props
13
- }) {
24
+ }: InputProps) {
14
25
  const { isSubmitted } = useFormContext()
15
26
  const [field, { error }] = useFormControl(name)
16
27
  const hasError = isSubmitted && error
@@ -12,9 +12,6 @@ const FormControlWrapper = ({
12
12
  }) => {
13
13
  return (
14
14
  <>
15
- <label htmlFor={id} className={labelClass}>
16
- {labelText}
17
- </label>
18
15
  {contentHint && (
19
16
  <span
20
17
  id={`${id}-content-hint`}
@@ -23,8 +20,15 @@ const FormControlWrapper = ({
23
20
  {contentHint}
24
21
  </span>
25
22
  )}
23
+
26
24
  <Error id={`${id}-error`} error={!validity && errorText} />
27
- {children}
25
+
26
+ <div className={className('form-control__wrapper')}>
27
+ <label htmlFor={id} className={labelClass}>
28
+ {labelText}
29
+ </label>
30
+ {children}
31
+ </div>
28
32
  </>
29
33
  )
30
34
  }
@@ -1,3 +1,4 @@
1
+ import { useSelector } from 'react-redux'
1
2
  import {
2
3
  useSeamlyCurrentAgent,
3
4
  useSeamlyHeaderData,
@@ -5,7 +6,7 @@ import {
5
6
  } from 'ui/hooks/seamly-hooks'
6
7
  import { useStartChatIcon } from 'domains/config/hooks'
7
8
  import { useI18n } from 'domains/i18n/hooks'
8
- import { useInterrupt } from 'domains/interrupt/hooks'
9
+ import { selectHasError } from 'domains/interrupt/selectors'
9
10
  import { useVisibility } from 'domains/visibility/hooks'
10
11
  import { className } from 'lib/css'
11
12
  import Icon from './icon'
@@ -16,10 +17,10 @@ const AgentInfo = () => {
16
17
  const unreadMessageCount = useSeamlyUnreadCount()
17
18
  const { isOpen } = useVisibility()
18
19
  const currentAgent = useSeamlyCurrentAgent()
19
- const { hasInterrupt } = useInterrupt()
20
+ const hasError = useSelector(selectHasError)
20
21
  const startChatIcon = useStartChatIcon()
21
22
  const src = currentAgent?.avatar ?? startChatIcon
22
- const displaySubtitle = hasInterrupt ? '' : subTitle
23
+ const displaySubtitle = hasError ? '' : subTitle
23
24
 
24
25
  const classNames = ['message-count']
25
26
 
@@ -1,21 +1,20 @@
1
+ import { useSelector } from 'react-redux'
1
2
  import AppOptions from 'ui/components/app-options'
2
3
  import ChatScrollProvider from 'ui/components/conversation/event/chat-scroll/chat-scroll-provider'
3
4
  import EntryContainer from 'ui/components/entry/entry-container'
4
5
  import CollapseButton from 'ui/components/view/window-view/collapse-button'
5
- import { useInterrupt } from 'domains/interrupt/hooks'
6
+ import { selectHasError } from 'domains/interrupt/selectors'
6
7
  import TranslationStatus from 'domains/translations/components/translation-status'
7
8
  import { useVisibility } from 'domains/visibility/hooks'
8
9
  import { className } from 'lib/css'
10
+ import Interrupt from './interrupt'
9
11
 
10
- function ChatFrame({ children, interruptComponent: InterruptComponent }) {
11
- const { hasInterrupt, meta } = useInterrupt()
12
+ function ChatFrame({ children }) {
13
+ const hasError = useSelector(selectHasError)
12
14
  const { isOpen } = useVisibility()
13
15
 
14
- if (hasInterrupt) {
15
- if (isOpen) {
16
- return <InterruptComponent {...meta} />
17
- }
18
- return null
16
+ if (hasError) {
17
+ return <Interrupt />
19
18
  }
20
19
 
21
20
  return (
@@ -1,20 +1,19 @@
1
+ import { useSelector } from 'react-redux'
1
2
  import AppOptions from 'ui/components/app-options'
2
3
  import ChatScrollProvider from 'ui/components/conversation/event/chat-scroll/chat-scroll-provider'
3
4
  import EntryContainer from 'ui/components/entry/entry-container'
4
- import { useInterrupt } from 'domains/interrupt/hooks'
5
+ import { selectHasError } from 'domains/interrupt/selectors'
5
6
  import TranslationStatus from 'domains/translations/components/translation-status'
6
7
  import { useVisibility } from 'domains/visibility/hooks'
7
8
  import { className } from 'lib/css'
9
+ import Interrupt from './interrupt'
8
10
 
9
- function ChatFrame({ children, interruptComponent: InterruptComponent }) {
10
- const { hasInterrupt, meta } = useInterrupt()
11
+ function ChatFrame({ children }) {
11
12
  const { isOpen } = useVisibility()
13
+ const hasError = useSelector(selectHasError)
12
14
 
13
- if (hasInterrupt) {
14
- if (isOpen) {
15
- return <InterruptComponent {...meta} />
16
- }
17
- return null
15
+ if (hasError) {
16
+ return <Interrupt />
18
17
  }
19
18
 
20
19
  return (
@@ -6,27 +6,18 @@ import {
6
6
  useSeamlyCommands,
7
7
  useSkiplinkTargetFocusing,
8
8
  } from 'ui/hooks/seamly-hooks'
9
+ import { useInterrupt } from 'domains/interrupt/hooks'
9
10
  import { className } from 'lib/css'
10
11
 
11
- const Interrupt = ({
12
- originalError,
13
- title,
14
- message,
15
- buttonText,
16
- action,
17
- srText,
18
- }) => {
12
+ const Interrupt = () => {
13
+ const {
14
+ meta: { originalError, title, message, buttonText, action, srText },
15
+ } = useInterrupt()
19
16
  const seamlyCommands = useSeamlyCommands()
20
17
  const headingId = useGeneratedId()
21
18
  const { sendPolite } = useLiveRegion()
22
19
  const focusSkiplinkTarget = useSkiplinkTargetFocusing()
23
- const isExpiredError = originalError.name === 'SeamlySessionExpiredError'
24
-
25
- useEffect(() => {
26
- if (isExpiredError && seamlyCommands[action]) {
27
- seamlyCommands[action]()
28
- }
29
- }, [action, seamlyCommands, isExpiredError])
20
+ const isExpiredError = originalError?.name === 'SeamlySessionExpiredError'
30
21
 
31
22
  useEffect(() => {
32
23
  if (!isExpiredError && srText) {
@@ -1,6 +1,7 @@
1
+ import { useSelector } from 'react-redux'
1
2
  import useEventComponentMapping from 'ui/hooks/use-event-component-mapping'
2
3
  import { useConfig } from 'domains/config/hooks'
3
- import { useInterrupt } from 'domains/interrupt/hooks'
4
+ import { selectHasError } from 'domains/interrupt/selectors'
4
5
  import { useVisibility } from 'domains/visibility/hooks'
5
6
  import { className } from 'lib/css'
6
7
 
@@ -11,10 +12,10 @@ export function PreChatMessageEvent({ event }) {
11
12
 
12
13
  export default function PreChatMessages() {
13
14
  const { preChatEvents, layoutMode } = useConfig()
14
- const { hasInterrupt } = useInterrupt()
15
+ const hasError = useSelector(selectHasError)
15
16
  const { isOpen } = useVisibility()
16
17
 
17
- const isVisible = !(hasInterrupt || !preChatEvents?.length || isOpen)
18
+ const isVisible = !(hasError || !preChatEvents?.length || isOpen)
18
19
 
19
20
  return (
20
21
  isVisible && (
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef } from 'preact/hooks'
2
+ import { useSelector } from 'react-redux'
2
3
  import SuggestionsList from 'ui/components/suggestions/suggestions-list'
3
4
  import InOutTransition, {
4
5
  transitionStartStates,
@@ -18,7 +19,7 @@ import { actionTypes } from 'ui/utils/seamly-utils'
18
19
  import { useUserHasResponded } from 'domains/app/hooks'
19
20
  import { useConfig } from 'domains/config/hooks'
20
21
  import { useI18n } from 'domains/i18n/hooks'
21
- import { useInterrupt } from 'domains/interrupt/hooks'
22
+ import { selectHasError } from 'domains/interrupt/selectors'
22
23
  import { useTranslatedEventData } from 'domains/translations/hooks'
23
24
  import { visibilityStates } from 'domains/visibility/constants'
24
25
  import { useVisibility } from 'domains/visibility/hooks'
@@ -37,7 +38,7 @@ const Suggestions = ({ isAside = false }) => {
37
38
  const containerRef = useRef(null)
38
39
  const { sendPolite } = useLiveRegion()
39
40
  // interrupt & countdown hooks
40
- const { hasInterrupt } = useInterrupt()
41
+ const hasError = useSelector(selectHasError)
41
42
  const { hasCountdown, endCountdown } = useSeamlyIdleDetachCountdown()
42
43
  const { hasPrompt, continueChat } = useSeamlyResumeConversationPrompt()
43
44
  // data hooks
@@ -45,8 +46,8 @@ const Suggestions = ({ isAside = false }) => {
45
46
  const payload = useSeamlyServiceData('suggestion')
46
47
  const { body: eventBody } = useTranslatedEventData({ payload })
47
48
  const suggestions = useMemo(
48
- () => (payload && !hasInterrupt ? eventBody : []),
49
- [payload, hasInterrupt, eventBody],
49
+ () => (payload && !hasError ? eventBody : []),
50
+ [payload, hasError, eventBody],
50
51
  )
51
52
 
52
53
  const prevSuggestions = useRef(null)
@@ -1,8 +1,9 @@
1
1
  import { useCallback } from 'preact/hooks'
2
+ import { useSelector } from 'react-redux'
2
3
  import ChatStatus from 'ui/components/chat-status'
3
4
  import { useSkiplinkTargetFocusing } from 'ui/hooks/focus-helper-hooks'
4
5
  import { useI18n } from 'domains/i18n/hooks'
5
- import { useInterrupt } from 'domains/interrupt/hooks'
6
+ import { selectHasError } from 'domains/interrupt/selectors'
6
7
  import {
7
8
  useLocaleNativeName,
8
9
  useTranslations,
@@ -12,7 +13,7 @@ import {
12
13
  export default function TranslationChatStatus() {
13
14
  const { t } = useI18n()
14
15
  const { id } = useTranslationsContainer()
15
- const { hasInterrupt } = useInterrupt()
16
+ const hasError = useSelector(selectHasError)
16
17
  const { disableTranslations, currentLocale } = useTranslations()
17
18
  const localeNativeName = useLocaleNativeName(currentLocale)
18
19
  const focusSkiplinkTarget = useSkiplinkTargetFocusing()
@@ -24,7 +25,7 @@ export default function TranslationChatStatus() {
24
25
  focusSkiplinkTarget()
25
26
  }, [disableTranslations, focusSkiplinkTarget])
26
27
 
27
- if (hasInterrupt) {
28
+ if (hasError) {
28
29
  return null
29
30
  }
30
31
 
@@ -1,12 +1,11 @@
1
1
  import Conversation from '../conversation/conversation'
2
2
  import Chat from '../layout/chat'
3
3
  import ChatFrame from '../layout/chat-frame'
4
- import Interrupt from '../layout/interrupt'
5
4
 
6
5
  const AppView = () => {
7
6
  return (
8
7
  <Chat>
9
- <ChatFrame interruptComponent={Interrupt}>
8
+ <ChatFrame>
10
9
  <Conversation />
11
10
  </ChatFrame>
12
11
  </Chat>
@@ -5,7 +5,6 @@ import DeprecatedToggleButton from '../entry/deprecated-toggle-button'
5
5
  import AgentInfo from '../layout/agent-info'
6
6
  import DeprecatedAppFrame from '../layout/deprecated-app-frame'
7
7
  import Header from '../layout/header'
8
- import Interrupt from '../layout/interrupt'
9
8
 
10
9
  const ShowInlineView = ({ children }) => {
11
10
  const { showInlineView, containerRef } = useShowInlineView()
@@ -24,7 +23,7 @@ const DeprecatedView = () => {
24
23
  <Header onCloseChat={closeChat}>
25
24
  <AgentInfo />
26
25
  </Header>
27
- <ChatFrame interruptComponent={Interrupt}>
26
+ <ChatFrame>
28
27
  <Conversation />
29
28
  </ChatFrame>
30
29
  </DeprecatedAppFrame>
@@ -1,12 +1,24 @@
1
- import { useCallback, useMemo } from 'preact/hooks'
1
+ import { useTranslatedEventData } from 'package/hooks'
2
+ import { useCallback, useEffect, useMemo } from 'preact/hooks'
3
+ import { useSelector } from 'react-redux'
2
4
  import AppView from 'ui/components/view/app-view'
3
5
  import InlineView from 'ui/components/view/inline-view'
4
6
  import WindowView from 'ui/components/view/window-view'
5
7
  import { useSeamlyAppContainerClassNames } from 'ui/hooks/component-helper-hooks'
6
8
  import { useSeamlyContainerElement } from 'ui/hooks/focus-helper-hooks'
9
+ import {
10
+ useSeamlyCurrentAgent,
11
+ useSeamlyServiceInfo,
12
+ useSeamlyUnreadCount,
13
+ } from 'ui/hooks/seamly-state-hooks'
14
+ import useNotification from 'ui/hooks/use-notifications'
7
15
  import { useUserHasResponded } from 'domains/app/hooks'
8
16
  import { useConfig } from 'domains/config/hooks'
9
17
  import { useI18n } from 'domains/i18n/hooks'
18
+ import {
19
+ selectLastUnreadEvent,
20
+ selectShowNotifications,
21
+ } from 'domains/store/selectors'
10
22
  import { useVisibility } from 'domains/visibility/hooks'
11
23
  import { className } from 'lib/css'
12
24
 
@@ -16,17 +28,50 @@ const ViewComponentsMap = {
16
28
  window: WindowView,
17
29
  }
18
30
 
31
+ function stripHtml(html) {
32
+ const tmp = document.createElement('div')
33
+ tmp.innerHTML = html
34
+ return tmp.textContent || tmp.innerText || ''
35
+ }
36
+
19
37
  const View = ({ children }) => {
38
+ const { sendNotification } = useNotification()
39
+ const unreadMessageCount = useSeamlyUnreadCount()
20
40
  const [, setSeamlyContainerElement] = useSeamlyContainerElement()
21
41
  const { namespace, layoutMode, zIndex } = useConfig()
42
+ const currentAgent = useSeamlyCurrentAgent()
43
+ const { proactiveMessages } = useSeamlyServiceInfo()
22
44
  const { isOpen, isVisible } = useVisibility()
23
45
  const appContainerClassNames = useSeamlyAppContainerClassNames()
24
46
  const userHasResponded = useUserHasResponded()
47
+ const lastUnreadEvent = useSelector(selectLastUnreadEvent)
48
+ const { body } = useTranslatedEventData(lastUnreadEvent)
49
+
50
+ const showNotifications = useSelector(selectShowNotifications)
25
51
  const { locale } = useI18n()
52
+
53
+ useEffect(() => {
54
+ if (unreadMessageCount === 0 || !proactiveMessages || !showNotifications)
55
+ return
56
+
57
+ sendNotification(currentAgent?.name, {
58
+ // @ts-ignore
59
+ body: stripHtml(body?.text || body.prompt?.text),
60
+ icon: currentAgent?.avatar,
61
+ })
62
+ }, [
63
+ sendNotification,
64
+ unreadMessageCount,
65
+ body,
66
+ currentAgent,
67
+ proactiveMessages,
68
+ showNotifications,
69
+ ])
26
70
  const ViewComponent = ViewComponentsMap[layoutMode]
27
71
 
28
72
  const containerElementRef = useCallback(
29
- (container) => {
73
+ (container: HTMLElement) => {
74
+ if (typeof setSeamlyContainerElement !== 'function') return
30
75
  setSeamlyContainerElement(container)
31
76
  },
32
77
  [setSeamlyContainerElement],
@@ -54,7 +99,11 @@ const View = ({ children }) => {
54
99
  `namespace--${namespace}`,
55
100
  ]
56
101
 
57
- const classNames = ['app', ...defaultClassNames, ...appContainerClassNames]
102
+ const classNames = ['app', ...defaultClassNames]
103
+
104
+ if (typeof appContainerClassNames !== 'function') {
105
+ classNames.push(...appContainerClassNames)
106
+ }
58
107
 
59
108
  if (!isOpen && layoutMode !== 'app') {
60
109
  classNames.push('app--collapsed')
@@ -69,7 +118,7 @@ const View = ({ children }) => {
69
118
  <div
70
119
  className={className(classNames)}
71
120
  lang={blockLang}
72
- tabIndex="-1"
121
+ tabIndex={-1}
73
122
  data-nosnippet
74
123
  style={{ zIndex }}
75
124
  ref={containerElementRef}