diff --git a/services/backend/src/modules/messages/messages.repository.ts b/services/backend/src/modules/messages/messages.repository.ts index 71ff8e6d..24ff19c1 100644 --- a/services/backend/src/modules/messages/messages.repository.ts +++ b/services/backend/src/modules/messages/messages.repository.ts @@ -491,26 +491,21 @@ export class MessagesRepository { */ async getRecentEdits(limit = 50, channelId?: string) { const db = getDatabase(); - const where = channelId - ? `WHERE m.channel_id = '${channelId.replace(/'/g, "''")}'` - : ""; - const result = await db.execute( - sql.raw(` - SELECT - e.id, - e.message_id, - e.old_content, - e.edited_at, - m.channel_id, - COALESCE(NULLIF((m.metadata::jsonb -> 'channel' ->> 'channelName'), ''), m.channel_id) AS channel_name, - m.username - FROM message_edits e - JOIN messages m ON m.id = e.message_id - ${where} - ORDER BY e.edited_at DESC - LIMIT ${limit} - `), - ); + const result = await db.execute(sql` + SELECT + e.id, + e.message_id, + e.old_content, + e.edited_at, + m.channel_id, + COALESCE(NULLIF((m.metadata::jsonb -> 'channel' ->> 'channelName'), ''), m.channel_id) AS channel_name, + m.username + FROM message_edits e + JOIN messages m ON m.id = e.message_id + ${channelId ? sql`WHERE m.channel_id = ${channelId}` : sql``} + ORDER BY e.edited_at DESC + LIMIT ${limit} + `); const rows = (result.rows as Record[]) || []; return rows.map((r) => ({ id: String(r.id), diff --git a/services/frontend/src/app/(dashboard)/messages/view.tsx b/services/frontend/src/app/(dashboard)/messages/view.tsx index 8146a4ca..1f3363f4 100644 --- a/services/frontend/src/app/(dashboard)/messages/view.tsx +++ b/services/frontend/src/app/(dashboard)/messages/view.tsx @@ -57,6 +57,7 @@ import type { AiStatus, EditHistoryRow, Guild, + MessageMetadata, MessageRecord, } from "@/lib/types"; import { useWebSocket } from "@/lib/ws/context"; @@ -622,6 +623,22 @@ function MessageDetail({ ); } +/** Parse metadata JSON safely — returns null on malformed / missing data. */ +function parseMeta(raw: string | null | undefined): MessageMetadata | null { + if (!raw) return null; + try { + return JSON.parse(raw) as MessageMetadata; + } catch { + return null; + } +} + +/** True when an attachment content-type looks like an image we can inline. */ +function isImageType(ct?: string | null): boolean { + if (!ct) return false; + return ct.startsWith("image/"); +} + /** Single message card used by both the live feed and the date-grouped timeline. */ function MessageRow({ m, @@ -632,6 +649,24 @@ function MessageRow({ selected: string | null; onSelect: (id: string) => void; }) { + const meta = parseMeta(m.metadata); + const attachments = meta?.attachments ?? []; + const embeds = meta?.embeds ?? []; + const stickers = meta?.stickers ?? []; + const imageAttachments = attachments.filter((a) => + isImageType(a.contentType), + ); + const fileAttachments = attachments.filter( + (a) => !isImageType(a.contentType), + ); + + // First embed image or sticker url for visual preview + const embedImage = + embeds.find((e) => e.image?.url)?.image?.url ?? + embeds.find((e) => e.thumbnail?.url)?.thumbnail?.url ?? + null; + const stickerUrl = stickers.find((s) => s.url)?.url ?? null; + return (