Fix grouping issue with sequence ID
This commit is contained in:
@@ -4,7 +4,7 @@ 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 { badge, icon, messageWithSequenceId, notificationTag, toNotificationParams } from "../src/app/notificationUtils";
|
||||||
import initI18n from "../src/app/i18n";
|
import initI18n from "../src/app/i18n";
|
||||||
import {
|
import {
|
||||||
EVENT_MESSAGE,
|
EVENT_MESSAGE,
|
||||||
@@ -38,6 +38,13 @@ const handlePushMessage = async (data) => {
|
|||||||
|
|
||||||
console.log("[ServiceWorker] Message received", data);
|
console.log("[ServiceWorker] Message received", data);
|
||||||
|
|
||||||
|
// Look up subscription for baseUrl and topic
|
||||||
|
const subscription = await db.subscriptions.get(subscriptionId);
|
||||||
|
if (!subscription) {
|
||||||
|
console.log("[ServiceWorker] Subscription not found", subscriptionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
if (sequenceId) {
|
if (sequenceId) {
|
||||||
@@ -65,10 +72,11 @@ const handlePushMessage = async (data) => {
|
|||||||
|
|
||||||
await self.registration.showNotification(
|
await self.registration.showNotification(
|
||||||
...toNotificationParams({
|
...toNotificationParams({
|
||||||
subscriptionId,
|
|
||||||
message,
|
message,
|
||||||
defaultTitle: message.topic,
|
defaultTitle: message.topic,
|
||||||
topicRoute: new URL(message.topic, self.location.origin).toString(),
|
topicRoute: new URL(message.topic, self.location.origin).toString(),
|
||||||
|
baseUrl: subscription.baseUrl,
|
||||||
|
topic: subscription.topic,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -81,18 +89,23 @@ const handlePushMessageDelete = async (data) => {
|
|||||||
const db = await dbAsync();
|
const db = await dbAsync();
|
||||||
console.log("[ServiceWorker] Deleting notification sequence", data);
|
console.log("[ServiceWorker] Deleting notification sequence", data);
|
||||||
|
|
||||||
|
// Look up subscription for baseUrl and topic
|
||||||
|
const subscription = await db.subscriptions.get(subscriptionId);
|
||||||
|
if (!subscription) {
|
||||||
|
console.log("[ServiceWorker] Subscription not found", subscriptionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
await db.notifications.where({ subscriptionId, sequenceId }).delete();
|
await db.notifications.where({ subscriptionId, sequenceId }).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close browser notification with matching tag
|
// Close browser notification with matching tag (scoped by topic)
|
||||||
const tag = message.sequence_id || message.id;
|
const tag = notificationTag(subscription.baseUrl, subscription.topic, message.sequence_id || message.id);
|
||||||
if (tag) {
|
const notifications = await self.registration.getNotifications({ tag });
|
||||||
const notifications = await self.registration.getNotifications({ tag });
|
notifications.forEach((notification) => notification.close());
|
||||||
notifications.forEach((notification) => notification.close());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update subscription last message id (for ?since=... queries)
|
// Update subscription last message id (for ?since=... queries)
|
||||||
await db.subscriptions.update(subscriptionId, {
|
await db.subscriptions.update(subscriptionId, {
|
||||||
@@ -108,18 +121,23 @@ const handlePushMessageClear = async (data) => {
|
|||||||
const db = await dbAsync();
|
const db = await dbAsync();
|
||||||
console.log("[ServiceWorker] Marking notification as read", data);
|
console.log("[ServiceWorker] Marking notification as read", data);
|
||||||
|
|
||||||
|
// Look up subscription for baseUrl and topic
|
||||||
|
const subscription = await db.subscriptions.get(subscriptionId);
|
||||||
|
if (!subscription) {
|
||||||
|
console.log("[ServiceWorker] Subscription not found", subscriptionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
await db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 });
|
await db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close browser notification with matching tag
|
// Close browser notification with matching tag (scoped by topic)
|
||||||
const tag = message.sequence_id || message.id;
|
const tag = notificationTag(subscription.baseUrl, subscription.topic, message.sequence_id || message.id);
|
||||||
if (tag) {
|
const notifications = await self.registration.getNotifications({ tag });
|
||||||
const notifications = await self.registration.getNotifications({ tag });
|
notifications.forEach((notification) => notification.close());
|
||||||
notifications.forEach((notification) => notification.close());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update subscription last message id (for ?since=... queries)
|
// Update subscription last message id (for ?since=... queries)
|
||||||
await db.subscriptions.update(subscriptionId, {
|
await db.subscriptions.update(subscriptionId, {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { playSound, topicDisplayName, topicShortUrl, urlB64ToUint8Array } from "./utils";
|
import { playSound, topicDisplayName, topicShortUrl, urlB64ToUint8Array } from "./utils";
|
||||||
import { toNotificationParams } from "./notificationUtils";
|
import { notificationTag, toNotificationParams } from "./notificationUtils";
|
||||||
import prefs from "./Prefs";
|
import prefs from "./Prefs";
|
||||||
import routes from "../components/routes";
|
import routes from "../components/routes";
|
||||||
|
|
||||||
@@ -23,21 +23,23 @@ class Notifier {
|
|||||||
const registration = await this.serviceWorkerRegistration();
|
const registration = await this.serviceWorkerRegistration();
|
||||||
await registration.showNotification(
|
await registration.showNotification(
|
||||||
...toNotificationParams({
|
...toNotificationParams({
|
||||||
subscriptionId: subscription.id,
|
|
||||||
message: notification,
|
message: notification,
|
||||||
defaultTitle,
|
defaultTitle,
|
||||||
topicRoute: new URL(routes.forSubscription(subscription), window.location.origin).toString(),
|
topicRoute: new URL(routes.forSubscription(subscription), window.location.origin).toString(),
|
||||||
|
baseUrl: subscription.baseUrl,
|
||||||
|
topic: subscription.topic,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancel(notification) {
|
async cancel(subscription, notification) {
|
||||||
if (!this.supported()) {
|
if (!this.supported()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const tag = notification.sequence_id || notification.id;
|
const sequenceId = notification.sequence_id || notification.id;
|
||||||
console.log(`[Notifier] Cancelling notification with ${tag}`);
|
const tag = notificationTag(subscription.baseUrl, subscription.topic, sequenceId);
|
||||||
|
console.log(`[Notifier] Cancelling notification with tag ${tag}`);
|
||||||
const registration = await this.serviceWorkerRegistration();
|
const registration = await this.serviceWorkerRegistration();
|
||||||
const notifications = await registration.getNotifications({ tag });
|
const notifications = await registration.getNotifications({ tag });
|
||||||
notifications.forEach((n) => n.close());
|
notifications.forEach((n) => n.close());
|
||||||
|
|||||||
@@ -50,8 +50,16 @@ export const isImage = (attachment) => {
|
|||||||
export const icon = "/static/images/ntfy.png";
|
export const icon = "/static/images/ntfy.png";
|
||||||
export const badge = "/static/images/mask-icon.svg";
|
export const badge = "/static/images/mask-icon.svg";
|
||||||
|
|
||||||
export const toNotificationParams = ({ message, defaultTitle, topicRoute }) => {
|
/**
|
||||||
|
* Computes a unique notification tag scoped by baseUrl, topic, and sequence ID.
|
||||||
|
* This ensures notifications from different topics with the same sequence ID don't collide.
|
||||||
|
*/
|
||||||
|
export const notificationTag = (baseUrl, topic, sequenceId) => `${baseUrl}/${topic}/${sequenceId}`;
|
||||||
|
|
||||||
|
export const toNotificationParams = ({ message, defaultTitle, topicRoute, baseUrl, topic }) => {
|
||||||
const image = isImage(message.attachment) ? message.attachment.url : undefined;
|
const image = isImage(message.attachment) ? message.attachment.url : undefined;
|
||||||
|
const sequenceId = message.sequence_id || message.id;
|
||||||
|
const tag = notificationTag(baseUrl, topic, sequenceId);
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API
|
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API
|
||||||
return [
|
return [
|
||||||
@@ -62,7 +70,7 @@ export const toNotificationParams = ({ message, defaultTitle, topicRoute }) => {
|
|||||||
icon,
|
icon,
|
||||||
image,
|
image,
|
||||||
timestamp: message.time * 1000,
|
timestamp: message.time * 1000,
|
||||||
tag: message.sequence_id || message.id, // Update notification if there is a sequence ID
|
tag, // Scoped by baseUrl/topic/sequenceId to avoid cross-topic collisions
|
||||||
renotify: true,
|
renotify: true,
|
||||||
silent: false,
|
silent: false,
|
||||||
// This is used by the notification onclick event
|
// This is used by the notification onclick event
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNotification = async (subscriptionId, notification) => {
|
const handleNotification = async (subscription, notification) => {
|
||||||
// This logic is (partially) duplicated in
|
// This logic is (partially) duplicated in
|
||||||
// - Android: SubscriberService::onNotificationReceived()
|
// - Android: SubscriberService::onNotificationReceived()
|
||||||
// - Android: FirebaseService::onMessageReceived()
|
// - Android: FirebaseService::onMessageReceived()
|
||||||
@@ -59,20 +59,20 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop
|
|||||||
// - Web app: sw.js:handleMessage(), sw.js:handleMessageClear(), ...
|
// - Web app: sw.js:handleMessage(), sw.js:handleMessageClear(), ...
|
||||||
|
|
||||||
if (notification.event === EVENT_MESSAGE_DELETE && notification.sequence_id) {
|
if (notification.event === EVENT_MESSAGE_DELETE && notification.sequence_id) {
|
||||||
await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, notification.sequence_id);
|
await subscriptionManager.deleteNotificationBySequenceId(subscription.id, notification.sequence_id);
|
||||||
await notifier.cancel(notification);
|
await notifier.cancel(subscription, notification);
|
||||||
} else if (notification.event === EVENT_MESSAGE_CLEAR && notification.sequence_id) {
|
} else if (notification.event === EVENT_MESSAGE_CLEAR && notification.sequence_id) {
|
||||||
await subscriptionManager.markNotificationReadBySequenceId(subscriptionId, notification.sequence_id);
|
await subscriptionManager.markNotificationReadBySequenceId(subscription.id, notification.sequence_id);
|
||||||
await notifier.cancel(notification);
|
await notifier.cancel(subscription, notification);
|
||||||
} else {
|
} else {
|
||||||
// Regular message: delete existing and add new
|
// Regular message: delete existing and add new
|
||||||
const sequenceId = notification.sequence_id || notification.id;
|
const sequenceId = notification.sequence_id || notification.id;
|
||||||
if (sequenceId) {
|
if (sequenceId) {
|
||||||
await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, sequenceId);
|
await subscriptionManager.deleteNotificationBySequenceId(subscription.id, sequenceId);
|
||||||
}
|
}
|
||||||
const added = await subscriptionManager.addNotification(subscriptionId, notification);
|
const added = await subscriptionManager.addNotification(subscription.id, notification);
|
||||||
if (added) {
|
if (added) {
|
||||||
await subscriptionManager.notify(subscriptionId, notification);
|
await subscriptionManager.notify(subscription.id, notification);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -89,7 +89,7 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop
|
|||||||
if (subscription.internal) {
|
if (subscription.internal) {
|
||||||
await handleInternalMessage(message);
|
await handleInternalMessage(message);
|
||||||
} else {
|
} else {
|
||||||
await handleNotification(subscriptionId, message);
|
await handleNotification(subscription, message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user