@ethora/chat-component 26.9.6 → 26.10.1

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 (133) hide show
  1. package/README.md +45 -3
  2. package/dist/{ChatProfileModal-DKuxJsdM.js → ChatProfileModal-Bf5jGPBZ.js} +566 -562
  3. package/dist/FilePreviewModal-Bwx0RZZw.js +1763 -0
  4. package/dist/{ManageDataModal-Dw4dCQll.js → ManageDataModal-Dhkp0LWH.js} +20 -20
  5. package/dist/{MarkdownBody-xtKDMR0h.js → MarkdownBody-_sUhW2O8.js} +1 -1
  6. package/dist/{MessageSearchModal-BVKU6H3v.js → MessageSearchModal-rwwWrILl.js} +106 -114
  7. package/dist/{PublicChatsModal-CscOnVaQ.js → PublicChatsModal-Byso6_vt.js} +13 -13
  8. package/dist/{SideDrawer-WneDrrWG.js → SideDrawer-CQPOf0ek.js} +38 -38
  9. package/dist/{UserProfileModal-CMHFq1I9.js → UserProfileModal-Vj2kICvc.js} +10 -10
  10. package/dist/{UserSettingsModal-BoZXZ0oR.js → UserSettingsModal-CioX4ZIV.js} +5 -5
  11. package/dist/{VideoCallOverlay-CQNi7wp5.js → VideoCallOverlay-DodAkiam.js} +3824 -3708
  12. package/dist/{VisibilityModal-BPsNiMzJ.js → VisibilityModal-CIl7rkBM.js} +12 -12
  13. package/dist/assets/icons.d.ts +2 -3
  14. package/dist/components/Files/fileCategory.d.ts +4 -2
  15. package/dist/components/InputComponents/MentionDropdown.d.ts +2 -0
  16. package/dist/components/MainComponents/AlbumGrid.d.ts +22 -0
  17. package/dist/components/MainComponents/AttachmentList.d.ts +6 -6
  18. package/dist/components/MainComponents/ImageUnavailable.d.ts +21 -0
  19. package/dist/components/MainComponents/MediaUploadOverlay.d.ts +31 -0
  20. package/dist/components/MainComponents/MessageContainer.d.ts +3 -0
  21. package/dist/components/MainComponents/scrollAnchor.d.ts +19 -0
  22. package/dist/components/MainComponents/useJumpToMessage.d.ts +42 -11
  23. package/dist/components/MainComponents/useScrollAnchor.d.ts +29 -0
  24. package/dist/components/MessageBubble/MediaCaption.d.ts +9 -0
  25. package/dist/components/MessageBubble/MessageAvatarPhoto.d.ts +17 -0
  26. package/dist/components/MessageBubble/MessageReaction.d.ts +6 -0
  27. package/dist/components/MessageBubble/QuickReplies.d.ts +2 -0
  28. package/dist/components/Modals/FilePreviewModal/FilePreviewModal.d.ts +8 -10
  29. package/dist/components/Modals/FilePreviewModal/ImageLightbox.d.ts +4 -0
  30. package/dist/components/Modals/FilePreviewModal/PdfView.d.ts +4 -0
  31. package/dist/components/Modals/FilePreviewModal/ViewerChrome.d.ts +64 -0
  32. package/dist/components/Modals/MessageSearchModal/MessageHitResults.d.ts +5 -0
  33. package/dist/components/Modals/styledModalComponents.d.ts +0 -8
  34. package/dist/components/RoomComponents/LastMessageItem.d.ts +2 -0
  35. package/dist/components/RoomComponents/styled/LastMessageMedia.d.ts +19 -0
  36. package/dist/components/RoomComponents/styled/LastTextMessage.d.ts +2 -2
  37. package/dist/components/RoomComponents/styled/StyledRoomComponents.d.ts +10 -3
  38. package/dist/components/VideoCalls/VideoCallSession.d.ts +13 -0
  39. package/dist/components/styled/FileCard.d.ts +20 -0
  40. package/dist/components/styled/MediaSurface.d.ts +50 -0
  41. package/dist/components/styled/MessageImage.d.ts +9 -10
  42. package/dist/components/styled/SendInput.d.ts +12 -2
  43. package/dist/components/styled/StyledComponents.d.ts +69 -2
  44. package/dist/components/styled/StyledInputComponents/MediaComponents.d.ts +36 -24
  45. package/dist/components/styled/UnsupportedType.d.ts +1 -0
  46. package/dist/components/styled/VideoMessage.d.ts +11 -6
  47. package/dist/e2ee/callKey.d.ts +32 -0
  48. package/dist/e2ee/fileEnvelope.d.ts +16 -5
  49. package/dist/e2ee/omemo.d.ts +18 -0
  50. package/dist/firebase-messaging-sw.js +20 -3
  51. package/dist/{handleCopyClick-B70vLjJD.js → handleCopyClick-DGaZhy_u.js} +1 -1
  52. package/dist/helpers/albumLayout.d.ts +53 -0
  53. package/dist/helpers/attachments.d.ts +22 -1
  54. package/dist/helpers/avatarImageCache.d.ts +9 -0
  55. package/dist/helpers/caption.d.ts +63 -0
  56. package/dist/helpers/checkUniqueUsers.d.ts +2 -2
  57. package/dist/helpers/composerFocus.d.ts +22 -0
  58. package/dist/helpers/createMainMessageForThread.d.ts +1 -1
  59. package/dist/helpers/dedupeMembers.d.ts +10 -0
  60. package/dist/helpers/downloadFile.d.ts +19 -0
  61. package/dist/helpers/fileKind.d.ts +17 -0
  62. package/dist/helpers/historyPreloadConfig.d.ts +25 -0
  63. package/dist/helpers/historyPreloadScheduler.d.ts +3 -0
  64. package/dist/helpers/insertMessageWithDelimiter.d.ts +1 -0
  65. package/dist/helpers/isMessageSearchEnabled.d.ts +9 -0
  66. package/dist/helpers/jumpThread.d.ts +56 -0
  67. package/dist/helpers/jumpWindow.d.ts +73 -0
  68. package/dist/helpers/lastMessagePreview.d.ts +55 -0
  69. package/dist/helpers/mapWithConcurrency.d.ts +8 -0
  70. package/dist/helpers/mediaBox.d.ts +110 -0
  71. package/dist/helpers/mediaBubble.d.ts +39 -0
  72. package/dist/helpers/mediaLabel.d.ts +10 -0
  73. package/dist/helpers/mediaSendRetry.d.ts +25 -0
  74. package/dist/helpers/messageRuns.d.ts +10 -0
  75. package/dist/helpers/openThreadForJump.d.ts +26 -0
  76. package/dist/helpers/pdf/loadPdfjs.d.ts +8 -1
  77. package/dist/helpers/repairFilenameEncoding.d.ts +11 -0
  78. package/dist/helpers/roomImageGallery.d.ts +34 -0
  79. package/dist/helpers/roomUserCount.d.ts +19 -0
  80. package/dist/helpers/safeUrl.d.ts +15 -0
  81. package/dist/helpers/secureFileUrl.d.ts +4 -1
  82. package/dist/helpers/userResolver.d.ts +57 -0
  83. package/dist/helpers/viewerSender.d.ts +47 -0
  84. package/dist/helpers/xmppIdShape.d.ts +1 -1
  85. package/dist/hooks/useAvatarImage.d.ts +21 -0
  86. package/dist/hooks/useChatWrapperInit.d.ts +6 -0
  87. package/dist/hooks/useIsMobileViewport.d.ts +5 -0
  88. package/dist/hooks/useMediaProbe.d.ts +40 -0
  89. package/dist/hooks/usePaneWidthVar.d.ts +11 -0
  90. package/dist/hooks/usePdfThumbnail.d.ts +2 -2
  91. package/dist/hooks/usePrefersReducedMotion.d.ts +2 -0
  92. package/dist/hooks/useRoomDirectory.d.ts +11 -0
  93. package/dist/hooks/useSendMessage.d.ts +4 -1
  94. package/dist/{main-rJpE4hh-.js → main-CUB_w8AZ.js} +29136 -24217
  95. package/dist/main.css +1 -1
  96. package/dist/main.js +16 -16
  97. package/dist/networking/api-requests/auth.api.d.ts +2 -0
  98. package/dist/networking/api-requests/roomMembers.api.d.ts +18 -0
  99. package/dist/networking/callKeyStanza.d.ts +7 -0
  100. package/dist/networking/ethoraEvents.d.ts +44 -0
  101. package/dist/networking/stanzaHandlers.d.ts +6 -4
  102. package/dist/networking/xmpp/allRoomPresences.xmpp.d.ts +18 -1
  103. package/dist/networking/xmpp/presenceInRoom.xmpp.d.ts +1 -1
  104. package/dist/networking/xmpp/sendDisplayedMarker.xmpp.d.ts +2 -0
  105. package/dist/networking/xmpp/sendMediaMessage.xmpp.d.ts +8 -0
  106. package/dist/networking/xmpp/sendReceivedReceipt.xmpp.d.ts +2 -0
  107. package/dist/networking/xmppClient.d.ts +61 -2
  108. package/dist/roomStore/Middleware/jumpThreadMiddleware.d.ts +2 -0
  109. package/dist/roomStore/callSlice.d.ts +6 -1
  110. package/dist/roomStore/index.d.ts +22 -0
  111. package/dist/roomStore/roomsSlice.d.ts +51 -4
  112. package/dist/roomStore/safeKey.d.ts +12 -0
  113. package/dist/roomStore/usersSetCap.d.ts +15 -0
  114. package/dist/styles/motion.d.ts +22 -2
  115. package/dist/types/models/attachment.model.d.ts +19 -0
  116. package/dist/types/models/config.model.d.ts +98 -5
  117. package/dist/types/models/customComponents.model.d.ts +2 -0
  118. package/dist/types/models/message.model.d.ts +9 -0
  119. package/dist/types/models/modal.model.d.ts +19 -0
  120. package/dist/types/models/room.model.d.ts +30 -0
  121. package/dist/types/models/xmpp.model.d.ts +24 -1
  122. package/dist/utils/messageNotificationContent.d.ts +34 -0
  123. package/dist/version.d.ts +1 -1
  124. package/package.json +2 -1
  125. package/public/firebase-messaging-sw.js +20 -3
  126. package/dist/FilePreviewModal-BcaSoO3-.js +0 -743
  127. package/dist/components/MainComponents/computeAnchoredScrollTop.d.ts +0 -9
  128. package/dist/components/Modals/NewChatModal/UpdNewChatModal.d.ts +0 -3
  129. package/dist/components/RoomComponents/styled/LastAudioMessage.d.ts +0 -6
  130. package/dist/components/RoomComponents/styled/LastMessageFile.d.ts +0 -6
  131. package/dist/components/RoomComponents/styled/LastMessagePhoto.d.ts +0 -6
  132. package/dist/components/RoomComponents/styled/LastMessageVideo.d.ts +0 -6
  133. package/dist/components/SortDropDown/index.d.ts +0 -8
