Three things an installed ihasmail did not do that a phone user expects. All of them are about the app once it is off the browser tab.
The unread count on the icon
The count was painted into the tab title and the favicon, and display: standalone has neither — so putting ihasmail on a home screen threw it away entirely. It now goes to the Badging API as well.
Web Push marks the icon while the app is closed, 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. Unsupported browsers show nothing, as does iOS until notification permission is granted.
Share, instead of Download
Everything that left ihasmail left as a download, which on a phone is close to a dead end. The share sheet is now on the message menu, on each attachment row, and in the file viewer — where an attachment is already open, and where the mail and Files callers meet.
A message shares as text, not 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 asked about separately from sharing at all — desktop Linux and Firefox have neither. Anything that fails 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 is almost always the tap's transient activation running out while a large attachment is fetched, it cannot be told apart from a refusal, and a toast about activation is not something a reader can act on.
Manifest
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 still deliberately no id, and the manifest now records 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 isstart_url, which is mount-correct. Writing one now would give every installed copy a new identity and orphan it as a second app.
Translations
Three new strings in all nine catalogues: Share, Share… and Could not share: {error}.
They are contextual keys rather than the existing "Share…". That one means granting another account access, and several languages use a different verb: German had "Freigeben" where a share sheet wants "Teilen", Russian "Открывать доступ" where it wants "Поделиться". Marked Beta like the rest — no native speaker has read them.
Verification
npm run typecheck, npm test (1127 web + 161 server) and npm run build all pass; npm run i18n:check reports no new missing keys.
Not driven on a device: the extension driving Chrome was not connected, and Chrome on Linux has no Web Share to drive in any case. The preview dialog is covered by a component test that stubs the browser both ways — button absent without Web Share, a File handed to the sheet with it, and the download fallback firing when the blob cannot be fetched — plus unit tests for the outcome mapping.
Worth a look on a real phone before merging, since that is the only place the badge and the sheet actually appear.
Three things an installed ihasmail did not do that a phone user expects. All of them are about the app once it is off the browser tab.
### The unread count on the icon
The count was painted into the tab title and the favicon, and `display: standalone` has neither — so putting ihasmail on a home screen threw it away entirely. It now goes to the Badging API as well.
Web Push marks the icon while the app is closed, 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. Unsupported browsers show nothing, as does iOS until notification permission is granted.
### Share, instead of Download
Everything that left ihasmail left as a download, which on a phone is close to a dead end. The share sheet is now on the message menu, on each attachment row, and in the file viewer — where an attachment is already open, and where the mail and Files callers meet.
A message shares as **text**, not 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 asked about separately from sharing at all — desktop Linux and Firefox have neither. Anything that fails 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 is almost always the tap's transient activation running out while a large attachment is fetched, it cannot be told apart from a refusal, and a toast about activation is not something a reader can act on.
### Manifest
`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 still deliberately **no `id`**, and the manifest now records 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`, which is mount-correct. Writing one now would give every installed copy a new identity and orphan it as a second app.
### Translations
Three new strings in all nine catalogues: `Share`, `Share…` and `Could not share: {error}`.
They are **contextual keys** rather than the existing `"Share…"`. That one means granting another account access, and several languages use a different verb: German had "Freigeben" where a share sheet wants "Teilen", Russian "Открывать доступ" where it wants "Поделиться". Marked Beta like the rest — no native speaker has read them.
### Verification
`npm run typecheck`, `npm test` (1127 web + 161 server) and `npm run build` all pass; `npm run i18n:check` reports no new missing keys.
Not driven on a device: the extension driving Chrome was not connected, and Chrome on Linux has no Web Share to drive in any case. The preview dialog is covered by a component test that stubs the browser both ways — button absent without Web Share, a `File` handed to the sheet with it, and the download fallback firing when the blob cannot be fetched — plus unit tests for the outcome mapping.
Worth a look on a real phone before merging, since that is the only place the badge and the sheet actually appear.
**Merged** 2026-09-07 as coffey-labs/ihasmail@f39d6ac30c2f
<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.
Three things an installed ihasmail did not do that a phone user expects. All of them are about the app once it is off the browser tab.
The unread count on the icon
The count was painted into the tab title and the favicon, and
display: standalonehas neither — so putting ihasmail on a home screen threw it away entirely. It now goes to the Badging API as well.Web Push marks the icon while the app is closed, 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. Unsupported browsers show nothing, as does iOS until notification permission is granted.
Share, instead of Download
Everything that left ihasmail left as a download, which on a phone is close to a dead end. The share sheet is now on the message menu, on each attachment row, and in the file viewer — where an attachment is already open, and where the mail and Files callers meet.
A message shares as text, not as the
.emlbeside it: a share sheet is aimed at everything that is not a mail client, and an.emlin a chat app is an attachment nobody can open.Every control feature-detects, and sharing a file is asked about separately from sharing at all — desktop Linux and Firefox have neither. Anything that fails falls through to the download the button sits beside, so the worst case costs a tap rather than the file.
NotAllowedErroris reported as unsupported for that reason: it is almost always the tap's transient activation running out while a large attachment is fetched, it cannot be told apart from a refusal, and a toast about activation is not something a reader can act on.Manifest
launch_handler: navigate-existing, so amailto:, 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-existingwould have been wrong: it only focuses and leaves the target URL tolaunchQueue, which nothing here consumes, so it would swallow the mailto.There is still deliberately no
id, and the manifest now records why. It is the one member resolved against the origin ofstart_urlrather than against the manifest's own address, so no relative form can name a subpath mount — and the default id already isstart_url, which is mount-correct. Writing one now would give every installed copy a new identity and orphan it as a second app.Translations
Three new strings in all nine catalogues:
Share,Share…andCould not share: {error}.They are contextual keys rather than the existing
"Share…". That one means granting another account access, and several languages use a different verb: German had "Freigeben" where a share sheet wants "Teilen", Russian "Открывать доступ" where it wants "Поделиться". Marked Beta like the rest — no native speaker has read them.Verification
npm run typecheck,npm test(1127 web + 161 server) andnpm run buildall pass;npm run i18n:checkreports no new missing keys.Not driven on a device: the extension driving Chrome was not connected, and Chrome on Linux has no Web Share to drive in any case. The preview dialog is covered by a component test that stubs the browser both ways — button absent without Web Share, a
Filehanded to the sheet with it, and the download fallback firing when the blob cannot be fetched — plus unit tests for the outcome mapping.Worth a look on a real phone before merging, since that is the only place the badge and the sheet actually appear.
Merged 2026-09-07 as coffey-labs/ihasmail@f39d6ac30c
Rebuilt from: git history, session transcript.