@seamly/web-ui 21.0.7 → 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 (164) hide show
  1. package/build/dist/lib/components.js +9354 -7909
  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 +7006 -5903
  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 +965 -384
  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 -5664
  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 +9461 -12461
  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 +1831 -6023
  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 +11598 -14588
  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/middleware.ts +7 -14
  67. package/src/javascripts/domains/interrupt/selectors.ts +4 -0
  68. package/src/javascripts/domains/interrupt/slice.ts +2 -2
  69. package/src/javascripts/domains/store/selectors.ts +23 -10
  70. package/src/javascripts/domains/store/slice.ts +63 -11
  71. package/src/javascripts/domains/store/store.types.ts +39 -1
  72. package/src/javascripts/domains/translations/components/options-button.tsx +1 -4
  73. package/src/javascripts/domains/translations/components/translation-status.tsx +4 -3
  74. package/src/javascripts/domains/translations/hooks.ts +11 -4
  75. package/src/javascripts/domains/translations/slice.ts +2 -0
  76. package/src/javascripts/index.ts +2 -0
  77. package/src/javascripts/lib/url-helpers.ts +24 -0
  78. package/src/javascripts/schema.ts +10 -0
  79. package/src/javascripts/style-guide/states.js +65 -0
  80. package/src/javascripts/ui/components/app-options/index.js +4 -3
  81. package/src/javascripts/ui/components/conversation/conversation.tsx +2 -0
  82. package/src/javascripts/ui/components/conversation/event/chat-scroll/chat-scroll-provider.tsx +2 -0
  83. package/src/javascripts/ui/components/conversation/event/choice-prompt.js +1 -1
  84. package/src/javascripts/ui/components/conversation/event/text.js +1 -1
  85. package/src/javascripts/ui/components/conversation/event/upload.js +50 -9
  86. package/src/javascripts/ui/components/conversation/use-chat-scroll.ts +3 -2
  87. package/src/javascripts/ui/components/core/seamly-event-subscriber.ts +16 -14
  88. package/src/javascripts/ui/components/core/seamly-file-upload.tsx +156 -0
  89. package/src/javascripts/ui/components/core/seamly-instance-functions-loader.js +5 -5
  90. package/src/javascripts/ui/components/entry/abort-transaction-button/abort-transaction-button.tsx +45 -0
  91. package/src/javascripts/ui/components/entry/deprecated-toggle-button.js +4 -3
  92. package/src/javascripts/ui/components/entry/text-entry/hooks.ts +108 -0
  93. package/src/javascripts/ui/components/entry/text-entry/index.js +7 -4
  94. package/src/javascripts/ui/components/entry/text-entry/{text-entry-form.js → text-entry-form.tsx} +8 -22
  95. package/src/javascripts/ui/components/faq/faq.js +5 -4
  96. package/src/javascripts/ui/components/form-controls/{input.js → input.tsx} +13 -2
  97. package/src/javascripts/ui/components/form-controls/{wrapper.js → wrapper.tsx} +8 -4
  98. package/src/javascripts/ui/components/layout/agent-info.js +4 -3
  99. package/src/javascripts/ui/components/layout/chat-frame.js +7 -8
  100. package/src/javascripts/ui/components/layout/deprecated-chat-frame.js +7 -8
  101. package/src/javascripts/ui/components/layout/interrupt.js +6 -15
  102. package/src/javascripts/ui/components/layout/pre-chat-messages.js +4 -3
  103. package/src/javascripts/ui/components/suggestions/index.js +5 -4
  104. package/src/javascripts/ui/components/translation-chat-status/index.tsx +4 -3
  105. package/src/javascripts/ui/components/view/app-view.js +1 -2
  106. package/src/javascripts/ui/components/view/deprecated-view.js +1 -2
  107. package/src/javascripts/ui/components/view/{index.js → index.tsx} +53 -4
  108. package/src/javascripts/ui/components/view/inline-view.js +1 -11
  109. package/src/javascripts/ui/components/view/window-view/{index.js → index.tsx} +15 -11
  110. package/src/javascripts/ui/components/view/window-view/window-open-button.js +4 -3
  111. package/src/javascripts/ui/components/widgets/{in-out-transition.js → in-out-transition.tsx} +67 -28
  112. package/src/javascripts/ui/hooks/sounds/beep.mp3 +0 -0
  113. package/src/javascripts/ui/hooks/use-click-outside.ts +5 -3
  114. package/src/javascripts/ui/hooks/use-notifications.ts +114 -0
  115. package/src/javascripts/ui/hooks/{use-seamly-chat.js → use-seamly-chat.ts} +5 -1
  116. package/src/javascripts/ui/hooks/use-session-expired-command.ts +17 -0
  117. package/src/javascripts/ui/hooks/use-timeout.ts +20 -0
  118. package/src/stylesheets/3-chat/_chat.scss +3 -5
  119. package/src/stylesheets/4-base/_formelements.scss +0 -36
  120. package/src/stylesheets/5-components/_abort-transaction.scss +10 -0
  121. package/src/stylesheets/5-components/_buttons.scss +18 -3
  122. package/src/stylesheets/5-components/_character-limit.scss +2 -2
  123. package/src/stylesheets/5-components/_chat-status.scss +26 -37
  124. package/src/stylesheets/5-components/_choice-prompt.scss +9 -10
  125. package/src/stylesheets/5-components/_conversation.scss +9 -62
  126. package/src/stylesheets/5-components/_disclaimer.scss +11 -3
  127. package/src/stylesheets/5-components/_error.scss +3 -2
  128. package/src/stylesheets/5-components/_idle.scss +3 -8
  129. package/src/stylesheets/5-components/_input.scss +34 -13
  130. package/src/stylesheets/5-components/_interrupt.scss +3 -10
  131. package/src/stylesheets/5-components/_loader.scss +1 -2
  132. package/src/stylesheets/5-components/_message-author.scss +2 -4
  133. package/src/stylesheets/5-components/_message-body.scss +33 -10
  134. package/src/stylesheets/5-components/_message-card.scss +2 -10
  135. package/src/stylesheets/5-components/_message-carousel.scss +4 -4
  136. package/src/stylesheets/5-components/_message-cta.scss +0 -6
  137. package/src/stylesheets/5-components/_message.scss +1 -0
  138. package/src/stylesheets/5-components/_modal.scss +2 -5
  139. package/src/stylesheets/5-components/_options.scss +17 -22
  140. package/src/stylesheets/5-components/_pre-chat-messages.scss +3 -1
  141. package/src/stylesheets/5-components/_prompt.scss +3 -7
  142. package/src/stylesheets/5-components/_skip-link.scss +2 -1
  143. package/src/stylesheets/5-components/_suggestions.scss +2 -2
  144. package/src/stylesheets/5-components/_translation-options.scss +5 -2
  145. package/src/stylesheets/5-components/_unread-messages.scss +33 -0
  146. package/src/stylesheets/5-components/_upload.scss +20 -27
  147. package/src/stylesheets/6-default-implementation/_hover.scss +14 -17
  148. package/src/stylesheets/7-deprecated/1-settings/_config.scss +17 -0
  149. package/src/stylesheets/7-deprecated/3-app/_app.scss +2 -1
  150. package/src/stylesheets/7-deprecated/5-components/_card.scss +1 -0
  151. package/src/stylesheets/7-deprecated/5-components/_chat-status.scss +66 -20
  152. package/src/stylesheets/7-deprecated/5-components/_conversation.scss +1 -4
  153. package/src/stylesheets/7-deprecated/5-components/_input.scss +6 -1
  154. package/src/stylesheets/7-deprecated/5-components/_interrupt.scss +1 -4
  155. package/src/stylesheets/7-deprecated/5-components/_message.scss +49 -12
  156. package/src/stylesheets/7-deprecated/5-components/_translation-options.scss +30 -37
  157. package/src/stylesheets/7-deprecated/5-components/_unread-messages.scss +38 -0
  158. package/src/stylesheets/deprecated-view.scss +1 -0
  159. package/src/stylesheets/styles.scss +2 -0
  160. package/webpack/config.common.js +6 -1
  161. package/webpack/config.package.js +18 -0
  162. package/webpack/defaults.js +1 -1
  163. package/src/javascripts/ui/components/core/seamly-file-upload.js +0 -86
  164. package/src/javascripts/ui/components/entry/text-entry/hooks.js +0 -46
