refactor(channels): scope module state in ChannelsProvider
Lift the Channels manager's shared local state (name filter, status chip,
active tab, column-reset token, focus-a-channel intent) out of App into a
split-context ChannelsProvider, nested inside NavigationProvider so its
focusChannel/goToFullHistory actions can drive navigation. The Channels page,
the header search box + sync chip, the feed filter sidebar, and the
notifications inbox all read it via useChannels()/useChannelsActions() instead
of props drilled through App. Channels also internalises onViewChannel /
onFilterByTag / onFocusChannel (nav + feed-filters straight from providers) and
takes only { me, onOpenWizard } now. App loses five useStates, the focusChannel
helper, the focus-clear effect, and a large block of per-page prop plumbing.
(Platform S6b.2)
This commit is contained in:
+4
-77
@@ -32,13 +32,11 @@ import {
|
||||
LS,
|
||||
readAccount,
|
||||
setAccountRaw,
|
||||
useAccountPersistedState,
|
||||
writeAccount,
|
||||
} from "./lib/storage";
|
||||
import type { PrefsController } from "./components/SettingsPanel";
|
||||
import { useNavigation, useNavigationActions } from "./components/NavigationProvider";
|
||||
import { useFeedFilters, useFeedFiltersActions } from "./components/FeedFiltersProvider";
|
||||
import type { ChannelStatusFilter, ChannelsView } from "./components/Channels";
|
||||
// Persistent shell — always mounted, so eagerly imported.
|
||||
import Welcome from "./components/Welcome";
|
||||
import SetupWizard from "./components/SetupWizard";
|
||||
@@ -95,7 +93,7 @@ type EditablePrefs = {
|
||||
export default function App() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { page, channelView, ytSearch } = useNavigation();
|
||||
const { setPage, openChannel, enterYtSearch, exitYtSearch } = useNavigationActions();
|
||||
const { setPage, enterYtSearch, exitYtSearch } = useNavigationActions();
|
||||
const [theme, setThemeState] = useState<ThemePrefs>(() => loadLocalTheme());
|
||||
// The global feed filters live in FeedFiltersProvider (per-account persistence + the account-load
|
||||
// effect + share-link capture all moved there). App reads them to hand to <Feed> and to the
|
||||
@@ -129,49 +127,10 @@ export default function App() {
|
||||
plex: loadLayout("plex"),
|
||||
playlists: loadLayout("playlists"),
|
||||
}));
|
||||
// Client-side name filters for the Channels + Playlists lists — lifted here so the shared top
|
||||
// SearchBar can drive them (the modules read/seed them via props).
|
||||
const [channelsSearch, setChannelsSearch] = useState("");
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
// Channel status chip, persisted so a reload (F5) keeps it; clamp a stale value at render.
|
||||
const [channelFilterRaw, setChannelFilter] = useAccountPersistedState(LS.channelFilter, "all");
|
||||
const channelFilter: ChannelStatusFilter = (
|
||||
["needs_full", "fully_synced", "hidden", "all"] as const
|
||||
).includes(channelFilterRaw as ChannelStatusFilter)
|
||||
? (channelFilterRaw as ChannelStatusFilter)
|
||||
: "all";
|
||||
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 [channelsViewRaw, setChannelsView] = useAccountPersistedState(LS.channelsView, "subscribed");
|
||||
const channelsView: ChannelsView =
|
||||
channelsViewRaw === "discovery"
|
||||
? "discovery"
|
||||
: channelsViewRaw === "blocked"
|
||||
? "blocked"
|
||||
: "subscribed";
|
||||
// Bumped to tell the channel manager to drop a stale column filter when we send the user
|
||||
// there to see a specific set (the header's "without full history" link).
|
||||
const [channelsFilterReset, setChannelsFilterReset] = useState(0);
|
||||
// "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 [focusChannelToken, setFocusChannelToken] = useState(0);
|
||||
const focusChannel = (name: string) => {
|
||||
setFocusChannelName(name);
|
||||
setFocusChannelToken((n) => n + 1); // re-seed even when the same channel is re-focused
|
||||
setChannelsView("subscribed"); // the name filter applies to the subscriptions table
|
||||
setPage("channels");
|
||||
};
|
||||
useEffect(() => {
|
||||
// Clear the channel-manager focus when leaving the Channels page so it doesn't re-apply later.
|
||||
// (Leaving the feed clearing the live search is now the NavigationProvider's job.)
|
||||
if (page !== "channels") setFocusChannelName(null);
|
||||
}, [page]);
|
||||
|
||||
function openReleaseNotes(highlight?: string) {
|
||||
setNotesHighlight(highlight);
|
||||
@@ -546,7 +505,6 @@ export default function App() {
|
||||
<Sidebar
|
||||
layout={panelLayouts.feed}
|
||||
setLayout={(l) => setPanelLayout("feed", l)}
|
||||
onFocusChannel={focusChannel}
|
||||
isDemo={meQuery.data!.is_demo}
|
||||
collapsed={filterCollapsed}
|
||||
onToggleCollapse={() => setFilterCollapsed(!filterCollapsed)}
|
||||
@@ -607,18 +565,7 @@ export default function App() {
|
||||
// fade would sit on it and flip on with the first scroll, so turn it off there.
|
||||
fadeTop={page !== "channels" && page !== "audit"}
|
||||
header={
|
||||
<Header
|
||||
me={meQuery.data!}
|
||||
channelsSearch={channelsSearch}
|
||||
setChannelsSearch={setChannelsSearch}
|
||||
onYtSearch={enterYtSearch}
|
||||
onGoToFullHistory={() => {
|
||||
setChannelFilter("needs_full");
|
||||
setChannelsView("subscribed"); // the status filter applies to the subscriptions tab
|
||||
setChannelsFilterReset((n) => n + 1); // drop any stale column filter hiding the rows
|
||||
setPage("channels");
|
||||
}}
|
||||
/>
|
||||
<Header me={meQuery.data!} onYtSearch={enterYtSearch} />
|
||||
}
|
||||
banners={
|
||||
<>
|
||||
@@ -629,27 +576,7 @@ export default function App() {
|
||||
>
|
||||
<Suspense fallback={pageFallback}>
|
||||
{page === "channels" ? (
|
||||
<Channels
|
||||
canWrite={meQuery.data!.can_write}
|
||||
isAdmin={meQuery.data!.role === "admin"}
|
||||
search={channelsSearch}
|
||||
setSearch={setChannelsSearch}
|
||||
focusChannelName={focusChannelName}
|
||||
focusChannelToken={focusChannelToken}
|
||||
onFocusChannel={focusChannel}
|
||||
statusFilter={channelFilter}
|
||||
setStatusFilter={setChannelFilter}
|
||||
view={channelsView}
|
||||
setView={setChannelsView}
|
||||
filtersResetToken={channelsFilterReset}
|
||||
onOpenWizard={() => setWizardOpen(true)}
|
||||
onViewChannel={(id, name) => openChannel(id, name)}
|
||||
onFilterByTag={(tagId, name) => {
|
||||
setFilters({ ...filters, tags: [tagId], channelIds: [] });
|
||||
setPage("feed");
|
||||
notify({ level: "info", message: t("channels.notify.filteredByTag", { name }) });
|
||||
}}
|
||||
/>
|
||||
<Channels me={meQuery.data!} onOpenWizard={() => setWizardOpen(true)} />
|
||||
) : page === "stats" ? (
|
||||
<Stats me={meQuery.data!} />
|
||||
) : page === "scheduler" && meQuery.data!.role === "admin" ? (
|
||||
@@ -663,7 +590,7 @@ export default function App() {
|
||||
) : page === "playlists" ? (
|
||||
<Playlists me={meQuery.data!} />
|
||||
) : page === "notifications" ? (
|
||||
<NotificationsPanel onFocusChannel={focusChannel} />
|
||||
<NotificationsPanel />
|
||||
) : page === "messages" && !meQuery.data!.is_demo ? (
|
||||
<div className="p-4 max-w-3xl w-full mx-auto">
|
||||
<Messages meId={meQuery.data!.id} />
|
||||
|
||||
Reference in New Issue
Block a user