@seamly/web-ui 20.0.0-beta.1 → 20.0.0-beta.4

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 (76) hide show
  1. package/build/dist/lib/components.js +1 -1
  2. package/build/dist/lib/components.min.js +1 -1
  3. package/build/dist/lib/deprecated-view.css +1 -0
  4. package/build/dist/lib/deprecated-view.js +1 -0
  5. package/build/dist/lib/index.debug.js +102 -57
  6. package/build/dist/lib/index.debug.min.js +1 -1
  7. package/build/dist/lib/index.debug.min.js.LICENSE.txt +28 -12
  8. package/build/dist/lib/index.js +14773 -13925
  9. package/build/dist/lib/index.min.js +1 -1
  10. package/build/dist/lib/index.min.js.LICENSE.txt +5 -0
  11. package/build/dist/lib/standalone.js +19922 -19625
  12. package/build/dist/lib/standalone.min.js +1 -1
  13. package/build/dist/lib/style-guide.js +5071 -10746
  14. package/build/dist/lib/style-guide.min.js +1 -2
  15. package/build/dist/lib/styles-default-implementation.css +1 -0
  16. package/build/dist/lib/styles-default-implementation.js +1 -0
  17. package/build/dist/lib/styles.css +1 -1
  18. package/package.json +8 -7
  19. package/src/.DS_Store +0 -0
  20. package/src/javascripts/domains/translations/components/options-button.js +1 -1
  21. package/src/javascripts/index.js +5 -2
  22. package/src/javascripts/lib/parse-body.js +1 -1
  23. package/src/javascripts/package/components.js +1 -1
  24. package/src/javascripts/style-guide/components/app.js +3 -3
  25. package/src/javascripts/style-guide/components/static-core.js +1 -1
  26. package/src/javascripts/style-guide/states.js +326 -69
  27. package/src/javascripts/ui/components/app-options/index.js +9 -3
  28. package/src/javascripts/ui/components/conversation/conversation.js +1 -1
  29. package/src/javascripts/ui/components/conversation/event/carousel-message/index.js +3 -1
  30. package/src/javascripts/ui/components/conversation/event/conversation-suggestions.js +12 -3
  31. package/src/javascripts/ui/components/conversation/event/hooks/use-text-rendering.js +35 -0
  32. package/src/javascripts/ui/components/conversation/event/participant.js +5 -2
  33. package/src/javascripts/ui/components/conversation/event/splash.js +2 -1
  34. package/src/javascripts/ui/components/conversation/event/text.js +2 -1
  35. package/src/javascripts/ui/components/entry/{toggle-button.js → deprecated-toggle-button.js} +0 -0
  36. package/src/javascripts/ui/components/entry/entry-container.js +1 -1
  37. package/src/javascripts/ui/components/faq/faq.js +162 -0
  38. package/src/javascripts/ui/components/layout/chat-frame.js +1 -1
  39. package/src/javascripts/ui/components/layout/chat.js +62 -0
  40. package/src/javascripts/ui/components/layout/{app-frame.js → deprecated-app-frame.js} +10 -24
  41. package/src/javascripts/ui/components/layout/header.js +1 -1
  42. package/src/javascripts/ui/components/options/options-button.js +2 -2
  43. package/src/javascripts/ui/components/suggestions/index.js +2 -2
  44. package/src/javascripts/ui/components/view/app-view.js +3 -3
  45. package/src/javascripts/ui/components/view/deprecated-view.js +6 -4
  46. package/src/javascripts/ui/components/view/index.js +61 -5
  47. package/src/javascripts/ui/components/view/inline-view.js +9 -3
  48. package/src/javascripts/ui/components/view/window-view/index.js +3 -3
  49. package/src/stylesheets/1-settings/_config.scss +6 -6
  50. package/src/stylesheets/{3-app/_app.scss → 3-chat/_chat.scss} +27 -25
  51. package/src/stylesheets/5-components/_conversation.scss +2 -2
  52. package/src/stylesheets/5-components/_disclaimer.scss +1 -1
  53. package/src/stylesheets/5-components/_error.scss +20 -10
  54. package/src/stylesheets/5-components/_message-count.scss +1 -0
  55. package/src/stylesheets/5-components/_modal.scss +2 -2
  56. package/src/stylesheets/5-components/_options.scss +8 -8
  57. package/src/stylesheets/5-components/_pre-chat-messages.scss +6 -2
  58. package/src/stylesheets/5-components/_suggestions.scss +12 -6
  59. package/src/stylesheets/5-components/_unstarted.scss +14 -8
  60. package/src/stylesheets/6-default-implementation/_hover.scss +153 -0
  61. package/src/stylesheets/{6-webui-only → 6-default-implementation}/_scrollbar.scss +1 -1
  62. package/src/stylesheets/7-deprecated/3-app/_app.scss +8 -8
  63. package/src/stylesheets/7-deprecated/5-components/_error.scss +19 -9
  64. package/src/stylesheets/7-deprecated/5-components/_input.scss +1 -1
  65. package/src/stylesheets/7-deprecated/5-components/_options.scss +8 -10
  66. package/src/stylesheets/styles-default-implementation.scss +3 -0
  67. package/src/stylesheets/styles.scss +8 -9
  68. package/webpack/config.common.js +7 -1
  69. package/webpack/config.package.js +9 -1
  70. package/webpack/config.test.js +1 -0
  71. package/webpack/defaults.js +3 -6
  72. package/CHANGELOG.md +0 -625
  73. package/build/dist/lib/style-guide.min.js.LICENSE.txt +0 -9
  74. package/src/javascripts/ui/components/layout/modal-wrapper.js +0 -0
  75. package/src/stylesheets/6-webui-only/_hover.scss +0 -151
  76. package/src/stylesheets/styles-webui-only.scss +0 -3
