@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.
- package/README.md +44 -2
- package/dist/{ChatProfileModal-DKuxJsdM.js → ChatProfileModal-3UH0H5Ec.js} +571 -567
- package/dist/{FilePreviewModal-BcaSoO3-.js → FilePreviewModal-Cgj1iHry.js} +168 -168
- package/dist/{ManageDataModal-Dw4dCQll.js → ManageDataModal-BblTuE9g.js} +14 -14
- package/dist/{MarkdownBody-xtKDMR0h.js → MarkdownBody-RMEx8yW2.js} +1 -1
- package/dist/{MessageSearchModal-BVKU6H3v.js → MessageSearchModal-CrYR2EH8.js} +105 -113
- package/dist/{PublicChatsModal-CscOnVaQ.js → PublicChatsModal-Bi5r4mrV.js} +13 -13
- package/dist/{SideDrawer-WneDrrWG.js → SideDrawer-CDAoobvX.js} +16 -16
- package/dist/{UserProfileModal-CMHFq1I9.js → UserProfileModal-4Frgvam-.js} +3 -3
- package/dist/{UserSettingsModal-BoZXZ0oR.js → UserSettingsModal-C-mb-9cb.js} +2 -2
- package/dist/{VideoCallOverlay-CQNi7wp5.js → VideoCallOverlay-92dFLb9U.js} +1 -1
- package/dist/{VisibilityModal-BPsNiMzJ.js → VisibilityModal-zIAV6dAP.js} +9 -9
- package/dist/components/InputComponents/MentionDropdown.d.ts +2 -0
- package/dist/components/MainComponents/scrollAnchor.d.ts +19 -0
- package/dist/components/MainComponents/useJumpToMessage.d.ts +42 -11
- package/dist/components/MainComponents/useScrollAnchor.d.ts +29 -0
- package/dist/components/Modals/MessageSearchModal/MessageHitResults.d.ts +5 -0
- package/dist/components/Modals/styledModalComponents.d.ts +0 -8
- package/dist/{handleCopyClick-B70vLjJD.js → handleCopyClick-BtXos0AJ.js} +1 -1
- package/dist/helpers/checkUniqueUsers.d.ts +2 -2
- package/dist/helpers/dedupeMembers.d.ts +10 -0
- package/dist/helpers/historyPreloadConfig.d.ts +25 -0
- package/dist/helpers/historyPreloadScheduler.d.ts +3 -0
- package/dist/helpers/insertMessageWithDelimiter.d.ts +1 -0
- package/dist/helpers/isMessageSearchEnabled.d.ts +9 -0
- package/dist/helpers/jumpThread.d.ts +56 -0
- package/dist/helpers/jumpWindow.d.ts +73 -0
- package/dist/helpers/openThreadForJump.d.ts +26 -0
- package/dist/helpers/pdf/loadPdfjs.d.ts +8 -1
- package/dist/helpers/roomUserCount.d.ts +19 -0
- package/dist/helpers/userResolver.d.ts +57 -0
- package/dist/helpers/xmppIdShape.d.ts +1 -1
- package/dist/hooks/useChatWrapperInit.d.ts +6 -0
- package/dist/hooks/usePdfThumbnail.d.ts +2 -1
- package/dist/hooks/useRoomDirectory.d.ts +11 -0
- package/dist/{main-rJpE4hh-.js → main-BnLtessp.js} +22850 -21155
- package/dist/main.css +1 -1
- package/dist/main.js +11 -11
- package/dist/networking/api-requests/roomMembers.api.d.ts +18 -0
- package/dist/networking/ethoraEvents.d.ts +44 -0
- package/dist/networking/stanzaHandlers.d.ts +2 -2
- package/dist/networking/xmpp/allRoomPresences.xmpp.d.ts +18 -1
- package/dist/networking/xmpp/presenceInRoom.xmpp.d.ts +1 -1
- package/dist/networking/xmppClient.d.ts +56 -2
- package/dist/roomStore/Middleware/jumpThreadMiddleware.d.ts +2 -0
- package/dist/roomStore/index.d.ts +12 -0
- package/dist/roomStore/roomsSlice.d.ts +33 -1
- package/dist/roomStore/safeKey.d.ts +12 -0
- package/dist/roomStore/usersSetCap.d.ts +15 -0
- package/dist/types/models/config.model.d.ts +70 -3
- package/dist/types/models/room.model.d.ts +14 -0
- package/dist/types/models/xmpp.model.d.ts +21 -1
- package/dist/version.d.ts +1 -1
- package/package.json +2 -1
- package/dist/components/MainComponents/computeAnchoredScrollTop.d.ts +0 -9
- package/dist/components/Modals/NewChatModal/UpdNewChatModal.d.ts +0 -3
- 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:
|
|
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 {
|
|
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
|
-
|
|
16
|
-
|
|
15
|
+
m as isRefreshFatalError,
|
|
16
|
+
y as logoutService,
|
|
17
17
|
T as omemo,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
f as parseQuickReplies,
|
|
19
|
+
I as refreshAuthTokens,
|
|
20
|
+
R as refreshAuthTokensQuietly,
|
|
21
|
+
g as resendMessage,
|
|
22
22
|
c as useInAppNotifications,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
|
@@ -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">,
|
|
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
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
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.
|
|
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.
|
|
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;
|