diff --git a/server/types.go b/server/types.go index 90995878..ba00b690 100644 --- a/server/types.go +++ b/server/types.go @@ -474,7 +474,7 @@ type apiWebPushUpdateSubscriptionRequest struct { Topics []string `json:"topics"` } -// List of possible Web Push events +// List of possible Web Push events (see sw.js) const ( webPushMessageEvent = "message" webPushExpiringEvent = "subscription_expiring" @@ -486,8 +486,8 @@ type webPushPayload struct { Message *message `json:"message"` } -func newWebPushPayload(subscriptionID string, message *message) webPushPayload { - return webPushPayload{ +func newWebPushPayload(subscriptionID string, message *message) *webPushPayload { + return &webPushPayload{ Event: webPushMessageEvent, SubscriptionID: subscriptionID, Message: message, @@ -498,8 +498,8 @@ type webPushControlMessagePayload struct { Event string `json:"event"` } -func newWebPushSubscriptionExpiringPayload() webPushControlMessagePayload { - return webPushControlMessagePayload{ +func newWebPushSubscriptionExpiringPayload() *webPushControlMessagePayload { + return &webPushControlMessagePayload{ Event: webPushExpiringEvent, } } diff --git a/web/public/sw.js b/web/public/sw.js index bf6e8dbe..98ae3d8f 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -5,7 +5,7 @@ import { NetworkFirst } from "workbox-strategies"; import { dbAsync } from "../src/app/db"; -import { getNotificationParams, icon, badge } from "../src/app/notificationUtils"; +import { toNotificationParams, icon, badge } from "../src/app/notificationUtils"; import i18n from "../src/app/i18n"; @@ -40,50 +40,70 @@ const addNotification = async ({ subscriptionId, message }) => { self.navigator.setAppBadge?.(badgeCount); }; +/** + * Handle a received web push message and show notification. + * + * Since the service worker cannot play a sound, we send a broadcast to the web app, which (if it is running) + * receives the broadcast and plays a sound (see web/src/app/WebPush.js). + */ +const handlePushMessage = async (data) => { + const { subscription_id: subscriptionId, message } = data; + + broadcastChannel.postMessage(message); // To potentially play sound + + await addNotification({ subscriptionId, message }); + await self.registration.showNotification( + ...toNotificationParams({ + subscriptionId, + message, + defaultTitle: message.topic, + topicRoute: new URL(message.topic, self.location.origin).toString(), + }) + ); +}; + +/** + * Handle a received web push subscription expiring. + */ +const handlePushSubscriptionExpiring = async (data) => { + await self.registration.showNotification(i18n.t("web_push_subscription_expiring_title"), { + body: i18n.t("web_push_subscription_expiring_body"), + icon, + data, + badge, + }); +}; + +/** + * Handle unknown push message. We can't ignore the push, since + * permission can be revoked by the browser. + */ +const handlePushUnknown = async (data) => { + await self.registration.showNotification(i18n.t("web_push_unknown_notification_title"), { + body: i18n.t("web_push_unknown_notification_body"), + icon, + data, + badge, + }); +}; + /** * Handle a received web push notification * @param {object} data see server/types.go, type webPushPayload */ const handlePush = async (data) => { - if (data.event === "subscription_expiring") { - await self.registration.showNotification(i18n.t("web_push_subscription_expiring_title"), { - body: i18n.t("web_push_subscription_expiring_body"), - icon, - data, - badge, - }); - } else if (data.event === "message") { - const { subscription_id: subscriptionId, message } = data; - - // see: web/src/app/WebPush.js - // the service worker cannot play a sound, so if the web app - // is running, it receives the broadcast and plays it. - broadcastChannel.postMessage(message); - - await addNotification({ subscriptionId, message }); - - await self.registration.showNotification( - ...getNotificationParams({ - subscriptionId, - message, - defaultTitle: message.topic, - topicRoute: new URL(message.topic, self.location.origin).toString(), - }) - ); + if (data.event === "message") { + await handlePushMessage(data); + } else if (data.event === "subscription_expiring") { + await handlePushSubscriptionExpiring(data); } else { - // We can't ignore the push, since permission can be revoked by the browser - await self.registration.showNotification(i18n.t("web_push_unknown_notification_title"), { - body: i18n.t("web_push_unknown_notification_body"), - icon, - data, - badge, - }); + await handlePushUnknown(data); } }; /** - * Handle a user clicking on the displayed notification from `showNotification` - * This is also called when the user clicks on an action button + * Handle a user clicking on the displayed notification from `showNotification`. + * This is also called when the user clicks on an action button. */ const handleClick = async (event) => { const clients = await self.clients.matchAll({ type: "window" }); @@ -195,7 +215,7 @@ self.addEventListener("notificationclick", (event) => { event.waitUntil(handleClick(event)); }); -// see https://vite-pwa-org.netlify.app/guide/inject-manifest.html#service-worker-code +// See https://vite-pwa-org.netlify.app/guide/inject-manifest.html#service-worker-code // self.__WB_MANIFEST is the workbox injection point that injects the manifest of the // vite dist files and their revision ids, for example: // [{"revision":"aaabbbcccdddeeefff12345","url":"/index.html"},...] @@ -204,7 +224,7 @@ precacheAndRoute( self.__WB_MANIFEST ); -// delete any cached old dist files from previous service worker versions +// Delete any cached old dist files from previous service worker versions cleanupOutdatedCaches(); if (import.meta.env.MODE !== "development") { diff --git a/web/src/app/Notifier.js b/web/src/app/Notifier.js index b0311f40..fa1498a3 100644 --- a/web/src/app/Notifier.js +++ b/web/src/app/Notifier.js @@ -1,5 +1,5 @@ import { playSound, topicDisplayName, topicShortUrl, urlB64ToUint8Array } from "./utils"; -import { getNotificationParams } from "./notificationUtils"; +import { toNotificationParams } from "./notificationUtils"; import prefs from "./Prefs"; import routes from "../components/routes"; @@ -22,7 +22,7 @@ class Notifier { const registration = await this.serviceWorkerRegistration(); await registration.showNotification( - ...getNotificationParams({ + ...toNotificationParams({ subscriptionId: subscription.id, message: notification, defaultTitle, diff --git a/web/src/app/notificationUtils.js b/web/src/app/notificationUtils.js index 77437729..adc56318 100644 --- a/web/src/app/notificationUtils.js +++ b/web/src/app/notificationUtils.js @@ -39,7 +39,7 @@ const isImage = (filenameOrUrl) => filenameOrUrl?.match(/\.(png|jpe?g|gif|webp)$ export const icon = "/static/images/ntfy.png"; export const badge = "/static/images/mask-icon.svg"; -export const getNotificationParams = ({ subscriptionId, message, defaultTitle, topicRoute }) => { +export const toNotificationParams = ({ subscriptionId, message, defaultTitle, topicRoute }) => { const image = isImage(message.attachment?.name) ? message.attachment.url : undefined; // https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API