@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.
- package/build/dist/lib/components.js +269 -138
- package/build/dist/lib/components.js.map +1 -1
- package/build/dist/lib/components.min.js +1 -1
- package/build/dist/lib/components.min.js.map +1 -1
- package/build/dist/lib/hooks.js +217 -41
- package/build/dist/lib/hooks.js.map +1 -1
- package/build/dist/lib/hooks.min.js +1 -1
- package/build/dist/lib/hooks.min.js.map +1 -1
- package/build/dist/lib/index.debug.js +43 -21
- package/build/dist/lib/index.debug.js.map +1 -1
- package/build/dist/lib/index.debug.min.js +1 -1
- package/build/dist/lib/index.debug.min.js.LICENSE.txt +12 -4
- package/build/dist/lib/index.debug.min.js.map +1 -1
- package/build/dist/lib/index.js +257 -133
- package/build/dist/lib/index.js.map +1 -1
- package/build/dist/lib/index.min.js +1 -1
- package/build/dist/lib/index.min.js.map +1 -1
- package/build/dist/lib/standalone.js +265 -133
- package/build/dist/lib/standalone.js.map +1 -1
- package/build/dist/lib/standalone.min.js +1 -1
- package/build/dist/lib/standalone.min.js.map +1 -1
- package/build/dist/lib/style-guide.js +274 -134
- package/build/dist/lib/style-guide.js.map +1 -1
- package/build/dist/lib/style-guide.min.js +1 -1
- package/build/dist/lib/style-guide.min.js.map +1 -1
- package/build/dist/lib/styles.css +1 -1
- package/build/dist/lib/utils.js +325 -171
- package/build/dist/lib/utils.js.map +1 -1
- package/build/dist/lib/utils.min.js +1 -1
- package/build/dist/lib/utils.min.js.map +1 -1
- package/package.json +1 -1
- package/src/javascripts/api/errors/seamly-api-error.ts +0 -1
- package/src/javascripts/api/index.ts +16 -8
- package/src/javascripts/domains/app/actions.ts +8 -3
- package/src/javascripts/domains/interrupt/selectors.ts +3 -2
- package/src/javascripts/domains/interrupt/slice.ts +2 -0
- package/src/javascripts/domains/redux/create-debounced-async-thunk.ts +109 -0
- package/src/javascripts/domains/redux/redux.types.ts +2 -1
- package/src/javascripts/domains/store/actions.ts +38 -0
- package/src/javascripts/domains/visibility/actions.ts +4 -1
- package/src/javascripts/style-guide/states.js +18 -1
- package/src/javascripts/ui/components/conversation/event/{card-component.js → card-component.tsx} +6 -4
- package/src/javascripts/ui/components/conversation/event/event-participant.js +1 -1
- package/src/javascripts/ui/components/core/seamly-event-subscriber.ts +14 -30
- package/src/javascripts/ui/components/view/window-view/window-open-button.js +8 -3
- package/src/javascripts/ui/hooks/use-session-expired-command.ts +31 -2
- package/src/stylesheets/5-components/_message-count.scss +11 -9
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
77
|
+
export const resetApp = createDebouncedAsyncThunk<unknown, void, ThunkAPI>(
|
|
77
78
|
'resetApp',
|
|
78
79
|
async (_, { dispatch, extra: { api } }) => {
|
|
79
80
|
await api.disconnect()
|
|
80
|
-
|
|
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 (
|
|
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
|
|
@@ -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
|
-
...(
|
|
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,
|
package/src/javascripts/ui/components/conversation/event/{card-component.js → card-component.tsx}
RENAMED
|
@@ -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=
|
|
77
|
+
tabIndex={-1} // set tabIndex of -1 so card can be focussed
|
|
78
78
|
ref={cardRef}
|
|
79
79
|
>
|
|
80
|
-
|
|
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 ?
|
|
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}
|
|
@@ -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 =
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return
|
|
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,
|
|
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
|
-
<
|
|
61
|
-
{!!count
|
|
62
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
11
|
+
transform: scale(0);
|
|
12
12
|
transform-origin: 50% 50%;
|
|
13
|
-
transition: transform
|
|
14
|
-
opacity $transition;
|
|
13
|
+
transition: transform $transition, opacity $transition;
|
|
15
14
|
border-radius: 50%;
|
|
16
|
-
opacity:
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
&.#{$n}-transition--visible {
|
|
23
|
+
display: flex;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&.#{$n}-transition--in {
|
|
27
|
+
transform: scale(1);
|
|
28
|
+
opacity: 1;
|
|
27
29
|
}
|
|
28
30
|
}
|