fix: clean up wallet favorites state

This commit is contained in:
TerryM
2026-06-02 00:57:37 +08:00
parent 05c2252b49
commit fc2ca62957
3 changed files with 43 additions and 39 deletions

View File

@@ -30,19 +30,22 @@ export function FavoritesProvider({ children }: { children: ReactNode }) {
const { t } = useI18n(); const { t } = useI18n();
const { showToast } = useToast(); const { showToast } = useToast();
const wallet = useWallet(); const wallet = useWallet();
const { address, openLoginModal, status, token } = wallet;
const [favoriteIds, setFavoriteIds] = useState<Set<string>>(() => new Set()); const [favoriteIds, setFavoriteIds] = useState<Set<string>>(() => new Set());
const [knownIds, setKnownIds] = useState<Set<string>>(() => new Set()); const [knownIds, setKnownIds] = useState<Set<string>>(() => new Set());
const [pendingIds, setPendingIds] = useState<Set<string>>(() => new Set()); const [pendingIds, setPendingIds] = useState<Set<string>>(() => new Set());
const pendingAfterLoginRef = useRef<string | null>(null); const pendingAfterLoginRef = useRef<string | null>(null);
const lastAddressRef = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
if (wallet.status === "loggedOut") { const nextAddress = status === "loggedIn" ? address : null;
if (lastAddressRef.current === nextAddress) return;
lastAddressRef.current = nextAddress;
setFavoriteIds(new Set()); setFavoriteIds(new Set());
setKnownIds(new Set()); setKnownIds(new Set());
setPendingIds(new Set()); setPendingIds(new Set());
pendingAfterLoginRef.current = null; if (!nextAddress) pendingAfterLoginRef.current = null;
} }, [address, status]);
}, [wallet.status]);
const markFavorite = useCallback((resourceId: string, favorited: boolean) => { const markFavorite = useCallback((resourceId: string, favorited: boolean) => {
setKnownIds((prev) => new Set(prev).add(resourceId)); setKnownIds((prev) => new Set(prev).add(resourceId));
@@ -56,12 +59,12 @@ export function FavoritesProvider({ children }: { children: ReactNode }) {
const ensureFavoriteIds = useCallback( const ensureFavoriteIds = useCallback(
async (resourceIds: string[]) => { async (resourceIds: string[]) => {
if (!wallet.token || wallet.status !== "loggedIn") return; if (!token || status !== "loggedIn") return;
const missing = [...new Set(resourceIds)].filter( const missing = [...new Set(resourceIds)].filter(
(id) => !knownIds.has(id), (id) => !knownIds.has(id),
); );
if (missing.length === 0) return; if (missing.length === 0) return;
const ids = await getFavoriteIds(wallet.token, missing); const ids = await getFavoriteIds(token, missing);
setKnownIds((prev) => { setKnownIds((prev) => {
const next = new Set(prev); const next = new Set(prev);
missing.forEach((id) => next.add(id)); missing.forEach((id) => next.add(id));
@@ -73,18 +76,18 @@ export function FavoritesProvider({ children }: { children: ReactNode }) {
return next; return next;
}); });
}, },
[knownIds, wallet.status, wallet.token], [knownIds, status, token],
); );
const runFavoriteMutation = useCallback( const runFavoriteMutation = useCallback(
async (resourceId: string) => { async (resourceId: string) => {
if (!wallet.token) return; if (!token) return;
const currentlyFavorite = favoriteIds.has(resourceId); const currentlyFavorite = favoriteIds.has(resourceId);
setPendingIds((prev) => new Set(prev).add(resourceId)); setPendingIds((prev) => new Set(prev).add(resourceId));
markFavorite(resourceId, !currentlyFavorite); markFavorite(resourceId, !currentlyFavorite);
try { try {
if (currentlyFavorite) await removeFavorite(wallet.token, resourceId); if (currentlyFavorite) await removeFavorite(token, resourceId);
else await addFavorite(wallet.token, resourceId); else await addFavorite(token, resourceId);
showToast( showToast(
currentlyFavorite ? t("favoriteRemoved") : t("favoriteAdded"), currentlyFavorite ? t("favoriteRemoved") : t("favoriteAdded"),
); );
@@ -100,29 +103,29 @@ export function FavoritesProvider({ children }: { children: ReactNode }) {
}); });
} }
}, },
[favoriteIds, markFavorite, showToast, t, wallet.token], [favoriteIds, markFavorite, showToast, t, token],
); );
const toggleFavorite = useCallback( const toggleFavorite = useCallback(
async (resourceId: string) => { async (resourceId: string) => {
if (!wallet.token || wallet.status !== "loggedIn") { if (!token || status !== "loggedIn") {
pendingAfterLoginRef.current = resourceId; pendingAfterLoginRef.current = resourceId;
wallet.openLoginModal(); openLoginModal();
showToast(t("favoriteLoginRequired")); showToast(t("favoriteLoginRequired"));
return; return;
} }
await runFavoriteMutation(resourceId); await runFavoriteMutation(resourceId);
}, },
[runFavoriteMutation, showToast, t, wallet], [openLoginModal, runFavoriteMutation, showToast, status, t, token],
); );
useEffect(() => { useEffect(() => {
if (wallet.status !== "loggedIn" || !wallet.token) return; if (status !== "loggedIn" || !token) return;
const pending = pendingAfterLoginRef.current; const pending = pendingAfterLoginRef.current;
if (!pending) return; if (!pending) return;
pendingAfterLoginRef.current = null; pendingAfterLoginRef.current = null;
void runFavoriteMutation(pending).catch(() => undefined); void runFavoriteMutation(pending).catch(() => undefined);
}, [runFavoriteMutation, wallet.status, wallet.token]); }, [runFavoriteMutation, status, token]);
const statusFor = useCallback( const statusFor = useCallback(
(resourceId: string): FavoriteStatus => { (resourceId: string): FavoriteStatus => {

View File

@@ -52,7 +52,7 @@ function FavoriteResourceCard({ resource }: { resource: Resource }) {
const lp = useLocalizedPath(); const lp = useLocalizedPath();
const unavailable = resource.availability === "unavailable"; const unavailable = resource.availability === "unavailable";
const cover = resource.coverImage || resource.previewUrl; const cover = resource.coverImage || resource.previewUrl;
const content = ( return (
<article <article
className={`group relative flex min-h-[132px] gap-4 rounded-2xl border bg-[#272632] p-3 transition md:p-4 ${ className={`group relative flex min-h-[132px] gap-4 rounded-2xl border bg-[#272632] p-3 transition md:p-4 ${
unavailable unavailable
@@ -60,7 +60,14 @@ function FavoriteResourceCard({ resource }: { resource: Resource }) {
: "border-[#27292E] hover:border-ark-gold/50" : "border-[#27292E] hover:border-ark-gold/50"
}`} }`}
> >
<div className="relative h-[96px] w-[112px] shrink-0 overflow-hidden rounded-xl bg-[#111116] md:h-[116px] md:w-[150px]"> {!unavailable ? (
<Link
to={lp(`/resource/${resource.id}`)}
aria-label={resource.title}
className="absolute inset-0 z-0 rounded-2xl outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80"
/>
) : null}
<div className="relative z-10 h-[96px] w-[112px] shrink-0 overflow-hidden rounded-xl bg-[#111116] md:h-[116px] md:w-[150px]">
{cover && !unavailable ? ( {cover && !unavailable ? (
<img <img
src={assetUrl(cover)} src={assetUrl(cover)}
@@ -81,7 +88,7 @@ function FavoriteResourceCard({ resource }: { resource: Resource }) {
) : null} ) : null}
</div> </div>
<div className="flex min-w-0 flex-1 flex-col gap-2 pr-11"> <div className="pointer-events-none relative z-10 flex min-w-0 flex-1 flex-col gap-2 pr-11">
<h2 className="line-clamp-2 text-base font-bold leading-snug text-white md:text-lg"> <h2 className="line-clamp-2 text-base font-bold leading-snug text-white md:text-lg">
{resource.title} {resource.title}
</h2> </h2>
@@ -107,17 +114,10 @@ function FavoriteResourceCard({ resource }: { resource: Resource }) {
<FavoriteButton <FavoriteButton
resourceId={resource.id} resourceId={resource.id}
className="absolute right-3 top-3 z-10" className="absolute right-3 top-3 z-20"
/> />
</article> </article>
); );
if (unavailable) return content;
return (
<Link to={lp(`/resource/${resource.id}`)} className="block">
{content}
</Link>
);
} }
export default function Favorites() { export default function Favorites() {

View File

@@ -21,7 +21,8 @@ type ModalState = "idle" | "tpLoading" | "tpPolling" | "rainbowSigning";
export function WalletLoginModal() { export function WalletLoginModal() {
const { t } = useI18n(); const { t } = useI18n();
const { showToast } = useToast(); const { showToast } = useToast();
const wallet = useWallet(); const { closeLoginModal, completeLogin, loginModalOpen, signInInjected } =
useWallet();
const { openConnectModal } = useConnectModal(); const { openConnectModal } = useConnectModal();
const { address, isConnected } = useAccount(); const { address, isConnected } = useAccount();
const { signMessageAsync } = useSignMessage(); const { signMessageAsync } = useSignMessage();
@@ -35,12 +36,12 @@ export function WalletLoginModal() {
const close = () => { const close = () => {
if (state === "tpLoading" || state === "rainbowSigning") return; if (state === "tpLoading" || state === "rainbowSigning") return;
wallet.closeLoginModal(); closeLoginModal();
setError(""); setError("");
}; };
useEffect(() => { useEffect(() => {
if (!wallet.loginModalOpen || !tpRequest) return; if (!loginModalOpen || !tpRequest) return;
if (state !== "tpPolling") return; if (state !== "tpPolling") return;
let cancelled = false; let cancelled = false;
@@ -55,7 +56,7 @@ export function WalletLoginModal() {
signature: result.signature, signature: result.signature,
}); });
if (cancelled) return; if (cancelled) return;
wallet.completeLogin(verified.token, verified.wallet); completeLogin(verified.token, verified.wallet);
showToast(t("walletLoginSuccess")); showToast(t("walletLoginSuccess"));
setState("idle"); setState("idle");
setTpRequest(null); setTpRequest(null);
@@ -76,7 +77,7 @@ export function WalletLoginModal() {
cancelled = true; cancelled = true;
window.clearInterval(timer); window.clearInterval(timer);
}; };
}, [state, tpRequest, t, showToast, wallet]); }, [completeLogin, loginModalOpen, state, tpRequest, t, showToast]);
useEffect(() => { useEffect(() => {
if (!rainbowPending || !isConnected || !address) return; if (!rainbowPending || !isConnected || !address) return;
@@ -94,7 +95,7 @@ export function WalletLoginModal() {
message: nonce.message, message: nonce.message,
signature, signature,
}); });
wallet.completeLogin(verified.token, verified.wallet); completeLogin(verified.token, verified.wallet);
showToast(t("walletLoginSuccess")); showToast(t("walletLoginSuccess"));
} catch (err) { } catch (err) {
const message = const message =
@@ -114,15 +115,15 @@ export function WalletLoginModal() {
showToast, showToast,
signMessageAsync, signMessageAsync,
t, t,
wallet, completeLogin,
]); ]);
if (!wallet.loginModalOpen) return null; if (!loginModalOpen) return null;
const startInjected = async () => { const startInjected = async () => {
setError(""); setError("");
setState("idle"); setState("idle");
await wallet.signInInjected().catch(() => undefined); await signInInjected().catch(() => undefined);
}; };
const startTokenPocketQr = async () => { const startTokenPocketQr = async () => {