From 65bc19936632e002d3a71ab19ee9b4b22fa1436a Mon Sep 17 00:00:00 2001 From: Mattias Thall Date: Tue, 26 May 2026 23:56:22 +0200 Subject: [PATCH] Add browser notification support for new videos - Settings: Notifications section with toggle that requests browser permission and stores preference in localStorage - Layout: fires a Notification when new_count increases and user isn't on /following - Works whenever the tab is open (foreground or background) Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/Layout.jsx | 20 ++++++++++ frontend/src/pages/Settings.jsx | 59 ++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 8dbe916..6a7fdd9 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -1,5 +1,6 @@ import { Outlet, NavLink, Link, useLocation } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; +import { useRef, useEffect } from "react"; import { useAuth } from "../hooks/useAuth"; import SearchBar from "./SearchBar"; import { getDownloads, getChannels, getActiveTasks, getMe } from "../api"; @@ -259,6 +260,24 @@ function useNewVideosCount() { return channels.reduce((sum, c) => sum + (c.new_count ?? 0), 0); } +function useNewVideoNotifications(newCount) { + const location = useLocation(); + const prevCount = useRef(newCount); + + useEffect(() => { + const enabled = localStorage.getItem("notifications_enabled") === "true"; + if (!enabled) return; + if (Notification.permission !== "granted") return; + if (location.pathname === "/following") return; + if (newCount > 0 && newCount > prevCount.current) { + new Notification("New videos", { + body: `${newCount} new video${newCount !== 1 ? "s" : ""} from channels you follow`, + }); + } + prevCount.current = newCount; + }, [newCount, location.pathname]); +} + function useOffline() { const { isError, error } = useQuery({ queryKey: ["health"], @@ -274,6 +293,7 @@ export default function Layout() { const { user, logout } = useAuth(); const newCount = useNewVideosCount(); const offline = useOffline(); + useNewVideoNotifications(newCount); return (
diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 5e6e1c9..af3a954 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -600,6 +600,62 @@ function OAuth2Section({ s, qc }) { ); } +function NotificationsSection() { + const [permission, setPermission] = useState(() => + "Notification" in window ? Notification.permission : "unsupported" + ); + const [enabled, setEnabled] = useState(() => localStorage.getItem("notifications_enabled") === "true"); + + const requestPermission = async () => { + if (!("Notification" in window)) return; + const result = await Notification.requestPermission(); + setPermission(result); + if (result === "granted") { + setEnabled(true); + localStorage.setItem("notifications_enabled", "true"); + new Notification("YTContinue notifications on", { + body: "You'll be notified when new videos arrive from channels you follow.", + }); + } + }; + + const toggle = () => { + if (permission !== "granted") { + requestPermission(); + return; + } + const next = !enabled; + setEnabled(next); + localStorage.setItem("notifications_enabled", next ? "true" : "false"); + }; + + if (permission === "unsupported") return null; + + return ( +
+ + {permission === "denied" ? ( + Blocked + ) : ( + + )} + +
+ ); +} + export default function SettingsPage() { const { user } = useAuth(); const qc = useQueryClient(); @@ -823,6 +879,9 @@ export default function SettingsPage() { + {/* Notifications */} + + {/* Data */}