@ethora/chat-component 26.9.8 → 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 (101) hide show
  1. package/README.md +1 -1
  2. package/dist/{ChatProfileModal-3UH0H5Ec.js → ChatProfileModal-Bf5jGPBZ.js} +44 -44
  3. package/dist/FilePreviewModal-Bwx0RZZw.js +1763 -0
  4. package/dist/{ManageDataModal-BblTuE9g.js → ManageDataModal-Dhkp0LWH.js} +17 -17
  5. package/dist/{MarkdownBody-RMEx8yW2.js → MarkdownBody-_sUhW2O8.js} +1 -1
  6. package/dist/{MessageSearchModal-CrYR2EH8.js → MessageSearchModal-rwwWrILl.js} +40 -40
  7. package/dist/{PublicChatsModal-Bi5r4mrV.js → PublicChatsModal-Byso6_vt.js} +19 -19
  8. package/dist/{SideDrawer-CDAoobvX.js → SideDrawer-CQPOf0ek.js} +35 -35
  9. package/dist/{UserProfileModal-4Frgvam-.js → UserProfileModal-Vj2kICvc.js} +10 -10
  10. package/dist/{UserSettingsModal-C-mb-9cb.js → UserSettingsModal-CioX4ZIV.js} +5 -5
  11. package/dist/{VideoCallOverlay-92dFLb9U.js → VideoCallOverlay-DodAkiam.js} +3824 -3708
  12. package/dist/{VisibilityModal-zIAV6dAP.js → VisibilityModal-CIl7rkBM.js} +11 -11
  13. package/dist/assets/icons.d.ts +2 -3
  14. package/dist/components/Files/fileCategory.d.ts +4 -2
  15. package/dist/components/MainComponents/AlbumGrid.d.ts +22 -0
  16. package/dist/components/MainComponents/AttachmentList.d.ts +6 -6
  17. package/dist/components/MainComponents/ImageUnavailable.d.ts +21 -0
  18. package/dist/components/MainComponents/MediaUploadOverlay.d.ts +31 -0
  19. package/dist/components/MainComponents/MessageContainer.d.ts +3 -0
  20. package/dist/components/MessageBubble/MediaCaption.d.ts +9 -0
  21. package/dist/components/MessageBubble/MessageAvatarPhoto.d.ts +17 -0
  22. package/dist/components/MessageBubble/MessageReaction.d.ts +6 -0
  23. package/dist/components/MessageBubble/QuickReplies.d.ts +2 -0
  24. package/dist/components/Modals/FilePreviewModal/FilePreviewModal.d.ts +8 -10
  25. package/dist/components/Modals/FilePreviewModal/ImageLightbox.d.ts +4 -0
  26. package/dist/components/Modals/FilePreviewModal/PdfView.d.ts +4 -0
  27. package/dist/components/Modals/FilePreviewModal/ViewerChrome.d.ts +64 -0
  28. package/dist/components/RoomComponents/LastMessageItem.d.ts +2 -0
  29. package/dist/components/RoomComponents/styled/LastMessageMedia.d.ts +19 -0
  30. package/dist/components/RoomComponents/styled/LastTextMessage.d.ts +2 -2
  31. package/dist/components/RoomComponents/styled/StyledRoomComponents.d.ts +10 -3
  32. package/dist/components/VideoCalls/VideoCallSession.d.ts +13 -0
  33. package/dist/components/styled/FileCard.d.ts +20 -0
  34. package/dist/components/styled/MediaSurface.d.ts +50 -0
  35. package/dist/components/styled/MessageImage.d.ts +9 -10
  36. package/dist/components/styled/SendInput.d.ts +12 -2
  37. package/dist/components/styled/StyledComponents.d.ts +69 -2
  38. package/dist/components/styled/StyledInputComponents/MediaComponents.d.ts +36 -24
  39. package/dist/components/styled/UnsupportedType.d.ts +1 -0
  40. package/dist/components/styled/VideoMessage.d.ts +11 -6
  41. package/dist/e2ee/callKey.d.ts +32 -0
  42. package/dist/e2ee/fileEnvelope.d.ts +16 -5
  43. package/dist/e2ee/omemo.d.ts +18 -0
  44. package/dist/firebase-messaging-sw.js +20 -3
  45. package/dist/{handleCopyClick-BtXos0AJ.js → handleCopyClick-DGaZhy_u.js} +1 -1
  46. package/dist/helpers/albumLayout.d.ts +53 -0
  47. package/dist/helpers/attachments.d.ts +22 -1
  48. package/dist/helpers/avatarImageCache.d.ts +9 -0
  49. package/dist/helpers/caption.d.ts +63 -0
  50. package/dist/helpers/composerFocus.d.ts +22 -0
  51. package/dist/helpers/createMainMessageForThread.d.ts +1 -1
  52. package/dist/helpers/downloadFile.d.ts +19 -0
  53. package/dist/helpers/fileKind.d.ts +17 -0
  54. package/dist/helpers/lastMessagePreview.d.ts +55 -0
  55. package/dist/helpers/mapWithConcurrency.d.ts +8 -0
  56. package/dist/helpers/mediaBox.d.ts +110 -0
  57. package/dist/helpers/mediaBubble.d.ts +39 -0
  58. package/dist/helpers/mediaLabel.d.ts +10 -0
  59. package/dist/helpers/mediaSendRetry.d.ts +25 -0
  60. package/dist/helpers/messageRuns.d.ts +10 -0
  61. package/dist/helpers/repairFilenameEncoding.d.ts +11 -0
  62. package/dist/helpers/roomImageGallery.d.ts +34 -0
  63. package/dist/helpers/safeUrl.d.ts +15 -0
  64. package/dist/helpers/secureFileUrl.d.ts +4 -1
  65. package/dist/helpers/viewerSender.d.ts +47 -0
  66. package/dist/hooks/useAvatarImage.d.ts +21 -0
  67. package/dist/hooks/useIsMobileViewport.d.ts +5 -0
  68. package/dist/hooks/useMediaProbe.d.ts +40 -0
  69. package/dist/hooks/usePaneWidthVar.d.ts +11 -0
  70. package/dist/hooks/usePdfThumbnail.d.ts +0 -1
  71. package/dist/hooks/usePrefersReducedMotion.d.ts +2 -0
  72. package/dist/hooks/useSendMessage.d.ts +4 -1
  73. package/dist/{main-BnLtessp.js → main-CUB_w8AZ.js} +17712 -14488
  74. package/dist/main.js +10 -10
  75. package/dist/networking/api-requests/auth.api.d.ts +2 -0
  76. package/dist/networking/callKeyStanza.d.ts +7 -0
  77. package/dist/networking/stanzaHandlers.d.ts +4 -2
  78. package/dist/networking/xmpp/sendDisplayedMarker.xmpp.d.ts +2 -0
  79. package/dist/networking/xmpp/sendMediaMessage.xmpp.d.ts +8 -0
  80. package/dist/networking/xmpp/sendReceivedReceipt.xmpp.d.ts +2 -0
  81. package/dist/networking/xmppClient.d.ts +5 -0
  82. package/dist/roomStore/callSlice.d.ts +6 -1
  83. package/dist/roomStore/index.d.ts +10 -0
  84. package/dist/roomStore/roomsSlice.d.ts +18 -3
  85. package/dist/styles/motion.d.ts +22 -2
  86. package/dist/types/models/attachment.model.d.ts +19 -0
  87. package/dist/types/models/config.model.d.ts +28 -2
  88. package/dist/types/models/customComponents.model.d.ts +2 -0
  89. package/dist/types/models/message.model.d.ts +9 -0
  90. package/dist/types/models/modal.model.d.ts +19 -0
  91. package/dist/types/models/room.model.d.ts +16 -0
  92. package/dist/types/models/xmpp.model.d.ts +3 -0
  93. package/dist/utils/messageNotificationContent.d.ts +34 -0
  94. package/dist/version.d.ts +1 -1
  95. package/package.json +1 -1
  96. package/public/firebase-messaging-sw.js +20 -3
  97. package/dist/FilePreviewModal-Cgj1iHry.js +0 -743
  98. package/dist/components/RoomComponents/styled/LastAudioMessage.d.ts +0 -6
  99. package/dist/components/RoomComponents/styled/LastMessageFile.d.ts +0 -6
  100. package/dist/components/RoomComponents/styled/LastMessagePhoto.d.ts +0 -6
  101. package/dist/components/RoomComponents/styled/LastMessageVideo.d.ts +0 -6
