Correctly handle standalone (PWA) mode changes

- Also handle notification permission changes
- Remove web push schedule worker since this complicates
  things and doesn’t do _that_ much. We have the reminder
  notification if the user truly doesn’t reload ntfy in
  more than a week.
This commit is contained in:
nimbleghost 2023-06-25 21:25:30 +02:00
parent 532fd3c560
commit a8d3297c4e
6 changed files with 91 additions and 90 deletions

View file

@ -43,7 +43,7 @@ class Notifier {
}
}
async webPushSubscription() {
async webPushSubscription(hasWebPushTopics) {
if (!this.pushPossible()) {
throw new Error("Unsupported or denied");
}
@ -53,11 +53,11 @@ class Notifier {
return existingSubscription;
}
// Create a new subscription only if Web Push is enabled. It is possible that Web Push
// Create a new subscription only if there are new topics to subscribe to. It is possible that Web Push
// was previously enabled and then disabled again in which case there would be an existingSubscription.
// If, however, it was _not_ enabled previously, we create a new subscription if it is now enabled.
if (await this.pushEnabled()) {
if (hasWebPushTopics) {
return pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlB64ToUint8Array(config.web_push_public_key),
@ -119,11 +119,6 @@ class Notifier {
return this.pushSupported() && this.contextSupported() && this.granted() && !this.iosSupportedButInstallRequired();
}
async pushEnabled() {
const enabled = await prefs.webPushEnabled();
return this.pushPossible() && enabled;
}
/**
* Returns true if this is a HTTPS site, or served over localhost. Otherwise the Notification API
* is not supported, see https://developer.mozilla.org/en-US/docs/Web/API/notification

View file

@ -1,5 +1,4 @@
import db from "./db";
import { isLaunchedPWA } from "./utils";
class Prefs {
constructor(dbImpl) {
@ -35,7 +34,7 @@ class Prefs {
async webPushEnabled() {
const webPushEnabled = await this.db.prefs.get("webPushEnabled");
return webPushEnabled?.value ?? isLaunchedPWA();
return webPushEnabled?.value;
}
async setWebPushEnabled(enabled) {

View file

@ -2,7 +2,7 @@ import api from "./Api";
import notifier from "./Notifier";
import prefs from "./Prefs";
import db from "./db";
import { topicUrl } from "./utils";
import { isLaunchedPWA, topicUrl } from "./utils";
class SubscriptionManager {
constructor(dbImpl) {
@ -27,13 +27,17 @@ class SubscriptionManager {
* It is important to note that "mutedUntil" must be part of the where() query, otherwise the Dexie live query
* will not react to it, and the Web Push topics will not be updated when the user mutes a topic.
*/
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
const pushEnabled = await Promise.resolve(notifier.pushEnabled());
if (!pushEnabled) {
async webPushTopics(isStandalone = isLaunchedPWA(), pushPossible = notifier.pushPossible()) {
if (!pushPossible) {
return [];
}
// 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 (!(isStandalone || (await Promise.resolve(prefs.webPushEnabled())))) {
return [];
}
const subscriptions = await this.db.subscriptions.where({ baseUrl: config.base_url, mutedUntil: 0 }).toArray();
return subscriptions.filter(({ internal }) => !internal).map(({ topic }) => topic);
}
@ -117,14 +121,17 @@ class SubscriptionManager {
async updateWebPushSubscriptions(presetTopics) {
const topics = presetTopics ?? (await this.webPushTopics());
const browserSubscription = await notifier.webPushSubscription();
const hasWebPushTopics = topics.length > 0;
const browserSubscription = await notifier.webPushSubscription(hasWebPushTopics);
if (!browserSubscription) {
console.log("[SubscriptionManager] No browser subscription currently exists, so web push was never enabled. Skipping.");
return;
}
if (topics.length > 0) {
if (hasWebPushTopics) {
await api.updateWebPush(browserSubscription, topics);
} else {
await api.deleteWebPush(browserSubscription);

View file

@ -1,16 +1,15 @@
import { useState, useEffect } from "react";
import { useLiveQuery } from "dexie-react-hooks";
import notifier from "./Notifier";
import subscriptionManager from "./SubscriptionManager";
const intervalMillis = 13 * 60 * 1_000; // 13 minutes
const updateIntervalMillis = 60 * 60 * 1_000; // 1 hour
const broadcastChannel = new BroadcastChannel("web-push-broadcast");
/**
* Updates the Web Push subscriptions when the list of topics changes.
* Updates the Web Push subscriptions when the list of topics changes,
* as well as plays a sound when a new broadcat message is received from
* the service worker, since the service worker cannot play sounds.
*/
export const useWebPushTopicListener = () => {
const topics = useLiveQuery(() => subscriptionManager.webPushTopics());
const useWebPushListener = (topics) => {
const [lastTopics, setLastTopics] = useState();
useEffect(() => {
@ -29,63 +28,18 @@ export const useWebPushTopicListener = () => {
}
})();
}, [topics, lastTopics]);
useEffect(() => {
const onMessage = () => {
notifier.playSound(); // Service Worker cannot play sound, so we do it here!
};
broadcastChannel.addEventListener("message", onMessage);
return () => {
broadcastChannel.removeEventListener("message", onMessage);
};
});
};
/**
* 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;
this.messageHandler = this.onMessage.bind(this);
this.visibilityHandler = this.onVisibilityChange.bind(this);
}
startWorker() {
if (this.timer !== null) {
return;
}
this.timer = setInterval(() => this.updateSubscriptions(), intervalMillis);
this.broadcastChannel = new BroadcastChannel("web-push-broadcast");
this.broadcastChannel.addEventListener("message", this.messageHandler);
document.addEventListener("visibilitychange", this.visibilityHandler);
}
stopWorker() {
clearTimeout(this.timer);
this.broadcastChannel.removeEventListener("message", this.messageHandler);
this.broadcastChannel.close();
document.removeEventListener("visibilitychange", this.visibilityHandler);
}
onMessage() {
notifier.playSound(); // Service Worker cannot play sound, so we do it here!
}
onVisibilityChange() {
if (document.visibilityState === "visible") {
this.updateSubscriptions();
}
}
async updateSubscriptions() {
if (!notifier.pushPossible()) {
return;
}
if (!this.lastUpdate || Date.now() - this.lastUpdate > updateIntervalMillis) {
await subscriptionManager.updateWebPushSubscriptions();
this.lastUpdate = Date.now();
}
}
}
export const webPush = new WebPushWorker();
export default useWebPushListener;