From 58c0fa90baa32c4f3b71d62e669029a57864a86b Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 28 Oct 2024 12:12:23 +0100 Subject: [PATCH] Save the timestamp from the last read notification in a IndexedDB --- .../templates/headIncludeJavaScript.tpl | 1 + ts/WoltLabSuite/Core/BootstrapFrontend.ts | 2 + .../Core/Notification/ServiceWorker.ts | 19 ++- .../js/WoltLabSuite/Core/BootstrapFrontend.js | 2 +- .../Core/Notification/ServiceWorker.js | 13 +- .../UserNotificationHandler.class.php | 17 +++ .../install/files/service-worker/index.php | 118 +++++++++++++++--- 7 files changed, 153 insertions(+), 19 deletions(-) diff --git a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl index 3bb9f14111..d587217e64 100644 --- a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl +++ b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl @@ -82,6 +82,7 @@ window.addEventListener('pageshow', function(event) { publicKey: '{@SERVICE_WORKER_PUBLIC_KEY|encodeJS}', serviceWorkerJsUrl: '{$__wcf->getPath('wcf')}service-worker/', registerUrl: '{link controller="RegisterServiceWorker"}{/link}', + lastReadNotification: {$__wcf->getUserNotificationHandler()->getLastReadNotificationTime()} }, {/if} dynamicColorScheme: {if $__wcf->getStyleHandler()->getColorScheme() === 'system'}true{else}false{/if}, diff --git a/ts/WoltLabSuite/Core/BootstrapFrontend.ts b/ts/WoltLabSuite/Core/BootstrapFrontend.ts index be6881d2a1..3e8ae0d61f 100644 --- a/ts/WoltLabSuite/Core/BootstrapFrontend.ts +++ b/ts/WoltLabSuite/Core/BootstrapFrontend.ts @@ -29,6 +29,7 @@ interface BootstrapOptions { publicKey: string; serviceWorkerJsUrl: string; registerUrl: string; + lastReadNotification: number; }; dynamicColorScheme: boolean; endpointUserPopover: string; @@ -114,6 +115,7 @@ export function setup(options: BootstrapOptions): void { options.serviceWorker.publicKey, options.serviceWorker.serviceWorkerJsUrl, options.serviceWorker.registerUrl, + options.serviceWorker.lastReadNotification, ); } } diff --git a/ts/WoltLabSuite/Core/Notification/ServiceWorker.ts b/ts/WoltLabSuite/Core/Notification/ServiceWorker.ts index f4a29fd0f5..5e52798d91 100644 --- a/ts/WoltLabSuite/Core/Notification/ServiceWorker.ts +++ b/ts/WoltLabSuite/Core/Notification/ServiceWorker.ts @@ -95,6 +95,13 @@ class ServiceWorker { } return outputArray; } + + public updateLastNotificationTime(timestamp: number): void { + window.navigator.serviceWorker.controller?.postMessage({ + type: "SAVE_LAST_NOTIFICATION_TIMESTAMP", + timestamp: timestamp, + }); + } } export function serviceWorkerSupported(): boolean { @@ -120,16 +127,26 @@ export function serviceWorkerSupported(): boolean { return true; } -export function setup(publicKey: string, serviceWorkerJsUrl: string, registerUrl: string): void { +export function setup( + publicKey: string, + serviceWorkerJsUrl: string, + registerUrl: string, + lastReadNotification: number, +): void { if (!serviceWorkerSupported()) { return; } _serviceWorker = new ServiceWorker(publicKey, serviceWorkerJsUrl, registerUrl); if (Notification.permission === "granted") { registerServiceWorker(); + _serviceWorker.updateLastNotificationTime(lastReadNotification); } } export function registerServiceWorker(): void { void _serviceWorker?.register(); } + +export function updateLastNotificationTime(timestamp: number): void { + _serviceWorker?.updateLastNotificationTime(timestamp); +} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js b/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js index 2ec14797d0..e5eafebf94 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js @@ -78,7 +78,7 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui if (User_1.default.userId) { UiFeedDialog.setup(); if (options.serviceWorker) { - (0, ServiceWorker_1.setup)(options.serviceWorker.publicKey, options.serviceWorker.serviceWorkerJsUrl, options.serviceWorker.registerUrl); + (0, ServiceWorker_1.setup)(options.serviceWorker.publicKey, options.serviceWorker.serviceWorkerJsUrl, options.serviceWorker.registerUrl, options.serviceWorker.lastReadNotification); } } (0, LazyLoader_1.whenFirstSeen)("woltlab-core-reaction-summary", () => { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Notification/ServiceWorker.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Notification/ServiceWorker.js index 6ffeba9ce9..46af1c244e 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Notification/ServiceWorker.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Notification/ServiceWorker.js @@ -11,6 +11,7 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend"], function (requi exports.serviceWorkerSupported = serviceWorkerSupported; exports.setup = setup; exports.registerServiceWorker = registerServiceWorker; + exports.updateLastNotificationTime = updateLastNotificationTime; let _serviceWorker = null; class ServiceWorker { #publicKey; @@ -87,6 +88,12 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend"], function (requi } return outputArray; } + updateLastNotificationTime(timestamp) { + window.navigator.serviceWorker.controller?.postMessage({ + type: "SAVE_LAST_NOTIFICATION_TIMESTAMP", + timestamp: timestamp, + }); + } } function serviceWorkerSupported() { if (location.protocol !== "https:") { @@ -107,16 +114,20 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend"], function (requi } return true; } - function setup(publicKey, serviceWorkerJsUrl, registerUrl) { + function setup(publicKey, serviceWorkerJsUrl, registerUrl, lastReadNotification) { if (!serviceWorkerSupported()) { return; } _serviceWorker = new ServiceWorker(publicKey, serviceWorkerJsUrl, registerUrl); if (Notification.permission === "granted") { registerServiceWorker(); + _serviceWorker.updateLastNotificationTime(lastReadNotification); } } function registerServiceWorker() { void _serviceWorker?.register(); } + function updateLastNotificationTime(timestamp) { + _serviceWorker?.updateLastNotificationTime(timestamp); + } }); diff --git a/wcfsetup/install/files/lib/system/user/notification/UserNotificationHandler.class.php b/wcfsetup/install/files/lib/system/user/notification/UserNotificationHandler.class.php index 95b5fe5bbc..f5b7fae90e 100644 --- a/wcfsetup/install/files/lib/system/user/notification/UserNotificationHandler.class.php +++ b/wcfsetup/install/files/lib/system/user/notification/UserNotificationHandler.class.php @@ -1099,4 +1099,21 @@ class UserNotificationHandler extends SingletonFactory return \array_intersect($userIDs, $filterUserIDs); } + + /** + * Returns the timestamp of the last read notification for the active user. + * Or `0` if no notification has been read yet. + * + * @return int + */ + public function getLastReadNotificationTime(): int + { + $sql = "SELECT MAX(confirmTime) + FROM wcf1_user_notification + WHERE userID = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([WCF::getUser()->userID]); + + return $statement->fetchSingleColumn() ?: 0; + } } diff --git a/wcfsetup/install/files/service-worker/index.php b/wcfsetup/install/files/service-worker/index.php index 16a7d05002..8bfc6ecdbf 100644 --- a/wcfsetup/install/files/service-worker/index.php +++ b/wcfsetup/install/files/service-worker/index.php @@ -19,22 +19,28 @@ self.addEventListener("push", (event) => { const payload = event.data.json(); - event.waitUntil( - removeOldNotifications(payload.notificationID, payload.time).then(() => - self.registration.showNotification(payload.title, { - body: payload.message, - icon: payload.icon, - timestamp: payload.time * 1000, - tag: payload.notificationID, - data: { - url: payload.url, - time: payload.time, - }, - }), - ).then(() => { - sendToClients(payload); - }), - ); + getLastNotificationTimestamp().then(lastNotificationTimestamp => { + if (!lastNotificationTimestamp || payload.time < lastNotificationTimestamp) { + return; + } + + event.waitUntil( + removeOldNotifications(payload.notificationID, payload.time).then(() => + self.registration.showNotification(payload.title, { + body: payload.message, + icon: payload.icon, + timestamp: payload.time * 1000, + tag: payload.notificationID, + data: { + url: payload.url, + time: payload.time, + }, + }), + ).then(() => { + sendToClients(payload); + }), + ); + }); }); self.addEventListener("notificationclick", (event) => { @@ -43,6 +49,12 @@ self.addEventListener("notificationclick", (event) => { event.waitUntil(self.clients.openWindow(event.notification.data.url)); }); +self.addEventListener('message', event => { + if (event.data && event.data.type === 'SAVE_LAST_NOTIFICATION_TIMESTAMP') { + saveLastNotificationTimestamp(event.data.timestamp); + } +}); + async function sendToClients(payload){ const allClients = await self.clients.matchAll({ includeUncontrolled: true, @@ -70,3 +82,77 @@ async function removeOldNotifications(notificationID, time) { notification.close(); }); } + +/** + * IndexedDB functions to store the last notification timestamp. + */ +function openDatabase() { + return new Promise((resolve, reject) => { + const request = indexedDB.open('WOLTLAB_SUITE_CORE', 1); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains('notifications')) { + db.createObjectStore('notifications'); + } + }; + + request.onsuccess = (event) => { + resolve(event.target.result); + + if (!db.objectStoreNames.contains('notifications')) { + db.createObjectStore('notifications'); + } + }; + + request.onerror = (event) => { + reject('Database error: ' + event.target.errorCode); + }; + }); +} + +function saveLastNotificationTimestamp(timestamp) { + if (!timestamp || timestamp <= 0) { + return; + } + + openDatabase().then(db => { + const tx = db.transaction('notifications', 'readwrite'); + const store = tx.objectStore('notifications'); + const getRequest = store.get('lastNotification'); + + getRequest.onsuccess = () => { + const storedTimestamp = getRequest.result; + + // Check if the new timestamp is greater than the stored timestamp + if (storedTimestamp === undefined || newTimestamp > storedTimestamp) { + store.put(timestamp, 'lastNotification'); + } + }; + + getRequest.onerror = () => { + console.error('Failed to retrieve timestamp', getRequest.error); + }; + + tx.onerror = () => { + console.error('Transaktionsfehler', tx.error); + }; + }).catch(err => console.error('Failed to open database', err)); +} + +function getLastNotificationTimestamp() { + return openDatabase().then(db => { + return new Promise((resolve, reject) => { + const tx = db.transaction('notifications', 'readonly'); + const store = tx.objectStore('notifications'); + const request = store.get('lastNotification'); + + request.onsuccess = () => { + resolve(request.result); + }; + request.onerror = () => { + reject('Failed to retrieve timestamp'); + }; + }); + }); +} -- 2.20.1