@seamly/web-ui 20.8.0-beta.2 → 20.8.0-beta.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seamly/web-ui",
3
- "version": "20.8.0-beta.2",
3
+ "version": "20.8.0-beta.5",
4
4
  "main": "build/dist/lib/index.js",
5
5
  "types": "build/src/javascripts/index.d.ts",
6
6
  "module": "",
@@ -1,4 +1,4 @@
1
- import { createAsyncThunk } from '@reduxjs/toolkit'
1
+ import { createAsyncThunk, unwrapResult } from '@reduxjs/toolkit'
2
2
  import SeamlySessionExpiredError from 'api/errors/seamly-session-expired-error'
3
3
  import SeamlyUnavailableError from 'api/errors/seamly-unavailable-error'
4
4
  import { Config } from 'config.types'
@@ -47,7 +47,6 @@ export const initializeApp = createAsyncThunk<
47
47
  body: { enabled: true, locale },
48
48
  })
49
49
  }
50
- dispatch(setLocale(locale))
51
50
  return { initialState: undefined, locale, config }
52
51
  }
53
52
  } catch (e) {
@@ -81,7 +80,14 @@ export const resetApp = createAsyncThunk<unknown, void, ThunkAPI>(
81
80
 
82
81
  dispatch(resetConfig())
83
82
  await dispatch(initializeConfig())
84
- await dispatch(initializeApp())
83
+
84
+ try {
85
+ const { locale } = await dispatch(initializeApp()).unwrap()
86
+ await dispatch(setLocale(locale))
87
+ } catch (rejectedValueOrSerializedError) {
88
+ // nothing to do
89
+ }
90
+
85
91
  dispatch(initializeVisibility())
86
92
  },
87
93
  )
@@ -16,6 +16,7 @@ export const initializeConfig = createAsyncThunk<any, void, ThunkAPI>(
16
16
  } = await api.getConfig()
17
17
 
18
18
  const locale = config?.context?.locale || defaultLocale
19
+ const { connectWhenInView } = config
19
20
 
