@ethora/chat-component 26.6.5 → 26.6.7
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/dist/assets/icons.d.ts +0 -4
- package/dist/components/MainComponents/ChatHeader.d.ts +1 -2
- package/dist/components/MainComponents/ProfileImagePlaceholder.d.ts +0 -2
- package/dist/components/MainComponents/Switch.d.ts +0 -8
- package/dist/components/MessageBubble/MessageTranslations.d.ts +10 -0
- package/dist/components/RoomComponents/ChatRoomItem.d.ts +2 -2
- package/dist/firebase-messaging-sw.js +4 -16
- package/dist/helpers/callLogMessage.d.ts +0 -22
- package/dist/helpers/createUserNameFromSetUser.d.ts +1 -36
- package/dist/helpers/resolveIconColor.d.ts +3 -5
- package/dist/hooks/useChatSettingState.d.ts +0 -2
- package/dist/hooks/useChatWrapperInit.d.ts +1 -3
- package/dist/hooks/useSendMessage.d.ts +0 -1
- package/dist/main.d.ts +2 -1
- package/dist/main.js +77070 -19
- package/dist/main.js.map +1 -1
- package/dist/networking/apiClient.d.ts +15 -7
- package/dist/networking/authRefresh.d.ts +102 -0
- package/dist/networking/xmppClient.d.ts +49 -2
- package/dist/roomStore/Middleware/unreadMidlleware.d.ts +0 -1
- package/dist/roomStore/callSlice.d.ts +0 -1
- package/dist/roomStore/chatSettingsSlice.d.ts +2 -25
- package/dist/roomStore/roomsSlice.d.ts +1 -8
- package/dist/types/models/config.model.d.ts +15 -122
- package/dist/types/models/language.model.d.ts +1 -1
- package/dist/types/models/message.model.d.ts +0 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/public/firebase-messaging-sw.js +4 -16
- package/dist/VideoCallOverlay-DQ2ajQ0G.js +0 -23647
- package/dist/VideoCallOverlay-DQ2ajQ0G.js.map +0 -1
- package/dist/components/MainComponents/LanguageSelectorModal.d.ts +0 -3
- package/dist/components/MessageBubble/MessageTranslate.d.ts +0 -25
- package/dist/components/MessageBubble/TranslatedMessageBody.d.ts +0 -21
- package/dist/components/Notification/NotificationPermissionBanner.d.ts +0 -16
- package/dist/components/RoomComponents/OnlineUsersPopover.d.ts +0 -11
- package/dist/helpers/isLikelyMucJid.d.ts +0 -1
- package/dist/helpers/scheduleAckCatchup.d.ts +0 -27
- package/dist/helpers/toBaseLanguage.d.ts +0 -8
- package/dist/hooks/useIsMobileViewport.d.ts +0 -1
- package/dist/hooks/useMessageTranslation.d.ts +0 -28
- package/dist/hooks/useRoomPresence.d.ts +0 -10
- package/dist/i18n/strings.d.ts +0 -15
- package/dist/i18n/useT.d.ts +0 -33
- package/dist/main-CvjGO1ik.js +0 -74444
- package/dist/main-CvjGO1ik.js.map +0 -1
- package/dist/networking/helpers/refreshChecker.d.ts +0 -1
- package/dist/utils/translateModePolicy.d.ts +0 -19
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
import { RefreshResult } from './authRefresh';
|
|
1
2
|
declare const http: import('axios').AxiosInstance;
|
|
2
3
|
declare let appToken: string;
|
|
3
4
|
export declare function setBaseURL(newBaseURL?: string, customAppToken?: string): void;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated Import `refreshAuthTokens` from `./authRefresh` instead.
|
|
7
|
+
*
|
|
8
|
+
* Kept as a thin forwarder so existing call sites keep working while
|
|
9
|
+
* they migrate. Declared as a function (not `const refresh =
|
|
10
|
+
* refreshAuthTokens`) on purpose: this module and `authRefresh` import
|
|
11
|
+
* each other, and a const would capture the binding before the other
|
|
12
|
+
* module finished evaluating.
|
|
13
|
+
*
|
|
14
|
+
* Behaviour change vs. the old implementation: it no longer dispatches
|
|
15
|
+
* `logout()` on every failure. Only a fatal verdict from the backend
|
|
16
|
+
* ends the session - see the interceptor below.
|
|
17
|
+
*/
|
|
18
|
+
export declare function refresh(): Promise<RefreshResult>;
|
|
11
19
|
export default http;
|
|
12
20
|
export { appToken };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE single refresh-token rotation point for the SDK.
|
|
3
|
+
*
|
|
4
|
+
* The backend now runs refresh-token ROTATION with REUSE DETECTION:
|
|
5
|
+
* every successful `/v1/users/login/refresh` burns the presented
|
|
6
|
+
* refresh token and issues a new one. Presenting an already-rotated
|
|
7
|
+
* token is indistinguishable, server-side, from a stolen token - so it
|
|
8
|
+
* is treated as theft and (once the backend leaves monitor mode and
|
|
9
|
+
* enables enforcing mode) kills the whole token family.
|
|
10
|
+
*
|
|
11
|
+
* That puts three hard obligations on the client:
|
|
12
|
+
*
|
|
13
|
+
* 1. Persist the NEW refreshToken from every response, immediately,
|
|
14
|
+
* before any other logic can throw and drop it on the floor.
|
|
15
|
+
* 2. Never rotate concurrently - including ACROSS TABS. Two parallel
|
|
16
|
+
* refreshes both start from the same token; the loser presents a
|
|
17
|
+
* burned one and looks like an attacker. Hence Web Locks below,
|
|
18
|
+
* plus a same-tab in-flight promise.
|
|
19
|
+
* 3. Route EVERY refresh path through here. One "side" refresh
|
|
20
|
+
* elsewhere (service worker, poller, host app) breaks the scheme
|
|
21
|
+
* for everyone.
|
|
22
|
+
*
|
|
23
|
+
* The subtle part is (2): serialising the requests is NOT enough. A tab
|
|
24
|
+
* that waited on the lock still holds the token it read before waiting,
|
|
25
|
+
* which the winner has since burned. The token must be RE-READ from
|
|
26
|
+
* cross-tab storage inside the lock - see `performRefresh`.
|
|
27
|
+
*
|
|
28
|
+
* Callers must also stop treating 401 as "log out". Two of the four
|
|
29
|
+
* refresh error codes are race signals, not auth failures - see
|
|
30
|
+
* `RefreshErrorCode` below and `RefreshFatalError`.
|
|
31
|
+
*/
|
|
32
|
+
export type RefreshErrorCode = 'REFRESH_IN_PROGRESS' | 'REFRESH_TOKEN_ALREADY_ROTATED' | 'REFRESH_TOKEN_REUSE_DETECTED' | 'REFRESH_TOKEN_NOT_FOUND';
|
|
33
|
+
export interface RefreshResult {
|
|
34
|
+
token: string;
|
|
35
|
+
refreshToken: string;
|
|
36
|
+
fileToken?: string;
|
|
37
|
+
xmppPassword?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Thrown when the session is genuinely dead and the caller must do a
|
|
41
|
+
* hard logout (clear storage, drop XMPP, send the user to login).
|
|
42
|
+
*
|
|
43
|
+
* Anything else this module rejects with - network errors, 5xx, an
|
|
44
|
+
* unrecognised 401 - is NOT fatal and must NOT trigger a logout.
|
|
45
|
+
*/
|
|
46
|
+
export declare class RefreshFatalError extends Error {
|
|
47
|
+
code: RefreshErrorCode;
|
|
48
|
+
constructor(code: RefreshErrorCode, message?: string);
|
|
49
|
+
}
|
|
50
|
+
export declare const isRefreshFatalError: (error: unknown) => error is RefreshFatalError;
|
|
51
|
+
/**
|
|
52
|
+
* All four codes come back as HTTP 401, so the status alone tells us
|
|
53
|
+
* nothing - the code in the body is the only signal.
|
|
54
|
+
*
|
|
55
|
+
* TODO(backend-confirm): the exact location of `code` in the payload is
|
|
56
|
+
* not documented yet. Until it is, probe every shape the API uses
|
|
57
|
+
* elsewhere and take the first hit. Once confirmed, collapse this to
|
|
58
|
+
* the single real path.
|
|
59
|
+
*/
|
|
60
|
+
export declare const parseRefreshErrorCode: (error: unknown) => RefreshErrorCode | null;
|
|
61
|
+
/**
|
|
62
|
+
* Is there anything left to rotate?
|
|
63
|
+
*
|
|
64
|
+
* Callers used to answer this by reading the redux copy, which is wrong
|
|
65
|
+
* in exactly the case that matters: `scrubSensitiveChatStateTransform`
|
|
66
|
+
* (roomStore/index.ts) blanks token/refreshToken before redux-persist
|
|
67
|
+
* writes them, so a freshly rehydrated second tab holds an EMPTY
|
|
68
|
+
* refresh token while the real one sits in `ETHORA_USER_SESSION`. Every
|
|
69
|
+
* such gate therefore has to go through storage first, same as the
|
|
70
|
+
* rotation itself.
|
|
71
|
+
*/
|
|
72
|
+
export declare const hasRotatableSession: () => boolean;
|
|
73
|
+
export interface RefreshOptions {
|
|
74
|
+
/**
|
|
75
|
+
* Rotate THIS token instead of the one in the store/storage.
|
|
76
|
+
*
|
|
77
|
+
* Only for bootstrap, where the session being restored isn't in the
|
|
78
|
+
* store yet (a host-supplied `userLogin.user`, a persisted record).
|
|
79
|
+
* Everywhere else, omitting it is the correct and safer choice: the
|
|
80
|
+
* module then always presents the newest token it knows about.
|
|
81
|
+
*/
|
|
82
|
+
refreshToken?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Rotate the tokens. Concurrent callers in this tab share ONE request;
|
|
86
|
+
* concurrent tabs are serialised by the Web Lock. This is the lock the
|
|
87
|
+
* new backend scheme requires.
|
|
88
|
+
*
|
|
89
|
+
* Rejects with `RefreshFatalError` when the session is dead (caller
|
|
90
|
+
* should hard-logout) and with a plain error otherwise (caller should
|
|
91
|
+
* surface the failure and leave the session alone).
|
|
92
|
+
*/
|
|
93
|
+
export declare function refreshAuthTokens(options?: RefreshOptions): Promise<RefreshResult>;
|
|
94
|
+
/**
|
|
95
|
+
* Fire-and-forget variant for call sites that only want "make sure the
|
|
96
|
+
* tokens are fresh" and have no meaningful error handling - chat
|
|
97
|
+
* bootstrap, XMPP reconnect. Never rejects.
|
|
98
|
+
*/
|
|
99
|
+
export declare function refreshAuthTokensQuietly(): Promise<RefreshResult | null>;
|
|
100
|
+
export declare function startCrossTabTokenSync(): () => void;
|
|
101
|
+
/** Test seam: drops any shared in-flight promise between cases. */
|
|
102
|
+
export declare function __resetAuthRefreshStateForTests(): void;
|
|
@@ -2,6 +2,10 @@ import { Client } from '@xmpp/client';
|
|
|
2
2
|
import { Iso639_1Codes, XmppClientInterface, xmppSettingsInterface, IMessage } from '../types/types';
|
|
3
3
|
import { AllRoomPresenceSummary } from './xmpp/allRoomPresences.xmpp';
|
|
4
4
|
type HistorySource = 'active' | 'send_ack' | 'background' | 'default';
|
|
5
|
+
export type XmppCredentialsProvider = () => Promise<{
|
|
6
|
+
username: string;
|
|
7
|
+
password: string;
|
|
8
|
+
}>;
|
|
5
9
|
export declare class XmppClient implements XmppClientInterface {
|
|
6
10
|
client: Client;
|
|
7
11
|
devServer: string | undefined;
|
|
@@ -18,7 +22,6 @@ export declare class XmppClient implements XmppClientInterface {
|
|
|
18
22
|
private reconnecting;
|
|
19
23
|
private reconnectPromise;
|
|
20
24
|
presencesReady: boolean;
|
|
21
|
-
private sendAllPresencesInFlight;
|
|
22
25
|
private connectionSteps;
|
|
23
26
|
private messageQueue;
|
|
24
27
|
private highLaneBurstLimit;
|
|
@@ -81,6 +84,51 @@ export declare class XmppClient implements XmppClientInterface {
|
|
|
81
84
|
private pausedDueToOfflineCap;
|
|
82
85
|
private authFailureDetected;
|
|
83
86
|
private intentionalLogout;
|
|
87
|
+
/**
|
|
88
|
+
* Ported from the RN client (bug #17). The XMPP password now expires
|
|
89
|
+
* after an hour, so a SASL `not-authorized` is no longer a dead end -
|
|
90
|
+
* it is the normal, expected end of a credential's life.
|
|
91
|
+
*
|
|
92
|
+
* Note the shape of the failure, confirmed with the backend: an
|
|
93
|
+
* ALREADY-ESTABLISHED connection is NOT dropped when its password
|
|
94
|
+
* expires - authentication happens once, at SASL bind. So a tab that
|
|
95
|
+
* simply stays open keeps working indefinitely, and the expiry only
|
|
96
|
+
* bites on the next (re)connect: a network blip, a laptop waking from
|
|
97
|
+
* sleep, a server restart. That makes it rare, intermittent and easy
|
|
98
|
+
* to misdiagnose - and before this hook it was permanent, because the
|
|
99
|
+
* client latched `authFailureDetected` and refused to reconnect for
|
|
100
|
+
* the rest of the page's life. Only a reload brought the chat back.
|
|
101
|
+
*/
|
|
102
|
+
private credentialsProvider;
|
|
103
|
+
private credentialsRefreshInFlight;
|
|
104
|
+
/**
|
|
105
|
+
* Bounded so a genuinely dead session (revoked account, wrong host)
|
|
106
|
+
* can't spin refresh -> reconnect -> auth-fail forever. Reset on every
|
|
107
|
+
* successful 'online'.
|
|
108
|
+
*/
|
|
109
|
+
private authRecoveryAttempts;
|
|
110
|
+
private maxAuthRecoveryAttempts;
|
|
111
|
+
/**
|
|
112
|
+
* Inject (or replace) the callback used to fetch fresh XMPP
|
|
113
|
+
* credentials after a SASL `not-authorized`. The provider refreshes
|
|
114
|
+
* whatever upstream credential is needed (the REST session, which now
|
|
115
|
+
* re-issues the hourly XMPP password) and returns username+password.
|
|
116
|
+
*/
|
|
117
|
+
setCredentialsProvider(provider: XmppCredentialsProvider | null): void;
|
|
118
|
+
updateCredentials(username: string, password: string): void;
|
|
119
|
+
/**
|
|
120
|
+
* Single-flight wrapper around the provider. Several reconnect paths
|
|
121
|
+
* (error handler, watchdog, visibility) can fire at once; they share
|
|
122
|
+
* one request and update the credentials exactly once.
|
|
123
|
+
*/
|
|
124
|
+
private refreshCredentialsOnce;
|
|
125
|
+
/**
|
|
126
|
+
* A SASL failure used to be terminal here. With an hourly XMPP
|
|
127
|
+
* password it is routine, so try to re-mint credentials and come back
|
|
128
|
+
* instead of latching the connection off for the rest of the page's
|
|
129
|
+
* life. Returns true when a retry was started.
|
|
130
|
+
*/
|
|
131
|
+
private recoverFromAuthFailure;
|
|
84
132
|
private isTerminalAuthFailure;
|
|
85
133
|
constructor(username: string, password: string, xmppSettings?: xmppSettingsInterface);
|
|
86
134
|
initializeClient(): Promise<void>;
|
|
@@ -93,7 +141,6 @@ export declare class XmppClient implements XmppClientInterface {
|
|
|
93
141
|
private startAdaptivePing;
|
|
94
142
|
private markActivity;
|
|
95
143
|
sendAllPresencesAndMarkReady(): Promise<void>;
|
|
96
|
-
private runAllPresencesSweep;
|
|
97
144
|
reconnect(): Promise<void>;
|
|
98
145
|
allRoomPresencesStanza(): Promise<AllRoomPresenceSummary>;
|
|
99
146
|
ensureConnected(timeout?: number): Promise<void>;
|
|
@@ -13,7 +13,6 @@ export interface CallState {
|
|
|
13
13
|
token: string | null;
|
|
14
14
|
error: string | null;
|
|
15
15
|
startedAt: number | null;
|
|
16
|
-
connectedAt: number | null;
|
|
17
16
|
}
|
|
18
17
|
export declare const startOutgoingCall: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
|
|
19
18
|
roomJid: string;
|
|
@@ -8,37 +8,14 @@ interface ChatState {
|
|
|
8
8
|
selectedUser?: IUser;
|
|
9
9
|
activeFile?: ModalFile;
|
|
10
10
|
langSource?: Iso639_1Codes;
|
|
11
|
-
/**
|
|
12
|
-
* Whether THIS reader's own outgoing messages get tagged with
|
|
13
|
-
* `<translate source="xx"/>` (see sendTextMessageWithTranslateTag) so
|
|
14
|
-
* translations can be generated for them. Defaults to true so existing
|
|
15
|
-
* hosts that already set `config.translates.enabled` see no change
|
|
16
|
-
* until a reader explicitly opts out via the language-selector toggle.
|
|
17
|
-
*
|
|
18
|
-
* Turning this off does NOT untag anything already sent - a message
|
|
19
|
-
* ships with whatever tag was active at send time, permanently. Turning
|
|
20
|
-
* it back on only affects messages sent from that point forward. Same
|
|
21
|
-
* asymmetry the other direction: turning it on doesn't retroactively
|
|
22
|
-
* tag old messages either - see the disclaimer surfaced next to the
|
|
23
|
-
* toggle in LanguageSelectorModal.
|
|
24
|
-
*/
|
|
25
|
-
translateSendEnabled?: boolean;
|
|
26
|
-
/**
|
|
27
|
-
* Reader's own auto/manual pick from the language-selector modal's
|
|
28
|
-
* switcher. Undefined means "never touched it" - falls back to
|
|
29
|
-
* `config.translates.mode` (see resolveTranslateMode). Ignored entirely
|
|
30
|
-
* when the host sets `config.translates.forceType: true`, which is also
|
|
31
|
-
* why the switcher itself doesn't render in that case - nothing should
|
|
32
|
-
* ever write here that a host has pinned.
|
|
33
|
-
*/
|
|
34
|
-
translateMode?: 'auto' | 'manual';
|
|
35
11
|
}
|
|
36
12
|
export declare const setUser: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<User, "chatSettingStore/setUser">, setConfig: import('@reduxjs/toolkit').ActionCreatorWithNonInferrablePayload<"chatSettingStore/setConfig">, refreshTokens: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
|
|
37
13
|
token: string;
|
|
38
14
|
refreshToken: string;
|
|
39
15
|
fileToken?: string;
|
|
16
|
+
xmppPassword?: string;
|
|
40
17
|
}, "chatSettingStore/refreshTokens">, logout: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"chatSettingStore/logout">, setActiveModal: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<string, "chatSettingStore/setActiveModal">, setDeleteModal: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<DeleteModal, "chatSettingStore/setDeleteModal">, setSelectedUser: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<IUser, "chatSettingStore/setSelectedUser">, updateUser: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
|
|
41
18
|
updates: Partial<User>;
|
|
42
|
-
}, "chatSettingStore/updateUser">, setActiveFile: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<ModalFile, "chatSettingStore/setActiveFile">, setLangSource: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<Iso639_1Codes, "chatSettingStore/setLangSource"
|
|
19
|
+
}, "chatSettingStore/updateUser">, setActiveFile: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<ModalFile, "chatSettingStore/setActiveFile">, setLangSource: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<Iso639_1Codes, "chatSettingStore/setLangSource">;
|
|
43
20
|
declare const _default: import('redux').Reducer<ChatState>;
|
|
44
21
|
export default _default;
|
|
@@ -9,7 +9,6 @@ interface RoomMessagesState {
|
|
|
9
9
|
editAction?: EditAction;
|
|
10
10
|
isLoading: boolean;
|
|
11
11
|
usersSet: Record<string, RoomMember>;
|
|
12
|
-
presenceByRoom: Record<string, string[]>;
|
|
13
12
|
reportRoom: {
|
|
14
13
|
isOpen: boolean;
|
|
15
14
|
};
|
|
@@ -68,13 +67,7 @@ export declare const addRoom: import('@reduxjs/toolkit').ActionCreatorWithOption
|
|
|
68
67
|
chatJID?: string;
|
|
69
68
|
}, "roomMessages/setRoomNoMessages">, setCurrentRoom: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
|
|
70
69
|
roomJID: string | null;
|
|
71
|
-
}, "roomMessages/setCurrentRoom">,
|
|
72
|
-
roomJID: string;
|
|
73
|
-
xmppUsername: string;
|
|
74
|
-
}, "roomMessages/setMemberOnline">, setMemberOffline: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
|
|
75
|
-
roomJID: string;
|
|
76
|
-
xmppUsername: string;
|
|
77
|
-
}, "roomMessages/setMemberOffline">, setChatUiVisible: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<boolean, "roomMessages/setChatUiVisible">, setRoomRole: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
|
|
70
|
+
}, "roomMessages/setCurrentRoom">, setChatUiVisible: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<boolean, "roomMessages/setChatUiVisible">, setRoomRole: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
|
|
78
71
|
chatJID: string;
|
|
79
72
|
role: string;
|
|
80
73
|
}, "roomMessages/setRoomRole">, setReactions: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<ReactionAction, "roomMessages/setReactions">, setLogoutState: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"roomMessages/setLogoutState">, setActiveMessage: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
|
|
@@ -201,10 +201,25 @@ export interface IConfig {
|
|
|
201
201
|
defaultRooms?: ConfigRoom[];
|
|
202
202
|
refreshTokens?: {
|
|
203
203
|
enabled: boolean;
|
|
204
|
+
/**
|
|
205
|
+
* Host-owned rotation. When set, the SDK calls this INSTEAD of
|
|
206
|
+
* `/v1/users/login/refresh` - always, including during bootstrap -
|
|
207
|
+
* and never rotates the Ethora refresh token itself. Serialised
|
|
208
|
+
* through the same lock (and the same Web Lock) as the built-in path.
|
|
209
|
+
*
|
|
210
|
+
* Return `refreshToken` whenever the host's flow actually rotates an
|
|
211
|
+
* Ethora refresh token: the backend now treats a re-presented one as
|
|
212
|
+
* theft, so omitting it leaves a burned token in storage that ends
|
|
213
|
+
* the session the next time it is used. It stays optional because
|
|
214
|
+
* some hosts re-mint an access token from a different authority
|
|
215
|
+
* (SuperTokens, an owner-session endpoint) and hold no Ethora
|
|
216
|
+
* refresh token at all.
|
|
217
|
+
*/
|
|
204
218
|
refreshFunction?: () => Promise<{
|
|
205
219
|
accessToken: string;
|
|
206
220
|
refreshToken?: string;
|
|
207
221
|
fileToken?: string;
|
|
222
|
+
xmppPassword?: string;
|
|
208
223
|
} | null>;
|
|
209
224
|
};
|
|
210
225
|
backgroundChat?: {
|
|
@@ -223,88 +238,6 @@ export interface IConfig {
|
|
|
223
238
|
translates?: {
|
|
224
239
|
enabled: boolean;
|
|
225
240
|
translations?: Iso639_1Codes;
|
|
226
|
-
/**
|
|
227
|
-
* 'auto' shows the translation inline automatically. 'manual' shows a
|
|
228
|
-
* "Translate" link the reader clicks (LinkedIn-style), then renders the
|
|
229
|
-
* result inline with a "Show original" toggle. Default 'auto'.
|
|
230
|
-
*
|
|
231
|
-
* Neither mode calls any translation service - both only ever display
|
|
232
|
-
* `message.translations`, whatever arrived attached to the stanza (see
|
|
233
|
-
* `readerLocale` below for how a message gets translated in the first
|
|
234
|
-
* place). This is purely a display choice: show it immediately, or let
|
|
235
|
-
* the reader ask for it.
|
|
236
|
-
*/
|
|
237
|
-
mode?: 'auto' | 'manual';
|
|
238
|
-
/**
|
|
239
|
-
* Pins `mode` as fixed host policy. When true, the language-selector
|
|
240
|
-
* modal's auto/manual switcher does not render at all, and the reader
|
|
241
|
-
* has no way to override `mode` - it's always whatever the host set.
|
|
242
|
-
* Defaults to false: the reader can flip between auto and manual
|
|
243
|
-
* themselves via the switcher, which then wins over `mode` (the host's
|
|
244
|
-
* declared default, used until the reader touches it).
|
|
245
|
-
*/
|
|
246
|
-
forceType?: boolean;
|
|
247
|
-
/**
|
|
248
|
-
* Reader's full locale (BCP-47, e.g. "fr-CA"). Falls back to
|
|
249
|
-
* `config.i18n.locale`. The region is passed through to `onTranslate` so
|
|
250
|
-
* the service can distinguish fr-CA vs fr-FR; the Translate button
|
|
251
|
-
* visibility comparison ignores the region (en-US vs en-CA => no button).
|
|
252
|
-
*
|
|
253
|
-
* This is also how a host drives the reader's language from OUTSIDE the
|
|
254
|
-
* chat component: set it (e.g. from your own app's language switcher)
|
|
255
|
-
* and pass the updated config down as a normal prop. Whenever it
|
|
256
|
-
* changes, the component syncs it into the same internal state the
|
|
257
|
-
* in-chat language picker writes to - so it drives both what a reader
|
|
258
|
-
* sees translated INTO and the source language declared on their own
|
|
259
|
-
* outgoing messages. Leave unset to let the reader manage it themselves
|
|
260
|
-
* via the picker (see `showLanguageSelector`).
|
|
261
|
-
*/
|
|
262
|
-
readerLocale?: string;
|
|
263
|
-
/**
|
|
264
|
-
* Shows/hides the globe-icon language picker in the chat header.
|
|
265
|
-
* Defaults to true whenever `enabled` is true. Set to false when the
|
|
266
|
-
* host manages the reader's language itself (via `readerLocale`) and
|
|
267
|
-
* an in-chat picker would just be a second, redundant control.
|
|
268
|
-
*/
|
|
269
|
-
showLanguageSelector?: boolean;
|
|
270
|
-
/**
|
|
271
|
-
* Shows/hides the list of selectable languages INSIDE the picker
|
|
272
|
-
* (English/Español/...). Defaults to true. Set to false to keep only
|
|
273
|
-
* the enable/disable-translates toggle - for hosts that drive the
|
|
274
|
-
* reader's language externally via `readerLocale` and don't want a
|
|
275
|
-
* second, redundant language list, but still want the reader to control
|
|
276
|
-
* whether their own outgoing messages get tagged for translation.
|
|
277
|
-
*/
|
|
278
|
-
showLanguageList?: boolean;
|
|
279
|
-
/**
|
|
280
|
-
* Host-provided translation function. When set, the manual Translate
|
|
281
|
-
* action calls this - wire it to your own service - instead of reading
|
|
282
|
-
* `message.translations`. Optional; omit to use only what already
|
|
283
|
-
* arrived over XMPP.
|
|
284
|
-
*/
|
|
285
|
-
onTranslate?: (text: string, ctx: {
|
|
286
|
-
sourceLocale?: string;
|
|
287
|
-
targetLocale: string;
|
|
288
|
-
message: IMessage;
|
|
289
|
-
}) => Promise<string>;
|
|
290
|
-
/**
|
|
291
|
-
* Host predicate deciding whether to show the Translate action for a given
|
|
292
|
-
* message. When omitted, the component compares base languages (message
|
|
293
|
-
* source vs reader, region ignored) and shows the action when they differ.
|
|
294
|
-
*/
|
|
295
|
-
showTranslateForMessage?: (message: IMessage) => boolean;
|
|
296
|
-
};
|
|
297
|
-
/**
|
|
298
|
-
* Static UI i18n (interface captions like "Search...", "Type message").
|
|
299
|
-
* `locale` is a BCP-47 tag the host passes from the device/user (e.g. "en",
|
|
300
|
-
* "fr-CA", "es-US"); captions resolve to its base language. `strings`
|
|
301
|
-
* overrides or extends any built-in caption by key (see src/i18n/strings.ts).
|
|
302
|
-
* Built-in languages: en, fr, es. Independent from `translates` (dynamic
|
|
303
|
-
* per-message translation).
|
|
304
|
-
*/
|
|
305
|
-
i18n?: {
|
|
306
|
-
locale?: string;
|
|
307
|
-
strings?: Record<string, string>;
|
|
308
241
|
};
|
|
309
242
|
disableRoomConfig?: boolean;
|
|
310
243
|
disableProfilesInteractions?: boolean;
|
|
@@ -444,46 +377,6 @@ export interface IConfig {
|
|
|
444
377
|
iconPath?: string;
|
|
445
378
|
badgePath?: string;
|
|
446
379
|
softAsk?: boolean;
|
|
447
|
-
/**
|
|
448
|
-
* In-app banner that nudges the user to turn on browser notifications.
|
|
449
|
-
* Off by default. The banner renders one of two states automatically from
|
|
450
|
-
* the current `Notification.permission`:
|
|
451
|
-
* - "default" (never asked) → an "Enable notifications" card with a button
|
|
452
|
-
* that triggers the browser permission prompt;
|
|
453
|
-
* - "denied" (blocked) → a "Notifications are blocked" card explaining how
|
|
454
|
-
* to re-allow it from the browser's site settings.
|
|
455
|
-
* It hides itself entirely once permission is "granted", when the browser
|
|
456
|
-
* has no Notification API, or when `pushNotifications.enabled` is not true.
|
|
457
|
-
* Every label is overridable so it can be localised.
|
|
458
|
-
*/
|
|
459
|
-
permissionBanner?: {
|
|
460
|
-
/** Master switch for the banner. Default false (opt-in). */
|
|
461
|
-
enabled?: boolean;
|
|
462
|
-
/** Show the prompt while permission is "default". Default true. */
|
|
463
|
-
showWhenDefault?: boolean;
|
|
464
|
-
/**
|
|
465
|
-
* Show the notice while permission is "denied". Default false: a blocked
|
|
466
|
-
* state can only be fixed by hand in the browser's site settings, so by
|
|
467
|
-
* default we don't nag. Set true to opt back into the reminder.
|
|
468
|
-
*/
|
|
469
|
-
showWhenBlocked?: boolean;
|
|
470
|
-
/** Render a dismiss (×) control. Default true. Dismissal lasts the tab session. */
|
|
471
|
-
dismissible?: boolean;
|
|
472
|
-
/** Where the fixed banner is anchored. Default "bottom-right". */
|
|
473
|
-
position?: 'top-left' | 'top-right' | 'top-center' | 'bottom-left' | 'bottom-right' | 'bottom-center';
|
|
474
|
-
/** Title for the "enable" state. Default "Enable notifications". */
|
|
475
|
-
enableTitle?: string;
|
|
476
|
-
/** Description for the "enable" state. */
|
|
477
|
-
enableDescription?: string;
|
|
478
|
-
/** Button label for the "enable" state. Default "Enable notifications". */
|
|
479
|
-
enableButtonLabel?: string;
|
|
480
|
-
/** Title for the "blocked" state. Default "Notifications are blocked". */
|
|
481
|
-
blockedTitle?: string;
|
|
482
|
-
/** Description for the "blocked" state. */
|
|
483
|
-
blockedDescription?: string;
|
|
484
|
-
/** Inline style overrides merged onto the banner container. */
|
|
485
|
-
style?: React.CSSProperties;
|
|
486
|
-
};
|
|
487
380
|
onClick?: (params: {
|
|
488
381
|
roomJID?: string;
|
|
489
382
|
messageId?: string;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.
|
|
1
|
+
export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.06.04";
|
package/package.json
CHANGED
|
@@ -384,10 +384,9 @@ async function handlePush(raw = {}, source = 'push') {
|
|
|
384
384
|
});
|
|
385
385
|
|
|
386
386
|
const visible = isVisibleClients(windowClients);
|
|
387
|
-
const hasAnyClient = windowClients.length > 0;
|
|
388
387
|
|
|
389
388
|
if (DEBUG) log('clients:', dumpClients(windowClients));
|
|
390
|
-
log('visibleClientExists:', visible
|
|
389
|
+
log('visibleClientExists:', visible);
|
|
391
390
|
|
|
392
391
|
await broadcastDebugMessage({
|
|
393
392
|
source,
|
|
@@ -446,20 +445,9 @@ async function handlePush(raw = {}, source = 'push') {
|
|
|
446
445
|
},
|
|
447
446
|
};
|
|
448
447
|
|
|
449
|
-
|
|
450
|
-
// visible/focused tab) is NOT the "nobody will hear this ring" case -
|
|
451
|
-
// VideoCallOverlay already rings its own OS notification for exactly
|
|
452
|
-
// that state (see its comment: this SW push is meant to be the
|
|
453
|
-
// fully-offline fallback). `visible` collapses "backgrounded but open"
|
|
454
|
-
// and "fully closed" into the same false, which duplicated that
|
|
455
|
-
// client-side ring every time the tab was merely hidden. Suppress on
|
|
456
|
-
// ANY open client for calls; regular messages keep the visible-only
|
|
457
|
-
// check they already had.
|
|
458
|
-
const suppress = isCall ? hasAnyClient : visible;
|
|
459
|
-
|
|
460
|
-
if (!suppress) {
|
|
448
|
+
if (!visible) {
|
|
461
449
|
log('SHOW NOTIFICATION', {
|
|
462
|
-
reason:
|
|
450
|
+
reason: 'NO_VISIBLE_CLIENT',
|
|
463
451
|
tag: options.tag,
|
|
464
452
|
isCall,
|
|
465
453
|
});
|
|
@@ -468,7 +456,7 @@ async function handlePush(raw = {}, source = 'push') {
|
|
|
468
456
|
}
|
|
469
457
|
|
|
470
458
|
log('SKIP NOTIFICATION', {
|
|
471
|
-
reason:
|
|
459
|
+
reason: 'VISIBLE_CLIENT',
|
|
472
460
|
});
|
|
473
461
|
}
|
|
474
462
|
|