diff --git a/docs/releases.md b/docs/releases.md index a0c0ad8e..835407bf 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1688,6 +1688,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Bug fixes + maintenance:** +* Web: Fix `clear=true` on action buttons not clearing the notification ([#1029](https://github.com/binwiederhier/ntfy/issues/1029), thanks to [@ElFishi](https://github.com/ElFishi) for reporting) * Fix crash when commit string is shorter than 7 characters in non-GitHub-Action builds ([#1493](https://github.com/binwiederhier/ntfy/issues/1493), thanks to [@cyrinux](https://github.com/cyrinux) for reporting) * Fix log spam from `http: response.WriteHeader on hijacked connection` for WebSocket errors ([#1362](https://github.com/binwiederhier/ntfy/issues/1362), thanks to [@bonfiresh](https://github.com/bonfiresh) for reporting) * Web: Fix Markdown message line height to match plain text (1.5 instead of 1.2) ([#1139](https://github.com/binwiederhier/ntfy/issues/1139), thanks to [@etfz](https://github.com/etfz) for reporting) diff --git a/web/public/sw.js b/web/public/sw.js index c8808145..6f807c24 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -237,8 +237,24 @@ const handleClick = async (event) => { if (event.action) { const action = event.notification.data.message.actions.find(({ label }) => event.action === label); + // Helper to clear notification and mark as read + const clearNotification = async () => { + event.notification.close(); + const { subscriptionId, message: msg } = event.notification.data; + const seqId = msg.sequence_id || msg.id; + if (subscriptionId && seqId) { + const db = await dbAsync(); + await db.notifications.where({ subscriptionId, sequenceId: seqId }).modify({ new: 0 }); + const badgeCount = await db.notifications.where({ new: 1 }).count(); + self.navigator.setAppBadge?.(badgeCount); + } + }; + if (action.action === "view") { self.clients.openWindow(action.url); + if (action.clear) { + await clearNotification(); + } } else if (action.action === "http") { try { const response = await fetch(action.url, { @@ -250,6 +266,11 @@ const handleClick = async (event) => { if (!response.ok) { throw new Error(`HTTP ${response.status} ${response.statusText}`); } + + // Only clear on success + if (action.clear) { + await clearNotification(); + } } catch (e) { console.error("[ServiceWorker] Error performing http action", e); self.registration.showNotification(`${t("notifications_actions_failed_notification")}: ${action.label} (${action.action})`, { @@ -259,10 +280,6 @@ const handleClick = async (event) => { }); } } - - if (action.clear) { - event.notification.close(); - } } else if (message.click) { self.clients.openWindow(message.click); diff --git a/web/src/app/notificationUtils.js b/web/src/app/notificationUtils.js index a3025f67..eb47cb97 100644 --- a/web/src/app/notificationUtils.js +++ b/web/src/app/notificationUtils.js @@ -60,6 +60,7 @@ export const toNotificationParams = ({ message, defaultTitle, topicRoute, baseUr const image = isImage(message.attachment) ? message.attachment.url : undefined; const sequenceId = message.sequence_id || message.id; const tag = notificationTag(baseUrl, topic, sequenceId); + const subscriptionId = `${baseUrl}/${topic}`; // https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API return [ @@ -75,6 +76,7 @@ export const toNotificationParams = ({ message, defaultTitle, topicRoute, baseUr silent: false, // This is used by the notification onclick event data: { + subscriptionId, message, topicRoute, }, diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index b7be8de2..3ee79c16 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -39,6 +39,7 @@ import { import { formatMessage, formatTitle, isImage } from "../app/notificationUtils"; import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles"; import subscriptionManager from "../app/SubscriptionManager"; +import notifier from "../app/Notifier"; import priority1 from "../img/priority-1.svg"; import priority2 from "../img/priority-2.svg"; import priority4 from "../img/priority-4.svg"; @@ -508,6 +509,15 @@ const updateActionStatus = (notification, action, progress, error) => { }); }; +const clearNotification = async (notification) => { + console.log(`[Notifications] Clearing notification ${notification.id}`); + const subscription = await subscriptionManager.get(notification.subscriptionId); + if (subscription) { + await notifier.cancel(subscription, notification); + } + await subscriptionManager.markNotificationRead(notification.id); +}; + const performHttpAction = async (notification, action) => { console.log(`[Notifications] Performing HTTP user action`, action); try { @@ -523,6 +533,9 @@ const performHttpAction = async (notification, action) => { const success = response.status >= 200 && response.status <= 299; if (success) { updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null); + if (action.clear) { + await clearNotification(notification); + } } else { updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`); } @@ -548,10 +561,16 @@ const UserAction = (props) => { ); } if (action.action === "view") { + const handleClick = () => { + openUrl(action.url); + if (action.clear) { + clearNotification(notification); + } + }; return (