From b1cbedc1953497c76ca6c9aca4fd821f4c41aa3c Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 23 Mar 2026 17:49:33 -0600 Subject: [PATCH] Try to handle iOS stacked notifications --- web/public/notifications-worker.js | 41 ++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/web/public/notifications-worker.js b/web/public/notifications-worker.js index a264a1e4e..b42a32a56 100644 --- a/web/public/notifications-worker.js +++ b/web/public/notifications-worker.js @@ -19,19 +19,40 @@ self.addEventListener("push", function (event) { break; } + const notificationOptions = { + body: data.message, + icon: "/images/maskable-icon.png", + image: data.image, + badge: "/images/maskable-badge.png", + tag: data.id, + data: { id: data.id, link: data.direct_url }, + actions, + }; + + // iOS Safari does not auto-coalesce notifications by tag (WebKit bug #258922). + // On iOS 18.3+ close() works, so we manually close duplicates before showing. + // On other platforms, tag-based replacement works natively — skip the extra work. + const isIOS = + /iPad|iPhone|iPod/.test(navigator.userAgent) && !self.MSStream; + + const show = () => + // @ts-expect-error we know this exists + self.registration.showNotification(data.title, notificationOptions); + // event.waitUntil is required on iOS Safari — without it, the browser // may consider this a "silent push" and revoke the subscription after 3 occurrences. event.waitUntil( - // @ts-expect-error we know this exists - self.registration.showNotification(data.title, { - body: data.message, - icon: "/images/maskable-icon.png", - image: data.image, - badge: "/images/maskable-badge.png", - tag: data.id, - data: { id: data.id, link: data.direct_url }, - actions, - }), // eslint-disable-line comma-dangle + isIOS + ? // @ts-expect-error we know this exists + self.registration + .getNotifications({ tag: data.id }) + .then((existing) => { + for (const n of existing) { + n.close(); + } + }) + .then(show) + : show(), // eslint-disable-line comma-dangle ); } else { // pass