refactor(chat): show images in page overlay

This commit is contained in:
2026-07-09 10:11:25 +08:00
parent cb7791dd8d
commit fc92c3a5d5
23 changed files with 253 additions and 627 deletions
+90 -1
View File
@@ -1,6 +1,8 @@
"use client";
import { useEffect, useMemo } from "react";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
import { useChatState } from "@/stores/chat/chat-context";
@@ -17,8 +19,14 @@ import {
ChatUnlockDialogs,
ExternalBrowserDialog,
FirstRechargeOfferBanner,
FullscreenImageViewer,
PwaInstallOverlay,
} from "./components";
import {
buildChatImageOverlayUrl,
buildChatWithoutImageOverlayUrl,
getChatImageOverlayMessageId,
} from "./chat-image-overlay-url";
import {
deriveIsGuest,
isChatDevelopmentEnvironment,
@@ -31,15 +39,44 @@ import { useExternalBrowserPrompt } from "./hooks/use-external-browser-prompt";
import styles from "./components/chat-screen.module.css";
export function ChatScreen() {
const router = useRouter();
const searchParams = useSearchParams();
const state = useChatState();
const authState = useAuthState();
const authDispatch = useAuthDispatch();
const imageMessageId = getChatImageOverlayMessageId(searchParams);
const imageReturnUrl = imageMessageId
? buildChatImageOverlayUrl(imageMessageId)
: ROUTES.chat;
const {
unlockPaywallRequest,
requestMessageUnlock,
closeInsufficientCreditsDialog,
confirmInsufficientCreditsDialog,
} = useChatUnlockNavigationFlow({ returnUrl: ROUTES.chat });
} = useChatUnlockNavigationFlow({
returnUrl: ROUTES.chat,
ignoredKind: "image",
});
const {
unlockPaywallRequest: imageUnlockPaywallRequest,
requestMessageUnlock: requestImageUnlock,
closeInsufficientCreditsDialog: closeImageInsufficientCreditsDialog,
confirmInsufficientCreditsDialog: confirmImageInsufficientCreditsDialog,
} = useChatUnlockNavigationFlow({
returnUrl: imageReturnUrl,
expectedKind: "image",
expectedMessageId: imageMessageId ?? undefined,
enabled: imageMessageId !== null,
});
const selectedImageMessage = useMemo(
() =>
imageMessageId
? state.messages.find(
(item) => item.id === imageMessageId && item.imageUrl,
) ?? null
: null,
[imageMessageId, state.messages],
);
// isGuest 派生(chat-screen 是唯一知道这个值的地方 —— chat 机器无 isGuest 字段)
const isGuest = deriveIsGuest(authState.loginStatus);
@@ -68,6 +105,21 @@ export function ChatScreen() {
loginStatus: authState.loginStatus,
});
useEffect(() => {
if (!imageMessageId || !state.historyLoaded || selectedImageMessage) {
return;
}
router.replace(buildChatWithoutImageOverlayUrl(searchParams), {
scroll: false,
});
}, [
imageMessageId,
router,
searchParams,
selectedImageMessage,
state.historyLoaded,
]);
function handleUnlockPrivateMessage(messageId: string): void {
requestMessageUnlock(messageId, "private");
}
@@ -76,6 +128,21 @@ export function ChatScreen() {
requestMessageUnlock(messageId, "voice");
}
function handleOpenImage(messageId: string): void {
router.push(buildChatImageOverlayUrl(messageId), { scroll: false });
}
function handleCloseImageViewer(): void {
router.replace(buildChatWithoutImageOverlayUrl(searchParams), {
scroll: false,
});
}
function handleUnlockImagePaywall(): void {
if (!imageMessageId) return;
requestImageUnlock(imageMessageId, "image");
}
return (
<MobileShell>
<div className={styles.shell}>
@@ -110,6 +177,7 @@ export function ChatScreen() {
unlockingMessageId={state.unlockingMessageId}
onUnlockPrivateMessage={handleUnlockPrivateMessage}
onUnlockVoiceMessage={handleUnlockVoiceMessage}
onOpenImage={handleOpenImage}
/>
{messageLimitBanner.visible ? (
@@ -141,6 +209,27 @@ export function ChatScreen() {
onCloseInsufficientCreditsDialog={closeInsufficientCreditsDialog}
onConfirmInsufficientCreditsDialog={confirmInsufficientCreditsDialog}
/>
<ChatUnlockDialogs
unlockPaywallRequest={imageUnlockPaywallRequest}
onCloseInsufficientCreditsDialog={closeImageInsufficientCreditsDialog}
onConfirmInsufficientCreditsDialog={
confirmImageInsufficientCreditsDialog
}
/>
{selectedImageMessage?.imageUrl ? (
<FullscreenImageViewer
messageId={selectedImageMessage.id}
imageUrl={selectedImageMessage.imageUrl}
imagePaywalled={selectedImageMessage.imagePaywalled === true}
isUnlockingImagePaywall={
state.isUnlockingMessage &&
state.unlockingMessageId === selectedImageMessage.id
}
onUnlockImagePaywall={handleUnlockImagePaywall}
onClose={handleCloseImageViewer}
/>
) : null}
</div>
</MobileShell>
);