diff --git a/src/components/BlossomViewer.tsx b/src/components/BlossomViewer.tsx index 8f7d4e2..ff503d2 100644 --- a/src/components/BlossomViewer.tsx +++ b/src/components/BlossomViewer.tsx @@ -1027,9 +1027,31 @@ function BlobDetailView({ const blobSha256 = blob?.sha256 || sha256; const mimeType = blob?.type; - const isImage = mimeType?.startsWith("image/"); - const isVideo = mimeType?.startsWith("video/"); - const isAudio = mimeType?.startsWith("audio/"); + // Detect media type from URL extension if mimeType not available + const getMediaTypeFromUrl = ( + url: string | null, + ): "image" | "video" | "audio" | null => { + if (!url) return null; + try { + const pathname = new URL(url).pathname.toLowerCase(); + const ext = pathname.split(".").pop(); + if (!ext) return null; + const imageExts = ["jpg", "jpeg", "png", "gif", "webp", "svg", "avif"]; + const videoExts = ["mp4", "webm", "mov", "avi", "mkv", "m4v"]; + const audioExts = ["mp3", "wav", "ogg", "flac", "m4a", "aac"]; + if (imageExts.includes(ext)) return "image"; + if (videoExts.includes(ext)) return "video"; + if (audioExts.includes(ext)) return "audio"; + return null; + } catch { + return null; + } + }; + + const urlMediaType = getMediaTypeFromUrl(blobUrl); + const isImage = mimeType?.startsWith("image/") || urlMediaType === "image"; + const isVideo = mimeType?.startsWith("video/") || urlMediaType === "video"; + const isAudio = mimeType?.startsWith("audio/") || urlMediaType === "audio"; if (!blobSha256) { return ( diff --git a/src/components/chat/ChatMediaRenderer.tsx b/src/components/chat/ChatMediaRenderer.tsx index 9e99372..1eaab98 100644 --- a/src/components/chat/ChatMediaRenderer.tsx +++ b/src/components/chat/ChatMediaRenderer.tsx @@ -1,16 +1,23 @@ /** * Chat-specific media renderer * - * Shows inline file info instead of embedded media: - * [icon] filename [size] [blossom-link] + * Shows compact inline file info with expandable media: + * [icon] truncated-hash [blossom] * - * Click on filename opens media in new tab. + * Click on filename expands to show the actual media inline. + * Tooltip shows full filename and size. */ +import { useState } from "react"; import { Image, Video, Music, File, HardDrive } from "lucide-react"; import { getHashFromURL } from "blossom-client-sdk/helpers/url"; import { useGrimoire } from "@/core/state"; import { formatFileSize } from "@/lib/imeta"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; import type { MediaRendererProps } from "@/components/nostr/RichText"; /** @@ -31,26 +38,49 @@ function getExtension(url: string): string | null { } /** - * Extract filename from URL + * Get display name for the file (for tooltip) */ -function getFilename(url: string): string { +function getFullFilename(url: string, alt?: string): string { + if (alt) return alt; try { const urlObj = new URL(url); const pathname = urlObj.pathname; const lastSegment = pathname.split("/").pop() || ""; - // If it's a blossom hash, truncate it - const hash = getHashFromURL(url); - if (hash) { - const ext = getExtension(url); - return ext ? `${hash.slice(0, 8)}...${ext}` : `${hash.slice(0, 8)}...`; - } - // Decode URI component for readable filenames return decodeURIComponent(lastSegment) || "file"; } catch { return "file"; } } +/** + * Get truncated hash display for compact view + */ +function getTruncatedHash(url: string): string { + const hash = getHashFromURL(url); + if (hash) { + const ext = getExtension(url); + // Show first 6 chars of hash + return ext ? `${hash.slice(0, 6)}…${ext}` : `${hash.slice(0, 6)}…`; + } + // Fallback: truncate filename + try { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const lastSegment = decodeURIComponent(pathname.split("/").pop() || "file"); + if (lastSegment.length > 12) { + const ext = getExtension(url); + if (ext) { + const nameWithoutExt = lastSegment.slice(0, -(ext.length + 1)); + return `${nameWithoutExt.slice(0, 6)}…${ext}`; + } + return `${lastSegment.slice(0, 8)}…`; + } + return lastSegment; + } catch { + return "file"; + } +} + /** * Parse blossom URL - returns sha256 and server URL if valid */ @@ -88,8 +118,10 @@ function MediaIcon({ type }: { type: "image" | "video" | "audio" }) { export function ChatMediaRenderer({ url, type, imeta }: MediaRendererProps) { const { addWindow } = useGrimoire(); + const [expanded, setExpanded] = useState(false); - const filename = imeta?.alt || getFilename(url); + const fullFilename = getFullFilename(url, imeta?.alt); + const truncatedHash = getTruncatedHash(url); const size = imeta?.size ? formatFileSize(imeta.size) : null; const blossom = parseBlossomUrl(url); @@ -112,35 +144,76 @@ export function ChatMediaRenderer({ url, type, imeta }: MediaRendererProps) { } }; - const handleMediaClick = (e: React.MouseEvent) => { + const handleToggleExpand = (e: React.MouseEvent) => { + e.preventDefault(); e.stopPropagation(); + setExpanded(!expanded); }; + // Build tooltip content + const tooltipContent = ( +