diff --git a/frontend/src/components/FilePreviewModal.css b/frontend/src/components/FilePreviewModal.css index b678426..68614ff 100644 --- a/frontend/src/components/FilePreviewModal.css +++ b/frontend/src/components/FilePreviewModal.css @@ -20,6 +20,15 @@ overflow: hidden; } +/* Wider and taller than the text/markdown case -- a PDF needs real room to + be readable, and its own viewer chrome (page controls, zoom) benefits + from more space than a text snippet ever would. */ +.file-preview-modal-pdf { + width: min(900px, 100%); + height: min(90vh, 1100px); + max-height: min(90vh, 1100px); +} + .file-preview-header { display: flex; align-items: center; @@ -76,6 +85,22 @@ overflow: auto; } +/* No padding, and let the iframe fill the available space -- the native + PDF viewer already has its own margins/toolbar, and needs the full body + height, not just its content's natural size. */ +.file-preview-body-pdf { + padding: 0; + flex: 1; + min-height: 0; + display: flex; +} + +.file-preview-pdf-frame { + flex: 1; + width: 100%; + border: none; +} + .file-preview-loading, .file-preview-error { font-size: 0.86rem; diff --git a/frontend/src/components/FilePreviewModal.tsx b/frontend/src/components/FilePreviewModal.tsx index daa500a..ecd2cb8 100644 --- a/frontend/src/components/FilePreviewModal.tsx +++ b/frontend/src/components/FilePreviewModal.tsx @@ -6,7 +6,7 @@ import type { MessageFileInfo } from '../types' import { MARKDOWN_OPTIONS } from './MessageContent' import './FilePreviewModal.css' -export type PreviewKind = 'markdown' | 'text' +export type PreviewKind = 'markdown' | 'text' | 'pdf' // Deliberately extension-based, not content_type-based: the browser-supplied // content_type for less-common extensions like .md is inconsistent (often @@ -16,6 +16,7 @@ export function getPreviewKind(filename: string): PreviewKind | null { const lower = filename.toLowerCase() if (lower.endsWith('.md') || lower.endsWith('.markdown')) return 'markdown' if (lower.endsWith('.txt')) return 'text' + if (lower.endsWith('.pdf')) return 'pdf' return null } @@ -29,35 +30,56 @@ interface FilePreviewModalProps { export function FilePreviewModal({ roomId, file, kind, onClose }: FilePreviewModalProps) { useEscapeKey(onClose) const [content, setContent] = useState(null) + const [pdfUrl, setPdfUrl] = useState(null) const [error, setError] = useState(null) const fileUrl = getRoomFileUrl(roomId, file.id) useEffect(() => { let cancelled = false + let objectUrl: string | null = null // A plain fetch() read is unaffected by the Content-Disposition: // attachment header the file-serve endpoint always sends -- that header // only steers the browser's own navigation/embed rendering, not a // script-initiated read of the response body. So no separate - // "inline"-flavored endpoint is needed just to preview text. - fetch(fileUrl, { credentials: 'include' }) - .then((res) => { - if (!res.ok) throw new Error(`Failed to load file (${res.status})`) - return res.text() - }) - .then((text) => { + // "inline"-flavored endpoint is needed just to preview text -- or, for + // PDF, to preview it either: fetching the bytes ourselves and handing + // the browser's native viewer a blob: URL (which carries no HTTP + // headers of its own) sidesteps Content-Disposition the same way, + // without needing an