@@ -28,6 +28,27 @@ export declare const serializeAttachments: (attachments: IAttachment[]) => strin
28
28
  * predates this feature - and every message from a client that does not
29
29
  * speak it - takes the second path.
30
30
  */
31
- export declare const getMessageAttachments: (message?: Pick<IMessage, "attachments" | "location" | "locationPreview" | "mimetype" | "originalName" | "fileName" | "size"> | null) => IAttachment[];
31
+ export declare const getMessageAttachments: (message?: (Pick<IMessage, "attachments" | "location" | "locationPreview" | "mimetype" | "originalName" | "fileName" | "size"> & {
32
+ duration?: unknown;
33
+ mediaDuration?: unknown;
34
+ }) | null) => IAttachment[];
35
+ /**
36
+ * True when the message carries a file: flagged `isMediafile`, or (older
37
+ * senders that never set the flag) with a real file URL or attachment. The
38
+ * one test every surface uses for "is this a media bubble".
39
+ */
40
+ export declare const isMediaMessage: (message?: (Partial<Pick<IMessage, "isDeleted" | "isMediafile" | "attachments">> & {
41
+ location?: unknown;
42
+ mimetype?: unknown;
43
+ originalName?: unknown;
44
+ fileName?: unknown;
45
+ locationPreview?: unknown;
46
+ }) | null) => boolean;
47
+ /**
48
+ * Maps one object from POST /files/ `results` to the attachment a sent
49
+ * message carries. Field names here follow the API's own spelling
50
+ * (`originalname`, `filename`), not the wire attribute names.
51
+ */
52
+ export declare const toUploadedAttachment: (raw: any) => IAttachment;
32
53
  /** Name to show for an attachment, with the URL tail as the last resort. */