@@ -55,6 +55,7 @@ const baseState = {
55
55
  optionsOverride: {},
56
56
  },
57
57
  currentUploads: [],
58
+ processingFileUploads: [],
58
59
  }
59
60
 
60
61
  const avatar =
@@ -701,12 +702,28 @@ const fileDownloadAgentMessage = {
701
702
  },
702
703
  }
703
704
 
705
+ const fileUploadAgentMessage = {
706
+ type: 'message',
707
+ payload: {
708
+ ...fileDownloadPayload,
709
+ fromClient: false,
710
+ participant: 'agent',
711
+ id: randomId(),
712
+ },
713
+ }
714
+
715
+ sessionStorage.setItem(
716
+ `image-${fileDownloadAgentMessage.payload.id}`,
717
+ fileDownloadAgentMessage.payload.body.url,
718
+ )
719
+
704
720
  const deletedFileDownloadAgentMessage = {
705
721
  ...fileDownloadAgentMessage,
706
722
  payload: {
707
723
  ...fileDownloadAgentMessage.payload,
708
724
  body: {
709
725
  ...fileDownloadAgentMessage.payload.body,
726
+ url: undefined,
710
727
  },
711
728
  id: randomId(),
712
729
  },
@@ -722,6 +739,11 @@ const fileDownloadUserMessage = {
722
739
  },
723
740
  }
