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 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 23:56:22 +02:00
parent 8029b2517f
commit 65bc199366
2 changed files with 79 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
import { Outlet, NavLink, Link, useLocation } from "react-router-dom"; import { Outlet, NavLink, Link, useLocation } from "react-router-dom";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useRef, useEffect } from "react";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
import { getDownloads, getChannels, getActiveTasks, getMe } from "../api"; import { getDownloads, getChannels, getActiveTasks, getMe } from "../api";
@@ -259,6 +260,24 @@ function useNewVideosCount() {
return channels.reduce((sum, c) => sum + (c.new_count ?? 0), 0); 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() { function useOffline() {
const { isError, error } = useQuery({ const { isError, error } = useQuery({
queryKey: ["health"], queryKey: ["health"],
@@ -274,6 +293,7 @@ export default function Layout() {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const newCount = useNewVideosCount(); const newCount = useNewVideosCount();
const offline = useOffline(); const offline = useOffline();
useNewVideoNotifications(newCount);
return ( return (
<div className="flex flex-col overflow-hidden" style={{ height: "100dvh" }}> <div className="flex flex-col overflow-hidden" style={{ height: "100dvh" }}>

View File

@@ -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 (
<Section title="Notifications">
<Row
label="New video alerts"
hint={
permission === "denied"
? "Blocked by browser allow notifications for this site in browser settings"
: "Get a browser notification when followed channels upload new videos"
}
>
{permission === "denied" ? (
<span className="text-xs text-zinc-600">Blocked</span>
) : (
<button
onClick={toggle}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${enabled && permission === "granted" ? "bg-accent" : "bg-zinc-700"}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enabled && permission === "granted" ? "translate-x-6" : "translate-x-1"}`} />
</button>
)}
</Row>
</Section>
);
}
export default function SettingsPage() { export default function SettingsPage() {
const { user } = useAuth(); const { user } = useAuth();
const qc = useQueryClient(); const qc = useQueryClient();
@@ -823,6 +879,9 @@ export default function SettingsPage() {
</Row> </Row>
</Section> </Section>
{/* Notifications */}
<NotificationsSection />
{/* Data */} {/* Data */}
<Section title="Data"> <Section title="Data">
<div className="px-5 py-4 flex items-center justify-between"> <div className="px-5 py-4 flex items-center justify-between">