Files
ihasmail/web/src/views/settings/NotificationsSettings.tsx
T
jcoffey-dev 7726665a48 Say what "even when ihasmail is closed" actually means
Web Push works, confirmed end to end against the live 0.16.19: with
Chrome open and every ihasmail tab closed, a notification arrives
immediately and names the sender and subject.

But "closed" means ihasmail, not the browser, and the switch did not say
so. Web Push is delivered over a connection the browser holds, so
something of it has to be running.

Observed on 2026-08-26, with Chrome fully quit and "Continue running
background apps" off: nothing arrived until Chrome was started again, at
which point the queued notification was delivered. Turning that setting
on keeps a process alive and restores immediate delivery.

Worth knowing that the queue is not indefinite -- a Web Push message
carries a TTL, and one that expires before the browser comes back is
dropped rather than delivered late. Being an installed PWA does not
change any of this on a desktop; it changes the window, not who holds
the connection. On Android it would, since the push service can wake the
browser from cold.

None of this is ihasmail's to fix. It is what Web Push is, and the only
thing worth doing about it is not implying otherwise -- which the
notification switch was quietly doing.
2026-08-26 14:13:19 -07:00

86 lines
4.0 KiB
TypeScript

import { useEffect, useState } from "react";
import { useSettings } from "@/store/settings";
import { Switch } from "@/ui/misc";
import { requestNotificationPermission, showNotification, playNewMailSound } from "@/lib/notify";
import { useSession } from "@/store/session";
import { disableWebPush, enableWebPush, webPushActive } from "@/lib/webpushEnable";
import { supportsEmailPush, webPushAvailable } from "@/lib/webpush";
import { toast } from "@/ui/toast";
export function NotificationsSettings() {
const s = useSettings((st) => st.settings);
const update = useSettings((st) => st.update);
const pushConnected = useSession((st) => st.pushConnected);
const [perm, setPerm] = useState<NotificationPermission | "unsupported">("Notification" in window ? Notification.permission : "unsupported");
const [background, setBackground] = useState(false);
const [busy, setBusy] = useState(false);
const canBackground = webPushAvailable();
useEffect(() => {
void webPushActive().then(setBackground);
}, []);
useEffect(() => {
if ("Notification" in window) setPerm(Notification.permission);
}, [s.desktopNotifications]);
return (
<div>
<h1>Notifications</h1>
<p className="lead">Live updates are delivered via JMAP push ({pushConnected ? "connected" : "reconnecting…"}).</p>
<Switch
checked={s.desktopNotifications}
onChange={async (v) => {
if (v) {
const p = await requestNotificationPermission();
setPerm(p);
if (p !== "granted") return;
}
update({ desktopNotifications: v });
}}
label="Desktop notifications while ihasmail is open"
hint={perm === "denied" ? "Notifications are blocked in your browser settings." : perm === "unsupported" ? "Not supported in this browser." : "Shows a system notification when new mail arrives in your Inbox while the tab is in the background."}
disabled={perm === "denied" || perm === "unsupported"}
/>
{/*
The distinction worth drawing for the user: the switch above needs a tab
open, this one does not. Everything before this shipped only the first
kind, while calling it "desktop notifications".
*/}
<Switch
checked={background}
disabled={!canBackground || busy || perm === "denied"}
onChange={async (v) => {
setBusy(true);
try {
if (v) {
const p = await requestNotificationPermission();
setPerm(p);
if (p !== "granted") return;
const res = await enableWebPush();
if (!res.ok) { toast.error(res.reason); return; }
setBackground(true);
toast.success("Background notifications are on");
} else {
await disableWebPush();
setBackground(false);
}
} finally {
setBusy(false);
}
}}
label="Notify me even when ihasmail is closed"
hint={
!canBackground
? "Needs a browser with the Push API and a mail server that publishes a push key."
: supportsEmailPush()
? "Your mail server delivers these straight to your browser, so they arrive with no ihasmail tab open, naming the sender and subject. Your browser still has to be running — if you quit it completely, notifications wait and arrive when you open it again."
: "Your mail server can wake this browser, but will not include the sender or subject. Your browser still has to be running."
}
/>
<Switch checked={s.notificationSound} onChange={(v) => update({ notificationSound: v })} label="Play a sound for new mail" />
<div className="row mt-16">
<button className="btn" onClick={() => { showNotification("ihasmail test", { body: "This is what a new-mail notification looks like." }); playNewMailSound(); }}>Test notification</button>
</div>
<p className="hint mt-8">The tab title and favicon always show your unread Inbox count.</p>
</div>
);
}