724
741
 
742
+ sessionStorage.setItem(
743
+ `image-${fileDownloadUserMessage.payload.id}`,
744
+ fileDownloadUserMessage.payload.body.url,
745
+ )
746
+
725
747
  const emptyUrlFileDownloadUserMessage = {
726
748
  ...fileDownloadUserMessage,
727
749
  payload: {
@@ -1143,6 +1165,7 @@ const standardState = {
1143
1165
  userMessageWithLinks,
1144
1166
  newTopicDivider,
1145
1167
  imageMessage,
1168
+ fileUploadAgentMessage,
1146
1169
  fileDownloadAgentMessage,
1147
1170
  deletedFileDownloadAgentMessage,
1148
1171
  userMessageLong,
@@ -1219,7 +1242,9 @@ const standardState = {
1219
1242
  imageMessage,
1220
1243
  videoMessage,
1221
1244
  imageMessageWithLightbox,
1245
+ fileUploadAgentMessage,
1222
1246
  fileDownloadAgentMessage,
1247
+ fileDownloadUserMessage,
1223
1248
  ],
1224
1249
  },
1225
1250
  systemMessages: {
@@ -1495,6 +1520,46 @@ const standardState = {
1495
1520
  optionsOverride: {},
1496
1521
  },
1497
1522
  },
1523
+ inputLabel: {
1524
+ category: categoryKeys.features,
1525
+ headingText: 'Input label',
1526
+ ...baseState,
1527
+ entryMeta: {
1528
+ default: 'text',
1529
+ active: 'text',
1530
+ options: {
1531
+ text: { limit: null },
1532
+ },
1533
+ optionsOverride: {
1534
+ text: {
1535
+ label: 'What is your name?',
1536
+ limit: null,
1537
+ placeholder: 'Please enter your name',
1538
+ },
1539
+ },
1540
+ },
1541
+ },
1542
+ abortTransactional: {
1543
+ category: categoryKeys.features,
1544
+ headingText: 'Abort transaction',
1545
+ ...baseState,
1546
+ events: [shortTextMessage, userMessage],
1547
+ entryMeta: {
1548
+ default: 'text',
1549
+ active: 'text',
1550
+ options: {
1551
+ text: { limit: null },
1552
+ },
1553
+ optionsOverride: {},
1554
+ actions: {
1555
+ abortTransaction: {
1556
+ label: 'Ask for another question',
1557
+ topicFallbackMessage: 'Oops...',
1558
+ topicName: 'abort_transactional',
1559
+ },
1560
+ },
1561
+ },
1562
+ },
1498
1563
  fileUploadToggle: {
1499
1564
  category: categoryKeys.uploads,
1500
1565
  headingText: 'File upload toggle button',
@@ -1,8 +1,9 @@
1
+ import { useSelector } from 'react-redux'
1
2
  import Icon from 'ui/components/layout/icon'
2
3
  import OptionsButton from 'ui/components/options/options-button'
3
4
  import { useSeamlyOptions } from 'ui/hooks/seamly-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 TranslationsOptionsButton from 'domains/translations/components/options-button'
7
8
  import {
8
9
  useLocaleNativeName,
@@ -13,14 +14,14 @@ import { className } from 'lib/css'
13
14
  export default function AppOptions() {
14
15
  const { menuOptions, allowOptionSelection } = useSeamlyOptions()
15
16
  const { isAvailable: isTranslationsAvailable } = useTranslations()
16
- const { hasInterrupt } = useInterrupt()
17
+ const hasError = useSelector(selectHasError)
17
18
  const { t, locale } = useI18n()
18
19
  const localeNativeName = useLocaleNativeName(locale)
19
20
 
20
21
  if (
21
22
  (!isTranslationsAvailable &&
22
23
  (!allowOptionSelection || !menuOptions.length)) ||
23
- hasInterrupt
24
+ hasError
24
25
  ) {
25
26
  return null
26
27
  }
@@ -8,6 +8,7 @@ import { useEvents } from 'ui/hooks/seamly-state-hooks'
8
8
  import { useI18n } from 'domains/i18n/hooks'
9
9
  import { useVisibility } from 'domains/visibility/hooks'
10
10
  import { className } from 'lib/css'
11
+ import AbortTransactionButton from '../entry/abort-transaction-button/abort-transaction-button'
11
12
  import ComponentFilter from './component-filter'
12
13
  import Event from './event/event'
13
14
  import Loader from './loader'
@@ -77,6 +78,7 @@ const Conversation = () => {
77
78
  <Events />
78
79
  </ComponentFilter>
79
80
  {isLoading && <Loader />}
81
+ <AbortTransactionButton />
80
82
  </ol>
81
83
  </div>
82
84
  </div>
@@ -11,6 +11,8 @@ const ChatScrollProvider = ({ children }) => {
11
11
  const eventRefs = useMemo(
12
12
  () =>
13
13
  events.reduce<Record<string, RefObject<HTMLElement>>>((acc, value) => {
14
+ if (!value.payload.id) return acc
15
+
14
16
  acc[value.payload.id] = createRef()
15
17
  return acc
16
18
  }, {}),
@@ -31,7 +31,7 @@ export const useChoicePrompt = (event) => {
31
31
  body: event.payload.body?.prompt,
32
32
  translatedBody: event.payload.translatedBody && {
33
33
  ...event.payload.translatedBody,
34
- data: event.payload.translatedBody.data.prompt,
34
+ data: event.payload.translatedBody.prompt,
35
35
  },
36
36
  },
37
37
  }
@@ -17,7 +17,7 @@ const Text = ({ event, ...props }) => {
17
17
  return {
18
18
  bodyProps: {
19
19
  dangerouslySetInnerHTML: {
20
- __html: body.text,
20
+ __html: body?.data?.text || body.text,
21
21
  },
22
22
  },
23
23
  }
@@ -1,16 +1,53 @@
1
1
  import { useMemo } from 'preact/hooks'
2
+ import { useSelector } from 'react-redux'
2
3
  import MessageContainer from 'ui/components/conversation/message-container'
3
4
  import Icon from 'ui/components/layout/icon'
4
5
  import { useI18n } from 'domains/i18n/hooks'
5
6
  import { useTranslatedEventData } from 'domains/translations/hooks'
6
7
  import { className } from 'lib/css'
7
8
 
9
+ const PROCESSING_IMAGE = 'PROCESSING_IMAGE'
10
+
11
+ const useImageFromStorage = (currentFileId) => {
12
+ const { processingFileUploads } = useSelector(({ state }) => state)
13
+
14
+ return useMemo(() => {
15
+ const isProcessingImg = processingFileUploads.some(
16
+ (fileId) => fileId === currentFileId,
17
+ )
18
+
19
+ if (isProcessingImg) return PROCESSING_IMAGE
20
+
21
+ try {
22
+ return sessionStorage.getItem(`image-${currentFileId}`)
23
+ } catch (error) {
24
+ return undefined
25
+ }
26
+ }, [currentFileId, processingFileUploads])
27
+ }
28
+
29
+ const UploadedImage = ({ img, filename }) => {
30
+ const { t } = useI18n()
31
+ const srText = t('fileUpload.srFileUploadedText', { fileName: filename })
32
+
33
+ return (
34
+ <>
35
+ <span className={className(['download', 'download--preview'])}>
36
+ <img src={img} alt={srText} />
37
+ <span aria-hidden="true" className={className('file-download')}>
38
+ {filename}
39
+ </span>
40
+ </span>
41
+ </>
42
+ )
43
+ }
44
+
8
45
  const UploadContent = ({ children, url, target }) =>
9
46
  url ? (
10
47
  <a
11
48
  href={url}
12
49
  download
13
- target={target || undefined}
50
+ target={target}
14
51
  className={className(['download', 'download-link'])}
15
52
  >
16
53
  {children}
@@ -22,8 +59,9 @@ const UploadContent = ({ children, url, target }) =>
22
59
  const Upload = ({ event, ...props }) => {
23
60
  const { t } = useI18n()
24
61
  const { body } = useTranslatedEventData(event)
25
- const { fromClient } = event.payload
62
+ const { fromClient, id } = event.payload
26
63
  const { filename, url } = body
64
+ const img = useImageFromStorage(id)
27
65
 
28
66
  const srText = useMemo(
29
67
  () =>
@@ -35,13 +73,16 @@ const Upload = ({ event, ...props }) => {
35
73
 
36
74
  return (
37
75
  <MessageContainer event={event} type="upload" {...props}>
38
- <UploadContent url={url} target={!fromClient ? '_blank' : undefined}>
39
- <Icon name="download" size="16" />
40
- <span aria-hidden="true" className={className('file-download')}>
41
- {filename}
42
- </span>
43
- <span className={className('visually-hidden')}>{srText}</span>
44
- </UploadContent>
76
+ {img && img !== PROCESSING_IMAGE ? (
77
+ <UploadedImage img={img} filename={filename} />
78
+ ) : (
79
+ <UploadContent url={url} target={!fromClient ? '_blank' : undefined}>
80
+ <Icon name="download" size="16" alt={srText} />
81
+ <span aria-hidden="true" className={className('file-download')}>
82
+ {filename}
83
+ </span>
84
+ </UploadContent>
85
+ )}
45
86
  </MessageContainer>
46
87
  )
47
88
  }
@@ -37,8 +37,8 @@ const useChatScroll = (
37
37
  const isLoading = useSeamlyIsLoading()
38
38
  const { isOpen } = useVisibility()
39
39
  const loadedImageEventIds = useLoadedImageEventIds()
40
- const isLastEventFromClient = useSelector(
41
- (state: RootState) => state.state.isLastEventFromClient,
40
+ const { processingFileUploads, isLastEventFromClient } = useSelector(
41
+ ({ state }: RootState) => state,
42
42
  )
43
43
 
44
44
  useEffect(() => {
@@ -84,6 +84,7 @@ const useChatScroll = (
84
84
  isLoading,
85
85
  isOpen,
86
86
  loadedImageEventIds,
87
+ processingFileUploads,
87
88
  scrollToBottom,
88
89
  ])
89
90
 
@@ -24,6 +24,7 @@ import {
24
24
  setHistory,
25
25
  setIsLoading,
26
26
  setParticipant,
27
+ setProactiveMessages,
27
28
  setServiceDataItem,
28
29
  setServiceEntryMetadata,
29
30
  } from 'domains/store/slice'
@@ -73,7 +74,7 @@ const SeamlyEventSubscriber = () => {
73
74
  channel?.leave()
74
75
  }
75
76
  }
76
- return () => {}
77
+ return () => undefined
77
78
  }, [api, api.connectionInfo, api.conversation])
78
79
 
79
80
  useEffect(() => {
@@ -201,7 +202,7 @@ const SeamlyEventSubscriber = () => {
201
202
  case 'system':
202
203
  if (payload.type === 'service_changed') {
203
204
  const { serviceSettings, ...eventPayload } = payload
204
- const { entry } = serviceSettings
205
+ const { entry, proactiveMessages } = serviceSettings
205
206
  const { upload } = entry.options
206
207
 
207
208
  dispatch(
@@ -211,6 +212,10 @@ const SeamlyEventSubscriber = () => {
211
212
  }),
212
213
  )
213
214
 
215
+ dispatch(
216
+ setProactiveMessages(proactiveMessages.enabled || false),
217
+ )
218
+
214
219
  dispatch(setServiceEntryMetadata(entry))
215
220
  if (payload.serviceSessionId) {
216
221
  dispatch(setActiveService(payload.serviceSessionId))
@@ -244,6 +249,7 @@ const SeamlyEventSubscriber = () => {
244
249
  )
245
250
  break
246
251
  case 'conversation_erred':
252
+ case 'attach_channel_erred':
247
253
  const seamlyGeneralError = new SeamlyGeneralError(event)
248
254
  dispatch(
249
255
  setInterrupt({
@@ -283,6 +289,10 @@ const SeamlyEventSubscriber = () => {
283
289
 
284
290
  const { channel } = api.conversation
285
291
 
292
+ if (messageChannelRef.current) {
293
+ channel?.off('message', messageChannelRef.current)
294
+ }
295
+
286
296
  messageChannelRef.current = channel.on('message', (payload) => {
287
297
  if (!EMITTABLE_MESSAGE_TYPES.includes(payload.type)) {
288
298
  return payload
@@ -301,13 +311,7 @@ const SeamlyEventSubscriber = () => {
301
311
 
302
312
  return true
303
313
  })
304
-
305
- return () => {
306
- api.conversation.channel?.off('message', messageChannelRef.current)
307
- }
308
314
  }
309
-
310
- return () => undefined
311
315
  }, [api, api.connectionInfo, api.conversation.channel, eventBus])
312
316
 
313
317
  useEffect(() => {
@@ -315,6 +319,10 @@ const SeamlyEventSubscriber = () => {
315
319
  api.conversation.onConnection(({ connected }) => {
316
320
  if (!connected) return false
317
321
 
322
+ if (syncChannelRef.current) {
323
+ api.conversation.channel?.off('sync', syncChannelRef.current)
324
+ }
325
+
318
326
  syncChannelRef.current = api.conversation.channel.on(
319
327
  'sync',
320
328
  (payload) => {
@@ -348,13 +356,7 @@ const SeamlyEventSubscriber = () => {
348
356
 
349
357
  return true
350
358
  })
351
-
352
- return () => {
353
- api.conversation.channel?.off('sync', syncChannelRef.current)
354
- }
355
359
  }
356
-
357
- return () => undefined
358
360
  }, [api, api.connectionInfo, api.conversation.channel, events, dispatch])
359
361
 
360
362
  return null
@@ -0,0 +1,156 @@
1
+ import { useCallback } from 'preact/hooks'
2
+ import { useDispatch } from 'react-redux'
3
+ import SeamlyFileUploadContext from 'ui/components/core/seamly-file-upload-context'
4
+ import { useSeamlyApiContext, useSeamlyCommands } from 'ui/hooks/seamly-hooks'
5
+ import { useI18n } from 'domains/i18n/hooks'
6
+ import {
7
+ doneProcessingImage,
8
+ registerUpload,
9
+ setUploadComplete,
10
+ setUploadError,
11
+ setUploadProgress,
12
+ startProcessingImage,
13
+ } from 'domains/store/slice'
14
+ import { MessageUpload } from 'domains/store/store.types'
15
+ import { randomId } from 'lib/id'
16
+
17
+ const calculateImgSize = (img) => {
18
+ const MAX_WIDTH = 600
19
+ const MAX_HEIGHT = 600
20
+
21
+ const { height, width } = img
22
+ if (width > height && width > MAX_WIDTH) {
23
+ return [MAX_WIDTH, (height * MAX_WIDTH) / width]
24
+ }
25
+
26
+ if (height > MAX_HEIGHT) {
27
+ return [(width * MAX_HEIGHT) / height, MAX_HEIGHT]
28
+ }
29
+
30
+ return [width, height]
31
+ }
32
+
33
+ const resizeImage = (img) => {
34
+ const canvas = document.createElement('canvas')
35
+ const ctx = canvas.getContext('2d')
36
+
37
+ const [width, height] = calculateImgSize(img)
38
+ canvas.width = width
39
+ canvas.height = height
40
+
41
+ ctx.drawImage(img, 0, 0, width, height)
42
+
43
+ return canvas.toDataURL()
44
+ }
45
+
46
+ const toBase64 = (file): Promise<string> =>
47
+ new Promise((resolve, reject) => {
48
+ const reader = new FileReader()
49
+ reader.readAsDataURL(file)
50
+ reader.onload = () =>
51
+ typeof reader.result === 'string' ? resolve(reader.result) : undefined
52
+ reader.onerror = (error) => reject(error)
53
+ })
54
+
55
+ const SeamlyFileUpload = ({ children }) => {
56
+ const { t } = useI18n()
57
+ const dispatch = useDispatch()
58
+ const api = useSeamlyApiContext()
59
+ const { addUploadBubble } = useSeamlyCommands()
60
+
61
+ const addImageToSessionStorage = useCallback(
62
+ async (file, fileId) => {
63
+ dispatch(startProcessingImage(fileId))
64
+ const base64 = await toBase64(file)
65
+ const img = new Image()
66
+
67
+ img.src = base64
68
+
69
+ await img.decode()
70
+ const newDataUri = resizeImage(img)
71
+
72
+ try {
73
+ sessionStorage.setItem(`image-${fileId}`, newDataUri)
74
+ } catch (error) {
75
+ // Nothing to do!
76
+ } finally {
77
+ dispatch(doneProcessingImage(fileId))
78
+ }
79
+ },
80
+ [dispatch],
81
+ )
82
+
83
+ const onUploadFileHandler = useCallback(
84
+ (file) => {
85
+ const fileId = randomId()
86
+
87
+ const uploadHandle = api.uploadFile(
88
+ file,
89
+ (p) => {
90
+ dispatch(setUploadProgress({ fileId, progress: Math.ceil(p) }))
91
+ },
92
+ async (result: MessageUpload) => {
93
+ const {
94
+ id,
95
+ transactionId,
96
+ occurredAt,
97
+ body: { contentType, filename, filesize, url },
98
+ } = result
99
+
100
+ dispatch(setUploadComplete(fileId))
101
+ addUploadBubble(
102
+ id,
103
+ transactionId,
104
+ occurredAt,
105
+ contentType,
106
+ filename,
107
+ filesize,
108
+ url,
109
+ )
110
+
111
+ await addImageToSessionStorage(file, id)
112
+ },
113
+ (err) => {
114
+ const errorKey = err?.error || ''
115
+ let errorText
116
+
117
+ switch (errorKey) {
118
+ case 'file_uploads_are_disabled':
119
+ errorText = t('fileUpload.errors.unavailable')
120
+ break
121
+ case 'request_entity_too_large':
122
+ errorText = t('fileUpload.errors.tooLarge')
123
+ break
124
+ case 'file_has_invalid_mime_type':
125
+ errorText = t('fileUpload.errors.wrongType')
126
+ break
127
+ case 'virus_found':
128
+ errorText = t('fileUpload.errors.virusFound')
129
+ break
130
+ default:
131
+ errorText = t('fileUpload.errors.general')
132
+ }
133
+
134
+ dispatch(setUploadError({ fileId, errorText }))
135
+ },
136
+ )
137
+
138
+ dispatch(
139
+ registerUpload({
140
+ fileId,
141
+ fileName: file.name,
142
+ uploadHandle,
143
+ }),
144
+ )
145
+ },
146
+ [addImageToSessionStorage, addUploadBubble, api, dispatch, t],
147
+ )
148
+
149
+ return (
150
+ <SeamlyFileUploadContext.Provider value={onUploadFileHandler}>
151
+ {children}
152
+ </SeamlyFileUploadContext.Provider>
153
+ )
154
+ }
155
+
156
+ export default SeamlyFileUpload
@@ -1,5 +1,5 @@
1
1
  import { useContext, useEffect, useRef } from 'preact/hooks'
2
- import { useDispatch } from 'react-redux'
2
+ import { useDispatch, useSelector } from 'react-redux'
3
3
  import { userParticipantId } from 'config'
4
4
  import {
5
5
  useSeamlyActivityEventHandler,
@@ -12,7 +12,7 @@ import {
12
12
  import { actionTypes, sourceTypes } from 'ui/utils/seamly-utils'
13
13
  import { useConfig } from 'domains/config/hooks'
14
14
  import { updateConfig } from 'domains/config/slice'
15
- import { useInterrupt } from 'domains/interrupt/hooks'
15
+ import { selectHasError } from 'domains/interrupt/selectors'
16
16
  import { useTranslations } from 'domains/translations/hooks'
17
17
  import { visibilityStates } from 'domains/visibility/constants'
18
18
  import { useVisibility } from 'domains/visibility/hooks'
@@ -47,7 +47,7 @@ const SeamlyInstanceFunctionsLoader = () => {
47
47
  const previousUnreadCount = useRef(null)
48
48
  const previousVisibilityState = useRef(null)
49
49
  const { isInline, isResolving } = useSeamlyLayoutMode()
50
- const { hasInterrupt } = useInterrupt()
50
+ const hasError = useSelector(selectHasError)
51
51
  const currentConversationUrl = useSeamlyConversationUrl()
52
52
  const prevConversationUrl = useRef(null)
53
53
  const onActivityHandler = useSeamlyActivityEventHandler()
@@ -142,7 +142,7 @@ const SeamlyInstanceFunctionsLoader = () => {
142
142
  )
143
143
 
144
144
  useEffect(() => {
145
- if (!isResolving && !hasInterrupt) {
145
+ if (!isResolving && !hasError) {
146
146
  // Check for app reset
147
147
  if (
148
148
  prevConversationUrl.current &&
@@ -178,7 +178,7 @@ const SeamlyInstanceFunctionsLoader = () => {
178
178
  eventBus,
179
179
  isInline,
180
180
  isResolving,
181
- hasInterrupt,
181
+ hasError,
182
182
  currentConversationUrl,
183
183
  ])
184
184
 
@@ -0,0 +1,45 @@
1
+ import { useSeamlyApiContext } from 'ui/hooks/seamly-api-hooks'
2
+ import { actionTypes } from 'ui/utils/seamly-utils'
3
+ import { className } from 'lib/css'
4
+ import { useEntryAbortTransaction } from '../text-entry/hooks'
5
+
6
+ export default function AbortTransactionButton() {
7
+ const { abortTransaction, clearEntryAbortTransaction } =
8
+ useEntryAbortTransaction()
9
+ const api = useSeamlyApiContext()
10
+
11
+ if (!abortTransaction) return null
12
+
13
+ const handleAbortTransaction = () => {
14
+ api.send('action', {
15
+ type: actionTypes.setTopic,
16
+ body: {
17
+ name: abortTransaction.topicName,
18
+ fallbackMessage: abortTransaction.topicFallbackMessage,
19
+ },
20
+ })
21
+
22
+ clearEntryAbortTransaction()
23
+ }
24
+
25
+ return (
26
+ <li
27
+ className={className([
28
+ 'cvco-conversation__item',
29
+ 'cvco-conversation__item--abort-transaction',
30
+ ])}
31
+ >
32
+ <button
33
+ className={className([
34
+ 'button',
35
+ 'button--secondary',
36
+ 'abort-transaction__button',
37
+ ])}
38
+ type="button"
39
+ onClick={handleAbortTransaction}
40
+ >
41
+ {abortTransaction.label}
42
+ </button>
43
+ </li>
44
+ )
45
+ }
@@ -1,4 +1,5 @@
1
1
  import { useLayoutEffect, useRef } from 'preact/hooks'
2
+ import { useSelector } from 'react-redux'
2
3
  import {
3
4
  useFocusIfSeamlyContainedFocus,
4
5
  useGeneratedId,
@@ -8,7 +9,7 @@ import {
8
9
  useSkiplinkTargetFocusing,
9
10
  } from 'ui/hooks/seamly-hooks'
10
11
  import { useI18n } from 'domains/i18n/hooks'
11
- import { useInterrupt } from 'domains/interrupt/hooks'
12
+ import { selectHasError } from 'domains/interrupt/selectors'
12
13
  import { useVisibility } from 'domains/visibility/hooks'
13
14
  import { className } from 'lib/css'
14
15
 
@@ -24,10 +25,10 @@ const DeprecatedToggleButton = ({ onOpenChat }) => {
24
25
  const focusIfContained = useFocusIfSeamlyContainedFocus()
25
26
  const currentAgent = useSeamlyCurrentAgent()
26
27
  const agentSubtitle = useSeamlyHeaderData().subTitle
27
- const { hasInterrupt } = useInterrupt()
28
+ const hasError = useSelector(selectHasError)
28
29
  const { headerCollapseButtonId } = useSeamlyStateContext()
29
30
 
30
- const showAgentInfo = currentAgent && !hasInterrupt
31
+ const showAgentInfo = currentAgent && !hasError
31
32
 
32
33
  useLayoutEffect(() => {
33
34
  // Because we can close the app from the external API we