Implement push notification handling

This commit is contained in:
Nicolas Mowen 2024-07-20 13:19:34 -06:00
parent f946690520
commit 6fbab846af
4 changed files with 125 additions and 41 deletions

View File

@ -44,7 +44,9 @@ def register_notifications():
sub = json.get("sub") sub = json.get("sub")
if not sub: if not sub:
return jsonify({"success": False, "message": "Subscription must be provided."}), 400 return jsonify(
{"success": False, "message": "Subscription must be provided."}
), 400
try: try:
User.update(notification_tokens=User.notification_tokens.append(sub)).where( User.update(notification_tokens=User.notification_tokens.append(sub)).where(

View File

@ -85,19 +85,24 @@ class WebPushClient(Communicator): # type: ignore[misc]
title = f"{', '.join(sorted_objects).replace('_', ' ').title()}{' was' if state == 'end' else ''} detected in {', '.join(payload['after']['data']['zones']).replace('_', ' ').title()}" title = f"{', '.join(sorted_objects).replace('_', ' ').title()}{' was' if state == 'end' else ''} detected in {', '.join(payload['after']['data']['zones']).replace('_', ' ').title()}"
message = f"Detected on {payload['after']['camera'].replace('_', ' ').title()}" message = f"Detected on {payload['after']['camera'].replace('_', ' ').title()}"
direct_url = f"{self.config.notifications.base_url}/review?id={reviewId}" direct_url = f"/review?id={reviewId}"
image = f'{self.config.notifications.base_url}{payload["after"]["thumb_path"].replace("/media/frigate", "")}' image = f'{payload["after"]["thumb_path"].replace("/media/frigate", "")}'
logger.info(f"the image for testing is {image}")
for pusher in self.web_pushers: for pusher in self.web_pushers:
pusher.send( pusher.send(
headers=self.claim_headers, headers=self.claim_headers,
ttl=0, ttl=0,
data=json.dumps({ data=json.dumps(
"title": title, {
"message": message, "title": title,
"direct_url": direct_url, "message": message,
"image": image, "direct_url": direct_url,
}), "image": image,
"id": reviewId,
}
),
) )
def stop(self) -> None: def stop(self) -> None:

View File

@ -1,9 +1,58 @@
// Notifications Worker // Notifications Worker
self.addEventListener("push", function (event) { self.addEventListener("push", function (event) {
// @ts-expect-error we know this exists
if (event.data) { if (event.data) {
console.log("This push event has data: ", event.data.text()); // @ts-expect-error we know this exists
const data = event.data.json();
// @ts-expect-error we know this exists
self.registration.showNotification(data.title, {
body: data.message,
icon: data.image,
image: data.image,
tag: data.id,
data: { id: data.id, link: data.direct_url },
actions: [
{
action: `view-${data.id}`,
title: "View",
},
],
});
} else { } else {
console.log("This push event has no data."); // pass
// This push event has no data
}
});
self.addEventListener("notificationclick", (event) => {
// @ts-expect-error we know this exists
if (event.notification) {
// @ts-expect-error we know this exists
event.notification.close();
// @ts-expect-error we know this exists
if (event.notification.data) {
// @ts-expect-error we know this exists
const url = event.notification.data.link;
// @ts-expect-error we know this exists
clients.matchAll({ type: "window" }).then((windowClients) => {
// Check if there is already a window/tab open with the target URL
for (let i = 0; i < windowClients.length; i++) {
const client = windowClients[i];
// If so, just focus it.
if (client.url === url && "focus" in client) {
return client.focus();
}
}
// If not, then open the target URL in a new window/tab.
// @ts-expect-error we know this exists
if (clients.openWindow) {
// @ts-expect-error we know this exists
return clients.openWindow(url);
}
});
}
} }
}); });

View File

@ -5,30 +5,58 @@ import { Toaster } from "@/components/ui/sonner";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import axios from "axios"; import axios from "axios";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
const NOTIFICATION_SERVICE_WORKER = "notifications-worker.ts"; const NOTIFICATION_SERVICE_WORKER = "notifications-worker.ts";
export default function NotificationView() { export default function NotificationView() {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
// notification key handling // notification key handling
const { data: publicKey } = useSWR( const { data: publicKey } = useSWR(
config?.notifications?.enabled ? "notifications/pubkey" : null, config?.notifications?.enabled ? "notifications/pubkey" : null,
{ revalidateOnFocus: false },
);
const subscribeToNotifications = useCallback(
(registration: ServiceWorkerRegistration) => {
if (registration) {
registration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: publicKey,
})
.then((pushSubscription) => {
axios.post("notifications/register", {
sub: pushSubscription,
});
});
}
},
[publicKey],
); );
// notification state // notification state
const [notificationsSubscribed, setNotificationsSubscribed] = const [registration, setRegistration] =
useState<boolean>(); useState<ServiceWorkerRegistration | null>();
useEffect(() => { useEffect(() => {
navigator.serviceWorker navigator.serviceWorker
.getRegistration(NOTIFICATION_SERVICE_WORKER) .getRegistration(NOTIFICATION_SERVICE_WORKER)
.then((worker) => { .then((worker) => {
setNotificationsSubscribed(worker != null); if (worker) {
setRegistration(worker);
} else {
setRegistration(null);
}
})
.catch(() => {
setRegistration(null);
}); });
}, []); }, []);
@ -70,34 +98,34 @@ export default function NotificationView() {
// TODO make the notifications button show enable / disable depending on current state // TODO make the notifications button show enable / disable depending on current state
} }
<Button <Button
disabled={ disabled={publicKey == undefined}
notificationsSubscribed == undefined ||
publicKey == undefined
}
onClick={() => { onClick={() => {
Notification.requestPermission().then((permission) => { if (registration == null) {
console.log("notification permissions are ", permission); Notification.requestPermission().then((permission) => {
if (permission === "granted") { if (permission === "granted") {
navigator.serviceWorker navigator.serviceWorker
.register(NOTIFICATION_SERVICE_WORKER) .register(NOTIFICATION_SERVICE_WORKER)
.then((registration) => { .then((registration) => {
registration.pushManager setRegistration(registration);
.subscribe({
userVisibleOnly: true, if (registration.active) {
applicationServerKey: publicKey, subscribeToNotifications(registration);
}) } else {
.then((pushSubscription) => { setTimeout(
console.log(pushSubscription.endpoint); () => subscribeToNotifications(registration),
axios.post("notifications/register", { 1000,
sub: pushSubscription, );
}); }
}); });
}); }
} });
}); } else {
registration.unregister();
setRegistration(null);
}
}} }}
> >
{`${notificationsSubscribed ? "Disable" : "Enable"} Notifications`} {`${registration != null ? "Unregister" : "Register"} for Notifications`}
</Button> </Button>
</div> </div>
</div> </div>