@seamly/web-ui 22.2.0 → 22.3.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 (47) hide show
  1. package/build/dist/lib/components.js +269 -138
  2. package/build/dist/lib/components.js.map +1 -1
  3. package/build/dist/lib/components.min.js +1 -1
  4. package/build/dist/lib/components.min.js.map +1 -1
  5. package/build/dist/lib/hooks.js +217 -41
  6. package/build/dist/lib/hooks.js.map +1 -1
  7. package/build/dist/lib/hooks.min.js +1 -1
  8. package/build/dist/lib/hooks.min.js.map +1 -1
  9. package/build/dist/lib/index.debug.js +43 -21
  10. package/build/dist/lib/index.debug.js.map +1 -1
  11. package/build/dist/lib/index.debug.min.js +1 -1
  12. package/build/dist/lib/index.debug.min.js.LICENSE.txt +12 -4
  13. package/build/dist/lib/index.debug.min.js.map +1 -1
  14. package/build/dist/lib/index.js +257 -133
  15. package/build/dist/lib/index.js.map +1 -1
  16. package/build/dist/lib/index.min.js +1 -1
  17. package/build/dist/lib/index.min.js.map +1 -1
  18. package/build/dist/lib/standalone.js +265 -133
  19. package/build/dist/lib/standalone.js.map +1 -1
  20. package/build/dist/lib/standalone.min.js +1 -1
  21. package/build/dist/lib/standalone.min.js.map +1 -1
  22. package/build/dist/lib/style-guide.js +274 -134
  23. package/build/dist/lib/style-guide.js.map +1 -1
  24. package/build/dist/lib/style-guide.min.js +1 -1
  25. package/build/dist/lib/style-guide.min.js.map +1 -1
  26. package/build/dist/lib/styles.css +1 -1
  27. package/build/dist/lib/utils.js +325 -171
  28. package/build/dist/lib/utils.js.map +1 -1
  29. package/build/dist/lib/utils.min.js +1 -1
  30. package/build/dist/lib/utils.min.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/javascripts/api/errors/seamly-api-error.ts +0 -1
  33. package/src/javascripts/api/index.ts +16 -8
  34. package/src/javascripts/domains/app/actions.ts +8 -3
  35. package/src/javascripts/domains/interrupt/selectors.ts +3 -2
  36. package/src/javascripts/domains/interrupt/slice.ts +2 -0
  37. package/src/javascripts/domains/redux/create-debounced-async-thunk.ts +109 -0
  38. package/src/javascripts/domains/redux/redux.types.ts +2 -1
  39. package/src/javascripts/domains/store/actions.ts +38 -0
  40. package/src/javascripts/domains/visibility/actions.ts +4 -1
  41. package/src/javascripts/style-guide/states.js +18 -1
  42. package/src/javascripts/ui/components/conversation/event/{card-component.js → card-component.tsx} +6 -4
  43. package/src/javascripts/ui/components/conversation/event/event-participant.js +1 -1
  44. package/src/javascripts/ui/components/core/seamly-event-subscriber.ts +14 -30
  45. package/src/javascripts/ui/components/view/window-view/window-open-button.js +8 -3
  46. package/src/javascripts/ui/hooks/use-session-expired-command.ts +31 -2
  47. package/src/stylesheets/5-components/_message-count.scss +11 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seamly/web-ui",
3
- "version": "22.2.0",
3
+ "version": "22.3.0-beta.1",
4
4
  "main": "build/dist/lib/index.js",
5
5
  "types": "build/src/javascripts/index.d.ts",
6
6
  "module": "",
@@ -1,4 +1,3 @@
1
- // @ts-ignore
2
1
  // eslint-disable-next-line no-undef
3
2
  interface ApiErrorOptions extends ErrorOptions {
4
3
  status?: number
@@ -128,7 +128,7 @@ export class API {
128
128
  }
129
129
 
130
130
  store: {
131
- get(_key: string): string
131
+ get(_key: string): string | object
132
132
  set(_key: string, _value: unknown): string
133
133
  delete(_key: string): string
134
134
  }
@@ -194,7 +194,8 @@ export class API {
194
194
  }
195
195
 
196
196
  #getAccessToken(): string {
197
- return this.store.get('accessToken')
197
+ const accessToken = this.store.get('accessToken') as string
198
+ return accessToken
198
199
  }
199
200
 
200
201
  #setAccessToken(accessToken: string) {
@@ -202,7 +203,8 @@ export class API {
202
203
  }
203
204
 
