diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx
index 584bfb3..747149b 100644
--- a/frontend/src/components/Feed.tsx
+++ b/frontend/src/components/Feed.tsx
@@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from "react";
import { useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query";
import { api, type FeedFilters, type Video } from "../lib/api";
-import { toast } from "../lib/notifications";
+import { notify } from "../lib/notifications";
import VideoCard from "./VideoCard";
const PAGE = 60;
@@ -73,7 +73,18 @@ export default function Feed({
.then(() => qc.invalidateQueries({ queryKey: ["feed"] }))
.catch(() => {});
if (status === "hidden") {
- toast("Video hidden", { label: "Undo", onClick: () => onState(id, "new") });
+ const v = (query.data?.pages ?? []).flatMap((p) => p.items).find((x) => x.id === id);
+ notify({
+ message: v?.title ? `Hidden “${v.title}”` : "Video hidden",
+ action: { label: "Undo", onClick: () => onState(id, "new") },
+ meta: {
+ kind: "video-hidden",
+ videoId: id,
+ title: v?.title ?? "this video",
+ channelId: v?.channel_id ?? "",
+ channelName: v?.channel_title ?? "",
+ },
+ });
}
}
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
index 1359e42..46f73c0 100644
--- a/frontend/src/components/Header.tsx
+++ b/frontend/src/components/Header.tsx
@@ -131,7 +131,7 @@ export default function Header({
)}
-
+
diff --git a/frontend/src/components/NotificationCenter.tsx b/frontend/src/components/NotificationCenter.tsx
index d0bd91c..38bf67c 100644
--- a/frontend/src/components/NotificationCenter.tsx
+++ b/frontend/src/components/NotificationCenter.tsx
@@ -1,13 +1,16 @@
-import { useEffect, useRef, useState } from "react";
-import { useSyncExternalStore } from "react";
-import { Bell, Trash2 } from "lucide-react";
+import { useEffect, useRef, useState, useSyncExternalStore } from "react";
+import { useQueryClient } from "@tanstack/react-query";
+import { Bell, Eye, Search, Trash2 } from "lucide-react";
+import { api, type FeedFilters } from "../lib/api";
import {
clearAll,
dismiss,
getNotifications,
getUnreadCount,
markAllRead,
+ notify,
subscribe,
+ type NotifMeta,
} from "../lib/notifications";
import { LEVEL_STYLE } from "./Toaster";
@@ -22,17 +25,55 @@ function relTime(ts: number): string {
return `${d}d ago`;
}
-export default function NotificationCenter() {
+export default function NotificationCenter({
+ filters,
+ setFilters,
+}: {
+ filters: FeedFilters;
+ setFilters: (f: FeedFilters) => void;
+}) {
const notifications = useSyncExternalStore(subscribe, getNotifications, getNotifications);
const unread = useSyncExternalStore(subscribe, getUnreadCount, getUnreadCount);
const [open, setOpen] = useState(false);
const wrap = useRef
(null);
+ const qc = useQueryClient();
+
+ // Close when clicking anywhere outside the panel.
+ useEffect(() => {
+ if (!open) return;
+ function onDown(e: MouseEvent) {
+ if (wrap.current && !wrap.current.contains(e.target as Node)) setOpen(false);
+ }
+ document.addEventListener("mousedown", onDown);
+ return () => document.removeEventListener("mousedown", onDown);
+ }, [open]);
// Opening the center marks everything read.
useEffect(() => {
if (open) markAllRead();
}, [open, notifications.length]);
+ function locateHidden(meta: NotifMeta) {
+ setFilters({
+ ...filters,
+ show: "hidden",
+ channelId: meta.channelId || undefined,
+ channelName: meta.channelName || undefined,
+ });
+ setOpen(false);
+ }
+
+ function unhide(meta: NotifMeta) {
+ api
+ .setState(meta.videoId, "new")
+ .then(() => {
+ qc.invalidateQueries({ queryKey: ["feed"] });
+ qc.invalidateQueries({ queryKey: ["feed-count"] });
+ notify({ level: "success", message: `Unhidden “${meta.title}”` });
+ })
+ .catch(() => {});
+ }
+
return (