From 119286db636e92494bd432951bb33cbcc2b44777 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 12 Jun 2026 17:39:28 +0200 Subject: [PATCH] feat(notifications): notify on 'watched' with an Unwatch action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the hide flow for watched: marking a video watched (card, modal toggle, or auto-watch) raises a toast and a bell-history entry with an Unwatch action — no Find-in-feed, just revert. NotifMeta becomes a discriminated union (video-hidden | video-watched); unhide/unwatch share one revert-to-new helper. --- frontend/src/components/Feed.tsx | 7 ++++++ .../src/components/NotificationCenter.tsx | 24 ++++++++++++++----- frontend/src/lib/notifications.ts | 8 ++++++- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index e157664..87d75ef 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -87,6 +87,13 @@ export default function Feed({ channelName: v?.channel_title ?? "", }, }); + } else if (status === "watched") { + const v = (query.data?.pages ?? []).flatMap((p) => p.items).find((x) => x.id === id); + notify({ + message: v?.title ? `Marked watched “${v.title}”` : "Marked watched", + action: { label: "Unwatch", onClick: () => onState(id, "new") }, + meta: { kind: "video-watched", videoId: id, title: v?.title ?? "this video" }, + }); } } diff --git a/frontend/src/components/NotificationCenter.tsx b/frontend/src/components/NotificationCenter.tsx index 5ba96e4..4603218 100644 --- a/frontend/src/components/NotificationCenter.tsx +++ b/frontend/src/components/NotificationCenter.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import { useQueryClient } from "@tanstack/react-query"; -import { Bell, Eye, Search, Trash2 } from "lucide-react"; +import { Bell, Eye, RotateCcw, Search, Trash2 } from "lucide-react"; import { api, type FeedFilters } from "../lib/api"; import { clearAll, @@ -10,7 +10,8 @@ import { markAllRead, notify, subscribe, - type NotifMeta, + type VideoHiddenMeta, + type VideoWatchedMeta, } from "../lib/notifications"; import { LEVEL_STYLE } from "./Toaster"; @@ -53,7 +54,7 @@ export default function NotificationCenter({ if (open) markAllRead(); }, [open, notifications.length]); - function locateHidden(meta: NotifMeta) { + function locateHidden(meta: VideoHiddenMeta) { setFilters({ ...filters, show: "hidden", @@ -63,13 +64,13 @@ export default function NotificationCenter({ setOpen(false); } - function unhide(meta: NotifMeta) { + function revertState(meta: VideoHiddenMeta | VideoWatchedMeta, verb: string) { api .setState(meta.videoId, "new") .then(() => { qc.invalidateQueries({ queryKey: ["feed"] }); qc.invalidateQueries({ queryKey: ["feed-count"] }); - notify({ level: "success", message: `Unhidden “${meta.title}”` }); + notify({ level: "success", message: `${verb} “${meta.title}”` }); }) .catch(() => {}); } @@ -113,6 +114,7 @@ export default function NotificationCenter({ const { icon: Icon, color } = LEVEL_STYLE[n.level]; const needsAction = n.requiresInteraction && !n.dismissed; const hidden = n.meta?.kind === "video-hidden" ? n.meta : null; + const watched = n.meta?.kind === "video-watched" ? n.meta : null; return (
+ ) : watched ? ( +
+ +
) : ( n.action && !n.dismissed && ( diff --git a/frontend/src/lib/notifications.ts b/frontend/src/lib/notifications.ts index f131d39..bf8dd27 100644 --- a/frontend/src/lib/notifications.ts +++ b/frontend/src/lib/notifications.ts @@ -12,13 +12,19 @@ export interface NotifAction { // Structured payload that lets the Notification Center offer in-app actions even // after a reload (when live `action` callbacks are gone). -export type NotifMeta = { +export type VideoHiddenMeta = { kind: "video-hidden"; videoId: string; title: string; channelId: string; channelName: string; }; +export type VideoWatchedMeta = { + kind: "video-watched"; + videoId: string; + title: string; +}; +export type NotifMeta = VideoHiddenMeta | VideoWatchedMeta; export interface Notification { id: number;