"use client"; import { useEffect, useMemo, type CSSProperties } from "react"; import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context"; import { useChatDispatch, useChatState, } from "@/stores/chat/chat-context"; import { useActiveCharacter, useActiveCharacterRoutes, } from "@/providers/character-provider"; import { useCharacterCatalog } from "@/providers/character-catalog-provider"; import { clearPendingChatNavigation } from "@/lib/navigation/chat_unlock_session"; import { buildGlobalPageUrl } from "@/router/global-route-context"; import { resolveAuthenticatedNavigation } from "@/router/navigation-resolver"; import { getCharacterRoutes, ROUTES } from "@/router/routes"; import { MobileShell } from "@/app/_components/core"; import { ChatArea, ChatHeader, ChatInputBar, ChatInsufficientCreditsBanner, ChatUnlockDialogs, FirstRechargeOfferBanner, FullscreenImageViewer, PwaInstallOverlay, } from "./components"; import { buildChatImageOverlayUrl, buildChatWithoutImageOverlayUrl, getChatImageOverlayMessageId, } from "./chat-image-overlay-url"; import { deriveIsGuest, } from "./chat-screen.helpers"; import { useFirstRechargeOfferBanner } from "./hooks/use-first-recharge-offer-banner"; import { useChatUnlockCoordinator } from "./hooks/use-chat-unlock-coordinator"; import { useChatGuestLogin } from "./hooks/use-chat-guest-login"; import { useChatMessageLimitBanner } from "./hooks/use-chat-message-limit-banner"; import { useChatPromotionBootstrap } from "./hooks/use-chat-promotion-bootstrap"; import { useSplashLatestMessageSync } from "./hooks/use-splash-latest-message-sync"; const chatShellStyle = { "--chat-message-font-size": "clamp(16px, 3.333vw, 18px)", "--chat-message-line-height": "1.5", } as CSSProperties; export function ChatScreen() { const router = useRouter(); const character = useActiveCharacter(); const characterCatalog = useCharacterCatalog(); const refreshCharacterCatalog = characterCatalog.refresh; const defaultCharacterSlug = characterCatalog.defaultCharacter.slug; const characterRoutes = useActiveCharacterRoutes(); const profileUrl = buildGlobalPageUrl( ROUTES.profile, characterRoutes.chat, ); const searchParams = useSearchParams(); const state = useChatState(); const chatDispatch = useChatDispatch(); const authState = useAuthState(); const authDispatch = useAuthDispatch(); useSplashLatestMessageSync({ characterId: state.characterId, historyLoaded: state.historyLoaded, loginStatus: authState.loginStatus, messages: state.historyMessages, }); const isPromotionBootstrapReady = useChatPromotionBootstrap( state.characterId, ); const visibleMessages = isPromotionBootstrapReady ? state.messages : state.historyMessages; const imageMessageId = getChatImageOverlayMessageId(searchParams); const imageReturnUrl = imageMessageId ? buildChatImageOverlayUrl(imageMessageId, characterRoutes.chat) : characterRoutes.chat; const unlockCoordinator = useChatUnlockCoordinator({ defaultReturnUrl: characterRoutes.chat, imageMessageId, imageReturnUrl, promotion: state.promotion, enabled: isPromotionBootstrapReady, }); const selectedImageMessage = useMemo( () => imageMessageId ? visibleMessages.find( (item) => (item.displayId === imageMessageId || item.remoteId === imageMessageId) && item.imageUrl, ) ?? null : null, [imageMessageId, visibleMessages], ); // Guest status belongs to auth state and is not duplicated in the Chat actor. const isGuest = deriveIsGuest(authState.loginStatus); // 发送能力由后端统一返回,当前只处理积分不足引导。 const messageLimitBanner = useChatMessageLimitBanner({ upgradePromptVisible: state.upgradePromptVisible, upgradeReason: state.upgradeReason, }); const firstRechargeOfferBanner = useFirstRechargeOfferBanner({ historyLoaded: state.historyLoaded, loginStatus: authState.loginStatus, }); const shouldShowPwaInstall = state.historyLoaded && state.historyMessages.length >= 10; useChatGuestLogin({ dispatch: authDispatch, hasInitialized: authState.hasInitialized, isLoading: authState.isLoading, loginStatus: authState.loginStatus, }); useEffect(() => { if ( !imageMessageId || !state.networkHistoryLoaded || selectedImageMessage ) { return; } router.replace( buildChatWithoutImageOverlayUrl(searchParams, characterRoutes.chat), { scroll: false }, ); }, [ characterRoutes.chat, imageMessageId, router, searchParams, selectedImageMessage, state.networkHistoryLoaded, ]); useEffect(() => { const errorCode = state.characterErrorCode; if (errorCode === "CHARACTER_MISMATCH") { chatDispatch({ type: "ChatHistoryRefreshRequested" }); return; } if ( errorCode !== "CHARACTER_DISABLED" && errorCode !== "CHARACTER_NOT_FOUND" ) { return; } let cancelled = false; void (async () => { if (errorCode === "CHARACTER_NOT_FOUND") { await clearPendingChatNavigation(); } await refreshCharacterCatalog(); if (!cancelled) { router.replace( getCharacterRoutes(defaultCharacterSlug).splash, ); } })(); return () => { cancelled = true; }; }, [ chatDispatch, defaultCharacterSlug, refreshCharacterCatalog, router, state.characterErrorCode, ]); function handleUnlockPrivateMessage( displayMessageId: string, remoteMessageId?: string, ): void { unlockCoordinator.requestMessageUnlock({ displayMessageId, remoteMessageId, kind: "private", }); } function handleUnlockVoiceMessage( displayMessageId: string, remoteMessageId?: string, ): void { unlockCoordinator.requestMessageUnlock({ displayMessageId, remoteMessageId, kind: "voice", }); } function handleUnlockImageMessage( displayMessageId: string, remoteMessageId?: string, ): void { unlockCoordinator.requestMessageUnlock({ displayMessageId, remoteMessageId, kind: "image", }); } function handleOpenImage(displayMessageId: string): void { router.push( buildChatImageOverlayUrl(displayMessageId, characterRoutes.chat), { scroll: false }, ); } function handleCloseImageViewer(): void { router.replace( buildChatWithoutImageOverlayUrl(searchParams, characterRoutes.chat), { scroll: false }, ); } function handleUnlockImagePaywall(): void { if (!selectedImageMessage) return; unlockCoordinator.requestMessageUnlock({ displayMessageId: selectedImageMessage.displayId, remoteMessageId: selectedImageMessage.remoteId, kind: "image", }); } function handleOpenUserProfile(): void { router.push( resolveAuthenticatedNavigation({ loginStatus: authState.loginStatus, targetUrl: profileUrl, }), ); } function handleOpenCharacterPrivateZone(): void { router.push(characterRoutes.privateZone); } return (
} /> { chatDispatch({ type: "ChatLoadMoreHistoryRequested" }); }} /> {messageLimitBanner.visible ? ( ) : ( )}
{/* PWA 安装提示触发器(聊天消息数量达到阈值后才允许弹出) */} {selectedImageMessage?.imageUrl ? ( ) : null}
); }