fix(channels): land on the subscriptions tab for channel-targeted navigation

The Channel manager's tab (subscriptions vs playlist discovery) was persisted
locally, so the header's "N without full history" link and the focus-channel
jump (subscribe notice, tag manager) could dump the user on the Discovery tab
while quietly applying a status/name filter that only affects the subscriptions
table — the targeted channel was there, just on the hidden tab.

Lift the tab state to App alongside the status filter it pairs with, and have
those navigation intents switch it back to "subscribed".
This commit is contained in:
npeter83 2026-06-19 03:38:57 +02:00
parent 5f6ff7653b
commit 39cd025e79
2 changed files with 24 additions and 13 deletions

View file

@ -26,7 +26,7 @@ import Header from "./components/Header";
import NavSidebar from "./components/NavSidebar";
import Sidebar from "./components/Sidebar";
import Feed from "./components/Feed";
import Channels, { type ChannelStatusFilter } from "./components/Channels";
import Channels, { type ChannelStatusFilter, type ChannelsView } from "./components/Channels";
import Playlists from "./components/Playlists";
import Stats from "./components/Stats";
import Scheduler from "./components/Scheduler";
@ -136,11 +136,24 @@ export default function App() {
const [aboutOpen, setAboutOpen] = useState(false);
const [notesOpen, setNotesOpen] = useState(false);
const [notesHighlight, setNotesHighlight] = useState<string | undefined>(undefined);
// The Channel manager's active tab (subscriptions vs playlist discovery). Lifted here and
// persisted so navigation intents that target the subscriptions table — the header's
// "without full history" link, focus-channel — can force it back to "subscribed" instead
// of dumping the user on whatever tab they last left open.
const CHANNELS_VIEW_KEY = "siftlode.channelsView";
const [channelsView, setChannelsViewState] = useState<ChannelsView>(() =>
localStorage.getItem(CHANNELS_VIEW_KEY) === "discovery" ? "discovery" : "subscribed"
);
const setChannelsView = (v: ChannelsView) => {
setChannelsViewState(v);
localStorage.setItem(CHANNELS_VIEW_KEY, v);
};
// "Focus this channel in the manager": jump to the Channels page and seed its name filter
// so the channel is isolated. Cleared when leaving the page so it doesn't re-apply later.
const [focusChannelName, setFocusChannelName] = useState<string | null>(null);
const focusChannel = (name: string) => {
setFocusChannelName(name);
setChannelsView("subscribed"); // the name filter applies to the subscriptions table
setPage("channels");
};
useEffect(() => {
@ -409,6 +422,7 @@ export default function App() {
page={page}
onGoToFullHistory={() => {
setChannelFilter("needs_full");
setChannelsView("subscribed"); // the status filter applies to the subscriptions tab
setPage("channels");
}}
/>
@ -433,6 +447,8 @@ export default function App() {
onFocusChannel={focusChannel}
statusFilter={channelFilter}
setStatusFilter={setChannelFilter}
view={channelsView}
setView={setChannelsView}
onOpenWizard={() => setWizardOpen(true)}
onViewChannel={(id, name) => {
setFilters({ ...filters, channelId: id, channelName: name, show: "all" });

View file

@ -23,7 +23,7 @@ import ChannelDiscovery from "./ChannelDiscovery";
import TagManager from "./TagManager";
import { useConfirm } from "./ConfirmProvider";
type ChannelsView = "subscribed" | "discovery";
export type ChannelsView = "subscribed" | "discovery";
export type ChannelStatusFilter = "all" | "needs_full" | "fully_synced" | "hidden";
@ -50,6 +50,8 @@ export default function Channels({
focusChannelName,
statusFilter,
setStatusFilter,
view,
setView,
onOpenWizard,
}: {
canWrite: boolean;
@ -60,23 +62,16 @@ export default function Channels({
focusChannelName: string | null;
statusFilter: ChannelStatusFilter;
setStatusFilter: (f: ChannelStatusFilter) => void;
// The active tab lives in App so navigation intents that target the subscriptions table
// (the header's "without full history" link, focus-channel) can force it back here.
view: ChannelsView;
setView: (v: ChannelsView) => void;
onOpenWizard: () => void;
}) {
const { t } = useTranslation();
const qc = useQueryClient();
const confirm = useConfirm();
// Which tab is showing: the user's subscriptions, or channels discovered from their
// playlists. Persisted so a reload keeps the active tab (see other siftlode.* keys).
const [view, setView] = useState<ChannelsView>(() =>
localStorage.getItem("siftlode.channelsView") === "discovery"
? "discovery"
: "subscribed"
);
useEffect(() => {
localStorage.setItem("siftlode.channelsView", view);
}, [view]);
// A YouTube-gated action (sync, backfill, unsubscribe) that 403s means the user hasn't
// granted the needed scope — surface that with a "Connect" action instead of a vague fail.
const notifyActionError = (err: unknown, fallbackKey: string) => {