diff --git a/web/src/api/chats.ts b/web/src/api/chats.ts index c75043b..e0b464f 100644 --- a/web/src/api/chats.ts +++ b/web/src/api/chats.ts @@ -83,7 +83,7 @@ export async function sendMessage(chatId: number, text: string, type: MessageTyp export async function sendMessageWithClientId( chatId: number, - text: string, + text: string | null, type: MessageType, clientMessageId: string, replyToMessageId?: number diff --git a/web/src/components/MessageComposer.tsx b/web/src/components/MessageComposer.tsx index b961507..0c3841a 100644 --- a/web/src/components/MessageComposer.tsx +++ b/web/src/components/MessageComposer.tsx @@ -508,24 +508,31 @@ export function MessageComposer() { }); } const kindSet = new Set(uploaded.map((item) => item.kind)); - const inferredType: "file" | "image" | "video" | "audio" = - kindSet.size === 1 ? uploaded[0].kind : "file"; + let inferredType: "file" | "image" | "video" | "audio" = "file"; + if ([...kindSet].every((kind) => kind === "image")) { + inferredType = "image"; + } else if ([...kindSet].every((kind) => kind === "image" || kind === "video")) { + inferredType = "video"; + } else if ([...kindSet].every((kind) => kind === "audio")) { + inferredType = "audio"; + } else if (kindSet.size === 1) { + inferredType = uploaded[0].kind; + } const messageType = inferredType === "video" && uploaded.length === 1 && sendAsCircle ? "circle_video" : inferredType; const caption = captionDraft.trim() || null; - const fallbackText = uploaded[0]?.fileUrl ?? null; const clientMessageId = makeClientMessageId(); addOptimisticMessage({ chatId: activeChatId, senderId: me.id, type: messageType, - text: caption || fallbackText, + text: caption, clientMessageId, }); const replyToMessageId = replyToByChat[activeChatId]?.id ?? undefined; const created = await sendMessageWithClientId( activeChatId, - caption || fallbackText || "", + caption, messageType, clientMessageId, replyToMessageId diff --git a/web/src/components/MessageList.tsx b/web/src/components/MessageList.tsx index 6e2e63b..2723e9e 100644 --- a/web/src/components/MessageList.tsx +++ b/web/src/components/MessageList.tsx @@ -865,6 +865,10 @@ function renderMessageContent( ] : []; const attachments = opts.attachments.length > 0 ? opts.attachments : legacyAttachment; + const captionText = + text && (!/^https?:\/\//i.test(text) || opts.attachments.length > 0) + ? text.trim() + : ""; if (messageType === "image" || messageType === "video" || messageType === "circle_video") { const mediaItems = attachments @@ -882,32 +886,9 @@ function renderMessageContent( if (mediaItems.length === 1) { const item = mediaItems[0]; return ( - - ); - } - return ( -
event.stopPropagation()}> - {mediaItems.slice(0, 6).map((item, index) => ( +
- ))} + {captionText ? ( +

+ ) : null} +

+ ); + } + const gridClass = getMediaGridClass(mediaItems.length); + return ( +
event.stopPropagation()}> +
+ {mediaItems.slice(0, 6).map((item, index) => { + const tileClass = getMediaTileClass(mediaItems.length, index); + return ( + + ); + })} +
+ {captionText ? ( +

+ ) : null}

); } @@ -1013,6 +1031,9 @@ function renderMessageContent(

{extractFileName(item.file_url)}

))} + {captionText ? ( +

+ ) : null}

); } @@ -1100,6 +1121,25 @@ function collectMediaItems( return items; } +function getMediaGridClass(count: number): string { + if (count <= 1) return "grid-cols-1"; + if (count === 2) return "grid-cols-2"; + return "grid-cols-2 auto-rows-[90px] md:auto-rows-[120px]"; +} + +function getMediaTileClass(count: number, index: number): string { + if (count <= 2) { + return "h-32 md:h-40"; + } + if (count === 3 && index === 0) { + return "row-span-2 h-full"; + } + if (count === 5 && index === 0) { + return "row-span-2 h-full"; + } + return "h-full"; +} + function getMessageAttachmentUrl(message: Message, attachments: ChatAttachment[]): string | null { if (attachments.length > 0 && attachments[0].file_url) { return attachments[0].file_url;