diff --git a/src/components/BlossomViewer.tsx b/src/components/BlossomViewer.tsx index ff503d2..94cb1cc 100644 --- a/src/components/BlossomViewer.tsx +++ b/src/components/BlossomViewer.tsx @@ -53,6 +53,8 @@ interface BlossomViewerProps { sha256?: string; /** Full blob URL with extension (for blob subcommand) */ blobUrl?: string; + /** Media type hint for preview (image/video/audio) */ + mediaType?: "image" | "video" | "audio"; } /** @@ -66,6 +68,7 @@ export function BlossomViewer({ targetServer, sha256, blobUrl, + mediaType, }: BlossomViewerProps) { switch (subcommand) { case "servers": @@ -82,6 +85,7 @@ export function BlossomViewer({ sha256={sha256!} serverUrl={serverUrl} blobUrl={blobUrl} + mediaType={mediaType} /> ); case "mirror": @@ -1007,6 +1011,7 @@ function BlobDetailView({ serverUrl, blobUrl: providedBlobUrl, blob: initialBlob, + mediaType: providedMediaType, onBack, }: { sha256?: string; @@ -1014,6 +1019,8 @@ function BlobDetailView({ /** Full blob URL with extension */ blobUrl?: string; blob?: BlobDescriptor; + /** Media type hint (image/video/audio) */ + mediaType?: "image" | "video" | "audio"; onBack?: () => void; }) { const { copy, copied } = useCopy(); @@ -1049,9 +1056,19 @@ function BlobDetailView({ }; 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"; + // Priority: mimeType from blob > provided mediaType hint > detected from URL + const isImage = + mimeType?.startsWith("image/") || + providedMediaType === "image" || + urlMediaType === "image"; + const isVideo = + mimeType?.startsWith("video/") || + providedMediaType === "video" || + urlMediaType === "video"; + const isAudio = + mimeType?.startsWith("audio/") || + providedMediaType === "audio" || + urlMediaType === "audio"; if (!blobSha256) { return ( diff --git a/src/components/chat/ChatMediaRenderer.tsx b/src/components/chat/ChatMediaRenderer.tsx index 1eaab98..a835fcc 100644 --- a/src/components/chat/ChatMediaRenderer.tsx +++ b/src/components/chat/ChatMediaRenderer.tsx @@ -4,8 +4,8 @@ * Shows compact inline file info with expandable media: * [icon] truncated-hash [blossom] * - * Click on filename expands to show the actual media inline. - * Tooltip shows full filename and size. + * Click on filename expands to show the actual media inline (not collapsible). + * Tooltip shows imeta info when available. */ import { useState } from "react"; @@ -18,6 +18,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { MediaEmbed } from "@/components/nostr/MediaEmbed"; import type { MediaRendererProps } from "@/components/nostr/RichText"; /** @@ -37,21 +38,6 @@ function getExtension(url: string): string | null { } } -/** - * Get display name for the file (for tooltip) - */ -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() || ""; - return decodeURIComponent(lastSegment) || "file"; - } catch { - return "file"; - } -} - /** * Get truncated hash display for compact view */ @@ -99,6 +85,15 @@ function parseBlossomUrl( } } +/** + * Format duration in seconds to human readable format + */ +function formatDuration(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`; +} + /** * Get icon component based on media type */ @@ -120,17 +115,15 @@ export function ChatMediaRenderer({ url, type, imeta }: MediaRendererProps) { const { addWindow } = useGrimoire(); const [expanded, setExpanded] = useState(false); - const fullFilename = getFullFilename(url, imeta?.alt); const truncatedHash = getTruncatedHash(url); - const size = imeta?.size ? formatFileSize(imeta.size) : null; const blossom = parseBlossomUrl(url); const handleBlossomClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (blossom) { - // Build command string for Edit functionality - const commandString = `blossom blob ${blossom.sha256} ${blossom.serverUrl}`; + // Build command string for Edit functionality - include media type + const commandString = `blossom blob ${blossom.sha256} ${blossom.serverUrl} --type ${type}`; addWindow( "blossom", { @@ -138,82 +131,83 @@ export function ChatMediaRenderer({ url, type, imeta }: MediaRendererProps) { sha256: blossom.sha256, serverUrl: blossom.serverUrl, blobUrl: url, // Pass full URL with extension + mediaType: type, // Pass media type for preview }, commandString, ); } }; - const handleToggleExpand = (e: React.MouseEvent) => { + const handleExpand = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - setExpanded(!expanded); + setExpanded(true); }; - // Build tooltip content - const tooltipContent = ( -
-
{fullFilename}
- {size &&
{size}
} -
- ); - + // When expanded, show plain MediaEmbed (not collapsible) if (expanded) { return ( - - {/* Collapsed toggle bar */} - - - - - {/* Expanded media */} - - {type === "image" && ( - {imeta?.alt - )} - {type === "video" && ( - + + ); } - return ( - - - - - - {blossom && ( - - )} - - - {tooltipContent} - + // Build tooltip content from imeta if available + const tooltipContent = imeta ? ( +
+ {imeta.alt &&
{imeta.alt}
} + {imeta.m &&
{imeta.m}
} + {imeta.dim &&
{imeta.dim}
} + {imeta.size && ( +
+ {formatFileSize(imeta.size)} +
+ )} + {imeta.duration && ( +
+ {formatDuration(imeta.duration)} +
+ )} +
+ ) : null; + + const compactView = ( + + + + {blossom && ( + + )} + ); + + // Only wrap in tooltip if we have imeta + if (tooltipContent) { + return ( + + {compactView} + {tooltipContent} + + ); + } + + return compactView; }