import { useEffect, useMemo, useState } from 'react' import Markdown from 'markdown-to-jsx' import { getRoomFileUrl } from '../api/rooms' import { useEscapeKey } from '../hooks/useEscapeKey' import type { MessageFileInfo } from '../types' import { createMarkdownOptions, preprocessMarkdown } from './MessageContent' import './FilePreviewModal.css' 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 // reported as empty or application/octet-stream), so it isn't reliable // enough to gate what gets parsed as markdown vs. shown as literal text. 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 } interface FilePreviewModalProps { roomId: string file: MessageFileInfo kind: PreviewKind onClose: () => void } 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) // #21: subscript/superscript and heading-id support -- see MessageContent // for why this needs to run before the Markdown component sees the text. const markdownPreview = useMemo( () => (content !== null ? preprocessMarkdown(content) : null), [content], ) 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 -- 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