@@ -0,0 +1,35 @@
1
+ import Mustache from 'mustache'
2
+
3
+ Mustache.escape = function (escapeText) {
4
+ return escapeText
5
+ }
6
+
7
+ const parseLinkVariable = (variable) => {
8
+ return `<a href='${variable.url}' data-link-id='${variable.id}' ${
9
+ variable.newTab ? 'target="_blank"' : ''
10
+ }>${variable.name}</a>`
11
+ }
12
+
13
+ export function parseRichText(text, variables = {}) {
14
+ const view = {}
15
+ Object.entries(variables).forEach(([key, variable]) => {
16
+ switch (variable.type) {
17
+ case 'link':
18
+ view[key] = parseLinkVariable(variable)
19
+ break
20
+ case 'text':
21
+ view[key] = variable.value
22
+ break
23
+ }
24
+ }, {})
25
+
26
+ // Disable escaping as we'll be generating HTML
27
+ const oldEscape = Mustache.escape
28
+ Mustache.escape = function (escapeText) {
29
+ return escapeText
30
+ }
31
+ const output = Mustache.render(text, view)
32
+ Mustache.escape = oldEscape
33
+
34
+ return output
35
+ }
@@ -1,4 +1,5 @@
1
1
  import { useMemo } from 'preact/hooks'
2
+ import Mustache from 'mustache'
2
3
  import parseBody from '../../../../lib/parse-body'
3
4
  import EventDivider from '../event-divider'
4
5
  import { useTranslatedEventData } from '../../../../domains/translations'
@@ -11,8 +12,10 @@ const Participant = ({ event }) => {
11
12
  const [introduction] = useTranslatedEventData(event)
12
13
 
13
14
  const intro = useMemo(() => {
14
- return introduction ? parseBody(introduction) : undefined
15
- }, [introduction])
15
+ return introduction
16
+ ? Mustache.render(parseBody(introduction), participant)
17
+ : undefined
18
+ }, [introduction, participant])
16
19
 
