diff --git a/web/src/app/Prefs.js b/web/src/app/Prefs.js index ac1d82db..b4cef0ac 100644 --- a/web/src/app/Prefs.js +++ b/web/src/app/Prefs.js @@ -33,8 +33,8 @@ class Prefs { } async webPushEnabled() { - const obj = await this.db.prefs.get("webPushEnabled"); - return obj?.value ?? false; + const webPushEnabled = await this.db.prefs.get("webPushEnabled"); + return webPushEnabled?.value ?? false; } async setWebPushEnabled(enabled) { diff --git a/web/src/app/SubscriptionManager.js b/web/src/app/SubscriptionManager.js index 88b95e7b..67b9faa3 100644 --- a/web/src/app/SubscriptionManager.js +++ b/web/src/app/SubscriptionManager.js @@ -20,16 +20,15 @@ class SubscriptionManager { ); } + /** List of topics for which Web Push is enabled, excludes internal topics; returns empty list if Web Push is disabled */ async webPushTopics() { // the Promise.resolve wrapper is not superfluous, without it the live query breaks: // https://dexie.org/docs/dexie-react-hooks/useLiveQuery()#calling-non-dexie-apis-from-querier - if (!(await Promise.resolve(notifier.pushEnabled()))) { + const pushEnabled = await Promise.resolve(notifier.pushEnabled()); + if (!pushEnabled) { return []; } - const subscriptions = await this.db.subscriptions.where({ mutedUntil: 0, baseUrl: config.base_url }).toArray(); - - // internal is currently a bool, it could be a 0/1 to be indexable, but for now just filter them out here return subscriptions.filter(({ internal }) => !internal).map(({ topic }) => topic); } @@ -111,7 +110,7 @@ class SubscriptionManager { ); } - async refreshWebPushSubscriptions(presetTopics) { + async updateWebPushSubscriptions(presetTopics) { const topics = presetTopics ?? (await this.webPushTopics()); const browserSubscription = await notifier.getBrowserSubscription(); diff --git a/web/src/app/WebPushWorker.js b/web/src/app/WebPush.js similarity index 69% rename from web/src/app/WebPushWorker.js rename to web/src/app/WebPush.js index b0d319c7..1a9b59eb 100644 --- a/web/src/app/WebPushWorker.js +++ b/web/src/app/WebPush.js @@ -3,21 +3,26 @@ import { useLiveQuery } from "dexie-react-hooks"; import notifier from "./Notifier"; import subscriptionManager from "./SubscriptionManager"; -export const useWebPushUpdateWorker = () => { +const intervalMillis = 13 * 60 * 1_000; // 13 minutes +const updateIntervalMillis = 60 * 60 * 1_000; // 1 hour + +/** + * Updates the Web Push subscriptions when the list of topics changes. + */ +export const useWebPushTopicListener = () => { const topics = useLiveQuery(() => subscriptionManager.webPushTopics()); const [lastTopics, setLastTopics] = useState(); useEffect(() => { - if (!notifier.pushPossible() || JSON.stringify(topics) === JSON.stringify(lastTopics)) { + const topicsChanged = JSON.stringify(topics) !== JSON.stringify(lastTopics); + if (!notifier.pushPossible() || !topicsChanged) { return; } (async () => { try { console.log("[useWebPushUpdateWorker] Refreshing web push subscriptions"); - - await subscriptionManager.refreshWebPushSubscriptions(topics); - + await subscriptionManager.updateWebPushSubscriptions(topics); setLastTopics(topics); } catch (e) { console.error("[useWebPushUpdateWorker] Error refreshing web push subscriptions", e); @@ -26,10 +31,13 @@ export const useWebPushUpdateWorker = () => { }, [topics, lastTopics]); }; -const intervalMillis = 13 * 60 * 1_000; // 13 minutes -const updateIntervalMillis = 60 * 60 * 1_000; // 1 hour - -class WebPushRefreshWorker { +/** + * Helper class for Web Push that does three things: + * 1. Updates the Web Push subscriptions on a schedule + * 2. Updates the Web Push subscriptions when the window is minimised / app switched + * 3. Listens to the broadcast channel from the service worker to play a sound when a message comes in + */ +class WebPushWorker { constructor() { this.timer = null; this.lastUpdate = null; @@ -43,7 +51,6 @@ class WebPushRefreshWorker { } this.timer = setInterval(() => this.updateSubscriptions(), intervalMillis); - this.broadcastChannel = new BroadcastChannel("web-push-broadcast"); this.broadcastChannel.addEventListener("message", this.messageHandler); @@ -60,7 +67,7 @@ class WebPushRefreshWorker { } onMessage() { - notifier.playSound(); + notifier.playSound(); // Service Worker cannot play sound, so we do it here! } onVisibilityChange() { @@ -75,10 +82,10 @@ class WebPushRefreshWorker { } if (!this.lastUpdate || Date.now() - this.lastUpdate > updateIntervalMillis) { - await subscriptionManager.refreshWebPushSubscriptions(); + await subscriptionManager.updateWebPushSubscriptions(); this.lastUpdate = Date.now(); } } } -export const webPushRefreshWorker = new WebPushRefreshWorker(); +export const webPush = new WebPushWorker(); diff --git a/web/src/components/hooks.js b/web/src/components/hooks.js index 815f0596..8da8fdf0 100644 --- a/web/src/components/hooks.js +++ b/web/src/components/hooks.js @@ -9,7 +9,7 @@ import pruner from "../app/Pruner"; import session from "../app/Session"; import accountApi from "../app/AccountApi"; import { UnauthorizedError } from "../app/errors"; -import { webPushRefreshWorker, useWebPushUpdateWorker } from "../app/WebPushWorker"; +import { webPush, useWebPushTopicListener } from "../app/WebPush"; /** * Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection @@ -134,18 +134,18 @@ const stopWorkers = () => { poller.stopWorker(); pruner.stopWorker(); accountApi.stopWorker(); - webPushRefreshWorker.stopWorker(); + webPush.stopWorker(); }; const startWorkers = () => { poller.startWorker(); pruner.startWorker(); accountApi.startWorker(); - webPushRefreshWorker.startWorker(); + webPush.startWorker(); }; export const useBackgroundProcesses = () => { - useWebPushUpdateWorker(); + useWebPushTopicListener(); useEffect(() => { console.log("[useBackgroundProcesses] mounting");