Files
webmail/hooks/use-app-badge.ts
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

53 lines
2.2 KiB
TypeScript

"use client";
import { useEffect } from 'react';
import { debug } from '@/lib/debug';
/**
* Badges the *installed* PWA's icon — the shortcut Chrome/Edge pin next to
* the address bar, or the OS taskbar/dock/Home Screen icon once launched
* standalone — with the inbox unread count, via the Badging API
* (`navigator.setAppBadge`/`clearAppBadge`).
*
* This is a different surface from `useFaviconBadge`: that one repaints the
* favicon *inside* the browser tab strip and works in any tab, installed or
* not. This one calls a spec API that targets the installed app icon —
* Chromium supports it there; WebKit supports it for iOS/iPadOS Home Screen
* web apps (not plain Safari tabs). Either way it's a no-op wherever
* unsupported: both methods are feature-detected per call, so a browser that
* has one but not the other (or neither) never throws.
*
* No unmount cleanup: the badge is scoped to the installed app/origin, not
* to this component or even this window — with several windows of the same
* app open, whichever last called set/clear wins for all of them (that's the
* API's own model, not a bug here). Clearing on every unmount would fight
* that: window A closing would blank window B's still-accurate badge. A
* `count` of zero (fully-read inbox) or `enabled: false` already clears it
* through the effect below; unmounting without either of those (e.g. a
* logout that doesn't first zero the store) can leave a stale number until
* the app's next unread-count change — accepted as inherent to a
* window/tab-agnostic API without adding cross-window coordination
* (service worker, BroadcastChannel) for it.
*/
export function useAppBadge(count: number, enabled = true): void {
useEffect(() => {
if (typeof navigator === 'undefined') return;
const effectiveCount = enabled ? count : 0;
void (async () => {
try {
if (effectiveCount > 0) {
if (typeof navigator.setAppBadge !== 'function') return;
await navigator.setAppBadge(effectiveCount);
} else {
if (typeof navigator.clearAppBadge !== 'function') return;
await navigator.clearAppBadge();
}
} catch (error) {
debug.log('[app-badge] failed:', error);
}
})();
}, [count, enabled]);
}