mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-05 10:11:12 +02:00
feat: improve ChatMediaRenderer UX
- Full dotted border with rounded corners (was bottom-only) - Increased gap between elements - Images/videos open in MediaDialog zoom viewer on click - Audio still opens in new tab - Better extension extraction for display https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
This commit is contained in:
@@ -5,12 +5,31 @@
|
|||||||
* [icon] filename [size] [blossom-link]
|
* [icon] filename [size] [blossom-link]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { Image, Video, Music, File, Flower2 } from "lucide-react";
|
import { Image, Video, Music, File, Flower2 } from "lucide-react";
|
||||||
import { getHashFromURL } from "blossom-client-sdk/helpers/url";
|
import { getHashFromURL } from "blossom-client-sdk/helpers/url";
|
||||||
import { useGrimoire } from "@/core/state";
|
import { useGrimoire } from "@/core/state";
|
||||||
import { formatFileSize } from "@/lib/imeta";
|
import { formatFileSize } from "@/lib/imeta";
|
||||||
|
import { MediaDialog } from "@/components/nostr/MediaDialog";
|
||||||
import type { MediaRendererProps } from "@/components/nostr/RichText";
|
import type { MediaRendererProps } from "@/components/nostr/RichText";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract file extension from URL
|
||||||
|
*/
|
||||||
|
function getExtension(url: string): string | null {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const pathname = urlObj.pathname;
|
||||||
|
const lastSegment = pathname.split("/").pop() || "";
|
||||||
|
if (lastSegment.includes(".")) {
|
||||||
|
return lastSegment.split(".").pop() || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract filename from URL
|
* Extract filename from URL
|
||||||
*/
|
*/
|
||||||
@@ -22,7 +41,7 @@ function getFilename(url: string): string {
|
|||||||
// If it's a blossom hash, truncate it
|
// If it's a blossom hash, truncate it
|
||||||
const hash = getHashFromURL(url);
|
const hash = getHashFromURL(url);
|
||||||
if (hash) {
|
if (hash) {
|
||||||
const ext = lastSegment.includes(".") ? lastSegment.split(".").pop() : "";
|
const ext = getExtension(url);
|
||||||
return ext ? `${hash.slice(0, 8)}...${ext}` : `${hash.slice(0, 8)}...`;
|
return ext ? `${hash.slice(0, 8)}...${ext}` : `${hash.slice(0, 8)}...`;
|
||||||
}
|
}
|
||||||
// Decode URI component for readable filenames
|
// Decode URI component for readable filenames
|
||||||
@@ -69,6 +88,7 @@ function MediaIcon({ type }: { type: "image" | "video" | "audio" }) {
|
|||||||
|
|
||||||
export function ChatMediaRenderer({ url, type, imeta }: MediaRendererProps) {
|
export function ChatMediaRenderer({ url, type, imeta }: MediaRendererProps) {
|
||||||
const { addWindow } = useGrimoire();
|
const { addWindow } = useGrimoire();
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
|
||||||
const filename = imeta?.alt || getFilename(url);
|
const filename = imeta?.alt || getFilename(url);
|
||||||
const size = imeta?.size ? formatFileSize(imeta.size) : null;
|
const size = imeta?.size ? formatFileSize(imeta.size) : null;
|
||||||
@@ -86,35 +106,47 @@ export function ChatMediaRenderer({ url, type, imeta }: MediaRendererProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileClick = (e: React.MouseEvent) => {
|
const handleMediaClick = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
// Images and videos open in dialog, audio opens in new tab
|
||||||
|
if (type === "image" || type === "video") {
|
||||||
|
setDialogOpen(true);
|
||||||
|
} else {
|
||||||
|
window.open(url, "_blank", "noopener,noreferrer");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center gap-1 border-b border-dotted border-muted-foreground/50">
|
<>
|
||||||
<MediaIcon type={type} />
|
<span className="inline-flex items-center gap-1.5 border border-dotted border-muted-foreground/40 rounded px-1">
|
||||||
<a
|
<MediaIcon type={type} />
|
||||||
href={url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
onClick={handleFileClick}
|
|
||||||
className="text-foreground hover:underline truncate max-w-48"
|
|
||||||
title={imeta?.alt || url}
|
|
||||||
>
|
|
||||||
{filename}
|
|
||||||
</a>
|
|
||||||
{size && (
|
|
||||||
<span className="text-muted-foreground text-xs shrink-0">{size}</span>
|
|
||||||
)}
|
|
||||||
{blossom && (
|
|
||||||
<button
|
<button
|
||||||
onClick={handleBlossomClick}
|
onClick={handleMediaClick}
|
||||||
className="text-muted-foreground hover:text-foreground"
|
className="text-foreground hover:underline truncate max-w-48 text-left"
|
||||||
title="View in Blossom"
|
title={imeta?.alt || url}
|
||||||
>
|
>
|
||||||
<Flower2 className="size-3" />
|
{filename}
|
||||||
</button>
|
</button>
|
||||||
)}
|
{size && (
|
||||||
</span>
|
<span className="text-muted-foreground text-xs shrink-0">{size}</span>
|
||||||
|
)}
|
||||||
|
{blossom && (
|
||||||
|
<button
|
||||||
|
onClick={handleBlossomClick}
|
||||||
|
className="text-muted-foreground hover:text-foreground"
|
||||||
|
title="View in Blossom"
|
||||||
|
>
|
||||||
|
<Flower2 className="size-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<MediaDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onOpenChange={setDialogOpen}
|
||||||
|
urls={[url]}
|
||||||
|
initialIndex={0}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user