From 8293a24cf9a91b9508c970057e9450fdabdca38d Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Fri, 17 Oct 2025 22:10:11 -0600 Subject: [PATCH] update notification text using sid in web app --- web/public/static/langs/en.json | 2 ++ web/public/sw.js | 10 +++++- web/src/app/SubscriptionManager.js | 51 ++++++++++++++++++++++++---- web/src/app/db.js | 4 +-- web/src/app/notificationUtils.js | 12 +++++-- web/src/components/Notifications.jsx | 22 ++++++++++++ 6 files changed, 90 insertions(+), 11 deletions(-) diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index 3ad04ea7..362a3192 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -70,6 +70,8 @@ "notifications_delete": "Delete", "notifications_copied_to_clipboard": "Copied to clipboard", "notifications_tags": "Tags", + "notifications_sid": "Sequence ID", + "notifications_revisions": "Revisions", "notifications_priority_x": "Priority {{priority}}", "notifications_new_indicator": "New notification", "notifications_attachment_image": "Attachment image", diff --git a/web/public/sw.js b/web/public/sw.js index 56d66f16..471cbee2 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -23,9 +23,17 @@ const broadcastChannel = new BroadcastChannel("web-push-broadcast"); const addNotification = async ({ subscriptionId, message }) => { const db = await dbAsync(); + const populatedMessage = message; + + if (!("mtime" in populatedMessage)) { + populatedMessage.mtime = message.time * 1000; + } + if (!("sid" in populatedMessage)) { + populatedMessage.sid = message.id; + } await db.notifications.add({ - ...message, + ...populatedMessage, subscriptionId, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation new: 1, diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index de99b642..00d15d89 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -156,18 +156,41 @@ class SubscriptionManager { // It's actually fine, because the reading and filtering is quite fast. The rendering is what's // killing performance. See https://dexie.org/docs/Collection/Collection.offset()#a-better-paging-approach - return this.db.notifications - .orderBy("time") // Sort by time first + const notifications = await this.db.notifications + .orderBy("mtime") // Sort by time first .filter((n) => n.subscriptionId === subscriptionId) .reverse() .toArray(); + + return this.groupNotificationsBySID(notifications); } async getAllNotifications() { - return this.db.notifications - .orderBy("time") // Efficient, see docs + const notifications = await this.db.notifications + .orderBy("mtime") // Efficient, see docs .reverse() .toArray(); + + return this.groupNotificationsBySID(notifications); + } + + // Collapse notification updates based on sids + groupNotificationsBySID(notifications) { + const results = {}; + notifications.forEach((notification) => { + const key = `${notification.subscriptionId}:${notification.sid}`; + if (key in results) { + if ("history" in results[key]) { + results[key].history.push(notification); + } else { + results[key].history = [notification]; + } + } else { + results[key] = notification; + } + }); + + return Object.values(results); } /** Adds notification, or returns false if it already exists */ @@ -177,9 +200,16 @@ class SubscriptionManager { return false; } try { + const populatedNotification = notification; + if (!("mtime" in populatedNotification)) { + populatedNotification.mtime = notification.time * 1000; + } + if (!("sid" in populatedNotification)) { + populatedNotification.sid = notification.id; + } // sw.js duplicates this logic, so if you change it here, change it there too await this.db.notifications.add({ - ...notification, + ...populatedNotification, subscriptionId, // New marker (used for bubble indicator); cannot be boolean; Dexie index limitation new: 1, @@ -195,7 +225,16 @@ class SubscriptionManager { /** Adds/replaces notifications, will not throw if they exist */ async addNotifications(subscriptionId, notifications) { - const notificationsWithSubscriptionId = notifications.map((notification) => ({ ...notification, subscriptionId })); + const notificationsWithSubscriptionId = notifications.map((notification) => { + const populatedNotification = notification; + if (!("mtime" in populatedNotification)) { + populatedNotification.mtime = notification.time * 1000; + } + if (!("sid" in populatedNotification)) { + populatedNotification.sid = notification.id; + } + return { ...populatedNotification, subscriptionId }; + }); const lastNotificationId = notifications.at(-1).id; await this.db.notifications.bulkPut(notificationsWithSubscriptionId); await this.db.subscriptions.update(subscriptionId, { diff --git a/web/src/app/db.js b/web/src/app/db.js index b28fb716..f4d36c1b 100644 --- a/web/src/app/db.js +++ b/web/src/app/db.js @@ -11,9 +11,9 @@ const createDatabase = (username) => { const dbName = username ? `ntfy-${username}` : "ntfy"; // IndexedDB database is based on the logged-in user const db = new Dexie(dbName); - db.version(2).stores({ + db.version(3).stores({ subscriptions: "&id,baseUrl,[baseUrl+mutedUntil]", - notifications: "&id,subscriptionId,time,new,[subscriptionId+new]", // compound key for query performance + notifications: "&id,sid,subscriptionId,time,mtime,new,[subscriptionId+new]", // compound key for query performance users: "&baseUrl,username", prefs: "&key", }); diff --git a/web/src/app/notificationUtils.js b/web/src/app/notificationUtils.js index 0bd5136d..2884e2f3 100644 --- a/web/src/app/notificationUtils.js +++ b/web/src/app/notificationUtils.js @@ -53,6 +53,14 @@ export const badge = "/static/images/mask-icon.svg"; export const toNotificationParams = ({ subscriptionId, message, defaultTitle, topicRoute }) => { const image = isImage(message.attachment) ? message.attachment.url : undefined; + let tag; + + if (message.sid) { + tag = message.sid; + } else { + tag = subscriptionId; + } + // https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API return [ formatTitleWithDefault(message, defaultTitle), @@ -61,8 +69,8 @@ export const toNotificationParams = ({ subscriptionId, message, defaultTitle, to badge, icon, image, - timestamp: message.time * 1_000, - tag: subscriptionId, + timestamp: message.mtime, + tag, renotify: true, silent: false, // This is used by the notification onclick event diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index dceb5b91..40789d08 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -233,10 +233,20 @@ const NotificationItem = (props) => { const handleDelete = async () => { console.log(`[Notifications] Deleting notification ${notification.id}`); await subscriptionManager.deleteNotification(notification.id); + notification.history?.forEach(async (revision) => { + console.log(`[Notifications] Deleting revision ${revision.id}`); + await subscriptionManager.deleteNotification(revision.id); + }); }; const handleMarkRead = async () => { console.log(`[Notifications] Marking notification ${notification.id} as read`); await subscriptionManager.markNotificationRead(notification.id); + notification.history + ?.filter((revision) => revision.new === 1) + .forEach(async (revision) => { + console.log(`[Notifications] Marking revision ${revision.id} as read`); + await subscriptionManager.markNotificationRead(revision.id); + }); }; const handleCopy = (s) => { navigator.clipboard.writeText(s); @@ -248,6 +258,8 @@ const NotificationItem = (props) => { const hasUserActions = notification.actions && notification.actions.length > 0; const showActions = hasAttachmentActions || hasClickAction || hasUserActions; + const showSid = notification.id !== notification.sid || notification.history; + return ( @@ -304,6 +316,16 @@ const NotificationItem = (props) => { {t("notifications_tags")}: {tags} )} + {showSid && ( + + {t("notifications_sid")}: {notification.sid} + + )} + {notification.history && ( + + {t("notifications_revisions")}: {notification.history.length + 1} + + )} {showActions && (