Files could only hand you the bytes — double-clicking a picture put it on disk and left you to go find it. The viewer already existed for mail attachments (MessageView.tsx: image, PDF, text); it was wired to one screen. It is now ui/filepreview.tsx, used by both.
What it needed before it was any use on Files
Type detection falls back to the file name. An upload carries whatever the browser guessed, and files.ts stores f.type || "application/octet-stream" — so the old exact-type check saw nothing to show in a .md that had just been uploaded. A specific type still wins over the extension: a .txt served as a zip is a zip.
Text is read with fetch, which ignores Content-Disposition — so Markdown previews even though the server will not serve it inline.
Two questions, kept apart
lib/preview.ts answers them separately, and says why in the file:
previewKind — can the app render it?
openableInTab — will the server hand it back inline? That mirrors isInlineSafe in server/src/app.ts:700, the actual security boundary. Navigating to a blob the server will not inline just starts a download, so the "open in a new tab" button has to ask this one. A test pins the two together.
SVG stays out of both. It carries script, the server already refuses to inline it, and deciding how to show one safely is its own question — it keeps downloading, exactly as before.
Printing
A picture or a text file prints from the dialog with everything else dropped. A PDF prints itself from its own iframe — the page around it cannot paginate someone else’s document — falling back to opening it in a tab if the viewer refuses.
Hiding .app was not enough: #root kept its height: 100% and printed a blank first page, the same trap as break-inside on a message card in #189.
Verified
Mock stack in a visible Chrome tab, uploading files through the real upload path:
result
notes.md (no MIME type)
previews as text — the case that used to fail
shot.png
renders in the dialog
context menu
Preview above Download, only where there is something to show
mail attachments
unchanged; caption and Download still there
Print paths by headless print-to-PDF against the real stylesheet: text preview 1 page, content from the top, no mail or file list behind it; image preview 1 page, unclipped. Before the #root fix it was 2 pages with the first blank.
typecheck clean, 587 web + 109 server tests pass, i18n:check clean, build clean.
Not in this
Rendered Markdown needs a dependency (the runtime list is deliberately 8 packages), so .md shows as source for now. Editing is v2 and deliberately absent — that is where conflict handling and quota churn live.
Files could only hand you the bytes — double-clicking a picture put it on disk and left you to go find it. The viewer already existed for mail attachments (`MessageView.tsx`: image, PDF, text); it was wired to one screen. It is now `ui/filepreview.tsx`, used by both.
## What it needed before it was any use on Files
**Type detection falls back to the file name.** An upload carries whatever the browser guessed, and `files.ts` stores `f.type || "application/octet-stream"` — so the old exact-type check saw nothing to show in a `.md` that had just been uploaded. A specific type still wins over the extension: a `.txt` served as a zip is a zip.
**Text is read with `fetch`,** which ignores `Content-Disposition` — so Markdown previews even though the server will not serve it inline.
## Two questions, kept apart
`lib/preview.ts` answers them separately, and says why in the file:
- `previewKind` — can the app render it?
- `openableInTab` — will the *server* hand it back inline? That mirrors `isInlineSafe` in `server/src/app.ts:700`, the actual security boundary. Navigating to a blob the server will not inline just starts a download, so the "open in a new tab" button has to ask this one. A test pins the two together.
SVG stays out of both. It carries script, the server already refuses to inline it, and deciding how to show one safely is its own question — it keeps downloading, exactly as before.
## Printing
A picture or a text file prints from the dialog with everything else dropped. A PDF prints itself from its own iframe — the page around it cannot paginate someone else’s document — falling back to opening it in a tab if the viewer refuses.
Hiding `.app` was not enough: `#root` kept its `height: 100%` and printed a blank first page, the same trap as `break-inside` on a message card in #189.
## Verified
Mock stack in a visible Chrome tab, uploading files through the real upload path:
| | result |
|---|---|
| `notes.md` (no MIME type) | previews as text — the case that used to fail |
| `shot.png` | renders in the dialog |
| context menu | Preview above Download, only where there is something to show |
| mail attachments | unchanged; caption and Download still there |
Print paths by headless print-to-PDF against the real stylesheet: text preview 1 page, content from the top, no mail or file list behind it; image preview 1 page, unclipped. Before the `#root` fix it was 2 pages with the first blank.
`typecheck` clean, 587 web + 109 server tests pass, `i18n:check` clean, build clean.
## Not in this
Rendered Markdown needs a dependency (the runtime list is deliberately 8 packages), so `.md` shows as source for now. Editing is v2 and deliberately absent — that is where conflict handling and quota churn live.
**Merged** 2026-09-01 as coffey-labs/ihasmail@c170b8554c9e
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Files could only hand you the bytes — double-clicking a picture put it on disk and left you to go find it. The viewer already existed for mail attachments (
MessageView.tsx: image, PDF, text); it was wired to one screen. It is nowui/filepreview.tsx, used by both.What it needed before it was any use on Files
Type detection falls back to the file name. An upload carries whatever the browser guessed, and
files.tsstoresf.type || "application/octet-stream"— so the old exact-type check saw nothing to show in a.mdthat had just been uploaded. A specific type still wins over the extension: a.txtserved as a zip is a zip.Text is read with
fetch, which ignoresContent-Disposition— so Markdown previews even though the server will not serve it inline.Two questions, kept apart
lib/preview.tsanswers them separately, and says why in the file:previewKind— can the app render it?openableInTab— will the server hand it back inline? That mirrorsisInlineSafeinserver/src/app.ts:700, the actual security boundary. Navigating to a blob the server will not inline just starts a download, so the "open in a new tab" button has to ask this one. A test pins the two together.SVG stays out of both. It carries script, the server already refuses to inline it, and deciding how to show one safely is its own question — it keeps downloading, exactly as before.
Printing
A picture or a text file prints from the dialog with everything else dropped. A PDF prints itself from its own iframe — the page around it cannot paginate someone else’s document — falling back to opening it in a tab if the viewer refuses.
Hiding
.appwas not enough:#rootkept itsheight: 100%and printed a blank first page, the same trap asbreak-insideon a message card in #189.Verified
Mock stack in a visible Chrome tab, uploading files through the real upload path:
notes.md(no MIME type)shot.pngPrint paths by headless print-to-PDF against the real stylesheet: text preview 1 page, content from the top, no mail or file list behind it; image preview 1 page, unclipped. Before the
#rootfix it was 2 pages with the first blank.typecheckclean, 587 web + 109 server tests pass,i18n:checkclean, build clean.Not in this
Rendered Markdown needs a dependency (the runtime list is deliberately 8 packages), so
.mdshows as source for now. Editing is v2 and deliberately absent — that is where conflict handling and quota churn live.Merged 2026-09-01 as coffey-labs/ihasmail@c170b8554c
Rebuilt from: git history, session transcript.