import { Download, FileIcon } from 'lucide-react';
import type { NostrEvent } from '@nostrify/nostrify';
import { Button } from '@/components/ui/button';
import { ImageGallery } from '@/components/ImageGallery';
import { VideoPlayer } from '@/components/VideoPlayer';
import { WebxdcEmbed } from '@/components/WebxdcEmbed';
import { AudioVisualizer } from '@/components/AudioVisualizer';
import { useAuthor } from '@/hooks/useAuthor';
import { getDisplayName } from '@/lib/getDisplayName';
import { genUserName } from '@/lib/genUserName';
import { getAvatarShape } from '@/lib/avatarShape';
import { sanitizeUrl } from '@/lib/sanitizeUrl';
import { cn } from '@/lib/utils';
/** Extract the first value of a tag by name. */
function getTag(tags: string[][], name: string): string | undefined {
return tags.find(([n]) => n === name)?.[1];
}
/** Format bytes into a human-readable string. */
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
/** YouTube-style description card rendered below media. */
function DescriptionCard({ title, text }: { title?: string; text?: string }) {
if (!title && !text) return null;
return (
{title && (
{title}
)}
{text && (
{text}
)}
);
}
/** Inner component for audio events — needs author data for avatar. */
function AudioFileContent({
event,
url,
mime,
description,
}: {
event: NostrEvent;
url: string;
mime: string;
description: string | undefined;
}) {
const author = useAuthor(event.pubkey);
const metadata = author.data?.metadata;
const displayName = getDisplayName(metadata, event.pubkey) ?? genUserName(event.pubkey);
return (
);
}
interface FileMetadataContentProps {
event: NostrEvent;
/** If true, render a more compact version for feed cards. */
compact?: boolean;
}
/**
* Renders the content of a NIP-94 (kind 1063) file metadata event.
*
* Media renders directly, and the description appears in a separate
* rounded card below it (similar to YouTube's description box).
*/
export function FileMetadataContent({ event, compact }: FileMetadataContentProps) {
const url = sanitizeUrl(getTag(event.tags, 'url'));
const mime = getTag(event.tags, 'm') ?? '';
const alt = getTag(event.tags, 'alt');
const webxdcId = getTag(event.tags, 'webxdc');
const dim = getTag(event.tags, 'dim');
const blurhash = getTag(event.tags, 'blurhash');
const thumb = getTag(event.tags, 'thumb') ?? getTag(event.tags, 'image');
const summary = getTag(event.tags, 'summary');
const size = getTag(event.tags, 'size');
if (!url) return null;
const description = event.content || undefined;
const altText = alt ?? undefined;
const fileName = url.split('/').pop() ?? 'file';
const sizeStr = size ? formatBytes(Number(size)) : undefined;
// ── Webxdc app ──────────────────────────────────────────────────────
if (mime === 'application/x-webxdc') {
const appName = altText?.replace(/^Webxdc app:\s*/i, '') ?? summary ?? fileName.replace('.xdc', '');
return (
);
}
// ── Image ───────────────────────────────────────────────────────────
if (mime.startsWith('image/')) {
const imetaMap = (dim || blurhash)
? new Map([[url, { dim, blurhash }]])
: undefined;
return (
{description && !compact && }
);
}
// ── Video ───────────────────────────────────────────────────────────
if (mime.startsWith('video/')) {
return (
{description && !compact && }
);
}
// ── Audio ───────────────────────────────────────────────────────────
if (mime.startsWith('audio/')) {
return ;
}
// ── Fallback: generic file ──────────────────────────────────────────
const displayName = altText ?? fileName;
const mimeLabel = mime ? mime.split('/').pop()?.toUpperCase() : undefined;
return (
{displayName}
{[mimeLabel, sizeStr].filter(Boolean).join(' · ') || 'File'}
{description &&
}
);
}