20
21
  return {
21
22
  features,
@@ -25,6 +26,7 @@ export const initializeConfig = createAsyncThunk<any, void, ThunkAPI>(
25
26
  userParticipant,
26
27
  startChatIcon,
27
28
  locale,
29
+ connectWhenInView,
28
30
  }
29
31
  } catch (e) {
30
32
  const err = new SeamlyUnavailableError()
@@ -1,24 +1,34 @@
1
1
  import { createAsyncThunk } from '@reduxjs/toolkit'
2
- import { selectLocale } from 'domains/i18n/selectors'
2
+ import type { I18nState } from 'domains/i18n/i18n.types'
3
3
  import { ThunkAPI } from 'domains/redux/redux.types'
4
4
 
5
5
  export const setLocale = createAsyncThunk<
6
6
  {
7
- translations: Record<string, string>
8
- locale: string
7
+ translations: I18nState['translations']
8
+ locale: I18nState['locale']
9
9
  },
10
10
  string,
11
11
  ThunkAPI
12
- >('setLocale', async (locale, { getState, extra: { api } }) => {
13
- const stateLocale = selectLocale(getState())
14
- if (locale === stateLocale) {
15
- return undefined
16
- }
12
+ >(
13
+ 'setLocale',
14
+ async (locale, { extra: { api } }) => {
15
+ const response = await api.getTranslations(locale)
17
16
 
18
- const response = await api.getTranslations(locale)
17
+ return {
18
+ translations: response,
19
+ locale,
20
+ }
21
+ },
22
+ {
23
+ condition: (locale, { getState }) => {
24
+ const {
25
+ i18n: { isLoading, locale: stateLocale },
26
+ } = getState()
19
27
 
20
- return {
21
- translations: response,
22
- locale,
23
- }
24
- })
28
+ if (locale === stateLocale || isLoading) {
29
+ // Already fetched or in progress, don't need to re-fetch
30
+ return false
31
+ }
32
+ },
33
+ },
34
+ )
@@ -3,6 +3,7 @@ import {
3
3
  pluralTypeHandler,
4
4
  selectTypeHandler,
5
5
  } from '@ultraq/icu-message-formatter'
6
+ import { RootState } from 'domains/store'
6
7
  import { useCallback } from 'preact/hooks'
7
8
  import { useSelector } from 'react-redux'
8
9
  import * as Selectors from './selectors'
@@ -19,16 +20,20 @@ export function useI18n() {
19
20
  const translations = useSelector(Selectors.selectTranslations)
20
21
  const locale = useSelector(Selectors.selectLocale)
21
22
  const initialLocale = useSelector(Selectors.selectInitialLocale)
23
+ const isLoading = useSelector((state: RootState) => state.i18n.isLoading)
24
+
22
25
  const t = useCallback(
23
26
  (key, values = {}) => {
24
27
  const translation = translations[key]
25
28
  if (!translation) {
29
+ if (isLoading) return null
30
+
26
31
  console.warn(`Translation key: ${key} is missing in locale: ${locale}`)
27
32
  return null
28
33
  }
29
34
  return formatter.format(translation, values)
30
35
  },
31
- [translations, locale],
36
+ [translations, locale, isLoading],
32
37
  )
33
38
 
34
39
  return {
@@ -1,5 +1,7 @@
1
+ import { operations } from 'schema'
2
+
1
3
  export interface I18nState {
2
- translations: Record<string, string>
4
+ translations: operations['getAccountTranslations']['responses']['200']['content']['application/json']['translations']
3
5
  isLoading: boolean
4
6
  initialLocale?: string
5
7
  locale?: string
@@ -50,9 +50,15 @@ export const i18nSlice = createSlice({
50
50
  .addCase(initializeConfig.fulfilled, (state, { payload }) => {
51
51
  state.initialLocale = payload.locale
52
52
  })
53
+ .addCase(setLocale.pending, (state) => {
54
+ state.isLoading = true
55
+ })
56
+ .addCase(setLocale.rejected, (state) => {
57
+ state.isLoading = false
58
+ })
53
59
  .addCase(setLocale.fulfilled, (state, { payload }) => {
60
+ state.isLoading = false
54
61
  if (!payload?.translations) {
55
- state.isLoading = false
56
62
  return
57
63
  }
58
64
 
@@ -27,6 +27,7 @@ export default function createI18nMiddleware({ dispatch, getState }) {
27
27
  case setInitialState.type:
28
28
  if (payload?.translation?.enabled) {
29
29
  dispatch(enableTranslation(payload.translation.locale))
30
+ dispatch(setLocale(payload.locale))
30
31
  }
31
32
  break
32
33
  case addEvent.type:
@@ -1,5 +1,6 @@
1
1
  import { createSlice, PayloadAction } from '@reduxjs/toolkit'
2
2
  import { VisibilityOptions } from 'config.types'
3
+ import { initializeConfig } from 'domains/config/actions'
3
4
  import { setVisibility } from 'domains/visibility/actions'
4
5
  import { visibilityStates } from 'domains/visibility/constants'
5
6
  import type { VisibilityState } from 'domains/visibility/visibility.types'
@@ -21,14 +22,21 @@ export const visibilitySlice = createSlice({
21
22
  },
22
23
  },
23
24
  extraReducers: (builder) => {
24
- builder.addCase(
25
- setVisibility.fulfilled,
26
- (state, { payload: visibility }: PayloadAction<VisibilityOptions>) => {
27
- if (visibility) {
28
- state.visibility = visibility
25
+ builder
26
+ .addCase(
27
+ setVisibility.fulfilled,
28
+ (state, { payload: visibility }: PayloadAction<VisibilityOptions>) => {
29
+ if (visibility) {
30
+ state.visibility = visibility
31
+ }
32
+ },
33
+ )
34
+ .addCase(initializeConfig.fulfilled, (state, { payload }) => {
35
+ // We only want to (always) show the inlineView when connectWhenInView is disabled
36
+ if (!payload.connectWhenInView) {
37
+ state.showInlineView = !payload.connectWhenInView
29
38
  }
30
- },
31
- )
39
+ })
32
40
  },
33
41
  })
34
42
 
@@ -80,7 +80,6 @@ export {
80
80
  useGeneratedId,
81
81
  useSeamlyChat,
82
82
  useSeamlyCommands,
83
- useSeamlyDispatchContext,
84
83
  useSeamlyEventStream,
85
84
  useSeamlyIdleDetachCountdown,
86
85
  useSeamlyMessageContainerClassNames,
@@ -90,3 +89,4 @@ export {
90
89
  export { getUrlParams, getUrlSearchString } from './ui/utils/general-utils'
91
90
  // Used by: Client
92
91
  export { eventTypes } from './ui/utils/seamly-utils'
92
+ export { useSeamlyActions } from 'ui/hooks/use-seamly-actions'
@@ -3,6 +3,7 @@ import { initializeApp } from 'domains/app/actions'
3
3
 
4
4
  import { initializeConfig } from 'domains/config/actions'
5
5
  import { setConfig } from 'domains/config/slice'
6
+ import { setLocale } from 'domains/i18n/actions'
6
7
 
7
8
  import { createStore } from 'domains/store'
8
9
  import { initializeVisibility } from 'domains/visibility/actions'
@@ -65,7 +66,12 @@ export default class Engine {
65
66
 
66
67
  store.dispatch(setConfig(renderConfig))
67
68
  await store.dispatch(initializeConfig())
68
- await store.dispatch(initializeApp())
69
+ try {
70
+ const { locale } = await store.dispatch(initializeApp()).unwrap()
71
+ await store.dispatch(setLocale(locale))
72
+ } catch (rejectedValueOrSerializedError) {
73
+ // nothing to do
74
+ }
69
75
  store.dispatch(initializeVisibility())
70
76
 
71
77
  if (View) {
@@ -5,7 +5,6 @@ export {
5
5
  useI18n,
6
6
  useSeamlyChat,
7
7
  useSeamlyCommands,
8
- useSeamlyDispatchContext,
9
8
  useSeamlyEventStream,
10
9
  useSeamlyIdleDetachCountdown,
11
10
  useSeamlyMessageContainerClassNames,
@@ -1,6 +1,4 @@
1
1
  import { useEffect } from 'preact/hooks'
2
- import { useDispatch } from 'react-redux'
3
-
4
2
  // Import extracted hooks here for use inside this file
5
3
  import { useSeamlyApiContext } from './seamly-api-hooks'
6
4
  // Export extracted hooks here,
@@ -49,7 +47,6 @@ export {
49
47
  useGeneratedId,
50
48
  useStableCallback,
51
49
  } from './utility-hooks'
52
- export { useDispatch as useSeamlyDispatchContext }
53
50
 
54
51
  // This hook isn't used within the core
55
52
  // But it is used in implementations
@@ -0,0 +1,102 @@
1
+ import {
2
+ addEvent,
3
+ clearEvents,
4
+ setHistory,
5
+ setEventsRead,
6
+ setLoadedImageEventIds,
7
+ ackEvent,
8
+ setIsLoading,
9
+ setParticipant,
10
+ setHeaderTitle,
11
+ setHeaderSubTitle,
12
+ resetHistoryLoadedFlag,
13
+ setActiveService,
14
+ initIdleDetachCountdown,
15
+ decrementIdleDetachCountdownCounter,
16
+ stopIdleDetachCountdownCounter,
17
+ clearIdleDetachCountdown,
18
+ initResumeConversationPrompt,
19
+ clearResumeConversationPrompt,
20
+ setServiceDataItem,
21
+ setFeatures,
22
+ setFeatureEnabledState,
23
+ clearFeatures,
24
+ setInitialState,
25
+ setUserSelectedOptions,
26
+ setUserSelectedOption,
27
+ showOption,
28
+ hideOption,
29
+ setServiceEntryMetadata,
30
+ setBlockAutoEntrySwitch,
31
+ setActiveEntryType,
32
+ setUserEntryType,
33
+ registerUpload,
34
+ setUploadProgress,
35
+ setUploadComplete,
36
+ setUploadError,
37
+ clearAllUploads,
38
+ setSeamlyContainerElement,
39
+ } from 'domains/store/slice'
40
+ import {
41
+ ChannelEvent,
42
+ HistoryResponse,
43
+ ParticipantEvent,
44
+ } from 'domains/store/store.types'
45
+ import { useDispatch } from 'react-redux'
46
+
47
+ export const useSeamlyActions = () => {
48
+ const dispatch = useDispatch()
49
+
50
+ return {
51
+ addEvent: (event: ChannelEvent) => dispatch(addEvent(event)),
52
+ clearEvents: () => dispatch(clearEvents()),
53
+ setHistory: (history?: HistoryResponse) => dispatch(setHistory(history)),
54
+ setEventsRead: (payload) => dispatch(setEventsRead(payload)),
55
+ setLoadedImageEventIds: (ids: string[]) =>
56
+ dispatch(setLoadedImageEventIds(ids)),
57
+ ackEvent: (event) => dispatch(ackEvent(event)),
58
+ setIsLoading: (isLoading: boolean) => dispatch(setIsLoading(isLoading)),
59
+ setParticipant: (participant: ParticipantEvent) =>
60
+ dispatch(setParticipant(participant)),
61
+ setHeaderTitle: (title: string) => dispatch(setHeaderTitle(title)),
62
+ setHeaderSubTitle: (title: string) => dispatch(setHeaderSubTitle(title)),
63
+ resetHistoryLoadedFlag: () => dispatch(resetHistoryLoadedFlag()),
64
+ setActiveService: () => dispatch(setActiveService()),
65
+ initIdleDetachCountdown: (payload) =>
66
+ dispatch(initIdleDetachCountdown(payload)),
67
+ decrementIdleDetachCountdownCounter: () =>
68
+ dispatch(decrementIdleDetachCountdownCounter()),
69
+ stopIdleDetachCountdownCounter: () =>
70
+ dispatch(stopIdleDetachCountdownCounter()),
71
+ clearIdleDetachCountdown: () => dispatch(clearIdleDetachCountdown()),
72
+ initResumeConversationPrompt: () =>
73
+ dispatch(initResumeConversationPrompt()),
74
+ clearResumeConversationPrompt: () =>
75
+ dispatch(clearResumeConversationPrompt()),
76
+ setServiceDataItem: (payload) => dispatch(setServiceDataItem(payload)),
77
+ setFeatures: (payload) => dispatch(setFeatures(payload)),
78
+ setFeatureEnabledState: (payload) =>
79
+ dispatch(setFeatureEnabledState(payload)),
80
+ clearFeatures: () => dispatch(clearFeatures()),
81
+ setInitialState: (initialState) => dispatch(setInitialState(initialState)),
82
+ setUserSelectedOptions: (payload) =>
83
+ dispatch(setUserSelectedOptions(payload)),
84
+ setUserSelectedOption: (payload) =>
85
+ dispatch(setUserSelectedOption(payload)),
86
+ showOption: (payload) => dispatch(showOption(payload)),
87
+ hideOption: () => dispatch(hideOption()),
88
+ setServiceEntryMetadata: (payload) =>
89
+ dispatch(setServiceEntryMetadata(payload)),
90
+ setBlockAutoEntrySwitch: (payload) =>
91
+ dispatch(setBlockAutoEntrySwitch(payload)),
92
+ setActiveEntryType: (payload) => dispatch(setActiveEntryType(payload)),
93
+ setUserEntryType: (payload) => dispatch(setUserEntryType(payload)),
94
+ registerUpload: (payload) => dispatch(registerUpload(payload)),
95
+ setUploadProgress: (payload) => dispatch(setUploadProgress(payload)),
96
+ setUploadComplete: (payload) => dispatch(setUploadComplete(payload)),
97
+ setUploadError: (payload) => dispatch(setUploadError(payload)),
98
+ clearAllUploads: () => dispatch(clearAllUploads()),
99
+ setSeamlyContainerElement: (payload) =>
100
+ dispatch(setSeamlyContainerElement(payload)),
101
+ }
102
+ }
@@ -7,10 +7,11 @@ import { useDispatch, useSelector } from 'react-redux'
7
7
  import { selectShowInlineView } from '../../domains/visibility/selectors'
8
8
  import { useLiveRegion } from './live-region-hooks'
9
9
  import { useSeamlyHasConversation } from './seamly-api-hooks'
10
- import { useSeamlyLayoutMode } from './seamly-state-hooks'
10
+ import { useEvents, useSeamlyLayoutMode } from './seamly-state-hooks'
11
11
  import useSeamlyCommands from './use-seamly-commands'
12
12
 
13
13
  const useSeamlyChat = () => {
14
+ const events = useEvents()
14
15
  const { t } = useI18n()
15
16
  const { isInline, isWindow } = useSeamlyLayoutMode()
16
17
  const { isOpen, isVisible, setVisibility } = useVisibility()
@@ -63,6 +64,18 @@ const useSeamlyChat = () => {
63
64
  }
64
65
  }, [dispatch])
65
66
 
67
+ useEffect(() => {
68
+ if (events.length) {
69
+ spinnerTimeout.current = setTimeout(() => {
70
+ dispatch(setIsLoading(false))
71
+ }, 5000)
72
+ }
73
+
74
+ return () => {
75
+ clearTimeout(spinnerTimeout.current)
76
+ }
77
+ }, [events, dispatch])
78
+
66
79
  useEffect(() => {
67
80
  // This is needed to reset the ref to allow connect and start to happen again.
68
81
  // Mostly due to Interrupt situations and a reset being called.