204
205
  getConversationUrl(): string {
205
- return this.store.get('conversationUrl')
206
+ const conversationUrl = this.store.get('conversationUrl') as string
207
+ return conversationUrl
206
208
  }
207
209
 
208
210
  #setConversationUrl(url: { href?: string }) {
@@ -214,9 +216,11 @@ export class API {
214
216
  }
215
217
 
216
218
  #getChannelTopic() {
219
+ const channelTopic = (this.store.get('channelTopic') ||
220
+ this.store.get('channelName')) as string
217
221
  // The `channelName` fallback is needed for seamless client upgrades.
218
222
  // TODO: Remove when all clients have been upgraded past v20.
219
- return this.store.get('channelTopic') || this.store.get('channelName')
223
+ return channelTopic
220
224
  }
221
225
 
222
226
  #setChannelTopic(topic: string) {
@@ -417,7 +421,7 @@ export class API {
417
421
  throw new SeamlyGeneralError(error)
418
422
  }
419
423
 
420
- throw error
424
+ throw new ApiError(error)
421
425
  }
422
426
  }
423
427
 
@@ -503,7 +507,11 @@ export class API {
503
507
  return xhr
504
508
  }
505
509
 
506
- async getConversationIntitialState() {
510
+ async getConversationIntitialState(): Promise<
511
+ InitialConversation & {
512
+ userResponded: boolean
513
+ }
514
+ > {
507
515
  try {
508
516
  const response = await fetchApi(
509
517
  `${this.#getUrlPrefix('http')}${this.getConversationUrl()}`,
@@ -519,7 +527,7 @@ export class API {
519
527
 
520
528
  this.#updateUrls(body)
521
529
  this.userResponded = body.conversation.userResponded
522
- return omit(body.conversation, ['accessToken', 'channelTopic'])
530
+ return omit(body.conversation, ['accessToken', 'channelTopic']) as any
523
531
  } catch (error) {
524
532
  if (error.status === 401) {
525
533
  throw new SeamlyUnauthorizedError(error)
@@ -554,7 +562,7 @@ export class API {
554
562
  throw new SeamlyGeneralError(error)
555
563
  }
556
564
 
557
- throw error
565
+ throw new ApiError(error)
558
566
  }
559
567
  }
560
568
 
@@ -5,6 +5,7 @@ import SeamlyUnavailableError from 'api/errors/seamly-unavailable-error'
5
5
  import { actionTypes } from 'ui/utils/seamly-utils'
6
6
  import { initializeConfig, resetConfig } from 'domains/config/actions'
7
7
  import { setLocale } from 'domains/i18n/actions'
8
+ import createDebouncedAsyncThunk from 'domains/redux/create-debounced-async-thunk'
8
9
  import { ThunkAPI } from 'domains/redux/redux.types'
9
10
  import type { RootState } from 'domains/store'
10
11
  import { initializeVisibility } from 'domains/visibility/actions'
@@ -73,11 +74,11 @@ export const initializeApp = createAsyncThunk<
73
74
  }
74
75
  })
75
76
 
76
- export const resetApp = createAsyncThunk<unknown, void, ThunkAPI>(
77
+ export const resetApp = createDebouncedAsyncThunk<unknown, void, ThunkAPI>(
77
78
  'resetApp',
78
79
  async (_, { dispatch, extra: { api } }) => {
79
80
  await api.disconnect()
80
- await api.clearStore()
81
+ api.clearStore()
81
82
 
82
83
  dispatch(resetConfig())
83
84
  await dispatch(initializeConfig())
@@ -85,10 +86,14 @@ export const resetApp = createAsyncThunk<unknown, void, ThunkAPI>(
85
86
  try {
86
87
  const { locale } = await dispatch(initializeApp()).unwrap()
87
88
  await dispatch(setLocale(locale))
88
- } catch (rejectedValueOrSerializedError) {
89
+ } catch (e) {
89
90
  // nothing to do
90
91
  }
91
92
 
92
93
  dispatch(initializeVisibility())
93
94
  },
95
+ {
96
+ wait: 2000,
97
+ leading: true,
98
+ },
94
99
  )
@@ -1,8 +1,9 @@
1
1
  import { createSelector } from '@reduxjs/toolkit'
2
+ import { RootState } from 'domains/store'
2
3
 
3
4
  export const selectError = createSelector(
4
- ({ interrupt }) => interrupt,
5
- ({ error }) => error,
5
+ ({ interrupt }: RootState) => interrupt,
6
+ ({ error }: RootState['interrupt']) => error,
6
7
  )
7
8
 
8
9
  export const selectHasError = createSelector(selectError, (error) =>
@@ -2,6 +2,7 @@ import { createSlice, isAnyOf } from '@reduxjs/toolkit'
2
2
  import { initializeApp } from 'domains/app/actions'
3
3
  import { initializeConfig } from 'domains/config/actions'
4
4
  import { setLocale } from 'domains/i18n/actions'
5
+ import { getConversation } from 'domains/store/actions'
5
6
  import { initializeVisibility, setVisibility } from 'domains/visibility/actions'
6
7
 
7
8
  const initialState = {
@@ -27,6 +28,7 @@ export const interruptSlice = createSlice({
27
28
  setLocale.rejected,
28
29
  setVisibility.rejected,
29
30
  initializeVisibility.rejected,
31
+ getConversation.rejected,
30
32
  ),
31
33
  (state, { payload }) => {
32
34
  state.error = payload
@@ -0,0 +1,109 @@
1
+ import {
2
+ AsyncThunk,
3
+ AsyncThunkPayloadCreator,
4
+ Dispatch,
5
+ createAsyncThunk,
6
+ } from '@reduxjs/toolkit'
7
+
8
+ type DebounceOptionsType = {
9
+ /**
10
+ * The number of milliseconds to delay
11
+ * @defaultValue `300`
12
+ */
13
+ wait: number
14
+
15
+ /**
16
+ * The maximum time `payloadCreator` is allowed to be delayed before
17
+ * it's invoked.
18
+ * @defaultValue `0`
19
+ */
20
+ maxWait?: number
21
+
22
+ /**
23
+ * Specify invoking on the leading edge of the timeout.
24
+ * @defaultValue `false`
25
+ */
26
+ leading?: boolean
27
+ }
28
+
29
+ type AsyncThunkConfig = {
30
+ state?: unknown
31
+ dispatch?: Dispatch
32
+ extra?: unknown
33
+ rejectValue?: unknown
34
+ serializedErrorType?: unknown
35
+ pendingMeta?: unknown
36
+ fulfilledMeta?: unknown
37
+ rejectedMeta?: unknown
38
+ }
39
+
40
+ /**
41
+ * A debounced analogue of the `createAsyncThunk` from `@reduxjs/toolkit`
42
+ * @param typePrefix - a string action type value
43
+ * @param payloadCreator - a callback function that should return a promise containing the result of some asynchronous logic
44
+ * @param debounceOptions - the debounce options object
45
+ */
46
+ const createDebouncedAsyncThunk = <
47
+ Returned,
48
+ ThunkArg,
49
+ ThunkApiConfig extends AsyncThunkConfig = {},
50
+ >(
51
+ typePrefix: string,
52
+ payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>,
53
+ debounceOptions?: DebounceOptionsType,
54
+ ): AsyncThunk<Returned, ThunkArg, ThunkApiConfig> => {
55
+ const { wait = 300, maxWait = 0, leading = false } = debounceOptions ?? {}
56
+
57
+ let debounceTimer: ReturnType<typeof setTimeout> = null
58
+ let maxWaitTimer: ReturnType<typeof setTimeout> = null
59
+ let resolve: ((_value: boolean) => void) | undefined
60
+
61
+ const cancel = (): void => {
62
+ if (resolve) {
63
+ resolve(false)
64
+ resolve = undefined
65
+ }
66
+ }
67
+
68
+ const invoke = (): void => {
69
+ clearTimeout(maxWaitTimer)
70
+ maxWaitTimer = undefined
71
+
72
+ if (resolve) {
73
+ resolve(true)
74
+ resolve = undefined
75
+ }
76
+ }
77
+
78
+ const debounceExecutionCondition = (): Promise<boolean> | boolean => {
79
+ const immediate = leading && !debounceTimer
80
+
81
+ // Start debounced condition resolution
82
+ clearTimeout(debounceTimer)
83
+ debounceTimer = setTimeout(() => {
84
+ invoke()
85
+ debounceTimer = null
86
+ }, wait)
87
+
88
+ if (immediate) {
89
+ return true
90
+ }
91
+
92
+ cancel()
93
+
94
+ // Start max wait condition resolution
95
+ if (maxWait && !maxWaitTimer) {
96
+ maxWaitTimer = setTimeout(invoke, maxWait)
97
+ }
98
+
99
+ return new Promise((res) => {
100
+ resolve = res
101
+ })
102
+ }
103
+
104
+ return createAsyncThunk<Returned, ThunkArg>(typePrefix, payloadCreator, {
105
+ condition: debounceExecutionCondition,
106
+ }) as AsyncThunk<Returned, ThunkArg, ThunkApiConfig>
107
+ }
108
+
109
+ export default createDebouncedAsyncThunk
@@ -1,10 +1,11 @@
1
+ import { API } from 'api'
1
2
  import { Config } from 'config.types'
2
3
  import type { RootState } from 'domains/store'
3
4
 
4
5
  export type ThunkAPI = {
5
6
  state: RootState
6
7
  extra: {
7
- api: any
8
+ api: API
8
9
  config: Config
9
10
  eventBus: any
10
11
  }
@@ -0,0 +1,38 @@
1
+ import { createAsyncThunk } from '@reduxjs/toolkit'
2
+ import { ConversationHistoryResponse } from 'api'
3
+ import { ThunkAPI } from 'domains/redux/redux.types'
4
+
5
+ export const getConversation = createAsyncThunk<
6
+ ConversationHistoryResponse | undefined,
7
+ {
8
+ lastEvent: { id: string; occurredAt: number }
9
+ },
10
+ ThunkAPI
11
+ >(
12
+ 'getConversation',
13
+ async (_, { extra: { api }, rejectWithValue }) => {
14
+ try {
15
+ return api.getConversation()
16
+ } catch (error) {
17
+ return rejectWithValue({
18
+ name: error?.name,
19
+ message: error?.message,
20
+ langKey: error?.langKey,
21
+ action: error?.action,
22
+ originalEvent: error?.originalEvent,
23
+ originalError: error?.originalError,
24
+ })
25
+ }
26
+ },
27
+ {
28
+ condition(payload, { getState }) {
29
+ const {
30
+ state: { events },
31
+ } = getState()
32
+
33
+ const lastEvent = events[events.length - 1]
34
+ const payloadLastEventId = payload?.lastEvent?.id
35
+ return lastEvent && payloadLastEventId !== lastEvent.payload.id
36
+ },
37
+ },
38
+ )
@@ -44,9 +44,12 @@ export const setVisibility = createAsyncThunk<
44
44
  if (previousVisibility === calculatedVisibility) {
45
45
  return undefined
46
46
  }
47
+
48
+ const visibility = api.store.get(StoreKey) as object | undefined
49
+
47
50
  // Store the user-requested visibility in order to reinitialize after refresh
48
51
  api.store.set(StoreKey, {
49
- ...(api.store.get(StoreKey) || {}),
52
+ ...(visibility || {}),
50
53
  [layoutMode]: requestedVisibility,
51
54
  })
52
55
  if (requestedVisibility) {
@@ -941,6 +941,23 @@ const cardTopic = {
941
941
  },
942
942
  },
943
943
  }
944
+ const cardNoImage = {
945
+ type: 'message',
946
+ payload: {
947
+ type: 'card',
948
+ id: randomId(),
949
+ body: {
950
+ action: {
951
+ ask: '',
952
+ type: 'ask',
953
+ },
954
+ buttonText: 'Ask about pizzas!',
955
+ description:
956
+ 'Pizza Margherita is a <strong>typical Neapolitan pizza</strong>.\n\nIt is made with San Marzano tomatoes, mozzarella cheese, fresh basil, salt, and extra-virgin olive oil.',
957
+ title: 'Pizza Margherita',
958
+ },
959
+ },
960
+ }
944
961
 
945
962
  const standardState = {
946
963
  base: {
@@ -1242,7 +1259,7 @@ const standardState = {
1242
1259
  serviceInfo: {
1243
1260
  activeServiceSessionId: '3942159e-9878-469e-9120-f44fd6be0f35',
1244
1261
  },
1245
- events: [cardAskText, cardNavigate, cardTopic],
1262
+ events: [cardAskText, cardNavigate, cardTopic, cardNoImage],
1246
1263
  },
1247
1264
  carousel: {
1248
1265
  category: categoryKeys.messages,
@@ -16,7 +16,7 @@ const CardComponent = ({
16
16
  const cardRef = useRef(null)
17
17
  const { sendMessage, sendAction, emitEvent } = useSeamlyCommands()
18
18
  const descriptionId = useGeneratedId()
19
- const isMounted = useRef()
19
+ const isMounted = useRef(false)
20
20
 
21
21
  const CardActionComponent =
22
22
  action.type === cardTypes.navigate ? 'a' : 'button'
@@ -74,10 +74,12 @@ const CardComponent = ({
74
74
  <div
75
75
  className={className('card__wrapper')}
76
76
  id={id}
77
- tabIndex="-1" // set tabIndex of -1 so card can be focussed
77
+ tabIndex={-1} // set tabIndex of -1 so card can be focussed
78
78
  ref={cardRef}
79
79
  >
80
- <img className={className('card__image')} src={image} alt="" />
80
+ {image ? (
81
+ <img className={className('card__image')} src={image} alt="" />
82
+ ) : null}
81
83
  <div className={className('card__content')} id={id}>
82
84
  {title && <h2 className={className('card__title')}>{title}</h2>}
83
85
  {description && (
@@ -87,7 +89,7 @@ const CardComponent = ({
87
89
  />
88
90
  )}
89
91
  <CardActionComponent
90
- tabIndex={isCarouselItem && !hasFocus ? '-1' : undefined} // disable to prevent tabbing through cards
92
+ tabIndex={isCarouselItem && !hasFocus ? -1 : undefined} // disable to prevent tabbing through cards
91
93
  className={className('button', 'button--primary')}
92
94
  aria-describedby={descriptionId}
93
95
  {...actionProps}
@@ -28,7 +28,7 @@ const EventParticipant = ({ eventPayload }) => {
28
28
  )
29
29
  }
30
30
 
31
- if (showName) {
31
+ if (showName && participantName) {
32
32
  authorInfo.push(
33
33
  <span className={className('message__author-name')}>
34
34
  {participantName}
@@ -1,11 +1,9 @@
1
1
  import { useContext, useEffect, useRef } from 'preact/hooks'
2
- import { useDispatch } from 'react-redux'
3
2
  import SeamlyGeneralError from 'api/errors/seamly-general-error'
4
3
  import SeamlyOfflineError from 'api/errors/seamly-offline-error'
5
4
  import SeamlySessionExpiredError from 'api/errors/seamly-session-expired-error'
6
5
  import { SeamlyEventBusContext } from 'ui/components/core/seamly-api-context'
7
6
  import {
8
- useEvents,
9
7
  useSeamlyApiContext,
10
8
  useSeamlyCommands,
11
9
  useSeamlyIdleDetachCountdown,
@@ -13,6 +11,8 @@ import {
13
11
  import { featureKeys } from 'ui/utils/seamly-utils'
14
12
  import { setHasResponded } from 'domains/app/slice'
15
13
  import { clearInterrupt, setInterrupt } from 'domains/interrupt/slice'
14
+ import { useAppDispatch } from 'domains/store'
15
+ import { getConversation } from 'domains/store/actions'
16
16
  import {
17
17
  ackEvent,
18
18
  addEvent,
@@ -41,8 +41,7 @@ const SeamlyEventSubscriber = () => {
41
41
  const api = useSeamlyApiContext()
42
42
  const syncChannelRef = useRef<number>()
43
43
  const messageChannelRef = useRef<number>()
44
- const dispatch = useDispatch()
45
- const events = useEvents()
44
+ const dispatch = useAppDispatch()
46
45
  const eventBus = useContext(SeamlyEventBusContext)
47
46
  const prevEmittedEventId = useRef(null)
48
47
  const { initCountdown, endCountdown } = useSeamlyIdleDetachCountdown()
@@ -314,39 +313,24 @@ const SeamlyEventSubscriber = () => {
314
313
 
315
314
  syncChannelRef.current = api.conversation.channel.on(
316
315
  'sync',
317
- (payload) => {
318
- const lastEvent = events[events.length - 1]
319
- const payloadLastEventId = payload?.lastEvent?.id
320
-
321
- if (lastEvent && payloadLastEventId === lastEvent.payload.id) {
322
- return payload
316
+ async (payload: {
317
+ lastEvent: { id: string; occurredAt: number }
318
+ }) => {
319
+ try {
320
+ const history = await dispatch(getConversation(payload)).unwrap()
321
+ if (!history) return
322
+
323
+ dispatch(setHistory(history))
324
+ } catch (_e) {
325
+ // nothing to do, the error is handled in the thunk
323
326
  }
324
-
325
- return api
326
- .getConversation()
327
- .then((history) => {
328
- if (!history) return
329
- dispatch(setHistory(history))
330
- })
331
- .catch((error) => {
332
- dispatch(
333
- setInterrupt({
334
- name: error?.name,
335
- message: error?.message,
336
- langKey: error?.langKey,
337
- action: error?.action,
338
- originalEvent: error?.originalEvent,
339
- originalError: error?.originalError,
340
- }),
341
- )
342
- })
343
327
  },
344
328
  )
345
329
 
346
330
  return true
347
331
  })
348
332
  }
349
- }, [api, api.connectionInfo, api.conversation.channel, events, dispatch])
333
+ }, [api, api.connectionInfo, api.conversation.channel, dispatch])
350
334
 
351
335
  return null
352
336
  }
@@ -57,9 +57,14 @@ const WindowOpenButton = ({ onClick }) => {
57
57
  aria-hidden={isOpen}
58
58
  onClick={handleClick}
59
59
  >
60
- <span className={className('message-count')} aria-hidden="true">
61
- {!!count && count}
62
- </span>
60
+ <InOutTransition
61
+ isActive={!!count}
62
+ transitionStartState={transitionStartStates.notRendered}
63
+ >
64
+ <span className={className('message-count')} aria-hidden="true">
65
+ {count}
66
+ </span>
67
+ </InOutTransition>
63
68
  <ButtonIcon />
64
69
  </button>
65
70
  </InOutTransition>
@@ -1,17 +1,46 @@
1
- import { useEffect } from 'preact/hooks'
1
+ import { useEffect, useRef } from 'preact/hooks'
2
+ import SeamlyGeneralError from 'api/errors/seamly-general-error'
2
3
  import { useInterrupt } from 'domains/interrupt/hooks'
4
+ import { setInterrupt } from 'domains/interrupt/slice'
5
+ import { useAppDispatch } from 'domains/store'
3
6
  import useSeamlyCommands from './use-seamly-commands'
4
7
 
5
8
  export default function useSessionExpiredCommand() {
6
9
  const {
7
10
  meta: { originalError, action },
8
11
  } = useInterrupt()
12
+ const dispatch = useAppDispatch()
9
13
  const seamlyCommands = useSeamlyCommands()
10
14
  const isExpiredError = originalError?.name === 'SeamlySessionExpiredError'
15
+ const limit = useRef(0)
16
+ const limitTimer = useRef<ReturnType<typeof setTimeout>>(null)
11
17
 
12
18
  useEffect(() => {
13
19
  if (isExpiredError && seamlyCommands[action]) {
20
+ if (limit.current >= 10) {
21
+ limitTimer.current = setTimeout(() => {
22
+ limit.current = 0
23
+ }, 10000)
24
+
25
+ const error = new SeamlyGeneralError()
26
+ dispatch(
27
+ setInterrupt({
28
+ name: error.name,
29
+ message: error.message,
30
+ langKey: error.langKey,
31
+ originalEvent: error.originalEvent,
32
+ originalError: error.originalError,
33
+ action: error.action,
34
+ }),
35
+ )
36
+ return () => {}
37
+ }
38
+ limit.current += 1
14
39
  seamlyCommands[action]()
15
40
  }
16
- }, [action, seamlyCommands, isExpiredError])
41
+
42
+ return () => {
43
+ if (limitTimer.current) clearTimeout(limitTimer.current)
44
+ }
45
+ }, [action, seamlyCommands, isExpiredError, dispatch])
17
46
  }
@@ -1,5 +1,5 @@
1
1
  .#{$n}-message-count {
2
- display: flex;
2
+ display: none;
3
3
  position: absolute;
4
4
  z-index: 1;
5
5
  top: $spacer * -0.5;
@@ -8,21 +8,23 @@
8
8
  justify-content: center;
9
9
  width: $messagecountsize;
10
10
  height: $messagecountsize;
11
- transform: scale(1);
11
+ transform: scale(0);
12
12
  transform-origin: 50% 50%;
13
- transition: transform 0.3s 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275),
14
- opacity $transition;
13
+ transition: transform $transition, opacity $transition;
15
14
  border-radius: 50%;
16
- opacity: 1;
15
+ opacity: 0;
17
16
  background-color: $negative;
18
17
  color: $white;
19
18
  font-size: $fontsize-small;
20
19
  font-weight: $fontweight-bold;
21
20
  line-height: 1;
22
21
 
23
- &:empty {
24
- transform: scale(0);
25
- transition: transform $transition, opacity $transition;
26
- opacity: 0;
22
+ &.#{$n}-transition--visible {
23
+ display: flex;
24
+ }
25
+
26
+ &.#{$n}-transition--in {
27
+ transform: scale(1);
28
+ opacity: 1;
27
29
  }
28
30
  }