33
54
  export declare const getAttachmentName: (attachment: IAttachment) => string;
@@ -0,0 +1,9 @@
1
+ export declare const FAILED_TTL_MS = 60000;
2
+ export declare const avatarIdentity: (url: string) => string;
3
+ /** The URL that already loaded for this file (any token), if any. */
4
+ export declare const getLoadedAvatarUrl: (url: string) => string | undefined;
5
+ export declare const markAvatarLoaded: (url: string) => void;
6
+ export declare const markAvatarFailed: (url: string) => void;
7
+ export declare const hasAvatarFailed: (url: string) => boolean;
8
+ /** Test helper. */
9
+ export declare const resetAvatarCache: () => void;
@@ -0,0 +1,63 @@
1
+ import { IMentionSpan } from '../types/types';
2
+ /**
3
+ * The text typed with a set of attachments, sent as part of the media
4
+ * message itself (never as a separate text stanza).
5
+ *
6
+ * `mentions` index into `text`, the same way a text message's mention spans
7
+ * index into its body.
8
+ */
9
+ export interface CaptionInput {
10
+ text: string;
11
+ mentions?: IMentionSpan[];
12
+ }
13
+ export interface NormalizedCaption {
14
+ caption: string;
15
+ mentions?: IMentionSpan[];
16
+ }
17
+ /**
18
+ * Mention spans for a caption, checked against the caption text: a span that
19
+ * no longer reads "@Name" at its offset is dropped. Undefined when nothing is
20
+ * left, so a caption without mentions carries no empty attribute.
21
+ */
22
+ export declare const normalizeCaptionMentions: (value: unknown, caption: string) => IMentionSpan[] | undefined;
23
+ /**
24
+ * Turns what the composer typed into the caption that travels with the
25
+ * media. Undefined for blank text, which means "no caption".
26
+ */
27
+ export declare const normalizeCaptionInput: (input?: CaptionInput | null) => NormalizedCaption | undefined;
28
+ /** What a media message's text is, and the @-mention spans that index into it. */
29
+ export interface ResolvedCaption {
30
+ text: string;
31
+ mentions?: IMentionSpan[];
32
+ }
33
+ export interface CaptionOptions {
34
+ /**
35
+ * The caller already knows this is a media message (a thin `lastMessage`
36
+ * that only says so by its id). Otherwise it is read off the message.
37
+ */
38
+ isMedia?: boolean;
39
+ /** The message is sealed: only a caption that came out of the envelope counts. */
40
+ sealed?: boolean;
41
+ /** Extra names of the sender, which a body may merely echo. */
42
+ senderNames?: string[];
43
+ }
44
+ /**
45
+ * The text of a media message, from whichever format carried it. The one
46
+ * place that knows the formats, so every surface (bubble, room list preview,
47
+ * notification, reply quote, copy) agrees:
48
+ *
49
+ * 1. `caption` (+ `captionMentions`): what this SDK sends. The only source
50
+ * for a sealed message, whose caption comes out of the envelope.
51
+ * 2. The caption under another name on `<data>` (`captionText`, `text`,
52
+ * `message`, `description`, `title`) from other clients.
53
+ * 3. A real `body`: older clients and other SDKs put the text in the
54
+ * `<body>` of the media message itself. "media", the file's name or URL,
55
+ * the sender's own name or e-mail and generated labels are not text.
56
+ *
57
+ * Never derived for a sealed message, and never for a message with no file.
58
+ */
59
+ export declare const resolveMessageCaption: (message?: object | null, options?: CaptionOptions) => ResolvedCaption | undefined;
60
+ /** The caption a message carries, or undefined when it has none. */
61
+ export declare const getMessageCaption: (message?: object | null, options?: CaptionOptions) => string | undefined;
62
+ /** The mention spans of `getMessageCaption`'s text. */
63
+ export declare const getMessageCaptionMentions: (message?: object | null, options?: CaptionOptions) => IMentionSpan[] | undefined;
@@ -1,3 +1,3 @@
1
1
  import { IMessage, RoomMember } from '../types/types';
