"use client"; /** * ChatScreen 编排层 */ import { useEffect, useRef, useState } from "react"; import Image from "next/image"; import { useAuthState } from "@/stores/auth/auth-context"; import type { LoginStatus } from "@/data/dto/auth"; import { AppStorage } from "@/data/storage/app/app_storage"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { UserStorage } from "@/data/storage/user/user_storage"; import { useChatState } from "@/stores/chat/chat-context"; import { AppEnvUtil, BrowserDetector, todayString, UrlLauncherUtil } from "@/utils"; import { ROUTE_BUILDERS, ROUTES } from "@/router/routes"; import { MobileShell } from "@/app/_components/core"; import { BrowserHintOverlay, ChatArea, ChatHeader, ChatInputBar, ChatQuotaExhaustedBanner, ExternalBrowserDialog, PwaInstallOverlay, } from "./components"; import styles from "./components/chat-screen.module.css"; /** * 派生 isGuest —— 单一来源:`auth.loginStatus` */ function deriveIsGuest(loginStatus: LoginStatus): boolean { return loginStatus === "guest"; } export function ChatScreen() { const state = useChatState(); const authState = useAuthState(); const [showExternalBrowserDialog, setShowExternalBrowserDialog] = useState(false); // isGuest 派生(chat-screen 是唯一知道这个值的地方 —— chat 机器无 isGuest 字段) const isGuest = deriveIsGuest(authState.loginStatus); // 游客配额耗尽 → 显示 Unlock CTA banner(替代 ChatInputBar) // 三重条件:游客 + 配额加载完 + 用户被配额 guard 拦截过至少一次 // - quotaLoaded 避免一进 /chat 就跳 banner(loadQuota 还在 in-flight) // - quotaExceededTrigger 而非 guestTotalQuota / guestRemainingQuota —— // trigger 由 chat 机器的 guard 在 ChatSendMessage / ChatSendImage 被拦截时 +1, // 是 "用户被配额拒绝过" 的权威信号(避免依赖 quota 数值本身的边界判断) const showExhaustedBanner = isGuest && state.quotaLoaded && state.quotaExceededTrigger > 0; const externalBrowserPromptShownRef = useRef(false); useEffect(() => { if (!authState.hasInitialized || authState.isLoading) return; const isDev = AppEnvUtil.isDevelopment(); const canTriggerPrompt = isDev || (authState.loginStatus === "facebook" && BrowserDetector.isInAppBrowser()); if (!canTriggerPrompt) return; let mounted = true; let timer: number | undefined; const init = async () => { if (externalBrowserPromptShownRef.current) return; const today = todayString(); const canShowResult = await AppStorage.canShowExternalBrowserDialog(today); if (!mounted || !canShowResult.success || !canShowResult.data) return; externalBrowserPromptShownRef.current = true; timer = window.setTimeout(() => { if (!mounted) return; setShowExternalBrowserDialog(true); void AppStorage.recordExternalBrowserDialogShown(today); }, 3000); }; void init(); return () => { mounted = false; if (timer !== undefined) window.clearTimeout(timer); }; }, [ authState.hasInitialized, authState.isLoading, authState.loginStatus, ]); async function handleOpenExternalBrowser(): Promise { setShowExternalBrowserDialog(false); const authStorage = AuthStorage.getInstance(); const userStorage = UserStorage.getInstance(); const [deviceIdR, facebookIdR, avatarUrlR] = await Promise.all([ authStorage.getDeviceId(), authStorage.getFacebookId(), userStorage.getAvatarUrl(), ]); const deviceId = deviceIdR.success ? deviceIdR.data : null; const facebookId = facebookIdR.success ? facebookIdR.data : null; const avatarUrl = avatarUrlR.success ? avatarUrlR.data : null; if (deviceId && facebookId) { UrlLauncherUtil.openUrlWithExternalBrowser( ROUTE_BUILDERS.chatDeviceId(deviceId), { queryParams: { fbid: facebookId, ...(avatarUrl ? { avatarUrl } : {}), }, }, ); return; } UrlLauncherUtil.openUrlWithExternalBrowser(ROUTES.splash); } return (
{/* 背景图(与原 Dart MobileLayout 一致) */}
{/* isGuest 派生自 auth.loginStatus */} {showExhaustedBanner ? ( ) : ( )}
{/* 浏览器提示(应用内浏览器检测) */} {/* PWA 安装提示触发器(无可见 UI,3.5s 后弹 dialog) */} setShowExternalBrowserDialog(false)} onConfirm={() => void handleOpenExternalBrowser()} />
); }