refactor(feed-view): move the feed view mode into FeedViewProvider
Lift the feed's view-mode state (server-synced: persists on pick, adopted from prefs on login) out of App into FeedViewProvider. App reads it via useFeedView() to hand to Feed/ChannelPage; the view adopt lines leave App's big prefs-adopt effect. Removes another App-owned cross-cutting state ahead of the render flip. (Platform S6b.3)
This commit is contained in:
+7
-14
@@ -26,7 +26,6 @@ import {
|
||||
} from "./lib/panelLayout";
|
||||
import { configureNotifications, getNotifSettings, notify, removeByMetaKind, type NotifSettings } from "./lib/notifications";
|
||||
import { hintsEnabled, setHintsEnabled } from "./lib/hints";
|
||||
import { parseFeedView, type FeedView } from "./lib/feedView";
|
||||
import {
|
||||
getAccountRaw,
|
||||
LS,
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
import type { PrefsController } from "./components/SettingsPanel";
|
||||
import { useNavigation, useNavigationActions } from "./components/NavigationProvider";
|
||||
import { useFeedFilters, useFeedFiltersActions } from "./components/FeedFiltersProvider";
|
||||
import { useFeedView, useFeedViewActions } from "./components/FeedViewProvider";
|
||||
// Persistent shell — always mounted, so eagerly imported.
|
||||
import Welcome from "./components/Welcome";
|
||||
import SetupWizard from "./components/SetupWizard";
|
||||
@@ -98,9 +98,10 @@ export default function App() {
|
||||
// handlers that mutate the feed view (onShowInFeed, tag-click, the feed scrollKey).
|
||||
const filters = useFeedFilters();
|
||||
const { setFilters } = useFeedFiltersActions();
|
||||
// The feed's view mode is picked from the feed's own toolbar; like every Settings pref it
|
||||
// persists the moment you choose it.
|
||||
const [view, setViewState] = useState<FeedView>(() => parseFeedView(readAccount(LS.feedView, null)));
|
||||
// The feed's view mode (card/row/tile density) lives in FeedViewProvider — server-synced, shared
|
||||
// by the feed page and the channel page. App reads it only to hand to <Feed>/<ChannelPage>.
|
||||
const view = useFeedView();
|
||||
const { setView } = useFeedViewActions();
|
||||
// Settings-page prefs: applied live by the effects below, and auto-saved (debounced) on change
|
||||
// — no draft, no Save button (see the auto-save effect).
|
||||
const [perf, setPerf] = useState(() => getAccountRaw(PERF_KEY) === "1");
|
||||
@@ -160,11 +161,6 @@ export default function App() {
|
||||
writeAccount(LS.navCollapsed, next);
|
||||
api.savePrefs({ navCollapsed: next }).catch(() => {});
|
||||
}
|
||||
function setView(next: FeedView) {
|
||||
setViewState(next);
|
||||
writeAccount(LS.feedView, next);
|
||||
api.savePrefs({ view: next }).catch(() => {});
|
||||
}
|
||||
function setFilterCollapsed(next: boolean) {
|
||||
setFilterCollapsedState(next);
|
||||
writeAccount(LS.filterCollapsed, next);
|
||||
@@ -357,12 +353,9 @@ export default function App() {
|
||||
setHints(adopted.hints);
|
||||
setNotif(adopted.notifications);
|
||||
baselineRef.current = adopted;
|
||||
// Out-of-scope prefs stay instant (edited outside the Settings page).
|
||||
// Out-of-scope prefs stay instant (edited outside the Settings page). The feed view is adopted
|
||||
// by FeedViewProvider (its own passive ["me"] observer), not here.
|
||||
const prefsRec = prefs as Record<string, unknown>;
|
||||
// parseFeedView also maps the pre-0.45 "grid" / "list" values forward.
|
||||
const adoptedView = parseFeedView(prefs.view);
|
||||
setViewState(adoptedView);
|
||||
writeAccount(LS.feedView, adoptedView);
|
||||
(["feed", "plex", "playlists"] as PanelId[]).forEach((p) => {
|
||||
const raw = prefsRec[PREF_KEY[p]];
|
||||
if (raw) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "../lib/api";
|
||||
import { parseFeedView, type FeedView } from "../lib/feedView";
|
||||
import { LS, readAccount, writeAccount } from "../lib/storage";
|
||||
|
||||
// Owns the feed's view mode (the card/row/tile density switcher), lifted out of App so both the feed
|
||||
// page and the channel page read it via hooks instead of prop-drilling `view`/`setView` through App.
|
||||
// Split State + Actions contexts, mirroring the other providers; composed in main.tsx around <App/>.
|
||||
//
|
||||
// The view mode is an account PREFERENCE, not a filter: it persists to the server on pick (savePrefs)
|
||||
// and is adopted from server prefs on login — so unlike the ephemeral list filters, this provider
|
||||
// carries the same server-sync the App god component used to. A localStorage mirror seeds the initial
|
||||
// render synchronously; the passive ["me"] observer adopts the server value once the account resolves.
|
||||
|
||||
interface FeedViewActions {
|
||||
setView: (v: FeedView) => void;
|
||||
}
|
||||
|
||||
const StateContext = createContext<FeedView>("cards");
|
||||
const ActionsContext = createContext<FeedViewActions>({ setView: () => {} });
|
||||
|
||||
export function useFeedView(): FeedView {
|
||||
return useContext(StateContext);
|
||||
}
|
||||
export function useFeedViewActions(): FeedViewActions {
|
||||
return useContext(ActionsContext);
|
||||
}
|
||||
|
||||
export function FeedViewProvider({ children }: { children: ReactNode }) {
|
||||
const [view, setViewState] = useState<FeedView>(() => parseFeedView(readAccount(LS.feedView, null)));
|
||||
// Passive observer of the account App fetches (enabled:false → reads cache, re-renders on update).
|
||||
const { data: me } = useQuery({ queryKey: ["me"], queryFn: api.me, enabled: false });
|
||||
|
||||
// A user pick persists both to the localStorage mirror (instant reload) and to the server.
|
||||
const setView = useCallback((next: FeedView) => {
|
||||
setViewState(next);
|
||||
writeAccount(LS.feedView, next);
|
||||
api.savePrefs({ view: next }).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Adopt the server-stored view on login / account switch. Writes the mirror but does NOT savePrefs
|
||||
// (that would echo the just-adopted value straight back). parseFeedView also maps the pre-0.45
|
||||
// "grid" / "list" values forward.
|
||||
useEffect(() => {
|
||||
const prefs = me?.preferences;
|
||||
if (!prefs) return;
|
||||
const adopted = parseFeedView(prefs.view);
|
||||
setViewState(adopted);
|
||||
writeAccount(LS.feedView, adopted);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [me?.id]);
|
||||
|
||||
const actions = useMemo<FeedViewActions>(() => ({ setView }), [setView]);
|
||||
|
||||
return (
|
||||
<ActionsContext.Provider value={actions}>
|
||||
<StateContext.Provider value={view}>{children}</StateContext.Provider>
|
||||
</ActionsContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { FeedFiltersProvider } from "./components/FeedFiltersProvider";
|
||||
import { PlaylistsProvider } from "./components/PlaylistsProvider";
|
||||
import { PlexProvider } from "./components/PlexProvider";
|
||||
import { ChannelsProvider } from "./components/ChannelsProvider";
|
||||
import { FeedViewProvider } from "./components/FeedViewProvider";
|
||||
import { WizardProvider } from "./components/WizardProvider";
|
||||
import "./i18n";
|
||||
import "./index.css";
|
||||
@@ -46,9 +47,11 @@ const root =
|
||||
<PlaylistsProvider>
|
||||
<PlexProvider>
|
||||
<ChannelsProvider>
|
||||
<WizardProvider>
|
||||
<App />
|
||||
</WizardProvider>
|
||||
<FeedViewProvider>
|
||||
<WizardProvider>
|
||||
<App />
|
||||
</WizardProvider>
|
||||
</FeedViewProvider>
|
||||
</ChannelsProvider>
|
||||
</PlexProvider>
|
||||
</PlaylistsProvider>
|
||||
|
||||
Reference in New Issue
Block a user