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'; /** 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({ text }: { text: string }) { return (

{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 (
{description && }
); } 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 = 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 (
{description && }
); } // ── 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 && }
); }