mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-01-18 16:17:26 +01:00
Fix service worker handling for updating/deleting
This commit is contained in:
@@ -86,8 +86,6 @@ var (
|
|||||||
|
|
||||||
webConfigPath = "/config.js"
|
webConfigPath = "/config.js"
|
||||||
webManifestPath = "/manifest.webmanifest"
|
webManifestPath = "/manifest.webmanifest"
|
||||||
webRootHTMLPath = "/app.html"
|
|
||||||
webServiceWorkerPath = "/sw.js"
|
|
||||||
accountPath = "/account"
|
accountPath = "/account"
|
||||||
matrixPushPath = "/_matrix/push/v1/notify"
|
matrixPushPath = "/_matrix/push/v1/notify"
|
||||||
metricsPath = "/metrics"
|
metricsPath = "/metrics"
|
||||||
@@ -111,7 +109,7 @@ var (
|
|||||||
apiAccountBillingSubscriptionCheckoutSuccessTemplate = "/v1/account/billing/subscription/success/{CHECKOUT_SESSION_ID}"
|
apiAccountBillingSubscriptionCheckoutSuccessTemplate = "/v1/account/billing/subscription/success/{CHECKOUT_SESSION_ID}"
|
||||||
apiAccountBillingSubscriptionCheckoutSuccessRegex = regexp.MustCompile(`/v1/account/billing/subscription/success/(.+)$`)
|
apiAccountBillingSubscriptionCheckoutSuccessRegex = regexp.MustCompile(`/v1/account/billing/subscription/success/(.+)$`)
|
||||||
apiAccountReservationSingleRegex = regexp.MustCompile(`/v1/account/reservation/([-_A-Za-z0-9]{1,64})$`)
|
apiAccountReservationSingleRegex = regexp.MustCompile(`/v1/account/reservation/([-_A-Za-z0-9]{1,64})$`)
|
||||||
staticRegex = regexp.MustCompile(`^/static/.+`)
|
staticRegex = regexp.MustCompile(`^/(static/.+|app.html|sw.js|sw.js.map)$`)
|
||||||
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
|
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
|
||||||
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
|
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
|
||||||
urlRegex = regexp.MustCompile(`^https?://`)
|
urlRegex = regexp.MustCompile(`^https?://`)
|
||||||
@@ -534,7 +532,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
|||||||
return s.handleMatrixDiscovery(w)
|
return s.handleMatrixDiscovery(w)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == metricsPath && s.metricsHandler != nil {
|
} else if r.Method == http.MethodGet && r.URL.Path == metricsPath && s.metricsHandler != nil {
|
||||||
return s.handleMetrics(w, r, v)
|
return s.handleMetrics(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && (staticRegex.MatchString(r.URL.Path) || r.URL.Path == webServiceWorkerPath || r.URL.Path == webRootHTMLPath) {
|
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
||||||
return s.ensureWebEnabled(s.handleStatic)(w, r, v)
|
return s.ensureWebEnabled(s.handleStatic)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
|
||||||
return s.ensureWebEnabled(s.handleDocs)(w, r, v)
|
return s.ensureWebEnabled(s.handleDocs)(w, r, v)
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from
|
|||||||
import { NavigationRoute, registerRoute } from "workbox-routing";
|
import { NavigationRoute, registerRoute } from "workbox-routing";
|
||||||
import { NetworkFirst } from "workbox-strategies";
|
import { NetworkFirst } from "workbox-strategies";
|
||||||
import { clientsClaim } from "workbox-core";
|
import { clientsClaim } from "workbox-core";
|
||||||
|
|
||||||
import { dbAsync } from "../src/app/db";
|
import { dbAsync } from "../src/app/db";
|
||||||
|
import { badge, icon, messageWithSequenceId, toNotificationParams } from "../src/app/notificationUtils";
|
||||||
import { toNotificationParams, icon, badge } from "../src/app/notificationUtils";
|
|
||||||
import initI18n from "../src/app/i18n";
|
import initI18n from "../src/app/i18n";
|
||||||
import { messageWithSequenceId } from "../src/app/utils";
|
import { EVENT_MESSAGE, EVENT_MESSAGE_CLEAR, EVENT_MESSAGE_DELETE, EVENT_SUBSCRIPTION_EXPIRING } from "../src/app/events";
|
||||||
import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE, EVENT_MESSAGE_CLEAR } from "../src/app/events";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General docs for service workers and PWAs:
|
* General docs for service workers and PWAs:
|
||||||
@@ -23,10 +20,17 @@ import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE, EVENT_MESSAGE_CLEAR } from "../src
|
|||||||
|
|
||||||
const broadcastChannel = new BroadcastChannel("web-push-broadcast");
|
const broadcastChannel = new BroadcastChannel("web-push-broadcast");
|
||||||
|
|
||||||
const addNotification = async ({ subscriptionId, message }) => {
|
/**
|
||||||
|
* 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;
|
||||||
const db = await dbAsync();
|
const db = await dbAsync();
|
||||||
|
|
||||||
// Note: SubscriptionManager duplicates this logic, so if you change it here, change it there too
|
console.log("[ServiceWorker] Message received", data);
|
||||||
|
|
||||||
// Delete existing notification with same sequence ID (if any)
|
// Delete existing notification with same sequence ID (if any)
|
||||||
const sequenceId = message.sequence_id || message.id;
|
const sequenceId = message.sequence_id || message.id;
|
||||||
@@ -46,22 +50,9 @@ const addNotification = async ({ subscriptionId, message }) => {
|
|||||||
last: message.id,
|
last: message.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update badge in PWA
|
||||||
const badgeCount = await db.notifications.where({ new: 1 }).count();
|
const badgeCount = await db.notifications.where({ new: 1 }).count();
|
||||||
console.log("[ServiceWorker] Setting new app badge count", { badgeCount });
|
|
||||||
self.navigator.setAppBadge?.(badgeCount);
|
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;
|
|
||||||
|
|
||||||
// Add notification to database
|
|
||||||
await addNotification({ subscriptionId, message });
|
|
||||||
|
|
||||||
// Broadcast the message to potentially play a sound
|
// Broadcast the message to potentially play a sound
|
||||||
broadcastChannel.postMessage(message);
|
broadcastChannel.postMessage(message);
|
||||||
@@ -82,11 +73,11 @@ const handlePushMessage = async (data) => {
|
|||||||
const handlePushMessageDelete = async (data) => {
|
const handlePushMessageDelete = async (data) => {
|
||||||
const { subscription_id: subscriptionId, message } = data;
|
const { subscription_id: subscriptionId, message } = data;
|
||||||
const db = await dbAsync();
|
const db = await dbAsync();
|
||||||
|
console.log("[ServiceWorker] Deleting notification sequence", data);
|
||||||
|
|
||||||
// Delete notification with the same sequence_id
|
// Delete notification with the same sequence_id
|
||||||
const sequenceId = message.sequence_id;
|
const sequenceId = message.sequence_id;
|
||||||
if (sequenceId) {
|
if (sequenceId) {
|
||||||
console.log("[ServiceWorker] Deleting notification with sequenceId", { subscriptionId, sequenceId });
|
|
||||||
await db.notifications.where({ subscriptionId, sequenceId }).delete();
|
await db.notifications.where({ subscriptionId, sequenceId }).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,11 +93,11 @@ const handlePushMessageDelete = async (data) => {
|
|||||||
const handlePushMessageClear = async (data) => {
|
const handlePushMessageClear = async (data) => {
|
||||||
const { subscription_id: subscriptionId, message } = data;
|
const { subscription_id: subscriptionId, message } = data;
|
||||||
const db = await dbAsync();
|
const db = await dbAsync();
|
||||||
|
console.log("[ServiceWorker] Marking notification as read", data);
|
||||||
|
|
||||||
// Mark notification as read (set new = 0)
|
// Mark notification as read (set new = 0)
|
||||||
const sequenceId = message.sequence_id;
|
const sequenceId = message.sequence_id;
|
||||||
if (sequenceId) {
|
if (sequenceId) {
|
||||||
console.log("[ServiceWorker] Marking notification as read", { subscriptionId, sequenceId });
|
|
||||||
await db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 });
|
await db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +117,7 @@ const handlePushMessageClear = async (data) => {
|
|||||||
*/
|
*/
|
||||||
const handlePushSubscriptionExpiring = async (data) => {
|
const handlePushSubscriptionExpiring = async (data) => {
|
||||||
const t = await initI18n();
|
const t = await initI18n();
|
||||||
|
console.log("[ServiceWorker] Handling incoming subscription expiring event", data);
|
||||||
|
|
||||||
await self.registration.showNotification(t("web_push_subscription_expiring_title"), {
|
await self.registration.showNotification(t("web_push_subscription_expiring_title"), {
|
||||||
body: t("web_push_subscription_expiring_body"),
|
body: t("web_push_subscription_expiring_body"),
|
||||||
@@ -141,6 +133,7 @@ const handlePushSubscriptionExpiring = async (data) => {
|
|||||||
*/
|
*/
|
||||||
const handlePushUnknown = async (data) => {
|
const handlePushUnknown = async (data) => {
|
||||||
const t = await initI18n();
|
const t = await initI18n();
|
||||||
|
console.log("[ServiceWorker] Unknown event received", data);
|
||||||
|
|
||||||
await self.registration.showNotification(t("web_push_unknown_notification_title"), {
|
await self.registration.showNotification(t("web_push_unknown_notification_title"), {
|
||||||
body: t("web_push_unknown_notification_body"),
|
body: t("web_push_unknown_notification_body"),
|
||||||
@@ -155,13 +148,15 @@ const handlePushUnknown = async (data) => {
|
|||||||
* @param {object} data see server/types.go, type webPushPayload
|
* @param {object} data see server/types.go, type webPushPayload
|
||||||
*/
|
*/
|
||||||
const handlePush = async (data) => {
|
const handlePush = async (data) => {
|
||||||
if (data.event === EVENT_MESSAGE) {
|
const { message } = data;
|
||||||
|
|
||||||
|
if (message.event === EVENT_MESSAGE) {
|
||||||
await handlePushMessage(data);
|
await handlePushMessage(data);
|
||||||
} else if (data.event === EVENT_MESSAGE_DELETE) {
|
} else if (message.event === EVENT_MESSAGE_DELETE) {
|
||||||
await handlePushMessageDelete(data);
|
await handlePushMessageDelete(data);
|
||||||
} else if (data.event === EVENT_MESSAGE_CLEAR) {
|
} else if (message.event === EVENT_MESSAGE_CLEAR) {
|
||||||
await handlePushMessageClear(data);
|
await handlePushMessageClear(data);
|
||||||
} else if (data.event === "subscription_expiring") {
|
} else if (message.event === EVENT_SUBSCRIPTION_EXPIRING) {
|
||||||
await handlePushSubscriptionExpiring(data);
|
await handlePushSubscriptionExpiring(data);
|
||||||
} else {
|
} else {
|
||||||
await handlePushUnknown(data);
|
await handlePushUnknown(data);
|
||||||
@@ -176,10 +171,8 @@ const handleClick = async (event) => {
|
|||||||
const t = await initI18n();
|
const t = await initI18n();
|
||||||
|
|
||||||
const clients = await self.clients.matchAll({ type: "window" });
|
const clients = await self.clients.matchAll({ type: "window" });
|
||||||
|
|
||||||
const rootUrl = new URL(self.location.origin);
|
const rootUrl = new URL(self.location.origin);
|
||||||
const rootClient = clients.find((client) => client.url === rootUrl.toString());
|
const rootClient = clients.find((client) => client.url === rootUrl.toString());
|
||||||
// perhaps open on another topic
|
|
||||||
const fallbackClient = clients[0];
|
const fallbackClient = clients[0];
|
||||||
|
|
||||||
if (!event.notification.data?.message) {
|
if (!event.notification.data?.message) {
|
||||||
@@ -295,6 +288,7 @@ precacheAndRoute(
|
|||||||
|
|
||||||
// Claim all open windows
|
// Claim all open windows
|
||||||
clientsClaim();
|
clientsClaim();
|
||||||
|
|
||||||
// Delete any cached old dist files from previous service worker versions
|
// Delete any cached old dist files from previous service worker versions
|
||||||
cleanupOutdatedCaches();
|
cleanupOutdatedCaches();
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import api from "./Api";
|
|||||||
import notifier from "./Notifier";
|
import notifier from "./Notifier";
|
||||||
import prefs from "./Prefs";
|
import prefs from "./Prefs";
|
||||||
import db from "./db";
|
import db from "./db";
|
||||||
import { messageWithSequenceId, topicUrl } from "./utils";
|
import { topicUrl } from "./utils";
|
||||||
import { EVENT_MESSAGE, EVENT_MESSAGE_DELETE, EVENT_MESSAGE_CLEAR } from "./events";
|
import { messageWithSequenceId } from "./notificationUtils";
|
||||||
|
import { EVENT_MESSAGE, EVENT_MESSAGE_CLEAR, EVENT_MESSAGE_DELETE } from "./events";
|
||||||
|
|
||||||
class SubscriptionManager {
|
class SubscriptionManager {
|
||||||
constructor(dbImpl) {
|
constructor(dbImpl) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const EVENT_MESSAGE = "message";
|
|||||||
export const EVENT_MESSAGE_DELETE = "message_delete";
|
export const EVENT_MESSAGE_DELETE = "message_delete";
|
||||||
export const EVENT_MESSAGE_CLEAR = "message_clear";
|
export const EVENT_MESSAGE_CLEAR = "message_clear";
|
||||||
export const EVENT_POLL_REQUEST = "poll_request";
|
export const EVENT_POLL_REQUEST = "poll_request";
|
||||||
|
export const EVENT_SUBSCRIPTION_EXPIRING = "subscription_expiring";
|
||||||
|
|
||||||
// Check if an event is a notification event (message, delete, or read)
|
// Check if an event is a notification event (message, delete, or read)
|
||||||
export const isNotificationEvent = (event) => event === EVENT_MESSAGE || event === EVENT_MESSAGE_DELETE || event === EVENT_MESSAGE_CLEAR;
|
export const isNotificationEvent = (event) => event === EVENT_MESSAGE || event === EVENT_MESSAGE_DELETE || event === EVENT_MESSAGE_CLEAR;
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ const formatTitleWithDefault = (m, fallback) => {
|
|||||||
|
|
||||||
export const formatMessage = (m) => {
|
export const formatMessage = (m) => {
|
||||||
if (m.title) {
|
if (m.title) {
|
||||||
return m.message;
|
return m.message || "";
|
||||||
}
|
}
|
||||||
const emojiList = toEmojis(m.tags);
|
const emojiList = toEmojis(m.tags);
|
||||||
if (emojiList.length > 0) {
|
if (emojiList.length > 0) {
|
||||||
return `${emojiList.join(" ")} ${m.message}`;
|
return `${emojiList.join(" ")} ${m.message || ""}`;
|
||||||
}
|
}
|
||||||
return m.message;
|
return m.message || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageRegex = /\.(png|jpe?g|gif|webp)$/i;
|
const imageRegex = /\.(png|jpe?g|gif|webp)$/i;
|
||||||
@@ -79,3 +79,10 @@ export const toNotificationParams = ({ message, defaultTitle, topicRoute }) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const messageWithSequenceId = (message) => {
|
||||||
|
if (message.sequenceId) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
return { ...message, sequenceId: message.sequence_id || message.id };
|
||||||
|
};
|
||||||
|
|||||||
@@ -103,13 +103,6 @@ export const maybeActionErrors = (notification) => {
|
|||||||
return actionErrors;
|
return actionErrors;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const messageWithSequenceId = (message) => {
|
|
||||||
if (message.sequenceId) {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
return { ...message, sequenceId: message.sequence_id || message.id };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const shuffle = (arr) => {
|
export const shuffle = (arr) => {
|
||||||
const returnArr = [...arr];
|
const returnArr = [...arr];
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,21 @@ import { registerSW as viteRegisterSW } from "virtual:pwa-register";
|
|||||||
const intervalMS = 60 * 60 * 1000;
|
const intervalMS = 60 * 60 * 1000;
|
||||||
|
|
||||||
// https://vite-pwa-org.netlify.app/guide/periodic-sw-updates.html
|
// https://vite-pwa-org.netlify.app/guide/periodic-sw-updates.html
|
||||||
const registerSW = () =>
|
const registerSW = () => {
|
||||||
|
console.log("[ServiceWorker] Registering service worker");
|
||||||
|
console.log("[ServiceWorker] serviceWorker in navigator:", "serviceWorker" in navigator);
|
||||||
|
|
||||||
|
if (!("serviceWorker" in navigator)) {
|
||||||
|
console.warn("[ServiceWorker] Service workers not supported");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
viteRegisterSW({
|
viteRegisterSW({
|
||||||
onRegisteredSW(swUrl, registration) {
|
onRegisteredSW(swUrl, registration) {
|
||||||
|
console.log("[ServiceWorker] Registered:", { swUrl, registration });
|
||||||
|
|
||||||
if (!registration) {
|
if (!registration) {
|
||||||
|
console.warn("[ServiceWorker] No registration returned");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,9 +34,16 @@ const registerSW = () =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resp?.status === 200) await registration.update();
|
if (resp?.status === 200) {
|
||||||
|
console.log("[ServiceWorker] Updating service worker");
|
||||||
|
await registration.update();
|
||||||
|
}
|
||||||
}, intervalMS);
|
}, intervalMS);
|
||||||
},
|
},
|
||||||
|
onRegisterError(error) {
|
||||||
|
console.error("[ServiceWorker] Registration error:", error);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default registerSW;
|
export default registerSW;
|
||||||
|
|||||||
Reference in New Issue
Block a user