@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.
Files changed (48) hide show
  1. package/dist/assets/icons.d.ts +0 -4
  2. package/dist/components/MainComponents/ChatHeader.d.ts +1 -2
  3. package/dist/components/MainComponents/ProfileImagePlaceholder.d.ts +0 -2
  4. package/dist/components/MainComponents/Switch.d.ts +0 -8
  5. package/dist/components/MessageBubble/MessageTranslations.d.ts +10 -0
  6. package/dist/components/RoomComponents/ChatRoomItem.d.ts +2 -2
  7. package/dist/firebase-messaging-sw.js +4 -16
  8. package/dist/helpers/callLogMessage.d.ts +0 -22
  9. package/dist/helpers/createUserNameFromSetUser.d.ts +1 -36
  10. package/dist/helpers/resolveIconColor.d.ts +3 -5
  11. package/dist/hooks/useChatSettingState.d.ts +0 -2
  12. package/dist/hooks/useChatWrapperInit.d.ts +1 -3
  13. package/dist/hooks/useSendMessage.d.ts +0 -1
  14. package/dist/main.d.ts +2 -1
  15. package/dist/main.js +77070 -19
  16. package/dist/main.js.map +1 -1
  17. package/dist/networking/apiClient.d.ts +15 -7
  18. package/dist/networking/authRefresh.d.ts +102 -0
  19. package/dist/networking/xmppClient.d.ts +49 -2
  20. package/dist/roomStore/Middleware/unreadMidlleware.d.ts +0 -1
  21. package/dist/roomStore/callSlice.d.ts +0 -1
  22. package/dist/roomStore/chatSettingsSlice.d.ts +2 -25
  23. package/dist/roomStore/roomsSlice.d.ts +1 -8
  24. package/dist/types/models/config.model.d.ts +15 -122
  25. package/dist/types/models/language.model.d.ts +1 -1
  26. package/dist/types/models/message.model.d.ts +0 -1
  27. package/dist/version.d.ts +1 -1
  28. package/package.json +1 -1
  29. package/public/firebase-messaging-sw.js +4 -16
  30. package/dist/VideoCallOverlay-DQ2ajQ0G.js +0 -23647
  31. package/dist/VideoCallOverlay-DQ2ajQ0G.js.map +0 -1
  32. package/dist/components/MainComponents/LanguageSelectorModal.d.ts +0 -3
  33. package/dist/components/MessageBubble/MessageTranslate.d.ts +0 -25
  34. package/dist/components/MessageBubble/TranslatedMessageBody.d.ts +0 -21
  35. package/dist/components/Notification/NotificationPermissionBanner.d.ts +0 -16
  36. package/dist/components/RoomComponents/OnlineUsersPopover.d.ts +0 -11
  37. package/dist/helpers/isLikelyMucJid.d.ts +0 -1
  38. package/dist/helpers/scheduleAckCatchup.d.ts +0 -27
  39. package/dist/helpers/toBaseLanguage.d.ts +0 -8
  40. package/dist/hooks/useIsMobileViewport.d.ts +0 -1
  41. package/dist/hooks/useMessageTranslation.d.ts +0 -28
  42. package/dist/hooks/useRoomPresence.d.ts +0 -10
  43. package/dist/i18n/strings.d.ts +0 -15
  44. package/dist/i18n/useT.d.ts +0 -33
  45. package/dist/main-CvjGO1ik.js +0 -74444
  46. package/dist/main-CvjGO1ik.js.map +0 -1
  47. package/dist/networking/helpers/refreshChecker.d.ts +0 -1
  48. 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
- export declare function refresh(): Promise<{
5
- data: {
6
- refreshToken: string;
7
- token: string;
8
- fileToken?: string;
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>;
@@ -1,3 +1,2 @@
1
1
  import { Middleware } from '@reduxjs/toolkit';
2
- export declare const resolveTouchedRooms: (action: any, prevState: any, nextState: any) => Set<string>;
3
2
  export declare const unreadMiddleware: Middleware;
@@ -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">, setTranslateSendEnabled: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<boolean, "chatSettingStore/setTranslateSendEnabled">, setTranslateMode: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<"auto" | "manual", "chatSettingStore/setTranslateMode">;
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">, setMemberOnline: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
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;
@@ -1,4 +1,4 @@
1
- export type Iso639_1Codes = 'en' | 'en-CA' | 'es' | 'es-US' | 'pt' | 'ht' | 'fr' | 'fr-CA' | 'zh';
1
+ export type Iso639_1Codes = 'en' | 'es' | 'pt' | 'ht' | 'zh';
2
2
  export interface Language {
3
3
  iso639_1: Iso639_1Codes;
4
4
  name: string;
@@ -22,7 +22,6 @@ export interface IMessage {
22
22
  activeMessage?: boolean;
23
23
  isReply?: boolean | string;
24
24
  isDeleted?: boolean;
25
- isEdited?: boolean;
26
25
  mainMessage?: string;
27
26
  reply?: IReply[];
28
27
  reaction?: Record<string, ReactionMessage>;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.6.1";
1
+ export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.06.04";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethora/chat-component",
3
- "version": "26.6.5",
3
+ "version": "26.6.7",
4
4
  "type": "module",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/main.d.ts",
@@ -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, 'anyClientExists:', hasAnyClient);
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
- // Calls: a client that's merely backgrounded (open, just not the
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: isCall ? 'NO_CLIENT_AT_ALL' : 'NO_VISIBLE_CLIENT',
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: isCall ? 'CLIENT_OPEN_WILL_RING_ITSELF' : 'VISIBLE_CLIENT',
459
+ reason: 'VISIBLE_CLIENT',
472
460
  });
473
461
  }
474
462