17
20
  if (!intro) {
18
21
  return null
@@ -1,5 +1,6 @@
1
1
  import parseBody from '../../../../lib/parse-body'
2
2
  import useEventLinkClickHandler from './hooks/use-event-link-click-handler'
3
+ import { parseRichText } from './hooks/use-text-rendering'
3
4
  import MessageContainer from '../message-container'
4
5
  import { useTranslatedEventData } from '../../../../domains/translations'
5
6
 
@@ -16,7 +17,7 @@ const Splash = ({ event, ...props }) => {
16
17
  {...props}
17
18
  bodyProps={{
18
19
  dangerouslySetInnerHTML: {
19
- __html: parseBody(body.text),
20
+ __html: parseRichText(parseBody(body.text), body.variables),
20
21
  },
21
22
  }}
22
23
  />
@@ -1,6 +1,7 @@
1
1
  import { useMemo } from 'preact/hooks'
2
2
  import parseBody from '../../../../lib/parse-body'
3
3
  import useEventLinkClickHandler from './hooks/use-event-link-click-handler'
4
+ import { parseRichText } from './hooks/use-text-rendering'
4
5
  import MessageContainer from '../message-container'
5
6
  import { useTranslatedEventData } from '../../../../domains/translations'
6
7
 
@@ -13,7 +14,7 @@ const Text = ({ event, ...props }) => {
13
14
  return {
14
15
  bodyProps: {
15
16
  dangerouslySetInnerHTML: {
16
- __html: parseBody(body.text),
17
+ __html: parseRichText(parseBody(body.text), body.variables),
17
18
  },
18
19
  },
19
20
  }
@@ -79,7 +79,7 @@ const EntryContainer = () => {
79
79
  const EntryComponent = entryComponents[renderEntry]
80
80
 
81
81
  return (
82
- <div className={className('app__entry')} ref={entryContainer}>
82
+ <div className={className('chat__entry')} ref={entryContainer}>
83
83
  {showCountdown && <IdleDetachWarning />}
84
84
  {showResumeConversationPrompt && <ResumeConversationPrompt />}
85
85
  <div
@@ -0,0 +1,162 @@
1
+ import { useEffect, useRef, useMemo } from 'preact/hooks'
2
+ import { className } from '../../../lib/css'
3
+ import Icon from '../layout/icon'
4
+ import { actionTypes } from '../../utils/seamly-utils'
5
+ import { runIfElementContainsOrHasFocus } from '../../utils/general-utils'
6
+ import useSeamlyCommands from '../../hooks/use-seamly-commands'
7
+ import {
8
+ useSeamlyServiceData,
9
+ useSeamlyLayoutMode,
10
+ } from '../../hooks/seamly-state-hooks'
11
+ import { useGeneratedId } from '../../hooks/utility-hooks'
12
+ import { useSkiplinkTargetFocusing } from '../../hooks/focus-helper-hooks'
13
+ import { useLiveRegion } from '../../hooks/live-region-hooks'
14
+ import useSeamlyIdleDetachCountdown from '../../hooks/use-seamly-idle-detach-countdown'
15
+ import useSeamlyResumeConversationPrompt from '../../hooks/use-seamly-resume-conversation-prompt'
16
+ import { useI18n } from '../../../domains/i18n'
17
+ import InOutTransition, {
18
+ transitionStartStates,
19
+ } from '../widgets/in-out-transition'
20
+ import { useTranslatedEventData } from '../../../domains/translations'
21
+ import { useInterrupt } from '../../../domains/interrupt'
22
+ import { useUserHasResponded } from '../../../domains/app'
23
+
24
+ const Faq = () => {
25
+ const { t } = useI18n()
26
+ const { sendAction, addMessageBubble } = useSeamlyCommands()
27
+ const sectionId = useGeneratedId()
28
+ const focusSkiplinkTarget = useSkiplinkTargetFocusing()
29
+ const { sendPolite } = useLiveRegion()
30
+ const { hasInterrupt } = useInterrupt()
31
+ const { hasCountdown, endCountdown } = useSeamlyIdleDetachCountdown()
32
+ const { hasPrompt, continueChat } = useSeamlyResumeConversationPrompt()
33
+
34
+ const lastFaqEventPayload = useSeamlyServiceData('suggestion')
35
+ const [eventBody] = useTranslatedEventData({ payload: lastFaqEventPayload })
36
+ const faqs = useMemo(() => {
37
+ const newFaqs = lastFaqEventPayload && !hasInterrupt ? eventBody : []
38
+ const itemBaseClass = `faqs__item`
39
+ return newFaqs.map(({ categories = [], ...faqRest }) => ({
40
+ ...faqRest,
41
+ categories,
42
+ classNames: [
43
+ itemBaseClass,
44
+ ...categories.map(
45
+ (cat) =>
46
+ `faqs__item--${String(cat)
47
+ .toLowerCase()
48
+ .replace(/[^a-z0-9_\\-]/, '')}`,
49
+ ),
50
+ ],
51
+ }))
52
+ }, [lastFaqEventPayload, hasInterrupt, eventBody])
53
+
54
+ const prevFaqs = useRef(null)
55
+ const prevHasFaqs = useRef(false)
56
+
57
+ const { isInline } = useSeamlyLayoutMode()
58
+ const hasResponded = useUserHasResponded()
59
+ const hideForWindow = !isInline && hasResponded
60
+ const prevHideForWindow = useRef(hideForWindow)
61
+
62
+ const hasFaqs = !!faqs.length
63
+ const showFaqContainer = hasFaqs && !hideForWindow
64
+ const previousRenderedFaqList = useRef([])
65
+ const renderedFaqList = hasFaqs ? faqs : previousRenderedFaqList.current
66
+ previousRenderedFaqList.current = renderedFaqList
67
+
68
+ const containerRef = useRef(null)
69
+
70
+ useEffect(() => {
71
+ if (prevFaqs.current !== faqs && !hideForWindow) {
72
+ if (hasFaqs) {
73
+ const politeText = prevHasFaqs.current
74
+ ? t('faq.srUpdatedText')
75
+ : t('faq.srAvailableText')
76
+ setTimeout(() => {
77
+ sendPolite(politeText)
78
+ }, 30)
79
+ } else if (prevHasFaqs.current) {
80
+ sendPolite(t('faq.srUnavailableText'))
81
+ }
82
+ prevFaqs.current = faqs
83
+ }
84
+
85
+ if (!prevHideForWindow.current && hideForWindow) {
86
+ runIfElementContainsOrHasFocus(containerRef.current, focusSkiplinkTarget)
87
+ sendPolite(t('faq.srUnavailableText'))
88
+ } else if (!hasFaqs && prevHasFaqs.current) {
89
+ runIfElementContainsOrHasFocus(containerRef.current, focusSkiplinkTarget)
90
+ }
91
+
92
+ prevHasFaqs.current = hasFaqs
93
+ prevHideForWindow.current = hideForWindow
94
+ }, [hasFaqs, faqs, hideForWindow, focusSkiplinkTarget, sendPolite, t])
95
+
96
+ const onFaqClickHandler = ({ id, question }) => {
97
+ if (hasCountdown) {
98
+ endCountdown(true)
99
+ }
100
+
101
+ if (hasPrompt) {
102
+ continueChat()
103
+ }
104
+
105
+ sendAction({
106
+ type: actionTypes.custom,
107
+ originMessage: lastFaqEventPayload.id,
108
+ body: {
109
+ type: 'faqclick',
110
+ body: {
111
+ faqId: id,
112
+ faqQuestion: question,
113
+ },
114
+ },
115
+ })
116
+
117
+ addMessageBubble(question)
118
+ focusSkiplinkTarget()
119
+ }
120
+
121
+ const headingText = t('faq.headingText')
122
+ const ContainerElement = headingText ? 'section' : 'div'
123
+
124
+ return (
125
+ <InOutTransition
126
+ isActive={showFaqContainer}
127
+ transitionStartState={transitionStartStates.notRendered}
128
+ >
129
+ <ContainerElement
130
+ className={className('faqs')}
131
+ aria-labelledby={headingText ? sectionId : null}
132
+ ref={containerRef}
133
+ >
134
+ {headingText && (
135
+ <h2 id={sectionId} className={className('faqs__heading')}>
136
+ {headingText}
137
+ </h2>
138
+ )}
139
+ {!!renderedFaqList.length && (
140
+ <ul className={className('faqs__list')}>
141
+ {renderedFaqList.map((faq) => (
142
+ <li key={faq.id.toString()} className={className(faq.classNames)}>
143
+ <button
144
+ type="button"
145
+ onClick={() => {
146
+ onFaqClickHandler(faq)
147
+ }}
148
+ className={className('button', 'button--secondary')}
149
+ >
150
+ <Icon name="chevronRight" size="8" />
151
+ {faq.question}
152
+ </button>
153
+ </li>
154
+ ))}
155
+ </ul>
156
+ )}
157
+ </ContainerElement>
158
+ </InOutTransition>
159
+ )
160
+ }
161
+
162
+ export default Faq
@@ -21,7 +21,7 @@ function ChatFrame({ children, interruptComponent: InterruptComponent }) {
21
21
  return (
22
22
  <>
23
23
  <TranslationsChatStatus />
24
- <div className={className('app__container')}>{getContent()}</div>
24
+ <div className={className('chat__container')}>{getContent()}</div>
25
25
  <AppOptions />
26
26
  </>
27
27
  )
@@ -0,0 +1,62 @@
1
+ import { className } from '../../../lib/css'
2
+ import {
3
+ useSeamlyAppContainerClassNames,
4
+ useSeamlyLayoutMode,
5
+ } from '../../hooks/seamly-hooks'
6
+ import { useConfig } from '../../../domains/config'
7
+ import { useUserHasResponded } from '../../../domains/app'
8
+ import { useI18n } from '../../../domains/i18n'
9
+ import { useVisibility, visibilityStates } from '../../../domains/visibility'
10
+ import Suggestions from '../suggestions'
11
+
12
+ const Chat = ({ children, className: givenClassName = '' }) => {
13
+ const { isOpen, isVisible, setVisibility } = useVisibility()
14
+ const { namespace, layoutMode } = useConfig()
15
+ const { isInline } = useSeamlyLayoutMode()
16
+ const appContainerClassNames = useSeamlyAppContainerClassNames()
17
+ const userResponded = useUserHasResponded()
18
+ const { t } = useI18n()
19
+
20
+ const defaultClassNames = [
21
+ 'chat',
22
+ `chat--layout-${layoutMode}`,
23
+ `namespace--${namespace}`,
24
+ ]
25
+
26
+ const classNames = [
27
+ ...defaultClassNames,
28
+ ...appContainerClassNames,
29
+ givenClassName,
30
+ ]
31
+
32
+ if (!isOpen && layoutMode !== 'app') {
33
+ classNames.push('chat--collapsed')
34
+ }
35
+
36
+ if (userResponded) {
37
+ classNames.push('chat--user-responded')
38
+ }
39
+
40
+ const onKeyDownHandler = (e) => {
41
+ if ((e.code && e.code === 'Escape') || e.keyCode === 27)
42
+ if (!isInline && isOpen) {
43
+ setVisibility(visibilityStates.minimized)
44
+ }
45
+ }
46
+
47
+ return (
48
+ isVisible && (
49
+ <section
50
+ className={className(classNames)}
51
+ onKeyDown={onKeyDownHandler}
52
+ tabIndex="-1"
53
+ aria-label={t('chat.srLabel')}
54
+ >
55
+ <div className={className('chat-wrapper')}>{children}</div>
56
+ {layoutMode === 'inline' && isOpen && <Suggestions isAside={true} />}
57
+ </section>
58
+ )
59
+ )
60
+ }
61
+
62
+ export default Chat
@@ -5,24 +5,20 @@ import {
5
5
  useSeamlyLayoutMode,
6
6
  useSeamlyContainerElement,
7
7
  } from '../../hooks/seamly-hooks'
8
+ import Faq from '../faq/faq'
8
9
  import { useConfig } from '../../../domains/config'
9
10
  import { useUserHasResponded } from '../../../domains/app'
10
11
  import { useI18n } from '../../../domains/i18n'
11
12
  import { useVisibility, visibilityStates } from '../../../domains/visibility'
12
- import Suggestions from '../suggestions'
13
13
 
14
- const AppFrame = ({
15
- children,
16
- className: givenClassName = '',
17
- isDeprecated,
18
- }) => {
14
+ const DeprecatedAppFrame = ({ children }) => {
19
15
  const [, setSeamlyContainerElement] = useSeamlyContainerElement()
20
16
  const { isOpen, isVisible, setVisibility } = useVisibility()
21
- const { zIndex, namespace, layoutMode } = useConfig()
17
+ const { zIndex, showFaq, layoutMode } = useConfig()
22
18
  const { isInline } = useSeamlyLayoutMode()
23
19
  const appContainerClassNames = useSeamlyAppContainerClassNames()
24
20
  const userResponded = useUserHasResponded()
25
- const { locale, t } = useI18n()
21
+ const { locale } = useI18n()
26
22
 
27
23
  const containerElementRef = useCallback(
28
24
  (container) => {
@@ -43,20 +39,9 @@ const AppFrame = ({
43
39
  return undefined
44
40
  }, [locale])
45
41
 
46
- const baseClassName = isDeprecated ? 'app--deprecated' : 'app'
47
- const defaultClassNames = [
48
- `app--layout-${layoutMode}`,
49
- `namespace--${namespace}`,
50
- ]
42
+ const classNames = ['app', 'app--deprecated', ...appContainerClassNames]
51
43
 
52
- const classNames = [
53
- baseClassName,
54
- ...defaultClassNames,
55
- ...appContainerClassNames,
56
- givenClassName,
57
- ]
58
-
59
- if (!isOpen && layoutMode !== 'app') {
44
+ if (!isOpen && layoutMode === 'window') {
60
45
  classNames.push('app--collapsed')
61
46
  }
62
47
 
@@ -64,6 +49,8 @@ const AppFrame = ({
64
49
  classNames.push('app--user-responded')
65
50
  }
66
51
 
52
+ classNames.push(`app--layout-${layoutMode}`)
53
+
67
54
  const onKeyDownHandler = (e) => {
68
55
  if ((e.code && e.code === 'Escape') || e.keyCode === 27)
69
56
  if (!isInline && isOpen) {
@@ -81,13 +68,12 @@ const AppFrame = ({
81
68
  ref={containerElementRef}
82
69
  style={{ zIndex }}
83
70
  data-nosnippet
84
- aria-label={t('app.srLabel')}
85
71
  >
86
72
  <div className={className('app-wrapper')}>{children}</div>
87
- {layoutMode === 'inline' && isOpen && <Suggestions isAside={true} />}
73
+ {showFaq && <Faq />}
88
74
  </section>
89
75
  )
90
76
  )
91
77
  }
92
78
 
93
- export default AppFrame
79
+ export default DeprecatedAppFrame
@@ -10,7 +10,7 @@ const Header = ({ children, onCloseChat }) => {
10
10
 
11
11
  const { t } = useI18n()
12
12
  return (
13
- <header className={className('app__header')}>
13
+ <header className={className('chat__header')}>
14
14
  {children}
15
15
  <div className={className('header-controls')}>
16
16
  <button
@@ -223,9 +223,9 @@ const OptionsButton = () => {
223
223
  className={className([
224
224
  'button',
225
225
  'button--secondary',
226
- 'app__options__button',
226
+ 'chat__options__button',
227
227
  ...(!multiMenu && firstOptionName
228
- ? [`app__options__button--${firstOptionName}`]
228
+ ? [`chat__options__button--${firstOptionName}`]
229
229
  : []),
230
230
  ...(!multiMenu && !firstOption.available ? ['button--disabled'] : []),
231
231
  ])}
@@ -152,9 +152,9 @@ const Suggestions = ({ isAside }) => {
152
152
  ref={containerRef}
153
153
  >
154
154
  {headingText && (
155
- <h2 id={sectionId} className={className('suggestions__heading')}>
155
+ <p id={sectionId} className={className('suggestions__heading')}>
156
156
  {headingText}
157
- </h2>
157
+ </p>
158
158
  )}
159
159
  {!!renderedSuggestions.length && (
160
160
  <SuggestionsList
@@ -1,4 +1,4 @@
1
- import AppFrame from '../layout/app-frame'
1
+ import Chat from '../layout/chat'
2
2
  import Interrupt from '../layout/interrupt'
3
3
  import Conversation from '../conversation/conversation'
4
4
  import EntryContainer from '../entry/entry-container'
@@ -9,12 +9,12 @@ const AppView = () => {
9
9
  useSeamlyChat()
10
10
 
11
11
  return (
12
- <AppFrame>
12
+ <Chat>
13
13
  <ChatFrame interruptComponent={Interrupt}>
14
14
  <Conversation />
15
15
  <EntryContainer />
16
16
  </ChatFrame>
17
- </AppFrame>
17
+ </Chat>
18
18
  )
19
19
  }
20
20
 
@@ -1,4 +1,4 @@
1
- import AppFrame from '../layout/app-frame'
1
+ import DeprecatedAppFrame from '../layout/deprecated-app-frame'
2
2
  import ChatFrame from '../layout/chat-frame'
3
3
  import AgentInfo from '../layout/agent-info'
4
4
  import Header from '../layout/header'
@@ -7,14 +7,16 @@ import EntryContainer from '../entry/entry-container'
7
7
  import Interrupt from '../layout/interrupt'
8
8
  import { useSeamlyChat } from '../../hooks/seamly-hooks'
9
9
  import { useVisibility } from '../../../domains/visibility'
10
+ import DeprecatedToggleButton from '../entry/deprecated-toggle-button'
10
11
 
11
12
  const DeprecatedView = () => {
12
13
  const { isVisible } = useVisibility()
13
- const { closeChat } = useSeamlyChat()
14
+ const { openChat, closeChat } = useSeamlyChat()
14
15
 
15
16
  return (
16
17
  isVisible && (
17
- <AppFrame isDeprecated={true}>
18
+ <DeprecatedAppFrame>
19
+ <DeprecatedToggleButton onOpenChat={openChat} />
18
20
  <Header onCloseChat={closeChat}>
19
21
  <AgentInfo />
20
22
  </Header>
@@ -22,7 +24,7 @@ const DeprecatedView = () => {
22
24
  <Conversation />
23
25
  <EntryContainer />
24
26
  </ChatFrame>
25
- </AppFrame>
27
+ </DeprecatedAppFrame>
26
28
  )
27
29
  )
28
30
  }
@@ -1,9 +1,14 @@
1
+ import { useCallback, useMemo } from 'preact/hooks'
1
2
  import { useConfig } from '../../../domains/config'
2
3
  import AppView from './app-view'
3
4
  import InlineView from './inline-view'
4
5
  import WindowView from './window-view'
5
- import { useSeamlyStateContext } from '../../hooks/seamly-state-hooks'
6
- import { visibilityStates } from '../../../domains/visibility'
6
+ import { useVisibility } from '../../../domains/visibility'
7
+ import { className } from '../../../lib/css'
8
+ import { useSeamlyAppContainerClassNames } from '../../hooks/component-helper-hooks'
9
+ import { useUserHasResponded } from '../../../domains/app'
10
+ import { useI18n } from '../../../domains/i18n'
11
+ import { useSeamlyContainerElement } from '../../hooks/focus-helper-hooks'
7
12
 
8
13
  const ViewComponentsMap = {
9
14
  app: AppView,
@@ -12,16 +17,67 @@ const ViewComponentsMap = {
12
17
  }
13
18
 
14
19
  const View = () => {
15
- const { layoutMode } = useConfig()
16
- const { visible } = useSeamlyStateContext()
20
+ const [, setSeamlyContainerElement] = useSeamlyContainerElement()
21
+ const { namespace, layoutMode, zIndex } = useConfig()
22
+ const { isOpen, isVisible } = useVisibility()
23
+ const appContainerClassNames = useSeamlyAppContainerClassNames()
24
+ const userResponded = useUserHasResponded()
25
+ const { locale } = useI18n()
17
26
  const ViewComponent = ViewComponentsMap[layoutMode]
18
27
 
28
+ const containerElementRef = useCallback(
29
+ (container) => {
30
+ setSeamlyContainerElement(container)
31
+ },
32
+ [setSeamlyContainerElement],
33
+ )
34
+
35
+ const blockLang = useMemo(() => {
36
+ if (locale) {
37
+ const htmlElementLang = document
38
+ .querySelector('html')
39
+ .getAttribute('lang')
40
+ if (htmlElementLang !== locale) {
41
+ return locale
42
+ }
43
+ }
44
+ return undefined
45
+ }, [locale])
46
+
19
47
  if (!ViewComponent) {
20
48
  console.warn('"layoutMode" should be one of "app", "inline" or "window"')
21
49
  return null
22
50
  }
23
51
 
24
- return visible !== visibilityStates.hidden && <ViewComponent />
52
+ const defaultClassNames = [
53
+ `app--layout-${layoutMode}`,
54
+ `namespace--${namespace}`,
55
+ ]
56
+
57
+ const classNames = ['app', ...defaultClassNames, ...appContainerClassNames]
58
+
59
+ if (!isOpen && layoutMode !== 'app') {
60
+ classNames.push('app--collapsed')
61
+ }
62
+
63
+ if (userResponded) {
64
+ classNames.push('app--user-responded')
65
+ }
66
+
67
+ return (
68
+ isVisible && (
69
+ <div
70
+ className={className(classNames)}
71
+ lang={blockLang}
72
+ tabIndex="-1"
73
+ data-nosnippet
74
+ style={{ zIndex }}
75
+ ref={containerElementRef}
76
+ >
77
+ <ViewComponent />
78
+ </div>
79
+ )
80
+ )
25
81
  }
26
82
 
27
83
  export default View
@@ -1,5 +1,5 @@
1
1
  import { className } from '../../../lib/css'
2
- import AppFrame from '../layout/app-frame'
2
+ import Chat from '../layout/chat'
3
3
  import Interrupt from '../layout/interrupt'
4
4
  import Conversation from '../conversation/conversation'
5
5
  import EntryContainer from '../entry/entry-container'
@@ -11,10 +11,16 @@ import Suggestions from '../suggestions'
11
11
  import InOutTransition, {
12
12
  transitionStartStates,
13
13
  } from '../widgets/in-out-transition'
14
+ import { useInterrupt } from '../../../domains/interrupt'
14
15
 
15
16
  const InlineView = () => {
16
17
  useSeamlyChat()
17
18
  const { isOpen } = useVisibility()
19
+ const { hasInterrupt, meta } = useInterrupt()
20
+
21
+ if (hasInterrupt && !isOpen) {
22
+ return <Interrupt {...meta} />
23
+ }
18
24
 
19
25
  return (
20
26
  <>
@@ -31,12 +37,12 @@ const InlineView = () => {
31
37
  isActive={isOpen}
32
38
  transitionStartState={transitionStartStates.rendered}
33
39
  >
34
- <AppFrame>
40
+ <Chat>
35
41
  <ChatFrame interruptComponent={Interrupt}>
36
42
  {isOpen && <Conversation />}
37
43
  <EntryContainer />
38
44
  </ChatFrame>
39
- </AppFrame>
45
+ </Chat>
40
46
  </InOutTransition>
41
47
  </>
42
48
  )
@@ -1,7 +1,7 @@
1
1
  import { useMemo } from 'preact/hooks'
2
2
  import { useSeamlyChat } from '../../../hooks/seamly-hooks'
3
3
  import { useVisibility } from '../../../../domains/visibility'
4
- import AppFrame from '../../layout/app-frame'
4
+ import Chat from '../../layout/chat'
5
5
  import ChatFrame from '../../layout/chat-frame'
6
6
  import Interrupt from '../../layout/interrupt'
7
7
  import Conversation from '../../conversation/conversation'
@@ -67,13 +67,13 @@ const WindowView = () => {
67
67
  isActive={isOpen}
68
68
  transitionStartState={transitionStartStates.notRendered}
69
69
  >
70
- <AppFrame>
70
+ <Chat>
71
71
  {isOpen && <CollapseButton onClick={closeChat} />}
72
72
  <ChatFrame interruptComponent={Interrupt}>
73
73
  <Conversation />
74
74
  <EntryContainer />
75
75
  </ChatFrame>
76
- </AppFrame>
76
+ </Chat>
77
77
  </InOutTransition>
78
78
  </>
79
79
  )