@ethora/chat-component 26.9.6 → 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 (57) hide show
  1. package/README.md +44 -2
  2. package/dist/{ChatProfileModal-DKuxJsdM.js → ChatProfileModal-3UH0H5Ec.js} +571 -567
  3. package/dist/{FilePreviewModal-BcaSoO3-.js → FilePreviewModal-Cgj1iHry.js} +168 -168
  4. package/dist/{ManageDataModal-Dw4dCQll.js → ManageDataModal-BblTuE9g.js} +14 -14
  5. package/dist/{MarkdownBody-xtKDMR0h.js → MarkdownBody-RMEx8yW2.js} +1 -1
  6. package/dist/{MessageSearchModal-BVKU6H3v.js → MessageSearchModal-CrYR2EH8.js} +105 -113
  7. package/dist/{PublicChatsModal-CscOnVaQ.js → PublicChatsModal-Bi5r4mrV.js} +13 -13
  8. package/dist/{SideDrawer-WneDrrWG.js → SideDrawer-CDAoobvX.js} +16 -16
  9. package/dist/{UserProfileModal-CMHFq1I9.js → UserProfileModal-4Frgvam-.js} +3 -3
  10. package/dist/{UserSettingsModal-BoZXZ0oR.js → UserSettingsModal-C-mb-9cb.js} +2 -2
  11. package/dist/{VideoCallOverlay-CQNi7wp5.js → VideoCallOverlay-92dFLb9U.js} +1 -1
  12. package/dist/{VisibilityModal-BPsNiMzJ.js → VisibilityModal-zIAV6dAP.js} +9 -9
  13. package/dist/components/InputComponents/MentionDropdown.d.ts +2 -0
  14. package/dist/components/MainComponents/scrollAnchor.d.ts +19 -0
  15. package/dist/components/MainComponents/useJumpToMessage.d.ts +42 -11
  16. package/dist/components/MainComponents/useScrollAnchor.d.ts +29 -0
  17. package/dist/components/Modals/MessageSearchModal/MessageHitResults.d.ts +5 -0
  18. package/dist/components/Modals/styledModalComponents.d.ts +0 -8
  19. package/dist/{handleCopyClick-B70vLjJD.js → handleCopyClick-BtXos0AJ.js} +1 -1
  20. package/dist/helpers/checkUniqueUsers.d.ts +2 -2
  21. package/dist/helpers/dedupeMembers.d.ts +10 -0
  22. package/dist/helpers/historyPreloadConfig.d.ts +25 -0
  23. package/dist/helpers/historyPreloadScheduler.d.ts +3 -0
  24. package/dist/helpers/insertMessageWithDelimiter.d.ts +1 -0
  25. package/dist/helpers/isMessageSearchEnabled.d.ts +9 -0
  26. package/dist/helpers/jumpThread.d.ts +56 -0
  27. package/dist/helpers/jumpWindow.d.ts +73 -0
  28. package/dist/helpers/openThreadForJump.d.ts +26 -0
  29. package/dist/helpers/pdf/loadPdfjs.d.ts +8 -1
  30. package/dist/helpers/roomUserCount.d.ts +19 -0
  31. package/dist/helpers/userResolver.d.ts +57 -0
  32. package/dist/helpers/xmppIdShape.d.ts +1 -1
  33. package/dist/hooks/useChatWrapperInit.d.ts +6 -0
  34. package/dist/hooks/usePdfThumbnail.d.ts +2 -1
  35. package/dist/hooks/useRoomDirectory.d.ts +11 -0
  36. package/dist/{main-rJpE4hh-.js → main-BnLtessp.js} +22850 -21155
  37. package/dist/main.css +1 -1
  38. package/dist/main.js +11 -11
  39. package/dist/networking/api-requests/roomMembers.api.d.ts +18 -0
  40. package/dist/networking/ethoraEvents.d.ts +44 -0
  41. package/dist/networking/stanzaHandlers.d.ts +2 -2
  42. package/dist/networking/xmpp/allRoomPresences.xmpp.d.ts +18 -1
  43. package/dist/networking/xmpp/presenceInRoom.xmpp.d.ts +1 -1
  44. package/dist/networking/xmppClient.d.ts +56 -2
  45. package/dist/roomStore/Middleware/jumpThreadMiddleware.d.ts +2 -0
  46. package/dist/roomStore/index.d.ts +12 -0
  47. package/dist/roomStore/roomsSlice.d.ts +33 -1
  48. package/dist/roomStore/safeKey.d.ts +12 -0
  49. package/dist/roomStore/usersSetCap.d.ts +15 -0
  50. package/dist/types/models/config.model.d.ts +70 -3
  51. package/dist/types/models/room.model.d.ts +14 -0
  52. package/dist/types/models/xmpp.model.d.ts +21 -1
  53. package/dist/version.d.ts +1 -1
  54. package/package.json +2 -1
  55. package/dist/components/MainComponents/computeAnchoredScrollTop.d.ts +0 -9
  56. package/dist/components/Modals/NewChatModal/UpdNewChatModal.d.ts +0 -3
  57. 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}[data-message-id].ethora-message-highlight{animation:ethoraMessageFlash 1s ease-out;border-radius:var(--ethora-radius-md, 12px)}@keyframes ethoraMessageFlash{0%{background-color:color-mix(in srgb,var(--ethora-color-primary, #0052cd) 35%,transparent)}to{background-color:transparent}}.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}}
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,4 +1,4 @@
1
- import { ba as a, bb as r, bc as b, bd as o, be as t, bf as u, bg as i, bh as p, bi as h, bj as n, bk as l, bl as d, bm as f, L as m, bn as T, bo as g, bp as y, bq as I, br as R, bs as c, bt as k, bu as C, bv as A, ak as E, al as Q, bw as v, bx as F } from "./main-rJpE4hh-.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,
@@ -12,18 +12,18 @@ export {
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
28
  v as useTypography,
29
29
  F as useUnread
@@ -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,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;
@@ -24,6 +36,12 @@ export declare class XmppClient implements XmppClientInterface {
24
36
  private reconnecting;
25
37
  private reconnectPromise;
26
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;
27
45
  private sendAllPresencesInFlight;
28
46
  private connectionSteps;
29
47
  private messageQueue;
@@ -49,8 +67,11 @@ export declare class XmppClient implements XmppClientInterface {
49
67
  private criticalSendUntil;
50
68
  private activeRoomBoostUntil;
51
69
  private sendStartedAtById;
70
+ private sessionOnlineAt;
71
+ private lastSentAtById;
52
72
  private pendingSendIdsByRoom;
53
73
  private maxInFlightHistory;
74
+ private joinHistoryStanzas;
54
75
  private softPauseAfterSendMs;
55
76
  private activeRoomBoostTtlMs;
56
77
  private activeSendBoostMs;
@@ -183,13 +204,21 @@ export declare class XmppClient implements XmppClientInterface {
183
204
  private startAdaptivePing;
184
205
  private markActivity;
185
206
  sendAllPresencesAndMarkReady(): Promise<void>;
207
+ private hasUnjoinedRooms;
186
208
  private runAllPresencesSweep;
209
+ private clearSweepRetry;
210
+ private nextSweepRetryAt;
211
+ private scheduleSweepRetry;
212
+ private runSweepRetry;
187
213
  reconnect(): Promise<void>;
188
- allRoomPresencesStanza(): Promise<AllRoomPresenceSummary>;
214
+ allRoomPresencesStanza(epoch?: number): Promise<AllRoomPresenceSummary>;
189
215
  ensureConnected(timeout?: number): Promise<void>;
190
216
  close(): Promise<void>;
191
217
  wrapWithConnectionCheck<T>(operation: () => Promise<T>): Promise<T>;
192
218
  private clearMamRegistry;
219
+ private static readonly HISTORY_WATCHDOG_MS;
220
+ private historyQueueGeneration;
221
+ private abandonMamRequest;
193
222
  private clearHistoryQueue;
194
223
  private getSendLane;
195
224
  setActiveRoomJid(roomJID: string | null): void;
@@ -210,6 +239,16 @@ export declare class XmppClient implements XmppClientInterface {
210
239
  private pickNextHistoryTask;
211
240
  private processHistoryQueue;
212
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;
213
252
  private parseMamMessages;
214
253
  private routeMamStanza;
215
254
  private requestMamHistory;
@@ -247,7 +286,22 @@ export declare class XmppClient implements XmppClientInterface {
247
286
  coalesceRoom?: boolean;
248
287
  skipIfPreloaded?: boolean;
249
288
  source?: HistorySource;
250
- }) => 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>;
251
305
  getLastMessageArchiveStanza(roomJID: string): void;
252
306
  setRoomImageStanza: (roomJid: string, roomThumbnail: string, type: string, roomBackground?: string) => void;
253
307
  getRoomInfoStanza: (roomJID: string) => void;
@@ -0,0 +1,2 @@
1
+ import { Middleware } from '@reduxjs/toolkit';
2
+ export declare const jumpThreadMiddleware: Middleware;
@@ -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>>;
@@ -44,6 +44,27 @@ export interface PendingJump {
44
44
  /** Epoch ms the request was made, to drop one nobody ever fulfilled. */
45
45
  at: number;
46
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;
47
68
  interface RoomMessagesState {
48
69
  rooms: {
49
70
  [jid: string]: IRoom;
@@ -64,6 +85,7 @@ interface RoomMessagesState {
64
85
  roomsLoadError: boolean;
65
86
  pendingJump: PendingJump | null;
66
87
  archivedMessage: ArchivedMessage | null;
88
+ jumpWindow: JumpWindow | null;
67
89
  joiningRoomJID: string | null;
68
90
  drafts: Record<string, string>;
69
91
  }
@@ -139,7 +161,17 @@ export declare const addRoom: import('@reduxjs/toolkit').ActionCreatorWithOption
139
161
  createdAt?: string;
140
162
  body?: string;
141
163
  preview?: ArchivedMessage;
142
- }, "roomMessages/requestJumpToMessage">, clearPendingJump: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"roomMessages/clearPendingJump">, showArchivedMessage: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<ArchivedMessage, "roomMessages/showArchivedMessage">, clearArchivedMessage: import('@reduxjs/toolkit').ActionCreatorWithoutPayload<"roomMessages/clearArchivedMessage">, setJoiningRoom: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<string, "roomMessages/setJoiningRoom">, clearJoiningRoom: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<string, "roomMessages/clearJoiningRoom">, setCurrentRoom: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
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<{
143
175
  roomJID: string | null;
144
176
  }, "roomMessages/setCurrentRoom">, setMemberOnline: import('@reduxjs/toolkit').ActionCreatorWithOptionalPayload<{
145
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;
@@ -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
  };
@@ -495,9 +502,17 @@ export interface IConfig {
495
502
  */
496
503
  disableUserCount?: boolean;
497
504
  /**
498
- * Hide the "Search messages" button in the chat header (and so the search
499
- * panel). Search is also hidden when `appId` is not set, since the archive
500
- * it queries is scoped by app.
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`.
501
516
  */
502
517
  disableMessageSearch?: boolean;
503
518
  /**
@@ -614,6 +629,44 @@ export interface IConfig {
614
629
  hideSearch?: boolean;
615
630
  };
616
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
+ };
617
670
  historyQoS?: {
618
671
  maxInFlightHistory?: number;
619
672
  softPauseAfterSendMs?: number;
@@ -629,6 +682,20 @@ export interface IConfig {
629
682
  stagedPreloadFirstPassSize?: number;
630
683
  stagedPreloadSecondPassSize?: number;
631
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;
632
699
  };
633
700
  inAppNotifications?: {
634
701
  enabled?: boolean;
@@ -63,6 +63,16 @@ export interface IRoom {
63
63
  lastViewedTimestamp?: number;
64
64
  unreadBaselineTimestamp?: number;
65
65
  unreadMessages?: number;
66
+ /**
67
+ * Unread count the server reported for this room in `GET /v1/chats/my`
68
+ * (`unreadCount`) and when it was read. Only present on backends that send
69
+ * it. The unread middleware uses it as a floor while the local message
70
+ * list is too short to count from (history not loaded yet): it lets the
71
+ * room list show a badge without a MAM round-trip per room. Ignored once
72
+ * the user has read past the API's last message, and cleared on open.
73
+ */
74
+ apiUnreadCount?: number;
75
+ apiUnreadSeededAt?: number;
66
76
  noMessages?: boolean;
67
77
  role?: string;
68
78
  messageStats?: {
@@ -109,6 +119,8 @@ export interface ApiRoom {
109
119
  description?: string;
110
120
  picture?: string;
111
121
  members?: RoomMember[];
122
+ /** True total; `members` is capped at 30 for big public rooms. */
123
+ usersCnt?: number;
112
124
  createdBy?: string;
113
125
  appId?: any;
114
126
  _id?: string;
@@ -118,6 +130,8 @@ export interface ApiRoom {
118
130
  __v?: string;
119
131
  /** See IRoom.muted - only present when the backend supports per-chat mute. */
120
132
  muted?: boolean;
133
+ /** See IRoom.apiUnreadCount - only present when the backend sends it. */
134
+ unreadCount?: number;
121
135
  /** See ApiRoomLastMessage - only present when the backend supports it. */
122
136
  lastMessage?: ApiRoomLastMessage;
123
137
  }
@@ -1,6 +1,6 @@
1
1
  import { Client } from '@xmpp/client';
2
2
  import { Iso639_1Codes } from './language.model';
3
- import { default as XmppClient } from '../../networking/xmppClient';
3
+ import { default as XmppClient, HistoryWindowPage } from '../../networking/xmppClient';
4
4
  import { IMessage } from './message.model';
5
5
  export interface XmppState {
6
6
  client: XmppClient | null;
@@ -27,6 +27,20 @@ export interface xmppSettingsInterface {
27
27
  stagedPreloadFirstPassSize?: number;
28
28
  stagedPreloadSecondPassSize?: number;
29
29
  stagedPreloadConcurrency?: number;
30
+ /**
31
+ * Messages the MUC service replays on every room join, sent as
32
+ * `<history maxstanzas="N"/>`. Default 0: history comes from MAM only, so
33
+ * joining many rooms no longer pulls up to the server default (20) per
34
+ * room. Raise it only if a host relies on the join replay (a room without
35
+ * MAM archiving).
36
+ */
37
+ joinHistoryStanzas?: number;
38
+ /**
39
+ * Rooms joined in parallel by the background join sweep that runs after
40
+ * the room list is shown (active room first, then most recent activity).
41
+ * Default 5.
42
+ */
43
+ joinConcurrency?: number;
30
44
  };
31
45
  }
32
46
  export interface MediaUploadData {
@@ -65,6 +79,12 @@ export interface XmppClientInterface {
65
79
  skipIfPreloaded?: boolean;
66
80
  source?: 'active' | 'send_ack' | 'background' | 'default';
67
81
  }): Promise<IMessage[] | undefined>;
82
+ getHistoryWindow(chatJID: string, max: number, cursor: {
83
+ before?: number;
84
+ after?: number;
85
+ start?: string;
86
+ end?: string;
87
+ }): Promise<HistoryWindowPage>;
68
88
  promoteRoomHistory(roomJID: string): void;
69
89
  setActiveRoomJid(roomJID: string | null): void;
70
90
  isActiveRoomGateOpen(): boolean;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.9.6";
1
+ export declare const ETHORA_CHAT_COMPONENT_VERSION = "26.9.8";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethora/chat-component",
3
- "version": "26.9.6",
3
+ "version": "26.9.8",
4
4
  "type": "module",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/main.d.ts",
@@ -97,6 +97,7 @@
97
97
  "eslint-plugin-prettier": "^5.5.0",
98
98
  "eslint-plugin-react-hooks": "^5.2.0",
99
99
  "eslint-plugin-react-refresh": "^0.4.20",
100
+ "globals": "^11.12.0",
100
101
  "jsdom": "^24.1.0",
101
102
  "prettier": "3.6.0",
102
103
  "react": "^18.3.1",
@@ -1,9 +0,0 @@
1
- /**
2
- * Where to put scrollTop after content was added ABOVE the reader.
3
- *
4
- * `liveTop` must be the scrollTop at the moment the content arrived, not the
5
- * one captured when the request started: the reader keeps scrolling while a
6
- * page is in flight, and restoring the old value throws them back down by
7
- * however far they travelled meanwhile.
8
- */
9
- export declare function computeAnchoredScrollTop(liveTop: number, heightBefore: number, heightAfter: number): number;
@@ -1,3 +0,0 @@
1
- import { default as React } from 'react';
2
- declare const NewChatModal: React.FC;
3
- export default NewChatModal;
@@ -1,8 +0,0 @@
1
- import { default as React } from 'react';
2
- interface DropdownProps {
3
- sortFunction: (value: string) => void;
4
- icon: string;
5
- values: string[];
6
- }
7
- declare const DropdownMenu: React.FC<DropdownProps>;
8
- export default DropdownMenu;