From e35fd00ece3e23ffe7a8503d7f70432d6aa4dbf5 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 20 Jul 2024 13:19:34 -0600 Subject: [PATCH] Implement push notification handling --- frigate/api/notification.py | 4 +- frigate/comms/webpush.py | 21 +++-- web/public/notifications-worker.ts | 53 ++++++++++- .../settings/NotificationsSettingsView.tsx | 88 ++++++++++++------- 4 files changed, 125 insertions(+), 41 deletions(-) diff --git a/frigate/api/notification.py b/frigate/api/notification.py index b49f2dd5a6..d35ab5588d 100644 --- a/frigate/api/notification.py +++ b/frigate/api/notification.py @@ -44,7 +44,9 @@ def register_notifications(): sub = json.get("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: User.update(notification_tokens=User.notification_tokens.append(sub)).where( diff --git a/frigate/comms/webpush.py b/frigate/comms/webpush.py index fdebe964c5..9cf7d688ba 100644 --- a/frigate/comms/webpush.py +++ b/frigate/comms/webpush.py @@ -85,19 +85,24 @@ def send_message(self, payload: dict[str, any]) -> None: 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()}" - direct_url = f"{self.config.notifications.base_url}/review?id={reviewId}" - image = f'{self.config.notifications.base_url}{payload["after"]["thumb_path"].replace("/media/frigate", "")}' + direct_url = f"/review?id={reviewId}" + image = f'{payload["after"]["thumb_path"].replace("/media/frigate", "")}' + + logger.info(f"the image for testing is {image}") for pusher in self.web_pushers: pusher.send( headers=self.claim_headers, ttl=0, - data=json.dumps({ - "title": title, - "message": message, - "direct_url": direct_url, - "image": image, - }), + data=json.dumps( + { + "title": title, + "message": message, + "direct_url": direct_url, + "image": image, + "id": reviewId, + } + ), ) def stop(self) -> None: diff --git a/web/public/notifications-worker.ts b/web/public/notifications-worker.ts index 60ca960ed1..8b6067a609 100644 --- a/web/public/notifications-worker.ts +++ b/web/public/notifications-worker.ts @@ -1,9 +1,58 @@ // Notifications Worker self.addEventListener("push", function (event) { + // @ts-expect-error we know this exists 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 { - 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); + } + }); + } } }); diff --git a/web/src/views/settings/NotificationsSettingsView.tsx b/web/src/views/settings/NotificationsSettingsView.tsx index 7247b26c22..2b73714092 100644 --- a/web/src/views/settings/NotificationsSettingsView.tsx +++ b/web/src/views/settings/NotificationsSettingsView.tsx @@ -5,30 +5,58 @@ import { Toaster } from "@/components/ui/sonner"; import { Switch } from "@/components/ui/switch"; import { FrigateConfig } from "@/types/frigateConfig"; import axios from "axios"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import useSWR from "swr"; const NOTIFICATION_SERVICE_WORKER = "notifications-worker.ts"; export default function NotificationView() { - const { data: config } = useSWR("config"); + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); // notification key handling const { data: publicKey } = useSWR( 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 - const [notificationsSubscribed, setNotificationsSubscribed] = - useState(); + const [registration, setRegistration] = + useState(); useEffect(() => { navigator.serviceWorker .getRegistration(NOTIFICATION_SERVICE_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 }