@@ -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;
@@ -0,0 +1,10 @@
1
+ import { IMessage } from '../types/types';
2
+ /**
3
+ * Ids of the messages that end a run of one sender: the next row is another
4
+ * sender, a system row, or a message on the next day. A run is a block of
5
+ * consecutive messages from one person, which the bubbles group visually.
6
+ *
7
+ * Pass the full list, not a window of it, so the last row of a window still
8
+ * knows what follows it.
9
+ */
10
+ export declare const messageRunEnds: (list: IMessage[]) => Set<string>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Repairs file names the backend returns as UTF-8 bytes read as Latin-1
3
+ * (the classic multer/busboy `originalname` problem): "Список.pdf" arrives
4
+ * as "СпиÑок.pdf".
5
+ *
6
+ * Pure and conservative: a name is only replaced when every character fits
7
+ * in one byte, the bytes decode as strict UTF-8, the result differs, holds
8
+ * no replacement or control characters and contains at least one letter
9
+ * outside basic Latin. A correct name is returned untouched.
10
+ */
11
+ export declare const repairFilenameEncoding: (name: string) => string;
@@ -7,9 +7,27 @@ import { IMessage, ModalFile } from '../types/types';
7
7
  * download action, the secure `?ft=` token treatment, the alt text - keeps
