@seamly/web-ui 21.0.9 → 22.0.0

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 (141) hide show
  1. package/build/dist/lib/components.js +9228 -7777
  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 +6999 -5996
  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 +940 -370
  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 +334 -110
  24. package/build/dist/lib/index.debug.min.js.map +1 -0
  25. package/build/dist/lib/index.js +2810 -5472
  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 +10575 -13540
  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 +1701 -5859
  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 +11536 -14530
  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/store/selectors.ts +23 -10
  65. package/src/javascripts/domains/store/slice.ts +63 -11
  66. package/src/javascripts/domains/store/store.types.ts +41 -1
  67. package/src/javascripts/domains/translations/components/options-button.tsx +1 -4
  68. package/src/javascripts/domains/translations/hooks.ts +11 -4
  69. package/src/javascripts/index.ts +2 -0
  70. package/src/javascripts/lib/url-helpers.ts +24 -0
  71. package/src/javascripts/schema.ts +10 -0
  72. package/src/javascripts/style-guide/states.js +109 -0
  73. package/src/javascripts/ui/components/conversation/conversation.tsx +2 -0
  74. package/src/javascripts/ui/components/conversation/event/chat-scroll/chat-scroll-provider.tsx +2 -0
  75. package/src/javascripts/ui/components/conversation/event/choice-prompt.js +1 -1
  76. package/src/javascripts/ui/components/conversation/event/text.js +1 -1
  77. package/src/javascripts/ui/components/conversation/event/upload.js +50 -9
  78. package/src/javascripts/ui/components/conversation/use-chat-scroll.ts +3 -2
  79. package/src/javascripts/ui/components/core/seamly-event-subscriber.ts +7 -1
  80. package/src/javascripts/ui/components/core/seamly-file-upload.tsx +156 -0
  81. package/src/javascripts/ui/components/entry/abort-transaction-button/abort-transaction-button.tsx +45 -0
  82. package/src/javascripts/ui/components/entry/text-entry/hooks.ts +108 -0
  83. package/src/javascripts/ui/components/entry/text-entry/index.js +7 -4
  84. package/src/javascripts/ui/components/entry/text-entry/{text-entry-form.js → text-entry-form.tsx} +8 -22
  85. package/src/javascripts/ui/components/form-controls/{input.js → input.tsx} +13 -2
  86. package/src/javascripts/ui/components/form-controls/{wrapper.js → wrapper.tsx} +8 -4
  87. package/src/javascripts/ui/components/view/{index.js → index.tsx} +53 -4
  88. package/src/javascripts/ui/components/view/window-view/{index.js → index.tsx} +14 -2
  89. package/src/javascripts/ui/components/widgets/{in-out-transition.js → in-out-transition.tsx} +67 -28
  90. package/src/javascripts/ui/hooks/{seamly-api-hooks.js → seamly-api-hooks.ts} +1 -1
  91. package/src/javascripts/ui/hooks/sounds/beep.mp3 +0 -0
  92. package/src/javascripts/ui/hooks/use-click-outside.ts +5 -3
  93. package/src/javascripts/ui/hooks/use-notifications.ts +114 -0
  94. package/src/javascripts/ui/hooks/use-timeout.ts +20 -0
  95. package/src/stylesheets/3-chat/_chat.scss +3 -5
  96. package/src/stylesheets/4-base/_formelements.scss +0 -36
  97. package/src/stylesheets/5-components/_abort-transaction.scss +10 -0
  98. package/src/stylesheets/5-components/_buttons.scss +18 -3
  99. package/src/stylesheets/5-components/_character-limit.scss +2 -2
  100. package/src/stylesheets/5-components/_chat-status.scss +26 -37
  101. package/src/stylesheets/5-components/_choice-prompt.scss +9 -10
  102. package/src/stylesheets/5-components/_conversation.scss +9 -62
  103. package/src/stylesheets/5-components/_disclaimer.scss +11 -3
  104. package/src/stylesheets/5-components/_error.scss +3 -2
  105. package/src/stylesheets/5-components/_idle.scss +3 -8
  106. package/src/stylesheets/5-components/_input.scss +34 -13
  107. package/src/stylesheets/5-components/_interrupt.scss +3 -10
  108. package/src/stylesheets/5-components/_loader.scss +1 -2
  109. package/src/stylesheets/5-components/_message-author.scss +2 -4
  110. package/src/stylesheets/5-components/_message-body.scss +33 -10
  111. package/src/stylesheets/5-components/_message-card.scss +2 -10
  112. package/src/stylesheets/5-components/_message-carousel.scss +4 -4
  113. package/src/stylesheets/5-components/_message-cta.scss +0 -6
  114. package/src/stylesheets/5-components/_message.scss +1 -0
  115. package/src/stylesheets/5-components/_modal.scss +2 -5
  116. package/src/stylesheets/5-components/_options.scss +17 -22
  117. package/src/stylesheets/5-components/_pre-chat-messages.scss +3 -1
  118. package/src/stylesheets/5-components/_prompt.scss +3 -7
  119. package/src/stylesheets/5-components/_skip-link.scss +2 -1
  120. package/src/stylesheets/5-components/_suggestions.scss +2 -2
  121. package/src/stylesheets/5-components/_translation-options.scss +5 -2
  122. package/src/stylesheets/5-components/_unread-messages.scss +33 -0
  123. package/src/stylesheets/5-components/_upload.scss +20 -27
  124. package/src/stylesheets/6-default-implementation/_hover.scss +14 -17
  125. package/src/stylesheets/7-deprecated/1-settings/_config.scss +17 -0
  126. package/src/stylesheets/7-deprecated/3-app/_app.scss +2 -1
  127. package/src/stylesheets/7-deprecated/5-components/_card.scss +1 -0
  128. package/src/stylesheets/7-deprecated/5-components/_chat-status.scss +66 -20
  129. package/src/stylesheets/7-deprecated/5-components/_conversation.scss +1 -4
  130. package/src/stylesheets/7-deprecated/5-components/_input.scss +6 -1
  131. package/src/stylesheets/7-deprecated/5-components/_interrupt.scss +1 -4
  132. package/src/stylesheets/7-deprecated/5-components/_message.scss +49 -12
  133. package/src/stylesheets/7-deprecated/5-components/_translation-options.scss +30 -37
  134. package/src/stylesheets/7-deprecated/5-components/_unread-messages.scss +38 -0
  135. package/src/stylesheets/deprecated-view.scss +1 -0
  136. package/src/stylesheets/styles.scss +2 -0
  137. package/webpack/config.common.js +6 -1
  138. package/webpack/config.package.js +18 -0
  139. package/webpack/defaults.js +1 -1
  140. package/src/javascripts/ui/components/core/seamly-file-upload.js +0 -86
  141. package/src/javascripts/ui/components/entry/text-entry/hooks.js +0 -46
