@ethora/chat-component 26.9.5 → 26.9.8

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 (81) hide show
  1. package/README.md +45 -1
  2. package/dist/{ChatProfileModal-Pr4--RdW.js → ChatProfileModal-3UH0H5Ec.js} +725 -714
  3. package/dist/{FilePreviewModal-CYHlKzRc.js → FilePreviewModal-Cgj1iHry.js} +161 -161
  4. package/dist/{ManageDataModal-BE1eGlYh.js → ManageDataModal-BblTuE9g.js} +14 -14
  5. package/dist/{MarkdownBody-G0_J0NL6.js → MarkdownBody-RMEx8yW2.js} +1 -1
  6. package/dist/MessageSearchModal-CrYR2EH8.js +381 -0
  7. package/dist/PublicChatsModal-Bi5r4mrV.js +202 -0
  8. package/dist/{SideDrawer-BG2m7Bcm.js → SideDrawer-CDAoobvX.js} +34 -34
  9. package/dist/{UserProfileModal-B2Hp5bAF.js → UserProfileModal-4Frgvam-.js} +3 -3
  10. package/dist/{UserSettingsModal-D871FDAT.js → UserSettingsModal-C-mb-9cb.js} +2 -2
  11. package/dist/{VideoCallOverlay-Br0ji0nZ.js → VideoCallOverlay-92dFLb9U.js} +1 -1
  12. package/dist/{VisibilityModal-DdFBV1_g.js → VisibilityModal-zIAV6dAP.js} +9 -9
  13. package/dist/assets/icons.d.ts +9 -0
  14. package/dist/components/InputComponents/MentionDropdown.d.ts +2 -0
  15. package/dist/components/InputComponents/Search.d.ts +4 -0
  16. package/dist/components/MainComponents/ArchivedMessageCard.d.ts +11 -0
  17. package/dist/components/MainComponents/scrollAnchor.d.ts +19 -0
  18. package/dist/components/MainComponents/useJumpToMessage.d.ts +94 -0
  19. package/dist/components/MainComponents/useScrollAnchor.d.ts +29 -0
  20. package/dist/components/MainComponents/useStickToBottom.d.ts +27 -0
  21. package/dist/components/Modals/MessageSearchModal/MessageHitResults.d.ts +27 -0
  22. package/dist/components/Modals/MessageSearchModal/MessageSearchModal.d.ts +6 -0
  23. package/dist/components/Modals/MessageSearchModal/resolveSender.d.ts +33 -0
  24. package/dist/components/Modals/MessageSearchModal/searchFilters.d.ts +34 -0
  25. package/dist/components/Modals/MessageSearchModal/snippet.d.ts +14 -0
  26. package/dist/components/Modals/MessageSearchModal/useMessageSearch.d.ts +22 -0
  27. package/dist/components/Modals/PublicChatsModal/PublicChatsModal.d.ts +13 -0
  28. package/dist/components/Modals/styledModalComponents.d.ts +0 -8
  29. package/dist/components/styled/RoomListComponents/index.d.ts +3 -0
  30. package/dist/context/ToastContext.d.ts +7 -0
  31. package/dist/firebase-messaging-sw.js +21 -3
  32. package/dist/{handleCopyClick-DPjxMoJn.js → handleCopyClick-BtXos0AJ.js} +1 -1
  33. package/dist/helpers/checkUniqueUsers.d.ts +2 -2
  34. package/dist/helpers/constants/MODAL_TYPES.d.ts +2 -0
  35. package/dist/helpers/dedupeMembers.d.ts +10 -0
  36. package/dist/helpers/historyPreloadConfig.d.ts +25 -0
  37. package/dist/helpers/historyPreloadScheduler.d.ts +3 -0
  38. package/dist/helpers/insertMessageWithDelimiter.d.ts +1 -0
  39. package/dist/helpers/isMessageSearchEnabled.d.ts +9 -0
  40. package/dist/helpers/jumpThread.d.ts +56 -0
  41. package/dist/helpers/jumpWindow.d.ts +73 -0
  42. package/dist/helpers/nanoToMs.d.ts +1 -1
  43. package/dist/helpers/openRoomAtMessage.d.ts +17 -0
  44. package/dist/helpers/openThreadForJump.d.ts +26 -0
  45. package/dist/helpers/pdf/loadPdfjs.d.ts +8 -1
  46. package/dist/helpers/roomUserCount.d.ts +19 -0
  47. package/dist/helpers/userResolver.d.ts +57 -0
  48. package/dist/helpers/xmppIdShape.d.ts +1 -1
  49. package/dist/hooks/useChatWrapperInit.d.ts +6 -0
  50. package/dist/hooks/useMessageSearchShortcut.d.ts +14 -0
  51. package/dist/hooks/usePdfThumbnail.d.ts +2 -1
  52. package/dist/hooks/useRoomDirectory.d.ts +11 -0
  53. package/dist/hooks/useRoomState.d.ts +1 -0
  54. package/dist/{main-BRIZ98KV.js → main-BnLtessp.js} +24764 -21859
  55. package/dist/main.css +1 -1
  56. package/dist/main.js +16 -16
  57. package/dist/networking/api-requests/messageSearch.api.d.ts +56 -0
  58. package/dist/networking/api-requests/publicChats.api.d.ts +23 -0
  59. package/dist/networking/api-requests/roomMembers.api.d.ts +18 -0
  60. package/dist/networking/api-requests/xmppToken.api.d.ts +1 -0
  61. package/dist/networking/ethoraEvents.d.ts +44 -0
  62. package/dist/networking/stanzaHandlers.d.ts +2 -2
  63. package/dist/networking/xmpp/allRoomPresences.xmpp.d.ts +18 -1
  64. package/dist/networking/xmpp/presenceInRoom.xmpp.d.ts +1 -1
  65. package/dist/networking/xmppClient.d.ts +58 -2
  66. package/dist/roomStore/Middleware/jumpThreadMiddleware.d.ts +2 -0
  67. package/dist/roomStore/chatSettingsSlice.d.ts +1 -1
  68. package/dist/roomStore/index.d.ts +12 -0
  69. package/dist/roomStore/roomsSlice.d.ts +70 -1
  70. package/dist/roomStore/safeKey.d.ts +12 -0
  71. package/dist/roomStore/usersSetCap.d.ts +15 -0
  72. package/dist/styles/classNames.d.ts +2 -0
  73. package/dist/types/models/config.model.d.ts +78 -0
  74. package/dist/types/models/room.model.d.ts +14 -0
  75. package/dist/types/models/xmpp.model.d.ts +21 -1
  76. package/dist/utils/notificationUtils.d.ts +12 -0
  77. package/dist/version.d.ts +1 -1
  78. package/package.json +2 -1
  79. package/public/firebase-messaging-sw.js +21 -3
  80. package/dist/components/Modals/NewChatModal/UpdNewChatModal.d.ts +0 -3
  81. package/dist/components/SortDropDown/index.d.ts +0 -8
