"use client"; import { useState, useEffect, useRef } from "react"; import { useTranslations } from "next-intl"; import { X, Download, Loader2, ExternalLink } from "lucide-react"; import { Button } from "@/components/ui/button"; import { getFilePreviewKind, isMimeTypeSafeForInlinePreview } from "@/lib/file-preview"; import dynamic from "next/dynamic"; import { EmlPreview, type ParsedEml } from "@/components/files/eml-preview"; // pdf.js-based inline viewer for mobile (no native inline PDF viewer). Loaded // only on the mobile PDF path so pdfjs-dist + its worker never reach the // desktop bundle. const PdfMobileViewer = dynamic( () => import("@/components/files/pdf-mobile-viewer").then((m) => m.PdfMobileViewer), { ssr: false }, ); // Map a few well-known extensions back to canonical MIME types. Used when the // server returns application/octet-stream (or empty) for an attachment whose // actual type is obvious from the filename. The blob.type drives how browsers // render blob: URLs, so guessing wrong here means the inline preview silently // downgrades to a download. const EXT_TO_MIME: Record = { pdf: "application/pdf", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", avif: "image/avif", bmp: "image/bmp", mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", m4a: "audio/mp4", mp4: "video/mp4", webm: "video/webm", ogv: "video/ogg", }; function inferMimeFromName(name: string): string | undefined { const ext = name.toLowerCase().split(".").pop(); return ext ? EXT_TO_MIME[ext] : undefined; } interface FilePreviewModalProps { name: string; onClose: () => void; onDownload: () => Promise | void; getFileContent: () => Promise<{ blob: Blob; contentType: string }>; } function SimpleMarkdown({ content }: { content: string }) { const lines = content.split("\n"); const elements: React.ReactNode[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Headers if (line.startsWith("### ")) { elements.push(

{processInline(line.slice(4))}

); } else if (line.startsWith("## ")) { elements.push(

{processInline(line.slice(3))}

); } else if (line.startsWith("# ")) { elements.push(

{processInline(line.slice(2))}

); } else if (line.startsWith("---") || line.startsWith("***")) { elements.push(
); } else if (line.startsWith("- ") || line.startsWith("* ")) { elements.push(
  • {processInline(line.slice(2))}
  • ); } else if (/^\d+\. /.test(line)) { elements.push(
  • {processInline(line.replace(/^\d+\. /, ""))}
  • ); } else if (line.startsWith("> ")) { elements.push(
    {processInline(line.slice(2))}
    ); } else if (line.startsWith("```")) { // Code block - collect until closing ``` const codeLines: string[] = []; i++; while (i < lines.length && !lines[i].startsWith("```")) { codeLines.push(lines[i]); i++; } elements.push(
              {codeLines.join("\n")}
            
    ); } else if (line.trim() === "") { elements.push(
    ); } else { elements.push(

    {processInline(line)}

    ); } } return
    {elements}
    ; } function processInline(text: string): React.ReactNode { // Process bold, italic, code inline const parts: React.ReactNode[] = []; let remaining = text; let key = 0; while (remaining.length > 0) { // Bold const boldMatch = remaining.match(/\*\*(.+?)\*\*/); // Inline code const codeMatch = remaining.match(/`([^`]+)`/); // Italic const italicMatch = remaining.match(/(? (a!.match.index ?? 0) - (b!.match.index ?? 0)); if (matches.length === 0) { parts.push(remaining); break; } const first = matches[0]!; const idx = first.match.index ?? 0; if (idx > 0) { parts.push(remaining.slice(0, idx)); } if (first.type === "bold") { parts.push({first.match[1]}); } else if (first.type === "code") { parts.push({first.match[1]}); } else { parts.push({first.match[1]}); } remaining = remaining.slice(idx + first.match[0].length); } return parts.length === 1 ? parts[0] : <>{parts}; } export function FilePreviewModal({ name, onClose, onDownload, getFileContent }: FilePreviewModalProps) { const t = useTranslations("files"); const [content, setContent] = useState(null); const [objectUrl, setObjectUrl] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [resolvedFileType, setResolvedFileType] = useState(() => getFilePreviewKind(name)); const [pdfInlineSupported, setPdfInlineSupported] = useState(true); // Whether the resolved blob MIME is inert enough to open as a top-level // navigation. Blob URLs inherit our origin, so opening a script-bearing type // (text/html, image/svg+xml, ...) in a new tab would execute it in-origin. const [canOpenInNewTab, setCanOpenInNewTab] = useState(false); const [emlContent, setEmlContent] = useState(null); // Decide whether to render the PDF in a plain