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:
parent
a8ba66007f
commit
064018bc24
5 changed files with 35 additions and 2 deletions
|
|
@ -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!}
|
||||||
|
|
|
||||||
|
|
@ -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 &&
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue