@seamly/web-ui 19.0.0-beta.2 → 19.1.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 (69) hide show
  1. package/build/dist/lib/index.debug.js +152 -64
  2. package/build/dist/lib/index.debug.min.js +1 -1
  3. package/build/dist/lib/index.debug.min.js.LICENSE.txt +40 -8
  4. package/build/dist/lib/index.js +595 -462
  5. package/build/dist/lib/index.min.js +1 -1
  6. package/build/dist/lib/standalone.js +716 -490
  7. package/build/dist/lib/standalone.min.js +1 -1
  8. package/build/dist/lib/storage.js +8 -1
  9. package/build/dist/lib/storage.min.js +1 -1
  10. package/build/dist/lib/style-guide.js +1920 -1814
  11. package/build/dist/lib/style-guide.min.js +1 -1
  12. package/package.json +1 -1
  13. package/src/javascripts/api/errors/seamly-base-error.js +10 -0
  14. package/src/javascripts/api/errors/seamly-configuration-error.js +4 -6
  15. package/src/javascripts/api/errors/seamly-general-error.js +4 -6
  16. package/src/javascripts/api/errors/seamly-offline-error.js +4 -6
  17. package/src/javascripts/api/errors/seamly-session-expired-error.js +4 -6
  18. package/src/javascripts/api/errors/seamly-unauthorized-error.js +4 -6
  19. package/src/javascripts/api/errors/seamly-unavailable-error.js +17 -0
  20. package/src/javascripts/api/index.js +10 -11
  21. package/src/javascripts/domains/app/actions.js +33 -24
  22. package/src/javascripts/domains/app/index.js +2 -1
  23. package/src/javascripts/domains/config/reducer.js +1 -0
  24. package/src/javascripts/domains/config/selectors.js +1 -1
  25. package/src/javascripts/domains/errors/index.js +32 -0
  26. package/src/javascripts/domains/i18n/actions.js +9 -24
  27. package/src/javascripts/domains/i18n/reducer.js +15 -3
  28. package/src/javascripts/domains/i18n/utils.js +2 -7
  29. package/src/javascripts/domains/interrupt/middleware.js +12 -9
  30. package/src/javascripts/domains/interrupt/reducer.js +10 -9
  31. package/src/javascripts/domains/store/index.js +7 -2
  32. package/src/javascripts/domains/store/state-reducer.js +10 -6
  33. package/src/javascripts/domains/visibility/actions.js +73 -0
  34. package/src/javascripts/domains/visibility/constants.js +8 -0
  35. package/src/javascripts/domains/visibility/hooks.js +24 -0
  36. package/src/javascripts/domains/visibility/index.js +8 -0
  37. package/src/javascripts/domains/visibility/reducer.js +19 -0
  38. package/src/javascripts/domains/visibility/selectors.js +9 -0
  39. package/src/javascripts/domains/visibility/utils.js +42 -0
  40. package/src/javascripts/index.js +3 -12
  41. package/src/javascripts/lib/engine/index.js +1 -0
  42. package/src/javascripts/lib/redux-helpers/index.js +45 -13
  43. package/src/javascripts/lib/store/providers/session-storage.js +6 -1
  44. package/src/javascripts/style-guide/components/app.js +1 -1
  45. package/src/javascripts/style-guide/components/static-core.js +6 -1
  46. package/src/javascripts/style-guide/states.js +48 -21
  47. package/src/javascripts/ui/components/conversation/conversation.js +2 -2
  48. package/src/javascripts/ui/components/conversation/event/hooks/use-text-rendering.js +1 -8
  49. package/src/javascripts/ui/components/conversation/event/text.js +19 -13
  50. package/src/javascripts/ui/components/core/seamly-core.js +3 -0
  51. package/src/javascripts/ui/components/core/seamly-event-subscriber.js +3 -3
  52. package/src/javascripts/ui/components/core/seamly-initializer.js +2 -6
  53. package/src/javascripts/ui/components/core/seamly-instance-functions-loader.js +2 -3
  54. package/src/javascripts/ui/components/core/seamly-new-notifications.js +2 -2
  55. package/src/javascripts/ui/components/core/seamly-read-state.js +2 -2
  56. package/src/javascripts/ui/components/entry/toggle-button.js +2 -2
  57. package/src/javascripts/ui/components/layout/agent-info.js +2 -2
  58. package/src/javascripts/ui/components/layout/app-frame.js +2 -3
  59. package/src/javascripts/ui/components/layout/chat-frame.js +2 -2
  60. package/src/javascripts/ui/components/layout/modal-wrapper.js +3 -6
  61. package/src/javascripts/ui/components/layout/view.js +3 -6
  62. package/src/javascripts/ui/hooks/seamly-hooks.js +0 -2
  63. package/src/javascripts/ui/hooks/use-seamly-chat.js +3 -5
  64. package/src/javascripts/ui/hooks/use-seamly-commands.js +7 -29
  65. package/src/javascripts/ui/hooks/use-seamly-idle-detach-countdown.js +2 -2
  66. package/src/javascripts/ui/utils/general-utils.js +0 -9
  67. package/src/javascripts/ui/utils/seamly-utils.js +0 -66
  68. package/src/javascripts/ui/hooks/use-seamly-stored-visibility.js +0 -31
  69. package/src/javascripts/ui/hooks/use-seamly-visibility.js +0 -98
@@ -195,7 +195,6 @@ export class API {
195
195
  // cookies in CORS requests.
196
196
  .withCredentials()
197
197
  .send({
198
- authorizationRequired: true,
199
198
  externalId: this.externalId || undefined,
200
199
  })
201
200
 
@@ -211,7 +210,7 @@ export class API {
211
210
  return initialState
212
211
  } catch (error) {
213
212
  if (error.status >= 500) {
214
- throw new SeamlyGeneralError()
213
+ throw new SeamlyGeneralError(error)
215
214
  }
216
215
  throw error
217
216
  }
@@ -260,13 +259,13 @@ export class API {
260
259
  }
261
260
  } catch (error) {
262
261
  if (error.status === 401) {
263
- throw new SeamlyUnauthorizedError()
262
+ throw new SeamlyUnauthorizedError(error)
264
263
  }
265
264
  if (error.status === 404) {
266
- throw new SeamlySessionExpiredError()
265
+ throw new SeamlySessionExpiredError(error)
267
266
  }
268
267
  if (error.status >= 500) {
269
- throw new SeamlyGeneralError()
268
+ throw new SeamlyGeneralError(error)
270
269
  }
271
270
  throw error
272
271
  }
@@ -353,10 +352,10 @@ export class API {
353
352
  })
354
353
  .catch((error) => {
355
354
  if (error.status === 404) {
356
- throw new SeamlyConfigurationError()
355
+ throw new SeamlyConfigurationError(error)
357
356
  }
358
357
  if (error.status >= 500) {
359
- throw new SeamlyGeneralError()
358
+ throw new SeamlyGeneralError(error)
360
359
  }
361
360
  throw error
362
361
  })
@@ -373,13 +372,13 @@ export class API {
373
372
  })
374
373
  .catch((error) => {
375
374
  if (error.status === 401) {
376
- throw new SeamlyUnauthorizedError()
375
+ throw new SeamlyUnauthorizedError(error)
377
376
  }
378
377
  if (error.status === 404) {
379
- throw new SeamlySessionExpiredError()
378
+ throw new SeamlySessionExpiredError(error)
380
379
  }
381
380
  if (error.status >= 500) {
382
- throw new SeamlyGeneralError()
381
+ throw new SeamlyGeneralError(error)
383
382
  }
384
383
  throw error
385
384
  })
@@ -402,7 +401,7 @@ export class API {
402
401
  return body.translations
403
402
  } catch (error) {
404
403
  if (error.status >= 500) {
405
- throw new SeamlyGeneralError()
404
+ throw new SeamlyGeneralError(error)
406
405
  }
407
406
  throw error
408
407
  }
@@ -1,8 +1,10 @@
1
1
  import { seamlyActions } from '../../ui/utils/seamly-utils'
2
2
  import { Actions as ConfigActions } from '../config'
3
- import { Actions as InterruptActions } from '../interrupt'
4
3
  import { Actions as I18nActions } from '../i18n'
4
+ import * as VisibilityActions from '../visibility/actions'
5
5
  import { createAction, createThunk } from './utils'
6
+ import SeamlyUnavailableError from '../../api/errors/seamly-unavailable-error'
7
+ import SeamlySessionExpiredError from '../../api/errors/seamly-session-expired-error'
6
8
 
7
9
  export const setHasResponded = createAction(
8
10
  'setHasResponded',
@@ -11,33 +13,40 @@ export const setHasResponded = createAction(
11
13
 
12
14
  export const initialize = createThunk(
13
15
  'initialize',
14
- (config) =>
15
- async (dispatch, getState, { api }) => {
16
- try {
17
- dispatch(ConfigActions.initialize(config))
16
+ async (config, { dispatch, extra: { api } }) => {
17
+ dispatch(ConfigActions.initialize(config))
18
+ let locale = config?.context?.locale
18
19
 
19
- const { features, defaultLocale } = await api.getConfig()
20
- dispatch({ type: seamlyActions.SET_FEATURES, features })
20
+ try {
21
+ const { features, defaultLocale } = await api.getConfig()
22
+ dispatch({ type: seamlyActions.SET_FEATURES, features })
23
+ locale = locale || defaultLocale
24
+ dispatch(I18nActions.setInitialLocale(locale))
25
+ } catch (e) {
26
+ throw new SeamlyUnavailableError()
27
+ }
21
28
 
22
- let locale = config?.context?.locale || defaultLocale
23
- dispatch(I18nActions.setInitialLocale(locale))
24
- try {
25
- if (api.hasConversation()) {
26
- const initialState = await api.getConversationIntitialState()
27
- dispatch({ type: seamlyActions.SET_INITIAL_STATE, initialState })
29
+ try {
30
+ if (api.hasConversation()) {
31
+ const initialState = await api.getConversationIntitialState()
32
+ dispatch({ type: seamlyActions.SET_INITIAL_STATE, initialState })
28
33
 
29
- locale = initialState.translation?.locale || locale
34
+ locale = initialState.translation?.locale || locale
30
35
 
31
- if ('userResponded' in initialState) {
32
- dispatch(setHasResponded(initialState.userResponded))
33
- }
34
- }
35
- } catch (error) {
36
- dispatch(InterruptActions.set(error))
36
+ if ('userResponded' in initialState) {
37
+ dispatch(setHasResponded(initialState.userResponded))
37
38
  }
38
- await dispatch(I18nActions.setLocale(locale))
39
- } catch (error) {
40
- dispatch(InterruptActions.set(error))
41
39
  }
42
- },
40
+ } catch (e) {
41
+ if (e instanceof SeamlySessionExpiredError) {
42
+ throw e
43
+ }
44
+ throw new SeamlyUnavailableError()
45
+ } finally {
46
+ await dispatch(I18nActions.setLocale(locale))
47
+ dispatch(VisibilityActions.initialize())
48
+ }
49
+ },
43
50
  )
51
+
52
+ export const reset = createAction('reset', () => {})
@@ -1,6 +1,7 @@
1
1
  import * as Actions from './actions'
2
+ import * as Selectors from './selectors'
2
3
 
3
4
  export * from './hooks'
4
5
  export { default as Reducer } from './reducer'
5
6
 
6
- export { Actions }
7
+ export { Actions, Selectors }
@@ -27,6 +27,7 @@ const configKeys = [
27
27
  'messages',
28
28
  'visible',
29
29
  'visibilityCallback',
30
+ 'errorCallback',
30
31
  ]
31
32
  const updateState = (state, { config }) => {
32
33
  const { messages, ...partialConfig } = pick(config, configKeys)
@@ -1,5 +1,5 @@
1
1
  import { createSelector } from 'reselect'
2
- import { visibilityStates } from '../../ui/utils/seamly-utils'
2
+ import { visibilityStates } from '../visibility/constants'
3
3
  import { selectState } from './utils'
4
4
 
5
5
  export const selectConfig = createSelector(selectState, (config) => {
@@ -0,0 +1,32 @@
1
+ import { createDomain } from '../../lib/redux-helpers'
2
+ import { Selectors as ConfigSelectors } from '../config'
3
+
4
+ const { createAction } = createDomain('errors')
5
+
6
+ export const catchError = createAction('catch-error', (error) => ({ error }))
7
+
8
+ export function createMiddleware({ api: seamlyApi }) {
9
+ return ({ getState }) => {
10
+ const handleError = (error) => {
11
+ const { errorCallback, namespace, api, layoutMode } =
12
+ ConfigSelectors.selectConfig(getState())
13
+ errorCallback?.(error, {
14
+ namespace,
15
+ api,
16
+ layoutMode,
17
+ conversationUrl: seamlyApi.getConversationUrl(),
18
+ })
19
+ }
20
+ return (next) => (action) => {
21
+ try {
22
+ if (action.error) {
23
+ handleError(action.error)
24
+ }
25
+ return next(action)
26
+ } catch (error) {
27
+ handleError(error)
28
+ throw error
29
+ }
30
+ }
31
+ }
32
+ }
@@ -1,35 +1,20 @@
1
1
  import createMutex from '../../lib/mutex'
2
2
  import { selectLocale } from './selectors'
3
- import { createAction, createActions, createThunk } from './utils'
3
+ import { createAction, createThunk } from './utils'
4
4
 
5
5
  export const setInitialLocale = createAction('setInitialLocale', (locale) => ({
6
6
  locale,
7
7
  }))
8
8
 
9
- export const [setLocaleStart, setLocaleResolve, setLocaleReject] =
10
- createActions('setLocale', {
11
- start: (locale) => ({ locale }),
12
- resolve: (locale, translations) => ({ locale, translations }),
13
- reject: (locale, error) => ({ locale, error }),
14
- })
15
-
16
9
  const mutex = createMutex()
17
10
  export const setLocale = createThunk(
18
11
  'setLocale',
19
- (locale) =>
20
- async (dispatch, getState, { api }) => {
21
- await mutex.runExclusively(async () => {
22
- const currentLocale = selectLocale(getState())
23
- if (currentLocale === locale) {
24
- return
25
- }
26
- dispatch(setLocaleStart(locale))
27
- try {
28
- const translations = await api.getTranslations(locale)
29
- dispatch(setLocaleResolve(locale, translations))
30
- } catch (error) {
31
- dispatch(setLocaleReject(locale, error))
32
- }
33
- })
34
- },
12
+ async (locale, { getState, extra: { api } }) => {
13
+ return mutex.runExclusively(() => {
14
+ if (locale === selectLocale(getState())) {
15
+ return undefined
16
+ }
17
+ return api.getTranslations(locale)
18
+ })
19
+ },
35
20
  )
@@ -18,6 +18,12 @@ const defaultState = {
18
18
  'errors.seamlyOffline.srText':
19
19
  'The chat has connection issues. There might be a problem with your or our network connection. The chat session should resume as soon as the connection is available again.',
20
20
  'errors.seamlyOffline.title': 'Connection issues',
21
+ 'errors.seamlyUnavailable.buttonText': 'Try again',
22
+ 'errors.seamlyUnavailable.message':
23
+ 'The server could not be reached. Try again in a little while.',
24
+ 'errors.seamlyUnavailable.srText':
25
+ 'The chat server could not be reached. Try again in a little while.',
26
+ 'errors.seamlyUnavailable.title': 'Server unavailable',
21
27
  },
22
28
  isLoading: false,
23
29
  initialLocale: undefined,
@@ -29,11 +35,17 @@ export default createReducer(
29
35
  ...state,
30
36
  initialLocale: locale,
31
37
  }),
32
- [Actions.setLocaleStart]: (state) => ({
38
+ [Actions.setLocale.pending]: (state) => ({
33
39
  ...state,
34
40
  isLoading: true,
35
41
  }),
36
- [Actions.setLocaleResolve]: (state, { locale, translations }) => {
42
+ [Actions.setLocale.fulfilled]: (
43
+ state,
44
+ { payload: translations, meta: { arg: locale } },
45
+ ) => {
46
+ if (!translations) {
47
+ return { ...state, isLoading: false }
48
+ }
37
49
  return {
38
50
  ...state,
39
51
  isLoading: false,
@@ -49,7 +61,7 @@ export default createReducer(
49
61
  ),
50
62
  }
51
63
  },
52
- [Actions.setLocaleReject]: (state) => ({
64
+ [Actions.setLocale.rejected]: (state) => ({
53
65
  ...state,
54
66
  isLoading: false,
55
67
  }),
@@ -1,9 +1,4 @@
1
1
  import { createDomain } from '../../lib/redux-helpers'
2
2
 
3
- export const {
4
- createAction,
5
- createActions,
6
- createThunk,
7
- createReducer,
8
- selectState,
9
- } = createDomain('i18n')
3
+ export const { createAction, createThunk, createReducer, selectState } =
4
+ createDomain('i18n')
@@ -3,7 +3,7 @@ import SeamlyConfigurationError from '../../api/errors/seamly-configuration-erro
3
3
  import SeamlySessionExpiredError from '../../api/errors/seamly-session-expired-error'
4
4
  import SeamlyOfflineError from '../../api/errors/seamly-offline-error'
5
5
  import SeamlyUnauthorizedError from '../../api/errors/seamly-unauthorized-error'
6
- import * as Actions from './actions'
6
+ import SeamlyUnavailableError from '../../api/errors/seamly-unavailable-error'
7
7
 
8
8
  const handledErrorTypes = [
9
9
  SeamlyGeneralError,
@@ -11,17 +11,20 @@ const handledErrorTypes = [
11
11
  SeamlySessionExpiredError,
12
12
  SeamlyOfflineError,
13
13
  SeamlyUnauthorizedError,
14
+ SeamlyUnavailableError,
14
15
  ]
15
16
 
16
- export default function createMiddleware() {
17
+ export default function createMiddleware({ api }) {
17
18
  return () => (next) => (action) => {
18
- if (action.type === String(Actions.set)) {
19
- if (
20
- !handledErrorTypes.some(
21
- (ErrorType) => action.error instanceof ErrorType,
22
- )
23
- ) {
24
- throw action.error
19
+ const { error } = action
20
+ if (error) {
21
+ if (!handledErrorTypes.some((ErrorType) => error instanceof ErrorType)) {
22
+ throw error
23
+ } else if (error.action === 'reset') {
24
+ // [SMLY-942] We clear the store before a reset to force a new conversation if the page is refreshed before the conversation is reset
25
+ api.disconnect().then(() => {
26
+ api.clearStore()
27
+ })
25
28
  }
26
29
  }
27
30
 
@@ -1,21 +1,22 @@
1
1
  import { createReducer } from './utils'
2
2
  import * as Actions from './actions'
3
+ import { Actions as AppActions } from '../app'
3
4
 
4
5
  const initialState = {
5
6
  error: undefined,
6
7
  }
7
8
 
9
+ const handleError = (state, { error }) => ({
10
+ ...state,
11
+ error,
12
+ })
13
+
8
14
  export default createReducer(
9
15
  {
10
- [Actions.set]: (state, { error }) => {
11
- return {
12
- ...state,
13
- error,
14
- }
15
- },
16
- [Actions.clear]: () => {
17
- return initialState
18
- },
16
+ [Actions.set]: handleError,
17
+ [AppActions.initialize.rejected]: handleError,
18
+ [Actions.clear]: () => initialState,
19
+ [AppActions.reset]: () => initialState,
19
20
  },
20
21
  initialState,
21
22
  )
@@ -11,14 +11,16 @@ import {
11
11
  createMiddleware as createI18nMiddleware,
12
12
  } from '../translations'
13
13
  import { Reducer as i18nReducer } from '../i18n'
14
+ import { Reducer as visibilityReducer } from '../visibility'
14
15
  import {
15
16
  Reducer as interruptReducer,
16
17
  createMiddleware as createInterruptMiddleware,
17
18
  } from '../interrupt'
18
19
  import { createMiddleware as createOptionsMiddleware } from '../options'
20
+ import { createMiddleware as createErrorsMiddleware } from '../errors'
19
21
  import stateReducer from './state-reducer'
20
22
 
21
- export function createStore({ initialState, api } = {}) {
23
+ export function createStore({ initialState, api, eventBus } = {}) {
22
24
  const store = createReduxStore({
23
25
  reducers: {
24
26
  state: stateReducer,
@@ -28,14 +30,17 @@ export function createStore({ initialState, api } = {}) {
28
30
  [String(translationsReducer)]: translationsReducer,
29
31
  [String(i18nReducer)]: i18nReducer,
30
32
  [String(interruptReducer)]: interruptReducer,
33
+ [String(visibilityReducer)]: visibilityReducer,
31
34
  },
32
35
  initialState,
33
36
  middlewares: [
37
+ createErrorsMiddleware({ api }),
34
38
  thunkMiddleware.withExtraArgument({
35
39
  api,
40
+ eventBus,
36
41
  }),
37
42
  createConfigMiddleware(),
38
- createInterruptMiddleware(),
43
+ createInterruptMiddleware({ api }),
39
44
  createOptionsMiddleware({ api }),
40
45
  createI18nMiddleware(),
41
46
  ],
@@ -1,11 +1,8 @@
1
1
  // Legacy state reducer. Do not add new features here but extract/create new reducers as needed
2
2
 
3
3
  import { randomId } from '../../lib/id'
4
- import {
5
- entryTypes,
6
- seamlyStateReducer,
7
- visibilityStates,
8
- } from '../../ui/utils/seamly-utils'
4
+ import { entryTypes, seamlyStateReducer } from '../../ui/utils/seamly-utils'
5
+ import { Actions as AppActions } from '../app'
9
6
 
10
7
  const initialState = {
11
8
  events: [],
@@ -14,7 +11,6 @@ const initialState = {
14
11
  isLoading: false,
15
12
  idleDetachCountdown: { hasCountdown: false, isActive: false },
16
13
  resumeConversationPrompt: false,
17
- visible: visibilityStates.hidden,
18
14
  serviceInfo: {
19
15
  activeServiceSessionId: '',
20
16
  },
@@ -52,5 +48,13 @@ const initialState = {
52
48
  }
53
49
 
54
50
  export default function stateReducer(state = initialState, action) {
51
+ if (action.type === String(AppActions.reset)) {
52
+ const { visible } = state
53
+ return {
54
+ ...initialState,
55
+ visible,
56
+ }
57
+ }
58
+
55
59
  return seamlyStateReducer(state, action)
56
60
  }
@@ -0,0 +1,73 @@
1
+ import { Selectors as ConfigSelectors } from '../config'
2
+ import * as AppSelectors from '../app/selectors'
3
+
4
+ import { visibilityStates, StoreKey } from './constants'
5
+ import { createAction, createThunk, calculateVisibility } from './utils'
6
+ import * as Selectors from './selectors'
7
+ import { selectState } from '../../ui/hooks/seamly-state-hooks'
8
+
9
+ export const setFromStorage = createAction('setFromStorage', (visibility) => ({
10
+ visibility,
11
+ }))
12
+
13
+ const validVisibilityStates = [
14
+ visibilityStates.open,
15
+ visibilityStates.minimized,
16
+ visibilityStates.hidden,
17
+ ]
18
+ export const setVisibility = createThunk(
19
+ 'set',
20
+ (requestedVisibility, { getState, extra: { api, eventBus } }) => {
21
+ const state = getState()
22
+ const previousVisibility = Selectors.selectVisibility(state)
23
+ const hasResponded = AppSelectors.selectUserHasResponded(state)
24
+ const hasConversation = api.hasConversation()
25
+ const config = ConfigSelectors.selectConfig(state)
26
+ const { visibilityCallback = calculateVisibility, layoutMode } = config
27
+ const { unreadEvents: unreadMessageCount } = selectState(state)
28
+
29
+ const calculatedVisibility = visibilityCallback({
30
+ hasConversation,
31
+ hasResponded,
32
+ previousVisibility,
33
+ requestedVisibility,
34
+ config,
35
+ })
36
+ if (!validVisibilityStates.includes(calculatedVisibility)) {
37
+ console.error(
38
+ 'The visibilityCallback function should return "open", "minimized" or "hidden".',
39
+ )
40
+ return undefined
41
+ }
42
+ if (previousVisibility === calculatedVisibility) {
43
+ return undefined
44
+ }
45
+ // Store the user-requested visibility in order to reinitialize after refresh
46
+ api.store.set(StoreKey, {
47
+ ...(api.store.get(StoreKey) || {}),
48
+ [layoutMode]: requestedVisibility,
49
+ })
50
+ if (requestedVisibility) {
51
+ eventBus.emit('ui.visible', requestedVisibility, {
52
+ visibility: requestedVisibility,
53
+ hasConversation,
54
+ hasResponded,
55
+ unreadMessageCount,
56
+ })
57
+ }
58
+ return calculatedVisibility
59
+ },
60
+ )
61
+
62
+ export const initialize = createThunk(
63
+ 'initialize',
64
+ async (locale, { dispatch, getState, extra: { api } }) => {
65
+ // initialize stored visibility
66
+ const { layoutMode } = ConfigSelectors.selectConfig(getState())
67
+ const storedVisibility = api.store.get(StoreKey)?.[layoutMode]
68
+ if (storedVisibility) {
69
+ dispatch(setFromStorage(storedVisibility))
70
+ }
71
+ dispatch(setVisibility(visibilityStates.initialize))
72
+ },
73
+ )
@@ -0,0 +1,8 @@
1
+ export const StoreKey = 'visibility'
2
+
3
+ export const visibilityStates = {
4
+ hidden: 'hidden',
5
+ minimized: 'minimized',
6
+ open: 'open',
7
+ initialize: null,
8
+ }
@@ -0,0 +1,24 @@
1
+ import { useCallback } from 'preact/hooks'
2
+ import { useConfig } from '../config'
3
+ import { useSelector, useStoreDispatch } from '../redux'
4
+ import * as Actions from './actions'
5
+ import * as Selectors from './selectors'
6
+ import { visibilityStates } from './constants'
7
+
8
+ export const useVisibility = () => {
9
+ const dispatch = useStoreDispatch()
10
+ const visible = useSelector(Selectors.selectVisibility)
11
+ const isVisible = visible ? visible !== visibilityStates.hidden : false
12
+ const { layoutMode } = useConfig()
13
+ const isOpen =
14
+ visible && layoutMode
15
+ ? visible === visibilityStates.open ||
16
+ (layoutMode === 'inline' && visible !== visibilityStates.hidden)
17
+ : false
18
+ const setVisibility = useCallback(
19
+ (visibility) => dispatch(Actions.setVisibility(visibility)),
20
+ [dispatch],
21
+ )
22
+
23
+ return { isVisible, isOpen, visible, setVisibility }
24
+ }
@@ -0,0 +1,8 @@
1
+ import * as Actions from './actions'
2
+ import * as Selectors from './selectors'
3
+
4
+ export * from './hooks'
5
+ export { visibilityStates } from './constants'
6
+ export { default as Reducer } from './reducer'
7
+ export { calculateVisibility } from './utils'
8
+ export { Actions, Selectors }
@@ -0,0 +1,19 @@
1
+ import { createReducer } from './utils'
2
+ import * as Actions from './actions'
3
+ import { visibilityStates } from './constants'
4
+
5
+ const initialState = {
6
+ visibility: visibilityStates.initialize,
7
+ }
8
+
9
+ export default createReducer(
10
+ {
11
+ [Actions.setFromStorage]: (state, { visibility }) => ({
12
+ ...state,
13
+ visibility,
14
+ }),
15
+ [Actions.setVisibility.fulfilled]: (state, { payload: visibility }) =>
16
+ visibility ? { ...state, visibility } : state,
17
+ },
18
+ initialState,
19
+ )
@@ -0,0 +1,9 @@
1
+ import { createSelector } from 'reselect'
2
+ import { selectState } from './utils'
3
+
4
+ export const selectVisibility = createSelector(
5
+ selectState,
6
+ (state) => state.visibility,
7
+ )
8
+
9
+ export { selectState }
@@ -0,0 +1,42 @@
1
+ import { createDomain } from '../../lib/redux-helpers'
2
+ import { visibilityStates } from './constants'
3
+
4
+ export const {
5
+ createAction,
6
+ createActions,
7
+ createThunk,
8
+ createReducer,
9
+ selectState,
10
+ } = createDomain('visibility')
11
+
12
+ export const calculateVisibility = ({
13
+ hasResponded,
14
+ previousVisibility,
15
+ requestedVisibility,
16
+ config,
17
+ }) => {
18
+ const { defaults, layoutMode, hideOnNoUserResponse } = config
19
+
20
+ const { visible: defaultVisibility } = defaults || {}
21
+
22
+ // Requesting open should override the responded check.
23
+ if (
24
+ layoutMode === 'window' &&
25
+ hideOnNoUserResponse &&
26
+ requestedVisibility !== visibilityStates.open
27
+ ) {
28
+ return hasResponded
29
+ ? requestedVisibility || previousVisibility || visibilityStates.open
30
+ : visibilityStates.hidden
31
+ }
32
+
33
+ const baseVisibility =
34
+ layoutMode === 'inline' ? visibilityStates.open : visibilityStates.minimized
35
+
36
+ return (
37
+ requestedVisibility ||
38
+ previousVisibility ||
39
+ defaultVisibility ||
40
+ baseVisibility
41
+ )
42
+ }