Files
webmail/components/favicon-badge.tsx
T
KazNIISA IT e7d3782344 feat(pwa): badge the installed app icon with unread count (Badging API)
Adds useAppBadge alongside the existing useFaviconBadge, mounted from the
same FaviconBadge component and gated by the same faviconUnreadBadge
setting. Targets the separate icon Chrome/Edge/WebKit show for an
installed PWA (taskbar/dock/Home Screen/pinned-app-shortcut), which does
not react to favicon changes at all - a merely pinned browser tab is
unaffected, only an actually installed app.

No unmount cleanup: the badge is scoped to the installed app/origin, not
to this component or window, so clearing on every unmount would blank
another still-open window's accurate badge. A count of zero or the
setting being off already clears it through the main effect.
2026-09-03 11:37:36 +05:00

43 lines
1.7 KiB
TypeScript

"use client";
import { useEmailStore } from "@/stores/email-store";
import { useSettingsStore } from "@/stores/settings-store";
import { useFaviconBadge } from "@/hooks/use-favicon-badge";
import { useAppBadge } from "@/hooks/use-app-badge";
/**
* Badges the browser-tab favicon, and — where supported — the installed PWA's
* app icon, with the inbox unread count, so new mail is visible without
* focusing the tab. See issue #560.
*
* Two independent mechanisms share the one unread count: `useFaviconBadge`
* repaints the in-tab favicon (works everywhere); `useAppBadge` calls the
* Badging API for the separate icon Chrome/Edge pin next to the address bar
* once the app is installed — Chromium supports it there, WebKit supports it
* for an iOS/iPadOS Home Screen web app; a silent no-op anywhere else.
*
* Both opt out via the single `faviconUnreadBadge` setting (Settings ->
* Appearance); on by default.
*
* Mounted in the root layout rather than on the mail route: the badge belongs
* to the tab/app, not to a page. Mounting it on the mail page unmounted it —
* and so cleared the badge, and flickered the icon — on every hop to
* /settings, /calendar or /contacts.
*
* Renders nothing.
*/
export function FaviconBadge() {
// The store's canonical inbox selector. `role === 'inbox'` alone is not
// enough: shared and group inboxes ship in the same `mailboxes` array, so on
// a delegated setup the first match can be somebody else's inbox.
const inboxUnread = useEmailStore(
(s) => s.mailboxes.find((m) => m.role === "inbox" && !m.isShared)?.unreadEmails ?? 0,
);
const enabled = useSettingsStore((s) => s.faviconUnreadBadge);
useFaviconBadge(inboxUnread, enabled);
useAppBadge(inboxUnread, enabled);
return null;
}