@@ -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}
@@ -8,14 +8,22 @@ import InOutTransition, {
8
8
  transitionStartStates,
9
9
  } from 'ui/components/widgets/in-out-transition'
10
10
  import { useUserHasResponded } from 'domains/app/hooks'
11
+ import { useConfig } from 'domains/config/hooks'
11
12
  import { useI18n } from 'domains/i18n/hooks'
12
13
  import { useVisibility } from 'domains/visibility/hooks'
13
14
  import { className } from 'lib/css'
14
15
  import WindowOpenButton from './window-open-button'
15
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
+
16
23
  const WindowView = () => {
17
24
  const { isOpen, openChat } = useVisibility()
18
25
  const userHasResponded = useUserHasResponded()
26
+ const { continueChat, preChat } = useConfig()
19
27
  const { t } = useI18n()
20
28
  const continueChatText = t('window.chat.continue')
21
29
  const continueChatEvent = useMemo(
@@ -33,7 +41,9 @@ const WindowView = () => {
33
41
  <>
34
42
  <WindowOpenButton onClick={openChat} />
35
43
  <InOutTransition
36
- isActive={!isOpen && !userHasResponded}
44
+ isActive={preChat && !isOpen && !userHasResponded}
45
+ exitAfter={getDelay(preChat, 'exitAfter')}
46
+ enterDelay={getDelay(preChat, 'enterDelay')}
37
47
  transitionStartState={transitionStartStates.rendered}
38
48
  >
39
49
  <div
@@ -46,7 +56,9 @@ const WindowView = () => {
46
56
  </div>
47
57
  </InOutTransition>
48
58
  <InOutTransition
49
- isActive={!isOpen && userHasResponded}
59
+ isActive={continueChat && !isOpen && userHasResponded}
60
+ exitAfter={getDelay(continueChat, 'exitAfter')}
61
+ enterDelay={getDelay(continueChat, 'enterDelay')}
50
62
  transitionStartState={transitionStartStates.notRendered}
51
63
  >
52
64
  <div
@@ -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
  }
@@ -6,7 +6,7 @@ export const useSeamlyApiContext = () => useContext(SeamlyApiContext)
6
6
  export const useSeamlyObjectStore = () => {
7
7
  const api = useSeamlyApiContext()
8
8
 
9
- return api.store || {}
9
+ return api.store
10
10
  }
11
11
 
12
12
  export const useSeamlyConversationUrl = () => {
@@ -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
@@ -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;