mirror of
https://github.com/YuzuZensai/VRC-Circle.git
synced 2026-09-13 10:58:59 +00:00
✨ feat: persist tab/search state across navigation
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useI18n } from "../../lib/i18n";
|
||||
import { Banner, Loader, PAGE_TITLE, Tabs } from "../../components/ui";
|
||||
import { useViewState } from "../navigation/NavContext";
|
||||
import { useAccountSettings } from "./useAccountSettings";
|
||||
import { DisplayNameSection } from "./sections/DisplayNameSection";
|
||||
import { EmailSection } from "./sections/EmailSection";
|
||||
@@ -22,7 +22,7 @@ type AccountTab = "account" | "security" | "privacy" | "data";
|
||||
export function AccountSettingsView() {
|
||||
const { t } = useI18n();
|
||||
const { state, set } = useAccountSettings();
|
||||
const [tab, setTab] = useState<AccountTab>("account");
|
||||
const [tab, setTab] = useViewState<AccountTab>("account:tab", "account");
|
||||
|
||||
if (state.status === "loading") return <Loader className="absolute inset-0" />;
|
||||
if (state.status === "error")
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type FavoriteFolder,
|
||||
} from "../../store/avatars";
|
||||
import { useSelf, useSocial } from "../../store/social";
|
||||
import { useViewState } from "../navigation/NavContext";
|
||||
import { AvatarCard } from "./AvatarCard";
|
||||
import { FolderEditModal } from "./FolderEditModal";
|
||||
|
||||
@@ -41,8 +42,8 @@ function matchAvatar(query: string): AvatarFilter {
|
||||
|
||||
export function AvatarsView() {
|
||||
const t = useT();
|
||||
const [tab, setTab] = useState<Tab>("uploaded");
|
||||
const [query, setQuery] = useState("");
|
||||
const [tab, setTab] = useViewState<Tab>("avatars:tab", "uploaded");
|
||||
const [query, setQuery] = useViewState("avatars:query", "");
|
||||
const filter = useMemo(() => matchAvatar(query), [query]);
|
||||
const selfId = useSocial((s) => s.selfId);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, useMemo, useState } from "react";
|
||||
import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
||||
|
||||
export type View =
|
||||
| { kind: "user"; id: "me" | string }
|
||||
@@ -30,12 +30,15 @@ interface Nav {
|
||||
openGallery: () => void;
|
||||
openSearch: () => void;
|
||||
back: () => void;
|
||||
getViewState: (key: string) => unknown;
|
||||
setViewState: (key: string, value: unknown) => void;
|
||||
}
|
||||
|
||||
const NavCtx = createContext<Nav | null>(null);
|
||||
|
||||
export function NavProvider({ children }: { children: React.ReactNode }) {
|
||||
const [stack, setStack] = useState<View[]>([{ kind: "user", id: "me" }]);
|
||||
const viewState = useRef(new Map<string, unknown>());
|
||||
|
||||
const push = (view: View) =>
|
||||
setStack((s) => {
|
||||
@@ -64,6 +67,8 @@ export function NavProvider({ children }: { children: React.ReactNode }) {
|
||||
openGallery: () => root({ kind: "gallery" }),
|
||||
openSearch: () => root({ kind: "search" }),
|
||||
back: () => setStack((s) => (s.length > 1 ? s.slice(0, -1) : s)),
|
||||
getViewState: (key) => viewState.current.get(key),
|
||||
setViewState: (key, value) => viewState.current.set(key, value),
|
||||
}),
|
||||
[stack],
|
||||
);
|
||||
@@ -86,3 +91,19 @@ export function useNav(): Nav {
|
||||
if (!ctx) throw new Error("useNav must be used within NavProvider");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function useViewState<T>(key: string, initial: T): [T, (value: T) => void] {
|
||||
const nav = useNav();
|
||||
const [state, set] = useState<T>(() => {
|
||||
const saved = nav.getViewState(key);
|
||||
return saved === undefined ? initial : (saved as T);
|
||||
});
|
||||
const setBoth = useCallback(
|
||||
(value: T) => {
|
||||
nav.setViewState(key, value);
|
||||
set(value);
|
||||
},
|
||||
[nav, key],
|
||||
);
|
||||
return [state, setBoth];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { Banner, Loader, PAGE_TITLE, Tabs } from "../../components/ui";
|
||||
import { useT } from "../../lib/i18n";
|
||||
import { useViewState } from "../navigation/NavContext";
|
||||
import { useProfile } from "./useProfile";
|
||||
import { WorldsSection, FavoriteWorldsSection, WorldSearch } from "./WorldsSection";
|
||||
import { DiscoverSection } from "./DiscoverSection";
|
||||
@@ -12,7 +12,7 @@ type Tab = "discover" | "worlds" | "favorites";
|
||||
export function MyWorldsView() {
|
||||
const t = useT();
|
||||
const state = useProfile("me");
|
||||
const [tab, setTab] = useState<Tab>("discover");
|
||||
const [tab, setTab] = useViewState<Tab>("worlds:tab", "discover");
|
||||
|
||||
if (state.status === "loading") return <Loader className="absolute inset-0" />;
|
||||
if (state.status === "error")
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Tag,
|
||||
} from "../../components/ui";
|
||||
import { useUserMenu } from "../friends/useUserMenu";
|
||||
import { useViewState } from "../navigation/NavContext";
|
||||
import {
|
||||
avatarOf,
|
||||
bannerOf,
|
||||
@@ -47,7 +48,7 @@ export function ProfileView({ target }: { target: "me" | string }) {
|
||||
|
||||
function ProfileCard({ profile }: { profile: UserProfile }) {
|
||||
const t = useT();
|
||||
const [tab, setTab] = useState<TabId>("overview");
|
||||
const [tab, setTab] = useViewState<TabId>(`profile:${profile.id}:tab`, "overview");
|
||||
const [menu, setMenu] = useState<{ x: number; y: number } | null>(null);
|
||||
const canAdd = !profile.isSelf && !profile.isFriend;
|
||||
const addFriend = useAddFriend(profile.id);
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Tag,
|
||||
} from "../../components/ui";
|
||||
import { useT } from "../../lib/i18n";
|
||||
import { useNav } from "../navigation/NavContext";
|
||||
import { useNav, useViewState } from "../navigation/NavContext";
|
||||
import { useUserMenu } from "../friends/useUserMenu";
|
||||
import { WorldCard } from "../world/WorldCard";
|
||||
import { avatarOf, trustMeta } from "../../lib/vrchat";
|
||||
@@ -31,8 +31,8 @@ export function SearchView() {
|
||||
const t = useT();
|
||||
const { openUser, openWorld } = useNav();
|
||||
const { buildItems, modal } = useUserMenu();
|
||||
const [tab, setTab] = useState<SearchTab>("users");
|
||||
const [query, setQuery] = useState("");
|
||||
const [tab, setTab] = useViewState<SearchTab>("search:tab", "users");
|
||||
const [query, setQuery] = useViewState("search:query", "");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [users, setUsers] = useState<UserProfile[] | null>(null);
|
||||
const [worlds, setWorlds] = useState<World[] | null>(null);
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useTheme } from "../../lib/ThemeContext";
|
||||
import { useI18n } from "../../lib/i18n";
|
||||
import { useViewState } from "../navigation/NavContext";
|
||||
import { ACCENT_PRESETS, DEFAULT_ACCENT, type SchemeMode } from "../../lib/theme";
|
||||
import type { AppConfig, PreferredRegion, RegionPing } from "../../../../shared/types/appConfig";
|
||||
import type { UnityStatus } from "../../../../shared/types/unity";
|
||||
@@ -42,7 +43,7 @@ type SettingsTab = "appearance" | "game" | "creator" | "about";
|
||||
|
||||
export function SettingsView() {
|
||||
const { t } = useI18n();
|
||||
const [tab, setTab] = useState<SettingsTab>("appearance");
|
||||
const [tab, setTab] = useViewState<SettingsTab>("settings:tab", "appearance");
|
||||
return (
|
||||
<div className={SHELL}>
|
||||
<header>
|
||||
|
||||
Reference in New Issue
Block a user