2
- export declare const checkUniqueUsers: (messages: IMessage[]) => Promise<RoomMember[]>;
3
- export declare const checkSingleUser: (usersSet: Record<string, RoomMember>, xmppUsername: string) => Promise<RoomMember>;
2
+ export declare const checkUniqueUsers: (messages: IMessage[]) => any;
3
+ export declare const checkSingleUser: (usersSet: Record<string, RoomMember>, xmppUsername: string) => Promise<any>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Hands "focus the message input" from the room list to the composer.
3
+ *
4
+ * The room list and the composer live in different subtrees, and the
5
+ * composer may only mount after the switch (or re-render later), so a plain
6
+ * callback would race it. Instead the request is parked here until the
7
+ * mounted composer for that room claims it. A claim is one-shot, so a single
8
+ * click focuses exactly once.
9
+ */
10
+ type Listener = () => void;
11
+ /** Ask the composer for `roomJid` to take focus. Wakes mounted composers. */
12
+ export declare const requestComposerFocus: (roomJid: string | null | undefined) => void;
13
+ /** Mounted composers subscribe here to learn that a request arrived. */
14
+ export declare const subscribeComposerFocus: (listener: Listener) => (() => void);
15
+ /**
16
+ * Claims a pending request for `roomJid`. Returns true exactly once per
17
+ * request, and only for the room the request was made for.
18
+ */
19
+ export declare const takeComposerFocusRequest: (roomJid: string | null | undefined) => boolean;
20
+ /** Test helper: drops any parked request and all listeners. */
21
+ export declare const resetComposerFocusForTests: () => void;
22
+ export {};
@@ -1,2 +1,2 @@
1
1
  import { IMessage } from '../types/types';
2
- export declare const createMainMessageForThread: (message: IMessage) => string;
2
+ export declare const createMainMessageForThread: (message: IMessage, t?: (key: string, vars?: Record<string, string | number>) => string) => string;
@@ -0,0 +1,10 @@
1
+ import { RoomMember } from '../types/types';
2
+ /** Stable identity of a member: normalised xmppUsername local part, else _id. */
3
+ export declare const memberKey: (m: Partial<RoomMember> | null | undefined) => string;
4
+ /**
5
+ * Drops repeated members (keeps the first occurrence, order preserved).
6
+ * The room directory endpoint can return the same user on several pages;
7
+ * duplicates would produce duplicate React keys and inflated counts.
8
+ * Members without any identity are dropped.
9
+ */
10
+ export declare const dedupeMembers: <T extends Partial<RoomMember>>(list: ReadonlyArray<T> | null | undefined) => T[];
@@ -0,0 +1,19 @@
1
+ export type DownloadResult = 'blob' | 'direct' | 'http-error';
2
+ export interface DownloadDeps {
3
+ fetchImpl?: typeof fetch;
4
+ /** Hands a blob to the browser as a download. */
5
+ saveBlob?: (blob: Blob, fileName: string) => void;
6
+ /** Hands a plain URL to the browser as a download / new tab. */
7
+ saveUrl?: (url: string, fileName: string) => void;
8
+ }
9
+ /**
10
+ * Saves a media file. The blob route is preferred (it keeps the user's file
11
+ * name), but it needs CORS: <img>/<video>/<a> to a host such as
12
+ * secure-files.* never did, so a host without Access-Control-Allow-Origin
13
+ * makes fetch() reject with a bare TypeError. That says nothing about the
14
+ * file, so fall back to handing the URL to the browser directly.
15
+ *
16
+ * A reachable server that answers with an error status is different: its
17
+ * body is an error page, not the file, so nothing is saved.
18
+ */
19
+ export declare function downloadFile(url: string, fileName: string, deps?: DownloadDeps): Promise<DownloadResult>;
@@ -11,7 +11,24 @@ export type FileKind = 'image' | 'video' | 'audio' | 'pdf' | 'file';
11
11
  /** Lowercase extension without the dot; '' when the name carries none. */
12
12
  export declare const getFileExtension: (name?: string) => string;
13
13
  export declare const isPdfFile: (mimetype?: string, name?: string) => boolean;
14
+ /**
15
+ * SVG is deliberately not an image here. It does not render reliably as a
16
+ * message picture, can be blocked by the host's CSP, and is a script
17
+ * injection vector when opened directly. It is treated as a plain file.
18
+ */
19
+ export declare const isSvgFile: (mimetype?: string, name?: string) => boolean;
20
+ /** True when the file renders as a picture (never for SVG). */
21
+ export declare const isRasterImage: (mimetype?: string, name?: string) => boolean;
22
+ /**
23
+ * The mimetype a file name's extension stands for, for the pictures, videos,
24
+ * audio and PDFs a bubble can show. Undefined for anything else.
25
+ */
26
+ export declare const inferMimeFromName: (name?: string) => string | undefined;
27
+ /** A mimetype that says nothing: empty, or the storage's "could not sniff". */
28
+ export declare const isGenericMime: (mimetype?: string) => boolean;
14
29
  export declare const getFileKind: (mimetype?: string, name?: string) => FileKind;
15
30
  export declare const formatFileSize: (sizeInBytes?: string | number) => string;
16
31
  /** Middle-truncates the base name, always keeping the extension visible. */
17
32
  export declare const formatFileName: (name: string, maxLength: number) => string;
33
+ /** An animated-picture candidate: image/gif, or a name or URL ending in .gif. */
34
+ export declare const isGifFile: (mimetype?: string, nameOrUrl?: string) => boolean;
@@ -0,0 +1,25 @@
1
+ import { IConfig } from '../types/types';
2
+ export type HistoryPreloadMode = 'staged' | 'all' | 'off';
3
+ export interface ResolvedHistoryPreload {
4
+ /**
5
+ * 'legacy' only when a host explicitly pinned
6
+ * `historyQoS.stagedPreloadEnabled: false`: the old cache catch-up path
7
+ * (updateMessagesTillLast). Everything else resolves to a public mode.
8
+ */
9
+ mode: HistoryPreloadMode | 'legacy';
10
+ /** Rooms (by recent activity) the background sweep covers. 0 = no cap. */
11
+ topRooms: number;
12
+ concurrency: number;
13
+ firstPassSize: number;
14
+ secondPassSize: number;
15
+ }
16
+ export declare const DEFAULT_PRELOAD_TOP_ROOMS = 8;
17
+ export declare const DEFAULT_PRELOAD_CONCURRENCY = 3;
18
+ export declare const TEASER_ROOM_FACTOR = 4;
19
+ /**
20
+ * One place that turns `config.historyPreload` (and the older
21
+ * `historyQoS.stagedPreload*` / `preloadTopKRooms` names, still honoured)
22
+ * into the numbers the preload sweeps use. Staged preload is the default:
23
+ * the sweep covers the top N rooms, every other room loads when it is opened.
24
+ */
25
+ export declare const resolveHistoryPreloadConfig: (config?: Pick<IConfig, "historyPreload" | "historyQoS"> | null) => ResolvedHistoryPreload;
@@ -1,4 +1,5 @@
1
1
  import { default as XmppClient } from '../networking/xmppClient';