8
8
  * reading exactly the fields it read when a single file was opened by hand.
9
9
  */
10
+ export interface GallerySender {
11
+ id?: string;
12
+ username?: string;
13
+ /** The message's own display name, when it has one. */
14
+ name?: string;
15
+ firstName?: string;
16
+ lastName?: string;
17
+ /** Profile photo URL, when the sender has one. */
18
+ photo?: string;
19
+ }
10
20
  export interface GalleryImage extends ModalFile {
21
+ /** What the entry renders as. Images zoom; videos play. */
22
+ kind: 'image' | 'video';
11
23
  /** Stable per message+slot, so two identical URLs are still two entries. */
12
24
  key: string;
25
+ /** Low-resolution copy shown blurred while the full image loads. */
26
+ previewURL?: string;
27
+ /** Message timestamp (ISO), shown in the viewer's header. */
28
+ date?: string;
29
+ /** Who posted it; the viewer header shows their name and avatar. */
30
+ sender?: GallerySender;
13
31
  }
14
32
  /**
15
33
  * The gallery the lightbox pages through.
@@ -34,7 +52,17 @@ export interface GalleryImage extends ModalFile {
34
52
  * Non-images (video, PDF, audio, arbitrary files) are excluded, so opening a
35
53
  * PDF simply produces an empty gallery and no navigation chrome.
36
54
  */
55
+ /** The sender fields the viewer header needs, or undefined for no sender. */
56
+ export declare const senderFromMessage: (message?: Partial<IMessage> | null) => GallerySender | undefined;
57
+ /** ISO date string for a message, as the viewer header wants it. */
58
+ export declare const isoDateFromMessage: (message?: Partial<IMessage> | null) => string | undefined;
37
59
  export declare const collectRoomImages: (messages?: Partial<IMessage>[] | null) => GalleryImage[];
60
+ /**
61
+ * Same walk as `collectRoomImages`, but the viewer's gallery also pages
62
+ * through videos, so an album of photos and clips opens as one sequence.
63
+ * PDFs, audio and other files are still excluded.
64
+ */
65
+ export declare const collectRoomMedia: (messages?: Partial<IMessage>[] | null, kinds?: Array<"image" | "video">) => GalleryImage[];
38
66
  /**
39
67
  * Position of the open file inside the gallery, or -1 when it is not part of
40
68
  * it (a PDF, a video, a file opened from the Files panel of another room).
@@ -51,3 +79,9 @@ export declare const collectRoomImages: (messages?: Partial<IMessage>[] | null)
51
79
  * were stuck.
52
80
  */
53
81
  export declare const findGalleryIndex: (images: GalleryImage[], activeFile?: ModalFile | null) => number;
