feat(inbox): add navigation links to watched and scheduler notices

Extend the reload-safe meta+link pattern (introduced for "Subscribed") to
more inbox entries, so every notice that can point somewhere does:

- A "Marked watched" activity notice now offers "Find in feed" (jumping to
  the feed filtered to Watched + that channel), alongside Unwatch — matching
  what the "Hidden" notice already does. VideoWatchedMeta carries the
  channel, and locate() handles both hidden and watched.
- A scheduler job-completion notice (System) now offers "Open Scheduler",
  jumping to the dashboard.

Maintenance notices reference already-removed videos, so they stay
informational with nothing to navigate to.
This commit is contained in:
npeter83 2026-06-19 03:05:01 +02:00
parent a004fa4107
commit c2b877c14e
6 changed files with 38 additions and 7 deletions

View file

@ -174,7 +174,13 @@ export default function Feed({
? i18n.t("feed.markedWatchedNamed", { title: v.title })
: i18n.t("feed.markedWatched"),
action: { label: i18n.t("feed.unwatch"), onClick: () => onState(id, "new") },
meta: { kind: "video-watched", videoId: id, title: v?.title ?? "this video" },
meta: {
kind: "video-watched",
videoId: id,
title: v?.title ?? "this video",
channelId: v?.channel_id ?? "",
channelName: v?.channel_title ?? "",
},
});
} else if (status === "new") {
// Unhide / unwatch (from a card, the toast's Undo, or the center): quietly resolve

View file

@ -1,7 +1,7 @@
import { useEffect, useSyncExternalStore } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Bell, Check, CheckCheck, ExternalLink, Eye, RotateCcw, Search, Trash2, Tv, X } from "lucide-react";
import { Activity, Bell, Check, CheckCheck, ExternalLink, Eye, RotateCcw, Search, Trash2, Tv, X } from "lucide-react";
import { api, type AppNotification, type FeedFilters } from "../lib/api";
import { useLiveQuery } from "../lib/useLiveQuery";
import {
@ -94,11 +94,12 @@ export default function NotificationsPanel({
clearMut.mutate();
}
// "Find in feed" / undo, ported from the old bell.
function locateHidden(meta: VideoHiddenMeta) {
// "Find in feed" / undo, ported from the old bell. Works for both hidden and watched
// notices — jump to the feed filtered to that state and the video's channel.
function locate(meta: VideoHiddenMeta | VideoWatchedMeta) {
setFilters({
...filters,
show: "hidden",
show: meta.kind === "video-hidden" ? "hidden" : "watched",
channelId: meta.channelId || undefined,
channelName: meta.channelName || undefined,
});
@ -168,6 +169,7 @@ export default function NotificationsPanel({
t={t}
onRead={() => readMut.mutate(n.id)}
onDismiss={() => dismissMut.mutate(n.id)}
onOpenScheduler={() => setPage("scheduler")}
/>
))}
</div>
@ -184,7 +186,7 @@ export default function NotificationsPanel({
key={n.id}
n={n}
t={t}
onFind={locateHidden}
onFind={locate}
onRevert={revertState}
onFocusChannel={onFocusChannel}
onRemove={() => removeClient(n.id)}
@ -204,11 +206,13 @@ function NotificationRow({
t,
onRead,
onDismiss,
onOpenScheduler,
}: {
n: AppNotification;
t: (k: string, o?: any) => string;
onRead: () => void;
onDismiss: () => void;
onOpenScheduler: () => void;
}) {
// The maintenance producer ships the affected videos in `data.videos`; list a few titles.
const videos: { id: string; title: string | null }[] = Array.isArray(n.data?.videos)
@ -261,6 +265,15 @@ function NotificationRow({
)}
</ul>
)}
{isScheduler && (
<button
onClick={onOpenScheduler}
className="flex items-center gap-1 text-accent text-sm font-semibold hover:underline mt-1.5"
>
<Activity className="w-3.5 h-3.5" />
{t("inbox.openScheduler")}
</button>
)}
</div>
<div className="flex items-center gap-1 shrink-0">
{!n.read && (
@ -296,7 +309,7 @@ function ClientActivityRow({
}: {
n: ClientNotif;
t: (k: string, o?: any) => string;
onFind: (meta: VideoHiddenMeta) => void;
onFind: (meta: VideoHiddenMeta | VideoWatchedMeta) => void;
onRevert: (meta: VideoHiddenMeta | VideoWatchedMeta) => void;
onFocusChannel: (name: string) => void;
onRemove: () => void;
@ -342,6 +355,13 @@ function ClientActivityRow({
</div>
) : watched ? (
<div className="flex items-center gap-3 mt-1">
<button
onClick={() => onFind(watched)}
className="flex items-center gap-1 text-accent text-sm font-semibold hover:underline"
>
<Search className="w-3.5 h-3.5" />
{t("notifications.findInFeed")}
</button>
<button
onClick={() => onRevert(watched)}
className="flex items-center gap-1 text-accent text-sm font-semibold hover:underline"