Look at a file without downloading it first

Files could only hand you the bytes: double-clicking a picture put it on
disk and left you to find it. The viewer for this already existed --
images, PDFs and text, in the attachment preview in MessageView -- it was
just wired to one screen. It is now a component both screens use.

Two things it needed before it was any use on Files. The type detection
falls back to the file name: an upload carries whatever the browser
guessed, which for anything unusual is application/octet-stream, so the
old exact-type check saw nothing to show in a .md that had just been
uploaded. And text is read with fetch, which ignores Content-Disposition,
so Markdown previews even though the server will not serve it inline.

Whether we can show a file and whether the server will serve it inline
are separate questions, and lib/preview.ts answers them separately:
`openableInTab` mirrors the isInlineSafe allowlist in the blob route,
because navigating to a blob the server will not inline just starts a
download. SVG is left out of both -- it carries script, and how to show
one safely is its own question, not a detail of a file lister.

Printing goes with it. A picture or a text file prints from the dialog
with everything else dropped; a PDF prints itself from its own iframe,
since the page around it cannot paginate someone else's document. Hiding
`.app` alone was not enough there -- `#root` kept its height and printed a
blank first page, the same trap as the message card.
This commit is contained in:
2026-09-01 20:18:51 -07:00
parent 49f9a06009
commit 984f0474e3
6 changed files with 370 additions and 19 deletions
+18
View File
@@ -1244,6 +1244,24 @@ select optgroup { background-color: var(--bg-elev); color: var(--fg); }
*/
.printing-one .message:not(.print-target) { display: none !important; }
.printing-one .thread-subject > .muted { display: none !important; }
/*
* Printing from the file viewer. The dialog is the document: the mail or the
* file listing behind it is not what was asked for, and the chrome of the
* dialog itself -- title bar, Print and Download buttons -- is not part of
* the file. A PDF never comes through here; it prints itself from its own
* iframe (see ui/filepreview.tsx).
*/
/* The dialog is portalled to <body>, so the whole app goes -- hiding `.app`
alone left `#root` holding its `height: 100%` and printing a blank first
page, the same way `break-inside` did on a message. */
.printing-preview #root { display: none !important; }
.printing-preview body { height: auto !important; }
.printing-preview .dialog-backdrop { position: static !important; display: block !important; padding: 0 !important; background: none !important; backdrop-filter: none !important; animation: none !important; }
.printing-preview .dialog { max-width: none !important; max-height: none !important; border: 0 !important; box-shadow: none !important; animation: none !important; background: none !important; }
.printing-preview .dialog-head, .printing-preview .dialog-foot { display: none !important; }
.printing-preview .dialog-body { padding: 0 !important; overflow: visible !important; }
.printing-preview .dialog-body .code { max-height: none !important; overflow: visible !important; border: 0 !important; padding: 0 !important; }
.printing-preview .dialog-body img { max-height: none !important; }
.print-only { display: block; }
body { background: #fff; color: #000; }
}