- ) : null}
- {previewUrl && selectedType === "video" ? (
-
- No preview available
+
+
+ {previewUrl && selectedType === "image" ? (
+

+ ) : null}
+ {previewUrl && selectedType === "video" ? (
+
+ ) : null}
+ {!previewUrl ? (
+
+ No preview available
+
+ ) : null}
- ) : null}
-
setCaptionDraft(event.target.value)}
- />
+
+
setCaptionDraft(event.target.value)}
+ />
- {isUploading ? (
-
-
Uploading: {uploadProgress}%
-
-
+ {isUploading ? (
+
+
Uploading: {uploadProgress}%
+
+
+ ) : null}
+
+
+
- ) : null}
-
-
-
) : null}
diff --git a/web/src/components/MessageList.tsx b/web/src/components/MessageList.tsx
index 33c26fa..06504c8 100644
--- a/web/src/components/MessageList.tsx
+++ b/web/src/components/MessageList.tsx
@@ -28,6 +28,11 @@ type PendingDeleteState = {
timerId: number;
} | null;
+type MediaViewerState = {
+ items: Array<{ url: string; type: "image" | "video" }>;
+ index: number;
+} | null;
+
const QUICK_REACTIONS = ["👍", "❤️", "🔥", "😂", "😮", "😢"];
export function MessageList() {
@@ -60,6 +65,7 @@ export function MessageList() {
const [pendingDelete, setPendingDelete] = useState
(null);
const [undoTick, setUndoTick] = useState(0);
const [reactionsByMessage, setReactionsByMessage] = useState>({});
+ const [mediaViewer, setMediaViewer] = useState(null);
const messages = useMemo(() => {
if (!activeChatId) {
@@ -100,6 +106,7 @@ export function MessageList() {
setForwardSelectedChatIds(new Set());
setDeleteMessageId(null);
setSelectedIds(new Set());
+ setMediaViewer(null);
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
@@ -415,7 +422,14 @@ export function MessageList() {
void ensureReactionsLoaded(message.id);
const pos = getSafeContextPosition(event.clientX, event.clientY, 220, 280);
setCtx({ x: pos.x, y: pos.y, messageId: message.id, attachmentUrl: url });
- }
+ },
+ onOpenMedia: (url, type) => {
+ const items = messages
+ .filter((m) => (m.type === "image" || m.type === "video" || m.type === "circle_video") && !!m.text)
+ .map((m) => ({ url: m.text as string, type: (m.type === "image" ? "image" : "video") as "image" | "video" }));
+ const idx = items.findIndex((i) => i.url === url && i.type === type);
+ setMediaViewer({ items, index: idx >= 0 ? idx : 0 });
+ },
})}
{messageReactions.length > 0 ? (
@@ -620,6 +634,75 @@ export function MessageList() {
) : null}
+ {mediaViewer ? (
+
setMediaViewer(null)}>
+
event.stopPropagation()}>
+
+
+ {mediaViewer.items.length > 1 ? (
+ <>
+
+
+
+ >
+ ) : null}
+
+
+
+ {mediaViewer.items[mediaViewer.index]?.type === "image" ? (
+

+ ) : (
+
+ )}
+
+
+ ) : null}
+
{deleteMessageId ? (
setDeleteMessageId(null)}>
event.stopPropagation()}>
@@ -663,29 +746,52 @@ export function MessageList() {
function renderMessageContent(
messageType: string,
text: string | null,
- opts: { onAttachmentContextMenu: (event: MouseEvent
, url: string) => void }
+ opts: {
+ onAttachmentContextMenu: (event: MouseEvent, url: string) => void;
+ onOpenMedia: (url: string, type: "image" | "video") => void;
+ }
) {
if (!text) return [empty]
;
if (messageType === "image") {
return (
- opts.onAttachmentContextMenu(event, text)}>
+
+
);
}
if (messageType === "video" || messageType === "circle_video") {
return (
- opts.onAttachmentContextMenu(event, text)}>
-
-
+
);
}
if (messageType === "voice") {
return (
- opts.onAttachmentContextMenu(event, text)}>
+
{
+ event.stopPropagation();
+ opts.onAttachmentContextMenu(event, text);
+ }}>
🎤
Voice message
@@ -697,7 +803,10 @@ function renderMessageContent(
if (messageType === "audio") {
return (
-
opts.onAttachmentContextMenu(event, text)}>
+
{
+ event.stopPropagation();
+ opts.onAttachmentContextMenu(event, text);
+ }}>
🎵
@@ -714,7 +823,10 @@ function renderMessageContent(
return (