Three things an installed ihasmail did not do that a phone user expects,
and all three are about the app once it is off the browser tab.
The unread count was painted into the tab title and the favicon, neither
of which exists in `display: standalone` -- so putting ihasmail on a home
screen threw the count away entirely. It goes to the Badging API as well
now. Web Push marks the icon while the app is closed, and marks it with a
dot rather than a figure: the service worker has no session to ask how
many messages are unread, and a push carries the new mail rather than a
total, so counting the payload would badge "2" over an inbox holding
forty. The next tab to open writes the real count over it.
Sharing is new. Everything that left ihasmail left as a download, which
on a phone is close to a dead end -- the file lands in Downloads and
whoever meant to send it somewhere goes looking for it in a file manager.
The share sheet is now on the message menu, on each attachment row, and
in the file viewer, which is where an attachment is already open and
where both callers meet. A message shares as text rather than as the
.eml beside it: a share sheet is aimed at everything that is not a mail
client, and an .eml in a chat app is an attachment nobody can open.
Every control feature-detects, and sharing a file is a separate question
from sharing at all -- desktop Linux and Firefox have neither, and not
every browser with `share` takes files. Anything that fails, including
the transient activation running out while a large attachment is fetched,
falls through to the download the button sits beside, so the worst case
costs a tap rather than the file. `NotAllowedError` is reported as
unsupported for that reason: it cannot be told apart from a refusal, and
a toast about activation is not something a reader can act on.
The share strings are contextual keys rather than the existing "Share…".
That one means granting another account access, and several languages use
a different verb for it -- German had "Freigeben" where the sheet wants
"Teilen". Three new strings, in all nine catalogues.
The manifest gains `launch_handler: navigate-existing`, so a mailto:, a
shortcut or a notification tapped while ihasmail is running arrives in
the copy that is running: two windows on one inbox disagree about what
has been read. `focus-existing` would have been wrong -- it only focuses
and leaves the target URL to launchQueue, which nothing here consumes, so
it would swallow the mailto. There is deliberately still no `id`, and the
manifest now says why: it is the one member resolved against the origin
of start_url rather than against the manifest's own address, so no
relative form can name a subpath mount, and the default id already is
start_url -- writing one now would give every installed copy a new
identity and orphan it as a second app.
Verified by test rather than on a device: the extension driving Chrome
was not connected, and Chrome on Linux has no Web Share to drive anyway.
The preview dialog is covered by a component test that stubs the browser
both ways.
v2 of the viewer: Edit, on text and Markdown, in the dialog and on the
row menu. Save is explicit -- every save mints a new blob, so autosave
would burn quota and multiply the conflicts it cannot see.
Two people editing one file is the case worth getting right. `saveText`
re-reads the node and compares the blob the editor started from: if
somebody else saved in the meantime it refuses, says so, and leaves the
work in the box to copy out. `ifInState` is the obvious tool and the
wrong one -- it is the state of every FileNode in the account, so an
unrelated upload in another folder would fail the save, and a warning
that cries wolf is a warning people click through.
Editing is not offered where saving would lose something: a file
truncated for display would have its tail written away, and one that did
not decode as UTF-8 would have mojibake written over whatever encoding it
really is. Both open read-only and say which. Nor is it offered without
mayModifyContent -- a read-only share just has no Edit.
Closing or cancelling with unsaved changes asks first, Ctrl+S saves, and
mail attachments are unaffected: they pass no onSave, because a message
part is not a thing that can be written back.
A .md previewed as its own source, which is reading the punctuation
rather than the notes. It now opens rendered, with Rendered | Source in
the dialog footer for anyone who wants what the file actually says.
Markdown only; a .txt has nothing to toggle between.
Rendering is `marked`, sanitised by DOMPurify -- the one the app already
carries for mail. Markdown is not a safe subset of anything: raw HTML
passes through it by design, so a <script> in a file somebody uploaded or
shared into the account is a script tag unless something takes it out.
Images become links rather than pictures. An image in a Markdown file is
either a relative path, which has no base to resolve against here, or a
URL somewhere else, which fetches on open and tells that server the file
was read -- the tracking pixel this app blocks in mail. The link keeps
the alt text and the address, so nothing vanishes silently.
Fixes the PDF preview while here, which never worked: securityHeaders
put X-Frame-Options: DENY on every response including the blob route, so
the iframe showed Chrome's "refused to connect" where the file should
have been -- in Files today and in mail attachments long before that.
The middleware now leaves a header the route has set, and a PDF served
inline says SAMEORIGIN. Nothing else on the server is framable.
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.