feat(notifications): link the subscribe activity to the channel

The "Subscribed on YouTube" inbox entry now names the channel and offers
two convenience links that survive a reload (driven by the typed payload,
not the live callback): "Channel manager" (jumps to the manager focused on
that channel) and "Open on YouTube". Wires the panel to the app's
focus-channel navigation; trilingual strings.
This commit is contained in:
npeter83 2026-06-19 02:16:50 +02:00
parent fb6f0c5dcb
commit 75ecf19147
5 changed files with 35 additions and 2 deletions

View file

@ -451,7 +451,7 @@ export default function App() {
) : page === "playlists" ? ( ) : page === "playlists" ? (
<Playlists canWrite={meQuery.data!.can_write} /> <Playlists canWrite={meQuery.data!.can_write} />
) : page === "notifications" ? ( ) : page === "notifications" ? (
<NotificationsPanel filters={filters} setFilters={setFilters} setPage={setPage} /> <NotificationsPanel filters={filters} setFilters={setFilters} setPage={setPage} onFocusChannel={focusChannel} />
) : page === "settings" ? ( ) : page === "settings" ? (
<SettingsPanel <SettingsPanel
me={meQuery.data!} me={meQuery.data!}

View file

@ -1,7 +1,7 @@
import { useEffect, useSyncExternalStore } from "react"; import { useEffect, useSyncExternalStore } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Bell, Check, CheckCheck, Eye, RotateCcw, Search, Trash2, X } from "lucide-react"; import { Bell, Check, CheckCheck, ExternalLink, Eye, RotateCcw, Search, Trash2, Tv, X } from "lucide-react";
import { api, type AppNotification, type FeedFilters } from "../lib/api"; import { api, type AppNotification, type FeedFilters } from "../lib/api";
import { useLiveQuery } from "../lib/useLiveQuery"; import { useLiveQuery } from "../lib/useLiveQuery";
import { import {
@ -14,6 +14,7 @@ import {
resolveVideo, resolveVideo,
subscribe, subscribe,
type Notification as ClientNotif, type Notification as ClientNotif,
type ChannelSubscribedMeta,
type VideoHiddenMeta, type VideoHiddenMeta,
type VideoWatchedMeta, type VideoWatchedMeta,
} from "../lib/notifications"; } from "../lib/notifications";
@ -46,10 +47,12 @@ export default function NotificationsPanel({
filters, filters,
setFilters, setFilters,
setPage, setPage,
onFocusChannel,
}: { }: {
filters: FeedFilters; filters: FeedFilters;
setFilters: (f: FeedFilters) => void; setFilters: (f: FeedFilters) => void;
setPage: (p: Page) => void; setPage: (p: Page) => void;
onFocusChannel: (name: string) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const qc = useQueryClient(); const qc = useQueryClient();
@ -183,6 +186,7 @@ export default function NotificationsPanel({
t={t} t={t}
onFind={locateHidden} onFind={locateHidden}
onRevert={revertState} onRevert={revertState}
onFocusChannel={onFocusChannel}
onRemove={() => removeClient(n.id)} onRemove={() => removeClient(n.id)}
/> />
))} ))}
@ -287,18 +291,22 @@ function ClientActivityRow({
t, t,
onFind, onFind,
onRevert, onRevert,
onFocusChannel,
onRemove, onRemove,
}: { }: {
n: ClientNotif; n: ClientNotif;
t: (k: string, o?: any) => string; t: (k: string, o?: any) => string;
onFind: (meta: VideoHiddenMeta) => void; onFind: (meta: VideoHiddenMeta) => void;
onRevert: (meta: VideoHiddenMeta | VideoWatchedMeta) => void; onRevert: (meta: VideoHiddenMeta | VideoWatchedMeta) => void;
onFocusChannel: (name: string) => void;
onRemove: () => void; onRemove: () => void;
}) { }) {
const { icon: Icon, color } = LEVEL_STYLE[n.level]; const { icon: Icon, color } = LEVEL_STYLE[n.level];
const needsAction = n.requiresInteraction && !n.dismissed; const needsAction = n.requiresInteraction && !n.dismissed;
const hidden = n.meta?.kind === "video-hidden" ? n.meta : null; const hidden = n.meta?.kind === "video-hidden" ? n.meta : null;
const watched = n.meta?.kind === "video-watched" ? n.meta : null; const watched = n.meta?.kind === "video-watched" ? n.meta : null;
const subscribed: ChannelSubscribedMeta | null =
n.meta?.kind === "channel-subscribed" ? n.meta : null;
return ( return (
<div <div
className={`glass rounded-xl p-3.5 flex items-start gap-3 ${needsAction ? "ring-1 ring-accent/30" : ""}`} className={`glass rounded-xl p-3.5 flex items-start gap-3 ${needsAction ? "ring-1 ring-accent/30" : ""}`}
@ -342,6 +350,25 @@ function ClientActivityRow({
{t("notifications.unwatch")} {t("notifications.unwatch")}
</button> </button>
</div> </div>
) : subscribed ? (
<div className="flex items-center gap-3 mt-1">
<button
onClick={() => onFocusChannel(subscribed.channelName)}
className="flex items-center gap-1 text-accent text-sm font-semibold hover:underline"
>
<Tv className="w-3.5 h-3.5" />
{t("notifications.openInManager")}
</button>
<a
href={`https://www.youtube.com/channel/${subscribed.channelId}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-accent text-sm font-semibold hover:underline"
>
<ExternalLink className="w-3.5 h-3.5" />
{t("notifications.openOnYouTube")}
</a>
</div>
) : ( ) : (
n.action && n.action &&

View file

@ -8,6 +8,8 @@
"findInFeed": "Im Feed finden", "findInFeed": "Im Feed finden",
"unhide": "Einblenden", "unhide": "Einblenden",
"unwatch": "Nicht angesehen", "unwatch": "Nicht angesehen",
"openInManager": "Kanalverwaltung",
"openOnYouTube": "Auf YouTube öffnen",
"unhidden": "Eingeblendet „{{title}}”", "unhidden": "Eingeblendet „{{title}}”",
"unwatched": "Als nicht angesehen markiert „{{title}}”", "unwatched": "Als nicht angesehen markiert „{{title}}”",
"time": { "time": {

View file

@ -8,6 +8,8 @@
"findInFeed": "Find in feed", "findInFeed": "Find in feed",
"unhide": "Unhide", "unhide": "Unhide",
"unwatch": "Unwatch", "unwatch": "Unwatch",
"openInManager": "Channel manager",
"openOnYouTube": "Open on YouTube",
"unhidden": "Unhidden “{{title}}”", "unhidden": "Unhidden “{{title}}”",
"unwatched": "Unwatched “{{title}}”", "unwatched": "Unwatched “{{title}}”",
"time": { "time": {

View file

@ -8,6 +8,8 @@
"findInFeed": "Keresés a hírfolyamban", "findInFeed": "Keresés a hírfolyamban",
"unhide": "Megjelenítés", "unhide": "Megjelenítés",
"unwatch": "Megtekintés visszavonása", "unwatch": "Megtekintés visszavonása",
"openInManager": "Csatornakezelő",
"openOnYouTube": "Megnyitás a YouTube-on",
"unhidden": "Megjelenítve: „{{title}}”", "unhidden": "Megjelenítve: „{{title}}”",
"unwatched": "Megtekintés visszavonva: „{{title}}”", "unwatched": "Megtekintés visszavonva: „{{title}}”",
"time": { "time": {