2
+ import { IRoom } from '../types/types';
2
3
  interface HistoryPreloadSchedulerOptions {
3
4
  client: XmppClient;
4
5
  signal?: AbortSignal;
@@ -10,6 +11,8 @@ interface HistoryPreloadSchedulerOptions {
10
11
  defaultRoomJids?: string[];
11
12
  forceReload?: boolean;
12
13
  completionState?: 'done' | 'partial';
14
+ skipApiPreview?: boolean;
13
15
  }
16
+ export declare const hasApiPreview: (room?: IRoom) => boolean;
14
17
  export declare const runHistoryPreloadScheduler: (options: HistoryPreloadSchedulerOptions) => Promise<void>;
15
18
  export default runHistoryPreloadScheduler;
@@ -1,4 +1,5 @@
1
1
  import { IMessage } from '../types/types';
2
+ export declare function deepMerge(target: any, source: any): any;
2
3
  export declare function insertMessageWithDelimiter(roomMessages: Partial<IMessage>[], message: IMessage, lastViewedTimestamp: {
3
4
  toString: () => string;
4
5
  }): void;
@@ -0,0 +1,9 @@
1
+ import { IConfig } from '../types/types';
2
+ /**
3
+ * The single rule for message search. It is opt-in: the host must set
4
+ * `enableMessageSearch` AND provide an `appId` (the archive endpoint is scoped
5
+ * by app). A leftover `disableMessageSearch: true` still wins as a hard off.
6
+ * Every entry point (header button, profile entry, chat-list matches, the
7
+ * Ctrl/Cmd+F shortcut, the modal itself) goes through this helper.
8
+ */
9
+ export declare const isMessageSearchEnabled: (config?: Pick<IConfig, "enableMessageSearch" | "disableMessageSearch" | "appId"> | null) => boolean;
@@ -0,0 +1,56 @@
1
+ import { IMessage } from '../types/types';
2
+ /** How long a jump request may stay pending before it is dropped. */
3
+ export declare const JUMP_TTL_MS = 60000;
4
+ /**
5
+ * Index of the message a jump names.
6
+ *
7
+ * By id first (message.id is the MAM stanza id for history, xmppId the client
8
+ * message id). When the request has no id at all, which is common for archive
9
+ * rows, by content: same text, sent within a few seconds of the same time.
10
+ */
11
+ export declare const findMessageIndex: (messages: IMessage[], ids: string[], content?: {
12
+ createdAt?: string;
13
+ body?: string;
14
+ }) => number;
15
+ /**
16
+ * Where a jump to a thread REPLY lands, and the thread panel's parent message
17
+ * when that message is not in the room's live list.
18
+ *
19
+ * Replies never appear in the main list (it hides them unless they are also
20
+ * shown in the channel), so a jump to one has to open its parent's thread and
21
+ * highlight the reply there. Two things have to be remembered for that:
22
+ * - which list owns the jump: `at` is the pending request this entry serves,
23
+ * `parentId` the thread that owns it. The main list stays inert for a jump
24
+ * that has an owner thread, and a thread list stays inert for any jump that
25
+ * is not its own (see useJumpToMessage);
26
+ * - the parent and the replies when they exist only in a jump window or came
27
+ * from a separate fetch: the thread panel reads them from here, so it does
28
+ * not depend on the window that found them still being on screen.
29
+ *
30
+ * Kept outside redux on purpose: the room slice's thread flag lives on
31
+ * messages of the live list, which is exactly what these messages are not in.
32
+ */
33
+ export interface JumpThreadState {
34
+ roomJID: string;
35
+ parentId: string;
36
+ /** The pending request this entry serves; null for a thread opened by hand. */
37
+ at: number | null;
38
+ /** Set only when the parent is not in the room's live messages. */
39
+ parent: IMessage | null;
40
+ /** Replies known to belong to the thread beyond the live list. */
41
+ replies: IMessage[];
42
+ }
43
+ export declare const getJumpThread: () => JumpThreadState | null;
44
+ export declare const setJumpThread: (next: JumpThreadState | null) => void;
45
+ export declare const clearJumpThread: () => void;
46
+ export declare const subscribeJumpThread: (listener: () => void) => (() => void);
47
+ export declare const useJumpThread: () => JumpThreadState | null;
48
+ /** The thread that owns the jump `at`, or null when the main list does. */
49
+ export declare const ownerThreadFor: (thread: JumpThreadState | null, at: number | undefined) => string | null;
50
+ /**
51
+ * The parent id of a message that is a thread reply and nothing else, or null:
52
+ * a reply also shown in the channel is in the main list, so it needs no thread.
53
+ */
54
+ export declare const replyParentId: (message: IMessage) => string | null;
55
+ /** `a` then `b`, one entry per message id, oldest first. */
56
+ export declare const mergeById: (a: IMessage[], b: IMessage[]) => IMessage[];
@@ -0,0 +1,73 @@
1
+ import { HistoryWindowPage } from '../networking/xmppClient';
2
+ import { JumpWindow } from '../roomStore/roomsSlice';
3
+ import { IMessage } from '../types/types';
4
+ /** Messages shown above / below the target when a window opens (tunable). */
5
+ export declare const JUMP_WINDOW_BEFORE = 10;
6
+ export declare const JUMP_WINDOW_AFTER = 10;
7
+ /** Page size for loading more once the reader scrolls toward either edge. */
8
+ export declare const JUMP_WINDOW_PAGE = 20;
9
+ /** The slice of the XMPP client the window helpers need. */
10
+ export interface HistoryWindowClient {
11
+ getHistoryWindow: (chatJID: string, max: number, cursor: {
12
+ before?: number;
13
+ after?: number;
14
+ start?: string;
15
+ end?: string;
16
+ }) => Promise<HistoryWindowPage>;
17
+ }
18
+ export type JumpWindowResult =
19
+ /** The window around the target is ready to be shown. */
20
+ {
21
+ status: 'found';
22
+ window: JumpWindow;
23
+ }
24
+ /** The server answered, and the target is not in the archive. */
25
+ | {
26
+ status: 'missing';
27
+ }
28
+ /** No usable id, or a request failed: use the paging path instead. */
29
+ | {
30
+ status: 'unavailable';
31
+ };
32
+ /**
33
+ * How far apart the archive's timestamp and the transcript's may be for the
34
+ * same message. They are recorded by different components, so they are not
35
+ * identical, but never more than a moment apart.
36
+ */
37
+ export declare const CONTENT_MATCH_WINDOW_MS = 5000;
38
+ /** What a hit with no archive id can be located by. */
39
+ export interface JumpContent {
40
+ createdAt?: string;
41
+ body?: string;
42
+ }
43
+ export declare const mamIdCandidates: (ids: string[]) => string[];
44
+ /**
45
+ * Fetches about JUMP_WINDOW_BEFORE messages before the target, the target and
46
+ * about JUMP_WINDOW_AFTER after it: two MAM queries in parallel (the target
47
+ * itself comes with the "after" page, asked from one microsecond before it).
48
+ * Nothing is written to the store here.
49
+ */
50
+ export declare function loadJumpWindow(client: HistoryWindowClient, roomJID: string, ids: string[], content?: JumpContent): Promise<JumpWindowResult>;
51
+ /**
52
+ * For a hit with no usable archive id (older archive documents carry neither a
53
+ * stanza id nor a message id): ask the server for the rows within a few
54
+ * seconds of its timestamp (MAM start/end filter), find the one with the same
55
+ * text, and continue exactly as the id-based window does with that row's id.
56
+ * The server's answer is final: 'missing' only when it said the range holds no
57
+ * such row.
58
+ */
59
+ export declare function loadJumpWindowByTime(client: HistoryWindowClient, roomJID: string, content: JumpContent): Promise<JumpWindowResult>;
60
+ export interface OlderWindowPage {
61
+ messages: IMessage[];
62
+ olderCursor: number | null;
63
+ hasOlder: boolean;
64
+ }
65
+ export interface NewerWindowPage {
66
+ messages: IMessage[];
67
+ newerCursor: number | null;
68
+ hasNewer: boolean;
69
+ }
70
+ /** The page above the window; null when the request failed. */
71
+ export declare function loadOlderWindowPage(client: HistoryWindowClient, window: JumpWindow): Promise<OlderWindowPage | null>;
72
+ /** The page below the window; null when the request failed. */
73
+ export declare function loadNewerWindowPage(client: HistoryWindowClient, window: JumpWindow): Promise<NewerWindowPage | null>;
@@ -0,0 +1,55 @@
1
+ import { LastMessage } from '../types/types';
2
+ /**
3
+ * What the room list draws for a media last message.
4
+ *
5
+ * One derivation for every source of a last message: the live stanza that
6
+ * updates roomsSlice, the history loaded from MAM, and the flattened
7
+ * `lastMessage` the backend embeds in `GET /chats/my`. All three reach the row
8
+ * as the same loose message object, and since multi-attachment messages the
9
+ * file fields may sit in `attachments` only, with no top level `location` or
10
+ * `mimetype`, so the row reads through getMessageAttachments like the bubble.
11
+ */
12
+ export type MediaPreviewKind = 'image' | 'video' | 'voice' | 'audio' | 'file'
13
+ /** Media of unknown type: a thin backend payload that only says "media". */
14
+ | 'attachment' | 'sealed';
15
+ export interface MediaPreview {
16
+ kind: MediaPreviewKind;
17
+ /** How many attachments the message carries (at least 1). */
18
+ count: number;
19
+ /** Thumbnail source, before the viewer's file token is appended. */
20
+ thumbUrl?: string;
21
+ /** The single file's name, for the file kind. */
22
+ fileName?: string;
23
+ /** "0:12" for a voice message or audio file, when the duration is known. */
24
+ durationText?: string;
25
+ /** The text the sender typed next to the media, if any. */
26
+ caption?: string;
27
+ }
28
+ type PreviewSource = Partial<LastMessage> & {
29
+ attachments?: unknown;
30
+ fileName?: string;
31
+ location?: string;
32
+ /** The caption the sender typed with the files, carried on the message. */
33
+ caption?: unknown;
34
+ duration?: unknown;
35
+ mediaDuration?: unknown;
36
+ };
37
+ /** True when the last message should render as media rather than text. */
38
+ export declare const hasMediaPreview: (message?: PreviewSource | null) => boolean;
39
+ /**
40
+ * `sealed` is true for an e2ee room or a message flagged `clientEncrypted`.
41
+ * Sealed content never gets a thumbnail: the server holds ciphertext, so the
42
+ * row shows the icon and the label only.
43
+ */
44
+ export declare const deriveMediaPreview: (message?: PreviewSource | null, options?: {
45
+ sealed?: boolean;
46
+ senderNames?: string[];
47
+ }) => MediaPreview | undefined;
48
+ /**
49
+ * Which message the room row previews: the newest real message already in the
50
+ * room's `messages[]` (a live stanza or history), else the backend's
51
+ * `lastMessage` seed. Entries with neither text nor media (markers, receipts,
52
+ * state-only stanzas) are skipped.
53
+ */
54
+ export declare const pickPreviewSource: <T extends PreviewSource>(messages: T[] | undefined, lastMessage?: T | null) => T | undefined;
55
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Runs `worker` over `items` with at most `limit` calls in flight, and
3
+ * resolves with the results in INPUT order, whatever order the calls finish
4
+ * in. Workers that throw are reported as rejected entries instead of
5
+ * aborting the rest of the batch, so one failed upload never stops its
6
+ * siblings.
7
+ */
8
+ export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, worker: (item: T, index: number) => Promise<R>): Promise<PromiseSettledResult<R>[]>;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Pure sizing helpers for in-bubble media. Kept free of React and the DOM so
3
+ * the rules (clamping, never upscaling, duration formatting) are testable.
4
+ */
5
+ /**
6
+ * Reference box a solo picture is fitted into. It is the largest the
7
+ * responsive media width ever gets (see MEDIA_WIDTH_CSS), so a picture is
8
+ * never upscaled past what the widest pane could show; the real, smaller cap
9
+ * for the current pane is applied in CSS from the shared width variable.
10
+ */
11
+ export declare const SOLO_IMAGE_MAX_WIDTH_PX = 560;
12
+ export declare const SOLO_IMAGE_MAX_HEIGHT_PX = 784;
13
+ /** Box used while a solo image's dimensions are still unknown: 4:3. */
14
+ export declare const SOLO_DEFAULT_BOX: {
15
+ readonly width: 560;
16
+ readonly height: 420;
17
+ };
18
+ /** Box used while a solo video's dimensions are unknown: 16:9. */
19
+ export declare const SOLO_VIDEO_DEFAULT_BOX: {
20
+ readonly width: 560;
21
+ readonly height: 315;
22
+ };
23
+ /**
24
+ * The ONE width of the media block of a message: the album grid, a solo
25
+ * picture or video and the file cards under them all share it.
26
+ *
27
+ * `--ethora-pane-w` is the width of the message list pane (not the
28
+ * viewport), written by the message list's ResizeObserver, so a chat embedded
29
+ * in a narrow host sizes itself to that host. A container query would do the
30
+ * same, but `container-type` contains layout, which turns the list into the
31
+ * containing block of the context menu's `position: fixed` overlay.
32
+ *
33
+ * pane 390 -> 300 (the bubble then clips it to its own width)
34
+ * pane 700 -> 340
35
+ * pane 1000 -> 430
36
+ * pane 1400 -> 550
37
+ * pane 1600+ -> 560
38
+ */
39
+ export declare const MEDIA_WIDTH_VAR = "--ethora-media-w";
40
+ export declare const PANE_WIDTH_VAR = "--ethora-pane-w";
41
+ export declare const MEDIA_WIDTH_RULE = "clamp(300px, calc(var(--ethora-pane-w, 800px) * 0.3 + 130px), 560px)";
42
+ /** Use inside a stylesheet or inline style; falls back when the list did not set it. */
43
+ export declare const MEDIA_WIDTH_CSS = "var(--ethora-media-w, 340px)";
44
+ /** A lone file card is capped so it does not become a giant bar. */
45
+ export declare const FILE_ONLY_MAX_WIDTH_PX = 360;
46
+ /** Tallest a solo picture gets, relative to the media width. */
47
+ export declare const SOLO_MAX_HEIGHT_FACTOR = 1.4;
48
+ /** CSS length of the tallest a solo picture may be: 60vh, or 1.4 x the media width. */
49
+ export declare const SOLO_MAX_HEIGHT_CSS = "min(60vh, calc(1.4 * var(--ethora-media-w, 340px)))";
50
+ export interface MediaBox {
51
+ width: number;
52
+ height: number;
53
+ }
54
+ /**
55
+ * Fit a natural size inside a max box, keeping its aspect ratio. Never
56
+ * upscales: a picture smaller than the box keeps its natural size. Returns
57
+ * null when either dimension is unknown or not positive.
58
+ */
59
+ export declare const fitMediaBox: (naturalWidth: number | undefined, naturalHeight: number | undefined, maxWidth?: number, maxHeight?: number) => MediaBox | null;
60
+ /** A picture whose longest side is below this is shown at its natural size. */
61
+ export declare const SOLO_TINY_MAX_SIDE_PX = 120;
62
+ /** Smallest frame drawn around a tiny picture. */
63
+ export declare const SOLO_MIN_FRAME_PX = 64;
64
+ /** Beyond this long:short ratio a picture is cropped rather than letterboxed. */
65
+ export declare const SOLO_EXTREME_RATIO = 3;
66
+ /** Shorter side of the box of an extreme-ratio picture. */
67
+ export declare const SOLO_MIN_SHORT_SIDE_PX = 96;
68
+ /**
69
+ * How the picture sits in its box.
70
+ * fit - same aspect ratio as the box, fills it.
71
+ * cover - extreme ratio: fills the box and is gently cropped from the centre.
72
+ * natural - smaller than its frame: drawn at its own size, centred on a tile.
73
+ */
74
+ export type SoloMode = 'fit' | 'cover' | 'natural';
75
+ export interface SoloBox {
76
+ /** The frame (stage) size in CSS px. */
77
+ width: number;
78
+ height: number;
79
+ /** The drawn picture size; equals the frame unless `mode` is natural. */
80
+ imageWidth: number;
81
+ imageHeight: number;
82
+ mode: SoloMode;
83
+ }
84
+ /**
85
+ * The size policy of a single picture in a bubble.
86
+ *
87
+ * - Inside the max box (560 x 784 by default) at its own aspect ratio,
88
+ * never upscaled. CSS narrows it to the pane's media width and height.
89
+ * - Tiny (longest side < 120): natural size on a tile of at least 64px.
90
+ * - Extreme ratio (beyond 3:1 or 1:3): the shorter side is lifted to 96px
91
+ * and the picture is cover-cropped (the viewer shows all of it).
92
+ *
93
+ * `ratioOnly` is for a size read from a downscaled preview: only its aspect
94
+ * ratio is trusted, so the box is scaled up to the real maximum instead of
95
+ * staying thumbnail-sized (which would jump when the full file arrives).
96
+ * Returns null while a dimension is unknown.
97
+ */
98
+ export declare const computeSoloBox: (naturalWidth: number | undefined, naturalHeight: number | undefined, options?: {
99
+ ratioOnly?: boolean;
100
+ maxWidth?: number;
101
+ maxHeight?: number;
102
+ }) => SoloBox | null;
103
+ /** Parses a dimension that may arrive as a number or a numeric string. */
104
+ export declare const parseDimension: (value: unknown) => number | undefined;
105
+ /**
106
+ * Formats a duration in seconds as m:ss, or h:mm:ss past one hour. Returns
107
+ * an empty string when the value is missing or not a positive number, so
108
+ * callers can skip the chip entirely.
109
+ */
110
+ export declare const formatDuration: (seconds: string | number | undefined) => string;
@@ -0,0 +1,39 @@
1
+ import { IAttachment } from '../types/types';
2
+ /**
3
+ * How a message's files share one column:
4
+ * - album: two or more pictures or videos, one grid; cards stretch to it.
5
+ * - solo: one picture or video; the block shrink-wraps it (at most the media
6
+ * width) and any cards stretch to that.
7
+ * - files: documents only; the media width, capped so a lone card is no bar.
8
+ * - plain: voice notes only, which keep their own width.
9
+ */
10
+ export type MediaBlockMode = 'album' | 'solo' | 'files' | 'plain';
11
+ export interface AttachmentItem {
12
+ attachment: IAttachment;
13
+ /** Position in the message's attachments, used for retry. */
14
+ index: number;
15
+ }
16
+ export interface AttachmentPartition {
17
+ /** Pictures and videos rendered as one album grid (empty for a lone one). */
18
+ tiles: AttachmentItem[];
19
+ /** Everything else, rendered one after another below the grid. */
20
+ rows: AttachmentItem[];
21
+ }
22
+ export declare const attachmentKind: (attachment: IAttachment) => import('./fileKind').FileKind;
23
+ /**
24
+ * Splits a message's files into the album and the rows below it. Pictures and
25
+ * videos always come first, in the order they were picked; the remaining files
26
+ * (documents, svg, pdf, audio) follow, also in their picked order. A lone
27
+ * picture is not an album: it stays a row, but it still leads the message.
28
+ */
29
+ export declare const partitionAttachments: (attachments: IAttachment[]) => AttachmentPartition;
30
+ /** The block mode for a message's files, from the partition it renders. */
31
+ export declare const getMediaBlockMode: (attachments: IAttachment[]) => MediaBlockMode;
32
+ /**
33
+ * Whether the time can sit as a glass pill over the media (Telegram style):
34
+ * only when the bubble is pictures and videos and nothing else. A caption or
35
+ * any card or audio row puts the time on its own line at the bottom right.
36
+ */
37
+ export declare const usesMediaOverlayTime: (attachments: IAttachment[], options?: {
38
+ hasCaption?: boolean;
39
+ }) => boolean;
@@ -0,0 +1,10 @@
1
+ import { getMessageAttachments } from './attachments';
2
+ type Translate = (key: string, vars?: Record<string, string | number>) => string;
3
+ /**
4
+ * What to call a media message's files when there is no text to show:
5
+ * "Photo", "Video", "Voice message", "File", "3 photos", "2 attachments".
6
+ * Empty when the message carries no file. The kind comes from the normalised
7
+ * attachment, so a file sent with no mimetype is still a photo by its name.
8
+ */
9
+ export declare const getMediaLabel: (message: Parameters<typeof getMessageAttachments>[0], t: Translate) => string;
10
+ export {};
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Retry hooks for media messages whose uploads did not all land.
3
+ *
4
+ * The send closure lives inside useSendMessage, but the bubble that shows
5
+ * "Retry" is somewhere else in the tree and has no access to it. The send
6
+ * registers its retry runner under the message id while the message is
7
+ * unsent, and the bubble looks it up here. The runner is removed as soon as
8
+ * the message is delivered.
9
+ */
10
+ export type MediaRetryRunner = (indices?: number[]) => Promise<boolean>;
11
+ export declare const registerMediaRetry: (messageId: string, runner: MediaRetryRunner) => void;
12
+ export declare const unregisterMediaRetry: (messageId: string) => void;
13
+ export declare const hasMediaRetry: (messageId: string) => boolean;
14
+ /**
15
+ * Re-runs the uploads still missing for `messageId` (only `index` / `indices`
16
+ * when given), then sends the message once every file is in. Resolves to
17
+ * false when nothing is registered or the attempt did not deliver.
18
+ */
19
+ export declare const retryMediaSend: (messageId: string, index?: number | number[]) => Promise<boolean>;
20
+ /**
21
+ * Asks for one file of `messageId` to be retried. Requests made in the same
22
+ * tick are merged into one run: the runner takes a single run at a time, so
23
+ * a separate call per tile would drop every retry after the first.
24
+ */
25
+ export declare const requestMediaRetry: (messageId: string, index: number) => void;