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:
@@ -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 (
|
||||
<div className="flex flex-col overflow-hidden" style={{ height: "100dvh" }}>
|
||||
|
||||
@@ -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() {
|
||||
const { user } = useAuth();
|
||||
const qc = useQueryClient();
|
||||
@@ -823,6 +879,9 @@ export default function SettingsPage() {
|
||||
</Row>
|
||||
</Section>
|
||||
|
||||
{/* Notifications */}
|
||||
<NotificationsSection />
|
||||
|
||||
{/* Data */}
|
||||
<Section title="Data">
|
||||
<div className="px-5 py-4 flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user