package/dist/main.css CHANGED
@@ -1 +1 @@
1
- .ethora-chat-root{font-family:var( --ethora-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif );-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ethora-chat-root code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.ethora-chat-root,.ethora-chat-root *{scrollbar-width:thin;scrollbar-color:rgba(20,20,20,.22) transparent;scrollbar-color:var( --ethora-scrollbar-thumb, color-mix(in srgb, var(--ethora-icon-color, #0052cd) 35%, transparent) ) var(--ethora-scrollbar-track, transparent)}.ethora-chat-root::-webkit-scrollbar,.ethora-chat-root ::-webkit-scrollbar{width:8px;height:8px}.ethora-chat-root::-webkit-scrollbar-thumb,.ethora-chat-root ::-webkit-scrollbar-thumb{background-color:#14141438;background-color:var( --ethora-scrollbar-thumb, color-mix(in srgb, var(--ethora-icon-color, #0052cd) 35%, transparent) );border-radius:var(--ethora-radius-full, 999px);border:2px solid transparent;background-clip:content-box}.ethora-chat-root::-webkit-scrollbar-thumb:hover,.ethora-chat-root ::-webkit-scrollbar-thumb:hover{background-color:#14141466;background-color:var( --ethora-scrollbar-thumb-hover, color-mix(in srgb, var(--ethora-icon-color, #0052cd) 60%, transparent) );background-clip:content-box}.ethora-chat-root::-webkit-scrollbar-track,.ethora-chat-root ::-webkit-scrollbar-track{background-color:var(--ethora-scrollbar-track, transparent)}.ethora-apple-emoji{font-family:"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif}.ethora-chat-root .emoji-mart{max-width:320px;max-height:360px;overflow-y:auto;border-radius:12px;box-shadow:0 4px 12px #0000001a}.ethora-message-highlight{animation:ethoraMessageHighlight 2s ease-out;transition:background-color .3s ease}@keyframes ethoraMessageHighlight{0%{background-color:#2196f366;transform:scale(1.02);box-shadow:0 0 0 2px #2196f333}30%{background-color:#2196f34d;box-shadow:0 0 0 2px #2196f326}60%{background-color:#2196f333;box-shadow:0 0 0 1px #2196f31a}to{background-color:transparent;transform:scale(1);box-shadow:none}}@media(max-width:768px){.ethora-message-notification-container{left:50%!important;right:auto!important;transform:translate(-50%)!important;align-items:center!important}.ethora-message-notification-container[data-position-horizontal=left],.ethora-message-notification-container[data-position-horizontal=right]{left:50%!important;right:auto!important;transform:translate(-50%)!important}}
1
+ .ethora-chat-root{font-family:var( --ethora-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif );-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ethora-chat-root code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.ethora-chat-root,.ethora-chat-root *{scrollbar-width:thin;scrollbar-color:rgba(20,20,20,.22) transparent;scrollbar-color:var( --ethora-scrollbar-thumb, color-mix(in srgb, var(--ethora-icon-color, #0052cd) 35%, transparent) ) var(--ethora-scrollbar-track, transparent)}.ethora-chat-root::-webkit-scrollbar,.ethora-chat-root ::-webkit-scrollbar{width:8px;height:8px}.ethora-chat-root::-webkit-scrollbar-thumb,.ethora-chat-root ::-webkit-scrollbar-thumb{background-color:#14141438;background-color:var( --ethora-scrollbar-thumb, color-mix(in srgb, var(--ethora-icon-color, #0052cd) 35%, transparent) );border-radius:var(--ethora-radius-full, 999px);border:2px solid transparent;background-clip:content-box}.ethora-chat-root::-webkit-scrollbar-thumb:hover,.ethora-chat-root ::-webkit-scrollbar-thumb:hover{background-color:#14141466;background-color:var( --ethora-scrollbar-thumb-hover, color-mix(in srgb, var(--ethora-icon-color, #0052cd) 60%, transparent) );background-clip:content-box}.ethora-chat-root::-webkit-scrollbar-track,.ethora-chat-root ::-webkit-scrollbar-track{background-color:var(--ethora-scrollbar-track, transparent)}.ethora-apple-emoji{font-family:"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif}.ethora-chat-root .emoji-mart{max-width:320px;max-height:360px;overflow-y:auto;border-radius:12px;box-shadow:0 4px 12px #0000001a}[data-message-id].ethora-message-highlight{animation:none;transition:none}[data-message-id].ethora-message-highlight [data-ethora-bubble]{animation:ethoraBubblePulse 1s ease-out forwards}[data-message-id].ethora-message-highlight [data-ethora-bubble]:after{content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;z-index:1;background-color:color-mix(in srgb,var(--ethora-color-primary, #0052cd) 22%,transparent);animation:ethoraBubbleTint 1s ease-out forwards}@keyframes ethoraBubblePulse{0%{box-shadow:0 0 color-mix(in srgb,var(--ethora-color-primary, #0052cd) 55%,transparent)}35%{box-shadow:0 0 0 3px color-mix(in srgb,var(--ethora-color-primary, #0052cd) 60%,transparent),0 0 14px 2px color-mix(in srgb,var(--ethora-color-primary, #0052cd) 35%,transparent)}to{box-shadow:0 0 0 9px color-mix(in srgb,var(--ethora-color-primary, #0052cd) 0%,transparent)}}@keyframes ethoraBubbleTint{0%{opacity:0}20%{opacity:1}to{opacity:0}}@media(prefers-reduced-motion:reduce){[data-message-id].ethora-message-highlight [data-ethora-bubble]{animation:none;box-shadow:0 0 0 2px var(--ethora-color-primary, #0052cd)}[data-message-id].ethora-message-highlight [data-ethora-bubble]:after{animation:none;display:none}}.ethora-message-highlight{animation:ethoraMessageHighlight 1s ease-out;transition:background-color .3s ease}@keyframes ethoraMessageHighlight{0%{background-color:#2196f366;transform:scale(1.02);box-shadow:0 0 0 2px #2196f333}30%{background-color:#2196f34d;box-shadow:0 0 0 2px #2196f326}60%{background-color:#2196f333;box-shadow:0 0 0 1px #2196f31a}to{background-color:transparent;transform:scale(1);box-shadow:none}}@media(max-width:768px){.ethora-message-notification-container{left:50%!important;right:auto!important;transform:translate(-50%)!important;align-items:center!important}.ethora-message-notification-container[data-position-horizontal=left],.ethora-message-notification-container[data-position-horizontal=right]{left:50%!important;right:auto!important;transform:translate(-50%)!important}}
package/dist/main.js CHANGED
@@ -1,30 +1,30 @@
1
- import { b5 as a, b6 as r, b7 as b, b8 as o, b9 as t, ba as i, bb as p, bc as u, bd as h, be as n, bf as l, bg as d, bh as f, L as m, bi as T, bj as g, bk as y, bl as I, bm as R, bn as c, bo as k, bp as C, bq as A, ak as E, al as Q, br as F, bs as M } from "./main-BRIZ98KV.js";
1
+ import { bh as a, bi as r, bj as b, bk as o, bl as t, bm as u, bn as i, bo as p, bp as h, bq as n, br as l, bs as d, bt as m, L as y, bu as T, bv as f, bw as I, bx as R, by as g, bz as c, bA as C, bB as A, bC as E, al as k, am as Q, bD as v, bE as F } from "./main-BnLtessp.js";
2
2
  export {
3
3
  a as AuthTestIds,
4
4
  r as Chat,
5
5
  b as ChatInputTestIds,
6
6
  o as MessageBubbleTestIds,
7
7
  t as RefreshFatalError,
8
- i as RoomListTestIds,
9
- p as XmppProvider,
10
- u as applyTypography,
8
+ u as RoomListTestIds,
9
+ i as XmppProvider,
10
+ p as applyTypography,
11
11
  h as clearTypography,
12
12
  n as formatFingerprint,
13
13
  l as handleQRChatId,
14
14
  d as isE2eeEnabled,
15
- f as isRefreshFatalError,
16
- m as logoutService,
15
+ m as isRefreshFatalError,
16
+ y as logoutService,
17
17
  T as omemo,
18
- g as parseQuickReplies,
19
- y as refreshAuthTokens,
20
- I as refreshAuthTokensQuietly,
21
- R as resendMessage,
18
+ f as parseQuickReplies,
19
+ I as refreshAuthTokens,
20
+ R as refreshAuthTokensQuietly,
21
+ g as resendMessage,
22
22
  c as useInAppNotifications,
23
- k as useIsUserOnline,
24
- C as usePushNotifications,
25
- A as useQRCodeChat,
26
- E as useRoomMute,
23
+ C as useIsUserOnline,
24
+ A as usePushNotifications,
25
+ E as useQRCodeChat,
26
+ k as useRoomMute,
27
27
  Q as useRoomPresence,
28
- F as useTypography,
29
- M as useUnread
28
+ v as useTypography,
29
+ F as useUnread
30
30
  };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * GET /v2/apps/{appId}/messages/search: full-text / substring search over
3
+ * the chat archive. For a user token the server restricts it to chats the
4
+ * caller can see, so no client-side filtering is needed for privacy.
5
+ */
6
+ export interface MessageSearchHit {
7
+ /** The archive row's own id: the only identifier every hit is guaranteed to have. */
8
+ id: string;
9
+ /** Room name, the local part of the room JID. */
10
+ chatId: string;
11
+ chatType: string;
12
+ /** Full room JID. */
13
+ room: string;
14
+ /** Sender's full JID, WITH the xmpp host: `appId_userId@xmpp.host`. */
15
+ from: string;
16
+ fromUserId: string;
17
+ body: string;
18
+ messageId: string;
19
+ stanzaId: string;
20
+ createdAt: string;
21
+ }
22
+ export interface MessageSearchPage {
23
+ items: MessageSearchHit[];
24
+ /** Total matches on the server, not the size of this page. */
25
+ total: number;
26
+ offset: number;
27
+ limit: number;
28
+ /**
29
+ * Where the next page starts. Counted in SERVER rows, not in `items`:
30
+ * deleted messages are dropped from `items`, and paging by the filtered
31
+ * count would re-request rows already seen.
32
+ */
33
+ nextOffset: number;
34
+ }
35
+ export interface SearchMessagesParams {
36
+ q: string;
37
+ /** Room name (local part of the JID). Omit to search every visible chat. */
38
+ chatId?: string;
39
+ /** Only messages from this user (a room member's `_id`). */
40
+ fromUserId?: string;
41
+ /** ISO 8601 bounds; the server rejects anything else. */
42
+ since?: string;
43
+ until?: string;
44
+ limit?: number;
45
+ offset?: number;
46
+ signal?: AbortSignal;
47
+ }
48
+ export declare const MESSAGE_SEARCH_PAGE_SIZE = 20;
49
+ export declare function searchMessages({ q, chatId, fromUserId, since, until, limit, offset, signal, }: SearchMessagesParams): Promise<MessageSearchPage>;
50
+ /**
51
+ * A key that is unique per hit. stanzaId and messageId are NOT: on QA whole
52
+ * pages of older hits come back with both empty, which made every React key
53
+ * `chatId:` (rows were left behind when the list changed) and made a
54
+ * duplicate check by those ids throw real hits away.
55
+ */
56
+ export declare const hitKey: (hit: MessageSearchHit) => string;
@@ -0,0 +1,23 @@
1
+ /** One entry of GET /v1/chats/public: a chat anyone in the app may join. */
2
+ export interface PublicChat {
3
+ /** Room name: the local part of its JID. */
4
+ name: string;
5
+ title: string;
6
+ description: string;
7
+ picture: string;
8
+ e2ee: boolean;
9
+ }
10
+ export interface PublicChatsPage {
11
+ items: PublicChat[];
12
+ total: number;
13
+ offset: number;
14
+ limit: number;
15
+ /** Server rows consumed so far, not counting hidden ones. */
16
+ nextOffset: number;
17
+ }
18
+ export declare const PUBLIC_CHATS_PAGE_SIZE = 50;
19
+ export declare function getPublicChats({ offset, limit, signal, }?: {
20
+ offset?: number;
21
+ limit?: number;
22
+ signal?: AbortSignal;
23
+ }): Promise<PublicChatsPage>;
@@ -1,2 +1,20 @@
1
1
  import { RoomMember } from '../../types/types';
2
+ export type UserLookupRoute = 'auto' | 'v1' | 'v2';
3
+ export type UserLookupOutcome = {
4
+ status: 'ok';
5
+ user: RoomMember;
6
+ } | {
7
+ status: 'notfound' | 'forbidden' | 'error';
8
+ };
9
+ /** Module-level override of config.userLookupRoute (tests, non-React hosts). */
10
+ export declare const setUserLookupRoute: (route: UserLookupRoute | null) => void;
11
+ /** Forgets what was learned about the routes (account change, logout, tests). */
12
+ export declare const resetUserLookupRoute: () => void;
13
+ /** Visible for tests and diagnostics. */
14
+ export declare const isUserV1RouteUnsupported: () => boolean;
15
+ /**
16
+ * One user by `appId_userId`, through the route chain above. Never throws.
17
+ * `forbidden` and `notfound` are per-user answers, `error` is transient.
18
+ */
19
+ export declare function lookupUser(id: string, token: string): Promise<UserLookupOutcome>;
2
20
  export declare function getUserByXmppUsername(xmppUsername: string | undefined, token: string): Promise<RoomMember | null>;
@@ -0,0 +1 @@
1
+ export declare function fetchFreshXmppPassword(): Promise<string | null>;
@@ -0,0 +1,44 @@
1
+ import { Element } from 'ltx';
2
+ /**
3
+ * Server push `<ethora-event xmlns="urn:ethora:events:1" .../>` inside a
4
+ * `type="headline"` message from the server's admin account:
5
+ *
6
+ * user-profile-updated xmppUsername="<appId>_<userId>" uuid appId
7
+ * chat-meta-updated chatName="<appId>_<roomId>" appId
8
+ *
9
+ * The payload is a pointer only. The client re-reads the user or the room
10
+ * through the normal API, so nothing in the stanza is ever shown or stored.
11
+ * Everything in the stanza is untrusted input until proven otherwise.
12
+ */
13
+ export declare const ETHORA_EVENT_XMLNS = "urn:ethora:events:1";
14
+ export declare const ETHORA_EVENT_USER_PROFILE = "user-profile-updated";
15
+ export declare const ETHORA_EVENT_CHAT_META = "chat-meta-updated";
16
+ type HostLike = {
17
+ host?: string;
18
+ client?: {
19
+ jid?: {
20
+ getDomain?: () => string;
21
+ };
22
+ };
23
+ };
24
+ /**
25
+ * True when a headline came from the server and not from a user or a room.
26
+ * - from must be a BARE jid (a user's c2s stanza or a MUC occupant always
27
+ * carries a resource, and the server stamps it, so it cannot be spoofed)
28
+ * - its domain must be the session's own XMPP domain (not conference.*, not
29
+ * another server)
30
+ * - when config.trustedEventSenders is set, the sender must be listed
31
+ * The server's admin local part is deployment config (XMPP_ADMIN), so it
32
+ * cannot be hard-coded here.
33
+ */
34
+ export declare const isTrustedEventSender: (stanza: Element, xmppWs: HostLike | undefined) => boolean;
35
+ /**
36
+ * Handles `<ethora-event>` headlines. Returns true when the stanza carried
37
+ * one (trusted or not), so the caller can stop routing it. Unknown types,
38
+ * malformed attributes and untrusted senders are ignored silently. Never
39
+ * throws.
40
+ */
41
+ export declare function onEthoraEvent(stanza: Element, xmppWs?: HostLike): boolean;
42
+ /** Drops timers and per-room state (tests, logout). */
43
+ export declare function resetEthoraEvents(): void;
44
+ export {};
@@ -29,6 +29,6 @@ export type XMPPErrorInfo = {
29
29
  message: string;
30
30
  };
31
31
  export declare const handleErrorMessageStanza: (stanza: Element) => XMPPErrorInfo | null;
32
- declare const onUserUpdate: (stanza: Element) => Promise<void>;
33
- declare const onChatUpdate: (stanza: Element) => Promise<void>;
32
+ declare const onUserUpdate: (stanza: Element, xmppWs?: XmppClient) => Promise<void>;
33
+ declare const onChatUpdate: (stanza: Element, xmppWs?: XmppClient) => Promise<void>;
34
34
  export { onRealtimeMessage, onMessageHistory, onPresenceInRoom, onReactionHistory, onGetLastMessageArchive, handleComposing, onGetChatRooms, onNewRoomCreated, onGetMembers, onGetRoomInfo, onDeleteMessage, onEditMessage, onReactionMessage, onChatInvite, onRoomKicked, onRoomMembershipChange, onMembersRefreshSignal, onMessageError, onUserUpdate, onChatUpdate, };
@@ -11,4 +11,21 @@ export interface AllRoomPresenceSummary {
11
11
  }>;
12
12
  }
13
13
  export type RoomJoiner = (roomJid: string) => Promise<boolean>;
14
- export declare function allRoomPresences(client: Client, join?: RoomJoiner): Promise<AllRoomPresenceSummary>;
14
+ export interface AllRoomPresencesOptions {
15
+ /** Parallel joins. Default 5. */
16
+ concurrency?: number;
17
+ /** Pause after each join, per worker. Default 30 ms. */
18
+ pacingMs?: number;
19
+ /** Rooms the connection already joined: skipped, no round trip. */
20
+ isJoined?: (roomJid: string) => boolean;
21
+ /** The room the user has open: always joined next, ahead of the queue. */
22
+ getActiveRoomJid?: () => string | null | undefined;
23
+ /** Higher = joined earlier (recent activity). Default: store order. */
24
+ rank?: (roomJid: string) => number;
25
+ /** True once this sweep is obsolete (connection dropped and replaced). */
26
+ isCancelled?: () => boolean;
27
+ /** Fired once the first wave (the active room plus one pool-width of the
28
+ * most recent rooms) has settled, long before the rest of the sweep. */
29
+ onPriorityDone?: () => void;
30
+ }
31
+ export declare function allRoomPresences(client: Client, join?: RoomJoiner, options?: AllRoomPresencesOptions): Promise<AllRoomPresenceSummary>;
@@ -1,3 +1,3 @@
1
1
  import { Client } from '@xmpp/client';
2
2
  import { Element } from '@xmpp/xml';
3
- export declare const presenceInRoom: (client: Client, roomJID: string, delay?: number, timeoutMs?: number) => Promise<Element>;
3
+ export declare const presenceInRoom: (client: Client, roomJID: string, delay?: number, timeoutMs?: number, historyStanzas?: number) => Promise<Element>;
@@ -2,6 +2,18 @@ import { Client } from '@xmpp/client';
2
2
  import { Iso639_1Codes, XmppClientInterface, xmppSettingsInterface, IMessage, IMentionSpan } from '../types/types';
3
3
  import { AllRoomPresenceSummary } from './xmpp/allRoomPresences.xmpp';
4
4
  type HistorySource = 'active' | 'send_ack' | 'background' | 'default';
5
+ /** What the server's <fin> said about one windowed MAM page. */
6
+ export interface HistoryWindowFin {
7
+ complete: boolean;
8
+ first: number | null;
9
+ last: number | null;
10
+ }
11
+ /** One page of a windowed history query, returned instead of stored. */
12
+ export interface HistoryWindowPage extends HistoryWindowFin {
13
+ ok: boolean;
14
+ /** Ascending by time (oldest first), whichever direction was paged. */
15
+ messages: IMessage[];
16
+ }
5
17
  export type XmppCredentialsProvider = () => Promise<{
6
18
  username: string;
7
19
  password: string;
@@ -10,6 +22,8 @@ export declare class XmppClient implements XmppClientInterface {
10
22
  client: Client;
11
23
  devServer: string | undefined;
12
24
  host: string;
25
+ private configuredHost;
26
+ private configuredConference;
13
27
  service: string;
14
28
  conference: string;
15
29
  username: string;
@@ -22,6 +36,12 @@ export declare class XmppClient implements XmppClientInterface {
22
36
  private reconnecting;
23
37
  private reconnectPromise;
24
38
  presencesReady: boolean;
39
+ priorityPresencesReady: boolean;
40
+ private sweepEpoch;
41
+ private sweepRetryTimer;
42
+ private sweepRetryRunning;
43
+ private static readonly SWEEP_RETRY_DELAYS_MS;
44
+ private joinConcurrency;
25
45
  private sendAllPresencesInFlight;
26
46
  private connectionSteps;
27
47
  private messageQueue;
@@ -47,8 +67,11 @@ export declare class XmppClient implements XmppClientInterface {
47
67
  private criticalSendUntil;
48
68
  private activeRoomBoostUntil;
49
69
  private sendStartedAtById;
70
+ private sessionOnlineAt;
71
+ private lastSentAtById;
50
72
  private pendingSendIdsByRoom;
51
73
  private maxInFlightHistory;
74
+ private joinHistoryStanzas;
52
75
  private softPauseAfterSendMs;
53
76
  private activeRoomBoostTtlMs;
54
77
  private activeSendBoostMs;
@@ -181,13 +204,21 @@ export declare class XmppClient implements XmppClientInterface {
181
204
  private startAdaptivePing;
182
205
  private markActivity;
183
206
  sendAllPresencesAndMarkReady(): Promise<void>;
207
+ private hasUnjoinedRooms;
184
208
  private runAllPresencesSweep;
209
+ private clearSweepRetry;
210
+ private nextSweepRetryAt;
211
+ private scheduleSweepRetry;
212
+ private runSweepRetry;
185
213
  reconnect(): Promise<void>;
186
- allRoomPresencesStanza(): Promise<AllRoomPresenceSummary>;
214
+ allRoomPresencesStanza(epoch?: number): Promise<AllRoomPresenceSummary>;
187
215
  ensureConnected(timeout?: number): Promise<void>;
188
216
  close(): Promise<void>;
189
217
  wrapWithConnectionCheck<T>(operation: () => Promise<T>): Promise<T>;
190
218
  private clearMamRegistry;
219
+ private static readonly HISTORY_WATCHDOG_MS;
220
+ private historyQueueGeneration;
221
+ private abandonMamRequest;
191
222
  private clearHistoryQueue;
192
223
  private getSendLane;
193
224
  setActiveRoomJid(roomJID: string | null): void;
@@ -208,6 +239,16 @@ export declare class XmppClient implements XmppClientInterface {
208
239
  private pickNextHistoryTask;
209
240
  private processHistoryQueue;
210
241
  private executeHistoryTask;
242
+ /**
243
+ * Writes one archive page into the store. A LATEST page (no `before`)
244
+ * that is full and starts after everything the room has cached means more
245
+ * than a page arrived while this client was away: merging it would leave a
246
+ * silent hole between the cache and the page, so the cache is replaced
247
+ * (the same "cache reset" the startup catch-up does when its anchor is
248
+ * missing). Anything overlapping the cache is merged as before.
249
+ */
250
+ private storeHistoryPage;
251
+ private clearPreloadError;
211
252
  private parseMamMessages;
212
253
  private routeMamStanza;
213
254
  private requestMamHistory;
@@ -245,7 +286,22 @@ export declare class XmppClient implements XmppClientInterface {
245
286
  coalesceRoom?: boolean;
246
287
  skipIfPreloaded?: boolean;
247
288
  source?: HistorySource;
248
- }) => Promise<any>;
289
+ reissued?: boolean;
290
+ }) => Promise<IMessage[] | undefined>;
291
+ private guardHistoryRequest;
292
+ /**
293
+ * One page of archive for a window around a message, RETURNED rather than
294
+ * stored: no setRoomMessages, no messageStats / historyComplete update, so
295
+ * the live history is untouched. Pass `before` for the page ending just
296
+ * before that archive id, or `after` for the page starting just after it
297
+ * (both exclusive; microsecond MAM ids). Messages come back oldest first.
298
+ */
299
+ getHistoryWindow: (chatJID: string, max: number, cursor: {
300
+ before?: number;
301
+ after?: number;
302
+ start?: string;
303
+ end?: string;
304
+ }) => Promise<HistoryWindowPage>;
249
305
  getLastMessageArchiveStanza(roomJID: string): void;
250
306
  setRoomImageStanza: (roomJid: string, roomThumbnail: string, type: string, roomBackground?: string) => void;
251
307
  getRoomInfoStanza: (roomJID: string) => void;
@@ -0,0 +1,2 @@
1
+ import { Middleware } from '@reduxjs/toolkit';
2
+ export declare const jumpThreadMiddleware: Middleware;
@@ -38,7 +38,7 @@ export declare const setUser: import('@reduxjs/toolkit').ActionCreatorWithOption
38
38
  refreshToken: string;
39
39
  fileToken?: string;
40
40
  xmppPassword?: string;
41
- }, "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
+ }, "chatSettingStore/refreshTokens">, setXmppPassword: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<string, "chatSettingStore/setXmppPassword">, 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<{
42
42
  updates: Partial<User>;
43
43
  }, "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">;
44
44
  declare const _default: import('redux').Reducer<ChatState>;
@@ -32,6 +32,18 @@ export declare const optimizePersistedRooms: (rooms: Record<string, IRoom>, char
32
32
  export declare const compactUsersSetForPersist: (usersSet: Record<string, any>) => Record<string, any>;
33
33
  /** Puts back what compactUsersSetForPersist folded into the key. */
34
34
  export declare const restoreUsersSetForRehydrate: (usersSet: Record<string, any>) => Record<string, any>;
35
+ /**
36
+ * Per-session facts about a room that must not survive a reload. A persisted
37
+ * 'done' made the next page load treat the cached transcript as current:
38
+ * the preload sweep skipped the room and opening it returned the cache
39
+ * without asking the server, so anything sent while the page was closed was
40
+ * missing until a manual refetch. 'done'/'loading' go back to
41
+ * 'partial'/'idle' (the room is refetched once per session, and a page that
42
+ * does not overlap the cache replaces it instead of leaving a hole). The
43
+ * API's unread count is likewise a snapshot of that moment: the next
44
+ * /chats/my response re-seeds it.
45
+ */
46
+ export declare const resetSessionRoomState: (roomsMap: Record<string, IRoom>) => Record<string, IRoom>;
35
47
  export declare const limitMessagesTransform: import('redux-persist').Transform<any, any, Record<string, any>, Record<string, any>>;
36
48
  export declare const scrubSensitiveChatStateTransform: import('redux-persist').Transform<any, any, Record<string, any>, Record<string, any>>;
37
49
  export declare const sanitizeRoomsStateTransform: import('redux-persist').Transform<any, any, Record<string, any>, Record<string, any>>;
@@ -16,6 +16,55 @@ import { default as XmppClient } from '../networking/xmppClient';
16
16
  */
17
17
  export declare const MAX_DRAFT_LENGTH = 2000;
18
18
  export declare const MAX_PERSISTED_DRAFTS = 30;
19
+ /** A message found in the archive that the chat cannot show in place. */
20
+ export interface ArchivedMessage {
21
+ roomJID: string;
22
+ sender: string;
23
+ body: string;
24
+ /** ISO timestamp from the archive. */
25
+ createdAt: string;
26
+ }
27
+ /** A request to scroll a room's transcript to one message. */
28
+ export interface PendingJump {
29
+ roomJID: string;
30
+ /** Any id the message may be known by: message id, stanza id, xmpp id. */
31
+ ids: string[];
32
+ /**
33
+ * For a message that has no usable id (search archive rows often carry
34
+ * none): its timestamp and text, matched against the loaded transcript.
35
+ */
36
+ createdAt?: string;
37
+ body?: string;
38
+ /**
39
+ * What to show if the message cannot be reached in the transcript: the hit
40
+ * itself, so a tap on a search result always ends with the message on
41
+ * screen rather than an error.
42
+ */
43
+ preview?: ArchivedMessage;
44
+ /** Epoch ms the request was made, to drop one nobody ever fulfilled. */
45
+ at: number;
46
+ }
47
+ /**
48
+ * A short slice of a room's archive around one message, shown INSTEAD of the
49
+ * live list while the reader is away from the tail (a search jump to a message
50
+ * far back). Separate from room.messages on purpose: it is not contiguous with
51
+ * the live history, so it must not touch messageStats or historyComplete.
52
+ */
53
+ export interface JumpWindow {
54
+ roomJID: string;
55
+ /** Ascending by time, like room.messages. */
56
+ messages: IMessage[];
57
+ /** Id of the message the jump named, as it appears in `messages`. */
58
+ targetId: string;
59
+ /** RSM cursor (microseconds) to continue older paging from. */
60
+ olderCursor: number | null;
61
+ hasOlder: boolean;
62
+ /** RSM cursor (microseconds) to continue newer paging from. */
63
+ newerCursor: number | null;
64
+ hasNewer: boolean;
65
+ }
66
+ /** The window is trimmed on the far side past this many messages. */
67
+ export declare const JUMP_WINDOW_MAX_MESSAGES = 300;
19
68
  interface RoomMessagesState {
20
69
  rooms: {
21
70
  [jid: string]: IRoom;
@@ -34,6 +83,10 @@ interface RoomMessagesState {
34
83
  loadingText?: string;
35
84
  roomsLoadedOnce: boolean;
36
85
  roomsLoadError: boolean;
86
+ pendingJump: PendingJump | null;
87
+ archivedMessage: ArchivedMessage | null;
88
+ jumpWindow: JumpWindow | null;
89
+ joiningRoomJID: string | null;
37
90
  drafts: Record<string, string>;
38
91
  }
39
92
  interface PreloadRoomUpdate {
@@ -102,7 +155,23 @@ export declare const addRoom: import('@reduxjs/toolkit').ActionCreatorWithOption
102
155
  chatJID?: string;
103
156
  }, "roomMessages/setRoomNoMessages">, setRoomsLoadResolved: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
104
157
  success: boolean;
105
- }, "roomMessages/setRoomsLoadResolved">, setCurrentRoom: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
158
+ }, "roomMessages/setRoomsLoadResolved">, requestJumpToMessage: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
159
+ roomJID: string;
160
+ ids: string[];
161
+ createdAt?: string;
162
+ body?: string;
163
+ preview?: ArchivedMessage;
164
+ }, "roomMessages/requestJumpToMessage">, clearPendingJump: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"roomMessages/clearPendingJump">, showArchivedMessage: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<ArchivedMessage, "roomMessages/showArchivedMessage">, clearArchivedMessage: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"roomMessages/clearArchivedMessage">, setJumpWindow: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<JumpWindow, "roomMessages/setJumpWindow">, prependJumpWindowMessages: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
165
+ roomJID: string;
166
+ messages: IMessage[];
167
+ olderCursor: number | null;
168
+ hasOlder: boolean;
169
+ }, "roomMessages/prependJumpWindowMessages">, appendJumpWindowMessages: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
170
+ roomJID: string;
171
+ messages: IMessage[];
172
+ newerCursor: number | null;
173
+ hasNewer: boolean;
174
+ }, "roomMessages/appendJumpWindowMessages">, clearJumpWindow: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"roomMessages/clearJumpWindow">, setJoiningRoom: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<string, "roomMessages/setJoiningRoom">, clearJoiningRoom: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<string, "roomMessages/clearJoiningRoom">, setCurrentRoom: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
106
175
  roomJID: string | null;
107
176
  }, "roomMessages/setCurrentRoom">, setMemberOnline: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
108
177
  roomJID: string;
@@ -0,0 +1,12 @@
1
+ /** True when `key` is a non-empty string that is safe to use as an object key. */
2
+ export declare const isSafeKey: (key: unknown) => key is string;
3
+ /** True when `name` is a property name every object already has. */
4
+ export declare const isReservedName: (name: string) => boolean;
5
+ /**
6
+ * True for an `<appId>_<rest>` id that is safe to send and to store: the whole
7
+ * id and every underscore-separated segment after the first must not be a
8
+ * reserved object name, and no segment may start with `__` (which also
9
+ * rejects `<appId>___proto__`, where the split yields an empty segment and a
10
+ * `__`-prefixed one).
11
+ */
12
+ export declare const hasSafeIdSegments: (id: string) => boolean;
@@ -0,0 +1,15 @@
1
+ import { RoomMember } from '../types/types';
2
+ /**
3
+ * usersSet is persisted, and lazily resolved senders keep adding to it, so
4
+ * it needs a ceiling. Object key order is insertion order, so "least
5
+ * recently inserted" is simply the first keys.
6
+ */
7
+ export declare const USERS_SET_CAP = 5000;
8
+ /** Drops the oldest-inserted entries until at most `cap` remain. */
9
+ export declare const capUsersSet: (usersSet: Record<string, RoomMember>, cap?: number) => void;
10
+ /**
11
+ * Merges the members of a fresh /chats/my payload into usersSet: fresh
12
+ * entries win, entries fetched lazily earlier stay. A refreshed key moves to
13
+ * the newest position so the cap evicts genuinely stale ones first.
14
+ */
15
+ export declare const mergeUsersSet: (usersSet: Record<string, RoomMember>, fresh: Record<string, RoomMember>, cap?: number) => void;
@@ -29,6 +29,8 @@
29
29
  export declare const CHAT_ROOT_CLASS = "ethora-chat-root";
30
30
  /** Transient highlight flashed on a message when a notification is opened. */
31
31
  export declare const MESSAGE_HIGHLIGHT_CLASS = "ethora-message-highlight";
32
+ /** How long the highlight stays on a message; keep equal to the CSS animation. */
33
+ export declare const HIGHLIGHT_MS = 1000;
32
34
  /** Fixed-position container holding the in-app notification toasts. */
33
35
  export declare const NOTIFICATION_CONTAINER_CLASS = "ethora-message-notification-container";
34
36
  /** Forces the colour-emoji font on nodes that render a bare emoji glyph. */
@@ -174,6 +174,13 @@ export interface IConfig {
174
174
  * thumbnails, worth overriding if your users open large documents.
175
175
  */
176
176
  workerSrc?: string;
177
+ /**
178
+ * URL of a pdf.js ES module build to load at runtime instead of the copy
179
+ * bundled with the component. Pair it with `workerSrc`. For hosts that
180
+ * ship the component as one self-contained script and do not want 1.7 MB
181
+ * of pdf.js in it.
182
+ */
183
+ libUrl?: string;
177
184
  /** Documents above this are not auto-rendered. Default 25. */
178
185
  maxFileSizeMb?: number;
179
186
  };
@@ -494,6 +501,25 @@ export interface IConfig {
494
501
  * a presence state, not a count, so it is unaffected.
495
502
  */
496
503
  disableUserCount?: boolean;
504
+ /**
505
+ * Opt in to message search: the "Search messages" button in the chat header
506
+ * and profile panel, the Ctrl/Cmd+F shortcut inside the chat, and message
507
+ * matches under the chat-list search box. Off by default. Needs `appId`
508
+ * (the archive it queries is scoped by app) and a backend that serves
509
+ * `GET /v2/apps/{appId}/messages/search`.
510
+ */
511
+ enableMessageSearch?: boolean;
512
+ /**
513
+ * @deprecated Search is off unless `enableMessageSearch` is set. Kept so
514
+ * hosts that already pass `disableMessageSearch: true` stay off; it wins
515
+ * over `enableMessageSearch`.
516
+ */
517
+ disableMessageSearch?: boolean;
518
+ /**
519
+ * Hide the "Discover chats" entry of the room list menu, the directory of
520
+ * the app's public chats (GET /v1/chats/public).
521
+ */
522
+ disablePublicChatsDirectory?: boolean;
497
523
  disableSentLogic?: boolean;
498
524
  initBeforeLoad?: boolean;
499
525
  initBeforeLoadAuth?: {
@@ -603,6 +629,44 @@ export interface IConfig {
603
629
  hideSearch?: boolean;
604
630
  };
605
631
  useStoreConsoleEnabled?: boolean;
632
+ /**
633
+ * Route for looking up one unknown sender. `'auto'` (default) tries
634
+ * `GET /v1/apps/users/<xmppUsername>` and, when the backend does not accept
635
+ * the user token there (400/401, or a missing route), remembers that for 10
636
+ * minutes and uses `GET /v2/chats/users?xmppUsername=` instead, probing v1
637
+ * again after that. `'v1'` / `'v2'` pin one route with no fallback.
638
+ */
639
+ userLookupRoute?: 'auto' | 'v1' | 'v2';
640
+ /**
641
+ * Who may push `ethora-event` headlines (`urn:ethora:events:1`,
642
+ * user-profile-updated / chat-meta-updated). Default (unset or empty): any
643
+ * bare JID without a resource on the account's own XMPP domain, which is
644
+ * how the server sends them (from its admin account); room occupants,
645
+ * other users and other domains are never trusted. Set it to pin the exact
646
+ * sender(s), as full bare JIDs (`admin@xmpp.example.com`) or local parts
647
+ * (`admin`).
648
+ */
649
+ trustedEventSenders?: string[];
650
+ /**
651
+ * Background history preload after connect. Default `{ mode: 'staged',
652
+ * topRooms: 8, concurrency: 3 }`.
653
+ * - `'staged'`: the `topRooms` most recently active rooms are preloaded
654
+ * (a one-message preview pass for rooms the API gave no `lastMessage`
655
+ * for, then a full page), `concurrency` at a time; every other room
656
+ * loads when the user opens it.
657
+ * - `'all'`: preload every room (the pre-staged behaviour; for accounts
658
+ * with few rooms).
659
+ * - `'off'`: no background preload, rooms load when opened.
660
+ * A room the user opens jumps the queue; a reconnect only re-queues rooms
661
+ * that are not already preloaded.
662
+ * Falls back to the older `historyQoS.preloadTopKRooms` /
663
+ * `stagedPreloadConcurrency` when the matching field is not set.
664
+ */
665
+ historyPreload?: {
666
+ mode?: 'staged' | 'all' | 'off';
667
+ topRooms?: number;
668
+ concurrency?: number;
669
+ };
606
670
  historyQoS?: {
607
671
  maxInFlightHistory?: number;
608
672
  softPauseAfterSendMs?: number;
@@ -618,6 +682,20 @@ export interface IConfig {
618
682
  stagedPreloadFirstPassSize?: number;
619
683
  stagedPreloadSecondPassSize?: number;
620
684
  stagedPreloadConcurrency?: number;
685
+ /**
686
+ * Messages the MUC service replays on every room join, sent as
687
+ * `<history maxstanzas="N"/>`. Default 0: history comes from MAM only, so
688
+ * joining many rooms no longer pulls up to the server default (20) per
689
+ * room. Raise it only if a host relies on the join replay (a room without
690
+ * MAM archiving).
691
+ */
692
+ joinHistoryStanzas?: number;
693
+ /**
694
+ * Rooms joined in parallel by the background join sweep that runs after
695
+ * the room list is shown (active room first, then most recent activity).
696
+ * Default 5.
697
+ */
698
+ joinConcurrency?: number;
621
699
  };
622
700
  inAppNotifications?: {
623
701
  enabled?: boolean;