82
+ /**
83
+ * Builds gallery entries from the file list a caller handed over with the
84
+ * open request (an album grid passes all of its tiles). Items without a
85
+ * location, or that are neither image nor video, are dropped.
86
+ */
87
+ export declare const galleryFromFiles: (files?: ModalFile[] | null) => GalleryImage[];
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Image URLs that reach the DOM (`<img src>`, CSS `url()`) can come from
3
+ * user-controlled data: profile photos, message stanzas, link previews,
4
+ * attachment locations. Anything that is not a plain web or blob image URL is
5
+ * dropped here so the sink never receives an active or document-bearing URL.
6
+ */
7
+ /**
8
+ * Returns the trimmed input when it is a safe image URL, otherwise ''.
9
+ *
10
+ * Allowed: absolute or relative (resolved against the page origin) http:,
11
+ * https: and blob: URLs, and base64 data URLs of png, jpeg, gif, webp or
12
+ * avif images. Blocked: javascript:, vbscript:, data:text/html, data:image/svg+xml
13
+ * and anything that does not parse. Non-string input yields ''.
14
+ */
15
+ export declare const safeImageUrl: (value: unknown) => string;
@@ -9,4 +9,7 @@ export declare const appendFileToken: (url: string | null | undefined, fileToken
9
9
  */
10
10
  export declare const withoutFileToken: (url?: string | null) => string;
11
11
  export declare const withFileToken: (url?: string | null) => string;
12
- export declare const requestFileTokenRecovery: () => Promise<boolean>;
12
+ export declare const resetFileTokenRecovery: () => void;
13
+ export declare const requestFileTokenRecovery: (options?: {
14
+ force?: boolean;
15
+ }) => Promise<boolean>;
@@ -0,0 +1,47 @@
1
+ /** What the media viewer knows about a sender, however thin. */
2
+ export interface ViewerSenderInput {
3
+ id?: string;
4
+ username?: string;
5
+ /** `message.user.name`, which carries the display name for many senders. */
6
+ name?: string;
7
+ firstName?: string;
8
+ lastName?: string;
9
+ photo?: string;
10
+ }
11
+ interface DirectoryEntry {
12
+ firstName?: string;
13
+ lastName?: string;
14
+ profileImage?: string;
15
+ }
16
+ interface OwnProfile extends DirectoryEntry {
17
+ xmppUsername?: string;
18
+ name?: string;
19
+ }
20
+ export interface ResolveViewerSenderOptions {
21
+ sender?: ViewerSenderInput | null;
22
+ /** `rooms.usersSet`, keyed by the xmpp username. */
23
+ usersSet?: Record<string, DirectoryEntry | undefined>;
24
+ /** The signed-in user, for their own messages. */
25
+ me?: OwnProfile | null;
26
+ /** Only used for a 1:1 room, where the room is named after the peer. */
27
+ peerRoomName?: string;
28
+ }
29
+ export interface ResolvedViewerSender {
30
+ /** Empty when nobody could be named. The header then omits the name line. */
31
+ name: string;
32
+ photo?: string;
33
+ }
34
+ /**
35
+ * Resolves the sender shown in the viewer header. The order is the one the
36
+ * message bubble uses, with the signed-in user's own profile as the answer for
37
+ * their own messages (an own optimistic message often has no name on it):
38
+ *
39
+ * first + last on the message, the message's own `name`, the profile in
40
+ * `usersSet`, the signed-in user's profile (own messages), the username
41
+ * before the `@`, and for a 1:1 room the room's name.
42
+ *
43
+ * Never returns a placeholder: when nothing is known `name` is '' and the
44
+ * caller draws a neutral glyph rather than initials.
45
+ */
46
+ export declare const resolveViewerSender: ({ sender, usersSet, me, peerRoomName, }: ResolveViewerSenderOptions) => ResolvedViewerSender;
47
+ export {};
@@ -0,0 +1,21 @@
1
+ export type AvatarImageStatus = 'none' | 'loading' | 'loaded' | 'failed';
2
+ /**
3
+ * Resolves an avatar URL for rendering. Never waits on the network: the
4
+ * caller shows initials immediately and fades the <img> in once `status` is
5
+ * `loaded`.
6
+ *
7
+ * - appends the viewer's `?ft=` token for secure-files URLs synchronously
8
+ * from the store (no token round trip before the request starts);
9
+ * - reuses a URL that already loaded for the same file (see
10
+ * avatarImageCache) so a token rotation or a second component showing the
11
+ * same person does not trigger another download;
12
+ * - skips URLs that just failed (no repeated failing requests);
13
+ * - on a secure-file failure kicks the throttled fileToken recovery, whose
14
+ * refreshed token yields a new URL and therefore a new attempt.
15
+ */
16
+ export declare const useAvatarImage: (rawSrc: string | null | undefined) => {
17
+ src: string;
18
+ status: AvatarImageStatus;
19
+ onLoad: () => void;
20
+ onError: () => void;
21
+ };
@@ -1 +1,6 @@
1
1
  export declare const useIsMobileViewport: (breakpoint?: number) => boolean;
2
+ /**
3
+ * Non-hook form of the same check, for event handlers that need the answer
4
+ * at the moment of the event rather than as render state.
5
+ */
6
+ export declare const isMobileViewportNow: (breakpoint?: number) => boolean;
@@ -0,0 +1,40 @@
1
+ export type MediaProbeStatus = 'idle' | 'loading' | 'recovering' | 'loaded' | 'error';
2
+ export interface MediaProbe {
3
+ status: MediaProbeStatus;
4
+ /**
5
+ * Natural size. Known once `status` is 'loaded', and usually sooner: it is
6
+ * read from the first decoded header while the bytes are still arriving.
7
+ */
8
+ width: number;
9
+ height: number;
10
+ /**
11
+ * The URL whose bytes are on screen. While a picture is shown, a rotated
12
+ * `?ft=` token does not swap it for a fresh request: the URL that loaded
13
+ * stays, so a token refresh never blanks or re-downloads what is visible.
14
+ */
15
+ url: string;
16
+ }
17
+ /**
18
+ * No byte of the file (not even its header) arrived in this long: stop
19
+ * waiting and show the unavailable state with a manual retry. Once the size
20
+ * is known the file is on its way and is never cut off by this timer.
21
+ */
22
+ export declare const MEDIA_STALL_MS = 8000;
23
+ export declare const resetMediaProbeCaches: () => void;
24
+ /**
25
+ * Preloads one image off-DOM so the visible <img> only mounts once its bytes
26
+ * are cached.
27
+ *
28
+ * A failure never says anything about the file being deleted: a 404 from the
29
+ * storage host, an expired token, a CORS or DNS hiccup and a dead CDN edge all
30
+ * fail an <img> the same way, and the server's answer cannot be read across
31
+ * origins anyway. So the only outcome is the 'error' state, which the surface
32
+ * offers to `retry`. Deleting a message is driven by the server (a delete
33
+ * stanza) or by the sender's own Files panel action, never inferred here.
34
+ *
35
+ * A secure URL that fails gets at most one shared token refresh per token
36
+ * (see `requestFileTokenRecovery`); the refreshed URL re-runs this effect.
37
+ */
38
+ export declare const useMediaProbe: (url: string | undefined) => MediaProbe & {
39
+ retry: () => void;
40
+ };
@@ -0,0 +1,11 @@
1
+ import { RefObject } from 'react';
2
+ /**
3
+ * Publishes the width of an element as the `--ethora-pane-w` custom property
4
+ * on that element, so message media can size itself to the chat pane (which
5
+ * may be a narrow host) instead of the viewport. Updated on every resize.
6
+ *
7
+ * The element may mount after the first render (the list shows a loader
8
+ * first), so every commit checks whether the ref points at a new element
9
+ * and re-attaches the observer if so.
10
+ */
11
+ export declare const usePaneWidthVar: (ref: RefObject<HTMLElement | null>) => void;
@@ -15,7 +15,6 @@ interface UsePdfThumbnailResult {
15
15
  status: PdfThumbnailStatus;
16
16
  /** Object URL - owned by this hook, revoked on unmount/url change. */
17
17
  thumbnailUrl?: string;
18
- pageCount?: number;
19
18
  /** Re-runs a render that failed or was skipped for being too big. */
20
19
  retry: () => void;
21
20
  }
@@ -0,0 +1,2 @@
1
+ /** True while the user asks the OS for reduced motion; follows live changes. */
2
+ export declare const usePrefersReducedMotion: () => boolean;
@@ -1,8 +1,11 @@
1
1
  import { IMentionSpan } from '../types/types';
2
+ import { CaptionInput } from '../helpers/caption';
3
+ /** Files of one message uploaded at the same time. */
4
+ export declare const MEDIA_UPLOAD_CONCURRENCY = 4;
2
5
  export { shouldTagOutgoingTranslateSource } from '../utils/translateModePolicy';
3
6
  export declare const useSendMessage: () => {
4
7
  sendMessage: (message: string, activeRoomJID: string, isReply?: boolean, isChecked?: boolean, mainMessage?: string, mentions?: IMentionSpan[]) => Promise<void>;
5
- sendMedia: (data: File | File[] | Blob, type: string, activeRoomJID: string, isReply?: boolean, isChecked?: boolean, mainMessage?: string) => Promise<void>;
8
+ sendMedia: (data: File | File[] | Blob, type: string, activeRoomJID: string, isReply?: boolean, isChecked?: boolean, mainMessage?: string, caption?: CaptionInput) => Promise<boolean>;
6
9
  sendEditMessage: (message: string) => Promise<void>;
7
10
  isLastMessageFromUserAndProcessing: (roomJID: string) => boolean;
8
11
  };