refactor: scope providers by route and use XState selectors

This commit is contained in:
2026-07-13 19:07:53 +08:00
parent f9b5f22ee9
commit 37ae152abb
24 changed files with 512 additions and 336 deletions
@@ -1,6 +1,11 @@
"use client";
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
import { shallowEqual } from "@xstate/react";
import {
useChatDispatch,
useChatSelector,
} from "@/stores/chat/chat-context";
import type { ChatUnlockPaywallRequest } from "@/stores/chat/chat-state";
import { HistoryUnlockDialog } from "./history-unlock-dialog";
@@ -19,7 +24,15 @@ export function ChatUnlockDialogs({
onConfirmInsufficientCreditsDialog,
includeHistoryUnlock = true,
}: ChatUnlockDialogsProps) {
const chatState = useChatState();
const chatState = useChatSelector(
(state) => ({
isUnlockingHistory: state.context.isUnlockingHistory,
lockedHistoryCount: state.context.lockedHistoryCount,
unlockHistoryError: state.context.unlockHistoryError,
unlockHistoryPromptVisible: state.context.unlockHistoryPromptVisible,
}),
shallowEqual,
);
const chatDispatch = useChatDispatch();
return (
+2 -2
View File
@@ -8,7 +8,7 @@
* - AI[Avatar] [Content] [占位 spacer]
* - 用户:[占位 spacer] [Content] [Avatar]
*/
import { useUserState } from "@/stores/user/user-context";
import { useUserSelector } from "@/stores/user/user-context";
import { MessageAvatar } from "./message-avatar";
import { MessageContent } from "./message-content";
@@ -49,7 +49,7 @@ export function MessageBubble({
onUnlockImageMessage,
onOpenImage,
}: MessageBubbleProps) {
const { avatarUrl } = useUserState();
const avatarUrl = useUserSelector((state) => state.context.avatarUrl);
if (isFromAI) {
return (
@@ -4,7 +4,7 @@ import type { LoginStatus } from "@/data/dto/auth";
import { ROUTES } from "@/router/routes";
import { useAppNavigator } from "@/router/use-app-navigator";
import type { ChatUpgradeReason } from "@/stores/chat/chat-state";
import { useUserState } from "@/stores/user/user-context";
import { useUserSelector } from "@/stores/user/user-context";
import {
getInsufficientCreditsMessageLimitView,
@@ -31,7 +31,7 @@ export function useChatMessageLimitBanner({
loginStatus,
}: UseChatMessageLimitBannerInput): ChatMessageLimitBannerView {
const navigator = useAppNavigator();
const userState = useUserState();
const isVip = useUserSelector((state) => state.context.isVip);
const view = getInsufficientCreditsMessageLimitView(loginStatus);
function unlock(): void {
@@ -41,7 +41,7 @@ export function useChatMessageLimitBanner({
}
navigator.openSubscription({
type: getInsufficientCreditsSubscriptionType(userState.isVip),
type: getInsufficientCreditsSubscriptionType(isVip),
returnTo: "chat",
});
}
@@ -1,6 +1,7 @@
"use client";
import { useEffect } from "react";
import { shallowEqual } from "@xstate/react";
import type { ChatLockType } from "@/data/schemas/chat";
import {
@@ -11,9 +12,12 @@ import {
type PendingChatPromotion,
} from "@/lib/navigation/chat_unlock_session";
import { useAppNavigator } from "@/router/use-app-navigator";
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
import {
useChatDispatch,
useChatSelector,
} from "@/stores/chat/chat-context";
import type { ChatUnlockPaywallRequest } from "@/stores/chat/chat-state";
import { useUserState } from "@/stores/user/user-context";
import { useUserSelector } from "@/stores/user/user-context";
import { getInsufficientCreditsSubscriptionType } from "../chat-screen.helpers";
@@ -55,8 +59,14 @@ export function useChatUnlockNavigationFlow({
enabled = true,
}: UseChatUnlockNavigationFlowInput): UseChatUnlockNavigationFlowOutput {
const navigator = useAppNavigator();
const userState = useUserState();
const chatState = useChatState();
const isVip = useUserSelector((state) => state.context.isVip);
const chatState = useChatSelector(
(state) => ({
historyLoaded: state.context.historyLoaded,
unlockPaywallRequest: state.context.unlockPaywallRequest,
}),
shallowEqual,
);
const chatDispatch = useChatDispatch();
const unlockPaywallRequest = getScopedUnlockPaywallRequest({
request: chatState.unlockPaywallRequest,
@@ -161,7 +171,7 @@ export function useChatUnlockNavigationFlow({
clientLockId: unlockPaywallRequest.clientLockId,
promotion: unlockPaywallRequest.promotion,
returnUrl,
type: getInsufficientCreditsSubscriptionType(userState.isVip),
type: getInsufficientCreditsSubscriptionType(isVip),
});
}
@@ -1,11 +1,12 @@
"use client";
import { useEffect, useState } from "react";
import { shallowEqual } from "@xstate/react";
import type { LoginStatus } from "@/data/dto/auth";
import { useAppNavigator } from "@/router/use-app-navigator";
import { usePaymentDispatch, usePaymentState } from "@/stores/payment";
import { useUserState } from "@/stores/user/user-context";
import { usePaymentDispatch, usePaymentSelector } from "@/stores/payment";
import { useUserSelector } from "@/stores/user/user-context";
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
import {
getFirstRechargeOfferBannerDismissed,
@@ -55,12 +56,24 @@ export function useFirstRechargeOfferBanner({
loginStatus,
}: UseFirstRechargeOfferBannerInput): UseFirstRechargeOfferBannerOutput {
const navigator = useAppNavigator();
const payment = usePaymentState();
const payment = usePaymentSelector(
(state) => ({
isFirstRecharge: state.context.isFirstRecharge,
status: String(state.value),
}),
shallowEqual,
);
const paymentDispatch = usePaymentDispatch();
const userState = useUserState();
const user = useUserSelector(
(state) => ({
countryCode: state.context.currentUser?.countryCode,
id: state.context.currentUser?.id ?? null,
}),
shallowEqual,
);
const isAuthenticatedUser =
loginStatus !== "notLoggedIn" && loginStatus !== "guest";
const userId = userState.currentUser?.id ?? null;
const userId = user.id;
const hasUserIdentity = !isAuthenticatedUser || Boolean(userId);
const [dismissalState, setDismissalState] = useState<DismissalState>(() => ({
userId,
@@ -73,14 +86,14 @@ export function useFirstRechargeOfferBanner({
if (!isAuthenticatedUser) return;
if (payment.status !== "idle") return;
const defaultPayChannel = getDefaultPayChannelForCountryCode(
userState.currentUser?.countryCode,
user.countryCode,
);
paymentDispatch({ type: "PaymentInit", payChannel: defaultPayChannel });
}, [
payment.status,
paymentDispatch,
isAuthenticatedUser,
userState.currentUser?.countryCode,
user.countryCode,
]);
useEffect(() => {
+7
View File
@@ -0,0 +1,7 @@
import type { ReactNode } from "react";
import { ChatRouteProviders } from "@/providers/chat-route-providers";
export default function ChatLayout({ children }: { children: ReactNode }) {
return <ChatRouteProviders>{children}</ChatRouteProviders>;
}