refactor(app): tighten chat and sync boundaries

This commit is contained in:
2026-07-01 14:03:25 +08:00
parent f6e7adbe96
commit 760e4fe343
19 changed files with 215 additions and 677 deletions
+12 -79
View File
@@ -7,18 +7,12 @@ import { useRouter } from "next/navigation";
import { useAuthState } from "@/stores/auth/auth-context";
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
import { useUserState } from "@/stores/user/user-context";
import { ROUTE_BUILDERS, ROUTES } from "@/router/routes";
import { ROUTES } from "@/router/routes";
import {
openChatInExternalBrowser,
recordExternalBrowserPromptShown,
resolveExternalBrowserPromptEligibility,
} from "@/lib/chat/chat_external_browser";
import {
consumePendingChatUnlock,
peekPendingChatUnlock,
savePendingChatUnlock,
type PendingChatUnlockKind,
} from "@/lib/navigation/chat_unlock_session";
import { MobileShell } from "@/app/_components/core";
@@ -27,7 +21,7 @@ import {
ChatArea,
ChatHeader,
ChatInputBar,
ChatQuotaExhaustedBanner,
ChatInsufficientCreditsBanner,
ExternalBrowserDialog,
HistoryUnlockDialog,
InsufficientCreditsDialog,
@@ -40,6 +34,7 @@ import {
isChatDevelopmentEnvironment,
shouldStartExternalBrowserPrompt,
} from "./chat-screen.helpers";
import { useChatUnlockNavigationFlow } from "./hooks/use-chat-unlock-navigation-flow";
import styles from "./components/chat-screen.module.css";
export function ChatScreen() {
@@ -50,13 +45,15 @@ export function ChatScreen() {
const router = useRouter();
const [showExternalBrowserDialog, setShowExternalBrowserDialog] =
useState(false);
const unlockPaywallRequest = state.unlockPaywallRequest;
const {
unlockPaywallRequest,
requestMessageUnlock,
closeInsufficientCreditsDialog,
confirmInsufficientCreditsDialog,
} = useChatUnlockNavigationFlow({ returnUrl: ROUTES.chat });
// isGuest 派生(chat-screen 是唯一知道这个值的地方 —— chat 机器无 isGuest 字段)
const isGuest = deriveIsGuest(authState.loginStatus);
const isAuthenticatedUser =
authState.loginStatus !== "notLoggedIn" &&
authState.loginStatus !== "guest";
// 发送能力由后端统一返回,当前只处理积分不足引导。
const showMessageLimitBanner =
@@ -108,75 +105,11 @@ export function ChatScreen() {
authState.loginStatus,
]);
useEffect(() => {
if (!state.historyLoaded || !isAuthenticatedUser) return;
const pending = peekPendingChatUnlock();
if (!pending || pending.returnUrl !== ROUTES.chat) return;
const consumed = consumePendingChatUnlock();
if (!consumed) return;
chatDispatch({
type: "ChatUnlockMessageRequested",
messageId: consumed.messageId,
kind: consumed.kind,
});
}, [
chatDispatch,
isAuthenticatedUser,
state.historyLoaded,
]);
function handleCloseInsufficientCreditsDialog(): void {
chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" });
}
function handleConfirmInsufficientCreditsDialog(): void {
if (!unlockPaywallRequest) return;
savePendingChatUnlock({
messageId: unlockPaywallRequest.messageId,
kind: unlockPaywallRequest.kind,
returnUrl: ROUTES.chat,
stage: "payment",
});
chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" });
router.push(
ROUTE_BUILDERS.subscription(
getInsufficientCreditsSubscriptionType(userState.isVip),
{ returnTo: "chat" },
),
);
}
async function handleOpenExternalBrowser(): Promise<void> {
setShowExternalBrowserDialog(false);
await openChatInExternalBrowser();
}
function requestMessageUnlock(
messageId: string,
kind: PendingChatUnlockKind,
): void {
if (!isAuthenticatedUser) {
savePendingChatUnlock({
messageId,
kind,
returnUrl: ROUTES.chat,
stage: "auth",
});
router.push(ROUTE_BUILDERS.authWithRedirect(ROUTES.chat));
return;
}
chatDispatch({
type: "ChatUnlockMessageRequested",
messageId,
kind,
});
}
function handleUnlockPrivateMessage(messageId: string): void {
requestMessageUnlock(messageId, "private");
}
@@ -222,7 +155,7 @@ export function ChatScreen() {
/>
{showMessageLimitBanner ? (
<ChatQuotaExhaustedBanner
<ChatInsufficientCreditsBanner
title={messageLimitTitle}
ctaLabel={messageLimitCtaLabel}
onUnlock={handleMessageLimitUnlock}
@@ -258,8 +191,8 @@ export function ChatScreen() {
creditBalance={unlockPaywallRequest?.creditBalance ?? 0}
requiredCredits={unlockPaywallRequest?.requiredCredits ?? 0}
shortfallCredits={unlockPaywallRequest?.shortfallCredits ?? 0}
onClose={handleCloseInsufficientCreditsDialog}
onConfirm={handleConfirmInsufficientCreditsDialog}
onClose={closeInsufficientCreditsDialog}
onConfirm={confirmInsufficientCreditsDialog}
/>
</div>
</MobileShell>