diff --git a/src/components/chat/ChatMediaRenderer.tsx b/src/components/chat/ChatMediaRenderer.tsx
new file mode 100644
index 0000000..43c9c1b
--- /dev/null
+++ b/src/components/chat/ChatMediaRenderer.tsx
@@ -0,0 +1,126 @@
+/**
+ * Chat-specific media renderer
+ *
+ * Shows inline file info instead of embedded media:
+ * [icon] filename [size] [blossom-link]
+ */
+
+import { Image, Video, Music, File, Flower2 } from "lucide-react";
+import { useGrimoire } from "@/core/state";
+import { formatFileSize } from "@/lib/imeta";
+import type { MediaRendererProps } from "@/components/nostr/RichText";
+
+/**
+ * Extract filename from URL
+ */
+function getFilename(url: string): string {
+ try {
+ const urlObj = new URL(url);
+ const pathname = urlObj.pathname;
+ const lastSegment = pathname.split("/").pop() || "";
+ // If it's a hash (64 hex chars), truncate it
+ if (/^[0-9a-f]{64}$/i.test(lastSegment.replace(/\.[^.]+$/, ""))) {
+ const ext = lastSegment.includes(".") ? lastSegment.split(".").pop() : "";
+ const hash = lastSegment.replace(/\.[^.]+$/, "");
+ return ext ? `${hash.slice(0, 8)}...${ext}` : `${hash.slice(0, 8)}...`;
+ }
+ // Decode URI component for readable filenames
+ return decodeURIComponent(lastSegment) || "file";
+ } catch {
+ return "file";
+ }
+}
+
+/**
+ * Check if URL is a blossom URL (has sha256 hash in path)
+ * Returns the sha256 and server URL if it is, null otherwise
+ */
+function parseBlossomUrl(
+ url: string,
+): { sha256: string; serverUrl: string } | null {
+ try {
+ const urlObj = new URL(url);
+ const pathname = urlObj.pathname;
+ const segments = pathname.split("/").filter(Boolean);
+ const lastSegment = segments[segments.length - 1] || "";
+ // Remove extension if present
+ const possibleHash = lastSegment.replace(/\.[^.]+$/, "");
+
+ if (/^[0-9a-f]{64}$/i.test(possibleHash)) {
+ const serverUrl = `${urlObj.protocol}//${urlObj.host}`;
+ return { sha256: possibleHash.toLowerCase(), serverUrl };
+ }
+ return null;
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Get icon component based on media type
+ */
+function MediaIcon({ type }: { type: "image" | "video" | "audio" }) {
+ const iconClass = "size-4 shrink-0";
+ switch (type) {
+ case "image":
+ return ;
+ case "video":
+ return ;
+ case "audio":
+ return ;
+ default:
+ return ;
+ }
+}
+
+export function ChatMediaRenderer({ url, type, imeta }: MediaRendererProps) {
+ const { addWindow } = useGrimoire();
+
+ const filename = imeta?.alt || getFilename(url);
+ const size = imeta?.size ? formatFileSize(imeta.size) : null;
+ const blossom = parseBlossomUrl(url);
+
+ const handleBlossomClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (blossom) {
+ addWindow("blossom", {
+ subcommand: "blob",
+ sha256: blossom.sha256,
+ serverUrl: blossom.serverUrl,
+ });
+ }
+ };
+
+ const handleFileClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ };
+
+ return (
+
+
+
+ {filename}
+
+ {size && (
+ {size}
+ )}
+ {blossom && (
+
+ )}
+
+ );
+}