feat(chat): add promotional external entry flow
This commit is contained in:
@@ -34,6 +34,7 @@ import { useFirstRechargeOfferBanner } from "./hooks/use-first-recharge-offer-ba
|
||||
import { useChatUnlockNavigationFlow } from "./hooks/use-chat-unlock-navigation-flow";
|
||||
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 styles from "./components/chat-screen.module.css";
|
||||
|
||||
export function ChatScreen() {
|
||||
@@ -42,6 +43,10 @@ export function ChatScreen() {
|
||||
const state = useChatState();
|
||||
const authState = useAuthState();
|
||||
const authDispatch = useAuthDispatch();
|
||||
const isPromotionBootstrapReady = useChatPromotionBootstrap();
|
||||
const visibleMessages = isPromotionBootstrapReady
|
||||
? state.messages
|
||||
: state.historyMessages;
|
||||
const imageMessageId = getChatImageOverlayMessageId(searchParams);
|
||||
const imageReturnUrl = imageMessageId
|
||||
? buildChatImageOverlayUrl(imageMessageId)
|
||||
@@ -54,6 +59,8 @@ export function ChatScreen() {
|
||||
} = useChatUnlockNavigationFlow({
|
||||
returnUrl: ROUTES.chat,
|
||||
ignoredKind: "image",
|
||||
promotionScope: "exclude",
|
||||
enabled: isPromotionBootstrapReady,
|
||||
});
|
||||
const {
|
||||
unlockPaywallRequest: imageUnlockPaywallRequest,
|
||||
@@ -64,16 +71,28 @@ export function ChatScreen() {
|
||||
returnUrl: imageReturnUrl,
|
||||
expectedKind: "image",
|
||||
expectedMessageId: imageMessageId ?? undefined,
|
||||
enabled: imageMessageId !== null,
|
||||
promotionScope: "exclude",
|
||||
enabled: isPromotionBootstrapReady && imageMessageId !== null,
|
||||
});
|
||||
const {
|
||||
unlockPaywallRequest: promotionUnlockPaywallRequest,
|
||||
requestMessageUnlock: requestPromotionUnlock,
|
||||
closeInsufficientCreditsDialog: closePromotionInsufficientCreditsDialog,
|
||||
confirmInsufficientCreditsDialog:
|
||||
confirmPromotionInsufficientCreditsDialog,
|
||||
} = useChatUnlockNavigationFlow({
|
||||
returnUrl: ROUTES.chat,
|
||||
promotionScope: "only",
|
||||
enabled: isPromotionBootstrapReady && state.promotion !== null,
|
||||
});
|
||||
const selectedImageMessage = useMemo(
|
||||
() =>
|
||||
imageMessageId
|
||||
? state.messages.find(
|
||||
? visibleMessages.find(
|
||||
(item) => item.id === imageMessageId && item.imageUrl,
|
||||
) ?? null
|
||||
: null,
|
||||
[imageMessageId, state.messages],
|
||||
[imageMessageId, visibleMessages],
|
||||
);
|
||||
|
||||
// isGuest 派生(chat-screen 是唯一知道这个值的地方 —— chat 机器无 isGuest 字段)
|
||||
@@ -89,7 +108,8 @@ export function ChatScreen() {
|
||||
historyLoaded: state.historyLoaded,
|
||||
loginStatus: authState.loginStatus,
|
||||
});
|
||||
const shouldShowPwaInstall = state.historyLoaded && state.messages.length >= 10;
|
||||
const shouldShowPwaInstall =
|
||||
state.historyLoaded && state.historyMessages.length >= 10;
|
||||
const shouldShowBrowserHint = shouldStartExternalBrowserPrompt({
|
||||
hasInitialized: authState.hasInitialized,
|
||||
isLoading: authState.isLoading,
|
||||
@@ -119,13 +139,38 @@ export function ChatScreen() {
|
||||
]);
|
||||
|
||||
function handleUnlockPrivateMessage(messageId: string): void {
|
||||
if (requestPromotionMessageUnlock(messageId, "private")) return;
|
||||
requestMessageUnlock(messageId, "private");
|
||||
}
|
||||
|
||||
function handleUnlockVoiceMessage(messageId: string): void {
|
||||
if (requestPromotionMessageUnlock(messageId, "voice")) return;
|
||||
requestMessageUnlock(messageId, "voice");
|
||||
}
|
||||
|
||||
function handleUnlockImageMessage(messageId: string): void {
|
||||
if (requestPromotionMessageUnlock(messageId, "image")) return;
|
||||
requestMessageUnlock(messageId, "image");
|
||||
}
|
||||
|
||||
function requestPromotionMessageUnlock(
|
||||
messageId: string,
|
||||
kind: "private" | "voice" | "image",
|
||||
): boolean {
|
||||
const promotion = state.promotion;
|
||||
if (!promotion || promotion.message.id !== messageId) return false;
|
||||
|
||||
const temporaryMessageId = `promotion:${promotion.session.clientLockId}`;
|
||||
requestPromotionUnlock(messageId, kind, {
|
||||
remoteMessageId:
|
||||
messageId === temporaryMessageId ? undefined : messageId,
|
||||
lockType: promotion.session.lockType,
|
||||
clientLockId: promotion.session.clientLockId,
|
||||
promotion: promotion.session,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleOpenImage(messageId: string): void {
|
||||
router.push(buildChatImageOverlayUrl(messageId), { scroll: false });
|
||||
}
|
||||
@@ -169,12 +214,13 @@ export function ChatScreen() {
|
||||
/>
|
||||
|
||||
<ChatArea
|
||||
messages={state.messages}
|
||||
messages={visibleMessages}
|
||||
isReplyingAI={state.isReplyingAI}
|
||||
isUnlockingMessage={state.isUnlockingMessage}
|
||||
unlockingMessageId={state.unlockingMessageId}
|
||||
onUnlockPrivateMessage={handleUnlockPrivateMessage}
|
||||
onUnlockVoiceMessage={handleUnlockVoiceMessage}
|
||||
onUnlockImageMessage={handleUnlockImageMessage}
|
||||
onOpenImage={handleOpenImage}
|
||||
/>
|
||||
|
||||
@@ -201,8 +247,19 @@ export function ChatScreen() {
|
||||
onCloseInsufficientCreditsDialog={closeInsufficientCreditsDialog}
|
||||
onConfirmInsufficientCreditsDialog={confirmInsufficientCreditsDialog}
|
||||
/>
|
||||
<ChatUnlockDialogs
|
||||
unlockPaywallRequest={promotionUnlockPaywallRequest}
|
||||
includeHistoryUnlock={false}
|
||||
onCloseInsufficientCreditsDialog={
|
||||
closePromotionInsufficientCreditsDialog
|
||||
}
|
||||
onConfirmInsufficientCreditsDialog={
|
||||
confirmPromotionInsufficientCreditsDialog
|
||||
}
|
||||
/>
|
||||
<ChatUnlockDialogs
|
||||
unlockPaywallRequest={imageUnlockPaywallRequest}
|
||||
includeHistoryUnlock={false}
|
||||
onCloseInsufficientCreditsDialog={closeImageInsufficientCreditsDialog}
|
||||
onConfirmInsufficientCreditsDialog={
|
||||
confirmImageInsufficientCreditsDialog
|
||||
|
||||
Reference in New Issue
Block a user