@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
@@ -1,10 +1,8 @@
1
- import { useInterrupt } from 'domains/interrupt/hooks'
2
1
  import { useShowInlineView, useVisibility } from 'domains/visibility/hooks'
3
2
  import { className } from 'lib/css'
4
3
  import Conversation from '../conversation/conversation'
5
4
  import Chat from '../layout/chat'
6
5
  import ChatFrame from '../layout/chat-frame'
7
- import Interrupt from '../layout/interrupt'
8
6
  import PreChatMessages from '../layout/pre-chat-messages'
9
7
  import Suggestions from '../suggestions'
10
8
  import InOutTransition, {
@@ -15,12 +13,6 @@ const InlineView = () => {
15
13
  const { showInlineView, containerRef } = useShowInlineView()
16
14
 
17
15
  const { isOpen } = useVisibility()
18
- const { hasInterrupt, meta } = useInterrupt()
19
-
20
- if (hasInterrupt && !isOpen) {
21
- return <Interrupt {...meta} />
22
- }
23
-
24
16
  return (
25
17
  <>
26
18
  <InOutTransition
@@ -44,9 +36,7 @@ const InlineView = () => {
44
36
  >
45
37
  <Chat ref={containerRef}>
46
38
  {showInlineView && (
47
- <ChatFrame interruptComponent={Interrupt}>
48
- {isOpen && <Conversation />}
49
- </ChatFrame>
39
+ <ChatFrame>{isOpen && <Conversation />}</ChatFrame>
50
40
  )}
51
41
  </Chat>
52
42
  </InOutTransition>
@@ -3,23 +3,27 @@ import Conversation from 'ui/components/conversation/conversation'
3
3
  import Text from 'ui/components/conversation/event/text'
4
4
  import Chat from 'ui/components/layout/chat'
5
5
  import ChatFrame from 'ui/components/layout/chat-frame'
6
- import Interrupt from 'ui/components/layout/interrupt'
7
6
  import PreChatMessages from 'ui/components/layout/pre-chat-messages'
8
7
  import InOutTransition, {
9
8
  transitionStartStates,
10
9
  } from 'ui/components/widgets/in-out-transition'
11
10
  import { useUserHasResponded } from 'domains/app/hooks'
11
+ import { useConfig } from 'domains/config/hooks'
12
12
  import { useI18n } from 'domains/i18n/hooks'
13
- import { useInterrupt } from 'domains/interrupt/hooks'
14
13
  import { useVisibility } from 'domains/visibility/hooks'
15
14
  import { className } from 'lib/css'
16
15
  import WindowOpenButton from './window-open-button'
17
16
 
17
+ const getDelay = <T extends string>(
18
+ prop: Record<T, number> | boolean,
19
+ val: T,
20
+ init?: number,
21
+ ) => (typeof prop === 'object' ? prop[val] : init)
22
+
18
23
  const WindowView = () => {
19
24
  const { isOpen, openChat } = useVisibility()
20
25
  const userHasResponded = useUserHasResponded()
21
- const { hasInterrupt, meta } = useInterrupt()
22
-
26
+ const { continueChat, preChat } = useConfig()
23
27
  const { t } = useI18n()
24
28
  const continueChatText = t('window.chat.continue')
25
29
  const continueChatEvent = useMemo(
@@ -33,15 +37,13 @@ const WindowView = () => {
33
37
  [continueChatText],
34
38
  )
35
39
 
36
- if (hasInterrupt && !isOpen) {
37
- return <Interrupt {...meta} />
38
- }
39
-
40
40
  return (
41
41
  <>
42
42
  <WindowOpenButton onClick={openChat} />
43
43
  <InOutTransition
44
- isActive={!isOpen && !userHasResponded}
44
+ isActive={preChat && !isOpen && !userHasResponded}
45
+ exitAfter={getDelay(preChat, 'exitAfter')}
46
+ enterDelay={getDelay(preChat, 'enterDelay')}
45
47
  transitionStartState={transitionStartStates.rendered}
46
48
  >
47
49
  <div
@@ -54,7 +56,9 @@ const WindowView = () => {
54
56
  </div>
55
57
  </InOutTransition>
56
58
  <InOutTransition
57
- isActive={!isOpen && userHasResponded}
59
+ isActive={continueChat && !isOpen && userHasResponded}
60
+ exitAfter={getDelay(continueChat, 'exitAfter')}
61
+ enterDelay={getDelay(continueChat, 'enterDelay')}
58
62
  transitionStartState={transitionStartStates.notRendered}
59
63
  >
60
64
  <div
@@ -72,7 +76,7 @@ const WindowView = () => {
72
76
  transitionStartState={transitionStartStates.notRendered}
73
77
  >
74
78
  <Chat>
75
- <ChatFrame interruptComponent={Interrupt}>
79
+ <ChatFrame>
76
80
  <Conversation />
77
81
  </ChatFrame>
78
82
  </Chat>
@@ -1,4 +1,5 @@
1
1
  import { useCallback } 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,
@@ -11,15 +12,15 @@ import {
11
12
  } from 'ui/hooks/seamly-state-hooks'
12
13
  import { useStartChatIcon } from 'domains/config/hooks'
13
14
  import { useI18n } from 'domains/i18n/hooks'
14
- import { useInterrupt } from 'domains/interrupt/hooks'
15
+ import { selectHasError } from 'domains/interrupt/selectors'
15
16
  import { useVisibility } from 'domains/visibility/hooks'
16
17
  import { className } from 'lib/css'
17
18
 
18
19
  const ButtonIcon = () => {
19
20
  const startChatIcon = useStartChatIcon()
20
21
  const currentAgent = useSeamlyCurrentAgent()
21
- const { hasInterrupt } = useInterrupt()
22
- const isActiveConversation = currentAgent && !hasInterrupt
22
+ const hasError = useSelector(selectHasError)
23
+ const isActiveConversation = currentAgent && !hasError
23
24
  const src = isActiveConversation ? currentAgent.avatar : startChatIcon
24
25
  return src ? (
25
26
  <img
@@ -1,9 +1,19 @@
1
- import { cloneElement, toChildArray } from 'preact'
2
- import { useEffect, useRef, useState } from 'preact/hooks'
1
+ import { VNode, cloneElement, toChildArray } from 'preact'
2
+ import { FC } from 'preact/compat'
3
+ import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
3
4
  import { defaultTransitionTimeMs } from 'config'
4
5
  import { useStableCallback } from 'ui/hooks/seamly-hooks'
6
+ import useTimeout from 'ui/hooks/use-timeout'
5
7
  import { className } from 'lib/css'
6
8
 
9
+ type VDOMChild = string | number | VNode
10
+ type ValueOf<T> = T[keyof T]
11
+ function childIsVNode(
12
+ child: VDOMChild,
13
+ ): child is VNode<{ className?: string }> {
14
+ return typeof child === 'object'
15
+ }
16
+
7
17
  const transitionClasses = {
8
18
  visible: className('transition--visible'),
9
19
  in: className('transition--in'),
@@ -16,33 +26,57 @@ export const transitionStartStates = {
16
26
  notRendered: 'notRendered',
17
27
  rendered: 'rendered',
18
28
  visuallyHidden: 'visuallyHidden',
29
+ } as const
30
+
31
+ type InOutTransitionProps = {
32
+ isActive: boolean
33
+ timeout?: number
34
+ transitionStartState?: keyof typeof transitionStartStates
35
+ onInTransitionComplete?: Function
36
+ onOutTransitionComplete?: Function
37
+ exitAfter?: number
38
+ enterDelay?: number
19
39
  }
20
40
 
21
- const InOutTransition = ({
41
+ const InOutTransition: FC<InOutTransitionProps> = ({
22
42
  children,
23
43
  isActive,
24
44
  timeout = defaultTransitionTimeMs,
25
45
  transitionStartState = transitionStartStates.notRendered,
26
46
  onInTransitionComplete = () => {},
27
47
  onOutTransitionComplete = () => {},
48
+ exitAfter,
49
+ enterDelay = 0,
28
50
  }) => {
29
51
  const prevIsActive = useRef(false)
30
- const timeoutVal = parseInt(timeout, 10)
52
+
53
+ const isVisuallyHidden =
54
+ transitionStartState === transitionStartStates.visuallyHidden
55
+
56
+ const [activeTransitionClasses, setActiveTransitionClasses] = useState<
57
+ ValueOf<typeof transitionClasses>[]
58
+ >(() => (isVisuallyHidden ? [transitionClasses.visuallyHidden] : []))
59
+
60
+ const [inState, setInState] = useState(() => enterDelay === 0)
61
+
62
+ const transitionOutAfter = useMemo(
63
+ () => (exitAfter ? exitAfter + enterDelay : exitAfter),
64
+ [enterDelay, exitAfter],
65
+ )
66
+
67
+ useTimeout(() => setInState(false), isActive ? transitionOutAfter : undefined)
68
+ useTimeout(() => setInState(true), isActive ? enterDelay : undefined)
69
+
70
+ const transitionIn = useMemo(() => inState && isActive, [isActive, inState])
31
71
 
32
72
  const onInTransitionCompleteHandler = useStableCallback(
33
73
  onInTransitionComplete,
34
74
  )
75
+
35
76
  const onOutTransitionCompleteHandler = useStableCallback(
36
77
  onOutTransitionComplete,
37
78
  )
38
79
 
39
- const isVisuallyHidden =
40
- transitionStartState === transitionStartStates.visuallyHidden
41
-
42
- const [activeTransitionClasses, setActiveTransitionClasses] = useState(() =>
43
- isVisuallyHidden ? [transitionClasses.visuallyHidden] : [],
44
- )
45
-
46
80
  const renderChildren =
47
81
  transitionStartState !== 'notRendered' || activeTransitionClasses.length > 0
48
82
 
@@ -50,21 +84,22 @@ const InOutTransition = ({
50
84
  let activeTimeout = null
51
85
  let activeRaf = null
52
86
 
53
- if (prevIsActive.current && !isActive) {
87
+ if (prevIsActive.current && !transitionIn) {
54
88
  setActiveTransitionClasses([transitionClasses.visible])
55
89
  activeTimeout = setTimeout(() => {
56
90
  setActiveTransitionClasses([
57
91
  ...(isVisuallyHidden ? [transitionClasses.visuallyHidden] : []),
58
92
  ])
93
+
59
94
  if (onOutTransitionCompleteHandler) {
60
95
  activeRaf = requestAnimationFrame(() => {
61
96
  onOutTransitionCompleteHandler()
62
97
  })
63
98
  }
64
- }, timeoutVal)
99
+ }, timeout)
65
100
  }
66
101
 
67
- if (!prevIsActive.current && isActive) {
102
+ if (!prevIsActive.current && transitionIn) {
68
103
  setActiveTransitionClasses([transitionClasses.visible])
69
104
  // Doubling up on rAF as a single rAF can be too slow for the
70
105
  // animation transition to be resolved.
@@ -77,39 +112,43 @@ const InOutTransition = ({
77
112
  if (onInTransitionCompleteHandler) {
78
113
  activeTimeout = setTimeout(() => {
79
114
  onInTransitionCompleteHandler()
80
- }, timeoutVal)
115
+ }, timeout)
81
116
  }
82
117
  })
83
118
  })
84
119
  }
85
120
 
86
- prevIsActive.current = isActive
121
+ prevIsActive.current = transitionIn
87
122
 
88
123
  return () => {
89
124
  clearTimeout(activeTimeout)
90
125
  cancelAnimationFrame(activeRaf)
91
126
  }
92
127
  }, [
93
- isActive,
94
128
  isVisuallyHidden,
95
- timeoutVal,
96
129
  onInTransitionCompleteHandler,
97
130
  onOutTransitionCompleteHandler,
131
+ timeout,
132
+ transitionIn,
98
133
  ])
99
134
 
100
135
  return (
101
136
  <>
102
137
  {renderChildren &&
103
- toChildArray(children).map((child) => {
104
- const { className: childClassName = '' } = child.props
105
- const cleanClasses = childClassName
106
- .split(' ')
107
- .filter((cl) => !transitionClassesArray.includes(cl))
108
-
109
- return cloneElement(child, {
110
- className: [...cleanClasses, ...activeTransitionClasses].join(' '),
111
- })
112
- })}
138
+ toChildArray(children)
139
+ .filter(childIsVNode)
140
+ .map((child) => {
141
+ const { className: childClassName = '' } = child.props
142
+ const cleanClasses = childClassName
143
+ .split(' ')
144
+ .filter((cl) => !transitionClassesArray.includes(cl))
145
+
146
+ return cloneElement(child, {
147
+ className: [...cleanClasses, ...activeTransitionClasses].join(
148
+ ' ',
149
+ ),
150
+ })
151
+ })}
113
152
  </>
114
153
  )
115
154
  }
@@ -4,12 +4,14 @@ const useClickOutside = (callback: Function) => {
4
4
  const ref = useRef<HTMLElement>(null)
5
5
 
6
6
  useEffect(() => {
7
- const handler = ({ target }: TouchEvent | MouseEvent): void => {
7
+ const handler = (el: TouchEvent | MouseEvent): void => {
8
8
  if (
9
9
  ref.current &&
10
- target instanceof HTMLElement &&
11
- !ref.current.contains(target)
10
+ el.target instanceof HTMLElement &&
11
+ !ref.current.contains(el.target)
12
12
  ) {
13
+ el.preventDefault()
14
+ el.stopPropagation()
13
15
  callback()
14
16
  }
15
17
  }
@@ -0,0 +1,114 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
2
+ import { useSelector } from 'react-redux'
3
+ import { textEntryControlName } from 'ui/components/entry/text-entry'
4
+ import { useConfig } from 'domains/config/hooks'
5
+ import { selectShowNotifications } from 'domains/store/selectors'
6
+ import { useSeamlyServiceInfo } from './seamly-state-hooks'
7
+ import beep from './sounds/beep.mp3'
8
+
9
+ const hasNotificationSupport = !!window.Notification
10
+
11
+ const useNotification = () => {
12
+ const { proactiveMessages } = useSeamlyServiceInfo()
13
+
14
+ const { notificationAudioURL } = useConfig()
15
+ const [permission, setPermission] = useState(
16
+ hasNotificationSupport ? Notification.permission : 'default',
17
+ )
18
+ const [visibilityState, setVisibilityState] = useState(
19
+ document.visibilityState,
20
+ )
21
+
22
+ const AudioPlay = useMemo(
23
+ () => new Audio(notificationAudioURL || beep),
24
+ [notificationAudioURL],
25
+ )
26
+
27
+ const showNotifications = useSelector(selectShowNotifications)
28
+
29
+ const requestPermission = useCallback(async () => {
30
+ if (Notification && permission !== 'granted') {
31
+ const notificationPermission = await Notification.requestPermission()
32
+ setPermission(notificationPermission)
33
+ }
34
+ }, [permission])
35
+
36
+ useEffect(() => {
37
+ if (permission === 'default' && proactiveMessages) {
38
+ requestPermission()
39
+ }
40
+ }, [permission, proactiveMessages, requestPermission])
41
+
42
+ const sendNotification = useCallback(
43
+ // eslint-disable-next-line no-undef
44
+ async (title: string, options?: NotificationOptions) => {
45
+ if (!showNotifications || !hasNotificationSupport) return
46
+
47
+ if (permission === 'default') {
48
+ requestPermission()
49
+ }
50
+
51
+ if (
52
+ notificationAudioURL !== false &&
53
+ (permission === 'denied' || permission === 'default') &&
54
+ (visibilityState === 'hidden' || !document.hasFocus())
55
+ ) {
56
+ try {
57
+ await AudioPlay.play()
58
+ } catch (error) {
59
+ // Autoplay was prevented. See link for more details
60
+ // https://developer.chrome.com/blog/autoplay/#audiovideo-elements
61
+ console.warn(error)
62
+ }
63
+ }
64
+
65
+ if (
66
+ permission === 'granted' &&
67
+ (visibilityState === 'hidden' || !document.hasFocus())
68
+ ) {
69
+ const notification = new Notification(title, options)
70
+
71
+ notification.onclick = function () {
72
+ window.parent.focus()
73
+ window.focus()
74
+ notification.close()
75
+
76
+ const entry = document.querySelector<HTMLInputElement>(
77
+ `input[name=${textEntryControlName}]`,
78
+ )
79
+ if (entry) entry.focus()
80
+ }
81
+ }
82
+ },
83
+ [
84
+ AudioPlay,
85
+ notificationAudioURL,
86
+ permission,
87
+ requestPermission,
88
+ showNotifications,
89
+ visibilityState,
90
+ ],
91
+ )
92
+
93
+ useEffect(() => {
94
+ if (!showNotifications) return () => {}
95
+
96
+ function handleVisibilityChange() {
97
+ setVisibilityState(document.visibilityState)
98
+ }
99
+
100
+ document.addEventListener('visibilitychange', handleVisibilityChange, false)
101
+
102
+ return () => {
103
+ document.removeEventListener('visibilitychange', handleVisibilityChange)
104
+ }
105
+ }, [showNotifications])
106
+
107
+ return {
108
+ permission,
109
+ requestPermission,
110
+ sendNotification,
111
+ }
112
+ }
113
+
114
+ export default useNotification
@@ -8,6 +8,7 @@ import { selectShowInlineView } from '../../domains/visibility/selectors'
8
8
  import { useLiveRegion } from './live-region-hooks'
9
9
  import { useSeamlyHasConversation } from './seamly-api-hooks'
10
10
  import { useEvents, useSeamlyLayoutMode } from './seamly-state-hooks'
11
+ import useSessionExpiredCommand from './use-session-expired-command'
11
12
 
12
13
  const useSeamlyChat = () => {
13
14
  const events = useEvents()
@@ -22,6 +23,9 @@ const useSeamlyChat = () => {
22
23
  const connectCalled = useRef(false)
23
24
  const { sendAssertive } = useLiveRegion()
24
25
 
26
+ // Automatically reset conversation if the session has expired
27
+ useSessionExpiredCommand()
28
+
25
29
  useEffect(() => {
26
30
  if (isVisible) {
27
31
  // Wait for the live containers to stabilise in the DOM before injecting
@@ -97,7 +101,7 @@ const useSeamlyChat = () => {
97
101
  if (
98
102
  !apiConfigReady ||
99
103
  connectCalled.current ||
100
- (isWindow && !isOpen && !hasConversation()) ||
104
+ (isWindow && !isOpen) ||
101
105
  (isInline && (!isVisible || !showInlineView))
102
106
  ) {
103
107
  return
@@ -0,0 +1,17 @@
1
+ import { useEffect } from 'preact/hooks'
2
+ import { useInterrupt } from 'domains/interrupt/hooks'
3
+ import useSeamlyCommands from './use-seamly-commands'
4
+
5
+ export default function useSessionExpiredCommand() {
6
+ const {
7
+ meta: { originalError, action },
8
+ } = useInterrupt()
9
+ const seamlyCommands = useSeamlyCommands()
10
+ const isExpiredError = originalError?.name === 'SeamlySessionExpiredError'
11
+
12
+ useEffect(() => {
13
+ if (isExpiredError && seamlyCommands[action]) {
14
+ seamlyCommands[action]()
15
+ }
16
+ }, [action, seamlyCommands, isExpiredError])
17
+ }
@@ -0,0 +1,20 @@
1
+ import { useEffect, useRef } from 'preact/hooks'
2
+
3
+ export default function useTimeout(callback: Function, delay?: number) {
4
+ const timeoutRef = useRef<number | null>(null)
5
+ const savedCallback = useRef(callback)
6
+
7
+ useEffect(() => {
8
+ savedCallback.current = callback
9
+ }, [callback])
10
+
11
+ useEffect(() => {
12
+ if (typeof delay !== 'number') return () => undefined
13
+
14
+ timeoutRef.current = setTimeout(savedCallback.current, delay)
15
+
16
+ return () => clearTimeout(timeoutRef.current)
17
+ }, [delay])
18
+
19
+ return timeoutRef
20
+ }
@@ -53,10 +53,7 @@
53
53
  .#{$n}-chat--layout-app {
54
54
  position: fixed;
55
55
  z-index: $z-index;
56
- top: 0;
57
- right: 0;
58
- bottom: 0;
59
- left: 0;
56
+ inset: 0;
60
57
  width: 100%;
61
58
  max-width: 100%;
62
59
  height: 100%;
@@ -155,5 +152,6 @@
155
152
 
156
153
  .#{$n}-entry__body {
157
154
  display: flex;
158
- align-items: center;
155
+ align-items: flex-end;
156
+ gap: $spacer * 0.25;
159
157
  }
@@ -3,45 +3,9 @@
3
3
 
4
4
  // PARTS
5
5
  // ----
6
- .#{$n}-input__checkbox {
7
- margin-right: $spacer * 0.5;
8
-
9
- &[aria-disabled='true'] {
10
- opacity: 0.5;
11
- }
12
- }
13
-
14
6
  .#{$n}-label {
15
7
  display: block;
16
- margin-bottom: $spacer * 0.25;
17
8
  color: $brand3;
18
9
  font-size: $fontsize-medium;
19
10
  font-weight: $fontweight-bold;
20
11
  }
21
-
22
- .#{$n}-input__checkbox[aria-disabled='true'] + .#{$n}-label {
23
- opacity: 0.5;
24
- }
25
-
26
- .#{$n}-input__select {
27
- appearance: none;
28
- width: 100%;
29
- min-height: $buttonsize;
30
- padding: $spacer * 0.5;
31
- border: $thin-border solid $grey-b;
32
- border-radius: $borderradius-small;
33
- background-color: $white;
34
- background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxkZWZzLz48cGF0aCBmaWxsPSIjNEE0OEMxIiBkPSJNMi45OTgsNi41MDNjMC0wLjI0NywwLjA5MS0wLjQ5NCwwLjI3My0wLjY4N2MwLjM4LTAuNDAxLDEuMDEzLTAuNDE5LDEuNDE0LTAuMDRsMi41ODUsMi40NDYJYzAuMzk0LDAuMzczLDEuMDYsMC4zNzMsMS40NTQsMGwyLjU4Ni0yLjQ0NmMwLjQtMC4zNzksMS4wMzMtMC4zNjIsMS40MTMsMC4wNGMwLjM3OSwwLjQsMC4zNjIsMS4wMzQtMC4wMzksMS40MTRsLTIuNTg2LDIuNDQ2CWMtMS4xNTksMS4wOTYtMy4wNDMsMS4wOTYtNC4yMDMsMEwzLjMxLDcuMjI5QzMuMTAyLDcuMDMzLDIuOTk4LDYuNzY4LDIuOTk4LDYuNTAzeiIvPjwvc3ZnPg==');
35
- background-repeat: no-repeat;
36
- background-position: right $spacer * 0.5 top 50%;
37
- background-size: $spacer * 0.75 auto;
38
- color: $grey-e;
39
- font-size: $fontsize-default;
40
- resize: none;
41
-
42
- &[aria-disabled='true'] {
43
- border: $thin-border solid $grey-c;
44
- background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNnB4IiBoZWlnaHQ9IjE2cHgiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTYgMTYiPjxkZWZzLz48cGF0aCBmaWxsPSIjYTNiNGJmIiBkPSJNMi45OTgsNi41MDNjMC0wLjI0NywwLjA5MS0wLjQ5NCwwLjI3My0wLjY4N2MwLjM4LTAuNDAxLDEuMDEzLTAuNDE5LDEuNDE0LTAuMDRsMi41ODUsMi40NDYJYzAuMzk0LDAuMzczLDEuMDYsMC4zNzMsMS40NTQsMGwyLjU4Ni0yLjQ0NmMwLjQtMC4zNzksMS4wMzMtMC4zNjIsMS40MTMsMC4wNGMwLjM3OSwwLjQsMC4zNjIsMS4wMzQtMC4wMzksMS40MTRsLTIuNTg2LDIuNDQ2CWMtMS4xNTksMS4wOTYtMy4wNDMsMS4wOTYtNC4yMDMsMEwzLjMxLDcuMjI5QzMuMTAyLDcuMDMzLDIuOTk4LDYuNzY4LDIuOTk4LDYuNTAzeiIvPjwvc3ZnPg==');
45
- color: $grey-c;
46
- }
47
- }
@@ -0,0 +1,10 @@
1
+ .#{$n}-conversation__item--abort-transaction {
2
+ padding-top: $spacer * 0.5;
3
+
4
+ .#{$n}-abort-transaction__button {
5
+ padding: $spacer * 0.25 $spacer * 0.5;
6
+ border: 1px solid currentcolor;
7
+ border-radius: $borderradius;
8
+ background-color: $white;
9
+ }
10
+ }
@@ -1,5 +1,20 @@
1
+ a,
2
+ input,
3
+ button {
4
+ &:focus {
5
+ outline: none;
6
+ }
7
+
8
+ &:focus-visible {
9
+ outline: 1px dotted #212121;
10
+ outline: 1px auto -webkit-focus-ring-color;
11
+ }
12
+ }
13
+
1
14
  .#{$n}-button {
2
- display: inline-block;
15
+ display: inline-flex;
16
+ align-items: center;
17
+ gap: $spacer * 0.5;
3
18
  margin: 0;
4
19
  padding: 0;
5
20
  border: 0;
@@ -47,8 +62,8 @@
47
62
 
48
63
  .#{$n}-button__state {
49
64
  display: inline-block;
50
- margin: $spacer * -0.15 $spacer * -0.15 $spacer * -0.15 $spacer * 0.15;
51
- padding: $spacer * 0.15 $spacer * 0.25;
65
+ margin: 0;
66
+ padding: $spacer * 0.1 $spacer * 0.5;
52
67
  border-radius: $borderradius-small;
53
68
  background-color: $grey-a;
54
69
  color: inherit;
@@ -1,11 +1,11 @@
1
1
  .#{$n}-character-count {
2
2
  display: flex;
3
3
  position: absolute;
4
- top: 0;
5
4
  right: 0;
5
+ bottom: 0;
6
6
  align-items: center;
7
7
  justify-content: right;
8
- height: 100%;
8
+ height: $buttonsize;
9
9
  padding: 0 $spacer * 0.5;
10
10
  transform: translateX(100%);
11
11
  transition: transform $transition, opacity 0.2s 0.2s ease;