fix(pwa): N105 — notification clicks work after the tab is closed
OS notifications were shown via page-level `new Notification()` whose onclick only works while the originating tab is alive — clicking a notification after closing the tab did nothing. - New `showOsNotification()` (utils/dom) prefers `registration.showNotification()` so the notification is service-worker-owned and persists; falls back to `new Notification()` (with the previous onclick) when no SW is available, so worst case is unchanged behaviour. - sw.ts gains a `notificationclick` handler: focuses an existing app window and forwards the target path, or opens the app if none is open. - ClientNonUIFeatures forwards the SW `notificationClick` message to react-router `navigate()` (works for both hash and browser router configs), and uses a per-room `tag` to coalesce notifications (replacing the old notifRef.close() dedup a SW notification can't hold). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import LogoUnreadSVG from '../../../../public/res/lotus-unread.png';
|
||||
import LogoHighlightSVG from '../../../../public/res/lotus-highlight.png';
|
||||
import NotificationSound from '../../../../public/sound/notification.ogg';
|
||||
import InviteSound from '../../../../public/sound/invite.ogg';
|
||||
import { notificationPermission, setFavicon } from '../../utils/dom';
|
||||
import { notificationPermission, setFavicon, showOsNotification } from '../../utils/dom';
|
||||
import { NOTIFICATION_SOUND_MAP } from '../../utils/notificationSounds';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
@@ -141,17 +141,21 @@ function InviteNotifications() {
|
||||
return;
|
||||
}
|
||||
|
||||
const noti = new window.Notification('Invitation', {
|
||||
icon: LogoSVG,
|
||||
badge: LogoSVG,
|
||||
body: `You have ${count} new invitation request.`,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
noti.onclick = () => {
|
||||
if (!window.closed) navigate(getInboxInvitesPath());
|
||||
noti.close();
|
||||
};
|
||||
const invitesPath = getInboxInvitesPath();
|
||||
showOsNotification(
|
||||
'Invitation',
|
||||
{
|
||||
icon: LogoSVG,
|
||||
badge: LogoSVG,
|
||||
body: `You have ${count} new invitation request.`,
|
||||
silent: true,
|
||||
tag: 'lotus-invites',
|
||||
data: { path: invitesPath },
|
||||
},
|
||||
() => {
|
||||
if (!window.closed) navigate(invitesPath);
|
||||
},
|
||||
);
|
||||
},
|
||||
[navigate, setToast],
|
||||
);
|
||||
@@ -202,7 +206,6 @@ function PresenceUpdater() {
|
||||
|
||||
function MessageNotifications() {
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const notifRef = useRef<Notification | undefined>(undefined);
|
||||
const unreadCacheRef = useRef<Map<string, UnreadInfo>>(new Map());
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
@@ -276,26 +279,42 @@ function MessageNotifications() {
|
||||
// persists in the notification center / lock screen / is readable by other
|
||||
// apps). For encrypted rooms show only the sender; the in-page toast above
|
||||
// still shows the preview while the user is actively looking at the screen.
|
||||
const noti = new window.Notification(roomName, {
|
||||
icon: LogoSVG,
|
||||
badge: LogoSVG,
|
||||
body: !encrypted && body ? `${username}: ${body}`.slice(0, 120) : username,
|
||||
silent: true,
|
||||
});
|
||||
|
||||
noti.onclick = () => {
|
||||
window.focus();
|
||||
navigate(roomPath);
|
||||
noti.close();
|
||||
notifRef.current = undefined;
|
||||
};
|
||||
|
||||
notifRef.current?.close();
|
||||
notifRef.current = noti;
|
||||
showOsNotification(
|
||||
roomName,
|
||||
{
|
||||
icon: LogoSVG,
|
||||
badge: LogoSVG,
|
||||
body: !encrypted && body ? `${username}: ${body}`.slice(0, 120) : username,
|
||||
silent: true,
|
||||
// Coalesce repeated notifications for the same room (replaces the old
|
||||
// manual notifRef.close() dedup, which a SW notification can't hold).
|
||||
tag: roomId,
|
||||
data: { path: roomPath },
|
||||
},
|
||||
() => {
|
||||
window.focus();
|
||||
navigate(roomPath);
|
||||
},
|
||||
);
|
||||
},
|
||||
[navigate, setToast, mDirects],
|
||||
);
|
||||
|
||||
// N105: when a service-worker-owned notification is clicked, the SW focuses
|
||||
// this tab and forwards the target path here so we can route to it (works even
|
||||
// when the click happened while the tab was in the background / reopened).
|
||||
useEffect(() => {
|
||||
if (!('serviceWorker' in navigator)) return undefined;
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
const data = event.data ?? {};
|
||||
if (data.type === 'notificationClick' && typeof data.path === 'string') {
|
||||
navigate(data.path);
|
||||
}
|
||||
};
|
||||
navigator.serviceWorker.addEventListener('message', onMessage);
|
||||
return () => navigator.serviceWorker.removeEventListener('message', onMessage);
|
||||
}, [navigate]);
|
||||
|
||||
const playSound = useCallback(() => {
|
||||
const audioElement = audioRef.current;
|
||||
audioElement?.play();
|
||||
|
||||
Reference in New Issue
Block a user