refactor(chat): remove private unlock machine flow

This commit is contained in:
2026-06-26 19:17:29 +08:00
parent b6f18a1ef3
commit 1b73c3ac10
11 changed files with 27 additions and 302 deletions
+8 -30
View File
@@ -49,12 +49,6 @@ export function ChatScreen() {
state.upgradePromptVisible && state.upgradeReason === "daily_limit"; state.upgradePromptVisible && state.upgradeReason === "daily_limit";
const messageLimitTitle = "The limit for free chat times\nhas been reached"; const messageLimitTitle = "The limit for free chat times\nhas been reached";
const showPrivatePaywallBanner =
state.upgradePromptVisible && state.upgradeReason === "private_message";
const inlineUpgradeTitle = "Unlock VIP to view private messages";
const inlineUpgradeCta = "Activate VIP to view private messages";
const externalBrowserPromptShownRef = useRef(false); const externalBrowserPromptShownRef = useRef(false);
const chatPaywallSubscriptionUrl = ROUTE_BUILDERS.subscription("vip", { const chatPaywallSubscriptionUrl = ROUTE_BUILDERS.subscription("vip", {
returnTo: "chat", returnTo: "chat",
@@ -129,7 +123,7 @@ export function ChatScreen() {
UrlLauncherUtil.openUrlWithExternalBrowser(ROUTES.splash); UrlLauncherUtil.openUrlWithExternalBrowser(ROUTES.splash);
} }
function handleUnlockPrivateMessage(): void { function openChatPaywallSubscription(): void {
if (isGuest) { if (isGuest) {
router.push(ROUTE_BUILDERS.authWithRedirect(chatPaywallSubscriptionUrl)); router.push(ROUTE_BUILDERS.authWithRedirect(chatPaywallSubscriptionUrl));
return; return;
@@ -137,28 +131,20 @@ export function ChatScreen() {
router.push(chatPaywallSubscriptionUrl); router.push(chatPaywallSubscriptionUrl);
} }
function handleUnlockPrivateMessage(): void {
openChatPaywallSubscription();
}
function handleUnlockImagePaywall(): void { function handleUnlockImagePaywall(): void {
if (isGuest) { openChatPaywallSubscription();
router.push(ROUTE_BUILDERS.authWithRedirect(chatPaywallSubscriptionUrl));
return;
}
router.push(chatPaywallSubscriptionUrl);
} }
function handleMessageLimitUnlock(): void { function handleMessageLimitUnlock(): void {
if (isGuest) { openChatPaywallSubscription();
router.push(ROUTE_BUILDERS.authWithRedirect(chatPaywallSubscriptionUrl));
return;
}
router.push(chatPaywallSubscriptionUrl);
} }
function handleUnlockVoiceMessage(): void { function handleUnlockVoiceMessage(): void {
if (isGuest) { openChatPaywallSubscription();
router.push(ROUTE_BUILDERS.authWithRedirect(chatPaywallSubscriptionUrl));
return;
}
router.push(chatPaywallSubscriptionUrl);
} }
return ( return (
@@ -183,19 +169,11 @@ export function ChatScreen() {
messages={state.messages} messages={state.messages}
isReplyingAI={state.isReplyingAI} isReplyingAI={state.isReplyingAI}
isGuest={isGuest} isGuest={isGuest}
unlockingPrivateMessageId={state.unlockingPrivateMessageId}
onUnlockPrivateMessage={handleUnlockPrivateMessage} onUnlockPrivateMessage={handleUnlockPrivateMessage}
onUnlockImagePaywall={handleUnlockImagePaywall} onUnlockImagePaywall={handleUnlockImagePaywall}
onUnlockVoiceMessage={handleUnlockVoiceMessage} onUnlockVoiceMessage={handleUnlockVoiceMessage}
/> />
{showPrivatePaywallBanner ? (
<ChatQuotaExhaustedBanner
title={inlineUpgradeTitle}
ctaLabel={inlineUpgradeCta}
/>
) : null}
{showMessageLimitBanner ? ( {showMessageLimitBanner ? (
<ChatQuotaExhaustedBanner <ChatQuotaExhaustedBanner
title={messageLimitTitle} title={messageLimitTitle}
+2 -11
View File
@@ -26,8 +26,7 @@ export interface ChatAreaProps {
messages: readonly UiMessage[]; messages: readonly UiMessage[];
isReplyingAI: boolean; isReplyingAI: boolean;
isGuest: boolean; isGuest: boolean;
unlockingPrivateMessageId?: string | null; onUnlockPrivateMessage?: () => void;
onUnlockPrivateMessage?: (messageId: string) => void;
onUnlockImagePaywall?: () => void; onUnlockImagePaywall?: () => void;
onUnlockVoiceMessage?: () => void; onUnlockVoiceMessage?: () => void;
} }
@@ -35,7 +34,6 @@ export interface ChatAreaProps {
export function ChatArea({ export function ChatArea({
messages, messages,
isReplyingAI, isReplyingAI,
unlockingPrivateMessageId,
onUnlockPrivateMessage, onUnlockPrivateMessage,
onUnlockImagePaywall, onUnlockImagePaywall,
onUnlockVoiceMessage, onUnlockVoiceMessage,
@@ -60,7 +58,6 @@ export function ChatArea({
{renderMessagesWithDateHeaders( {renderMessagesWithDateHeaders(
messages, messages,
unlockingPrivateMessageId,
onUnlockPrivateMessage, onUnlockPrivateMessage,
onUnlockImagePaywall, onUnlockImagePaywall,
onUnlockVoiceMessage, onUnlockVoiceMessage,
@@ -74,8 +71,7 @@ export function ChatArea({
/** 渲染消息列表(按日期分组插入分隔条) */ /** 渲染消息列表(按日期分组插入分隔条) */
function renderMessagesWithDateHeaders( function renderMessagesWithDateHeaders(
messages: readonly UiMessage[], messages: readonly UiMessage[],
unlockingPrivateMessageId?: string | null, onUnlockPrivateMessage?: () => void,
onUnlockPrivateMessage?: (messageId: string) => void,
onUnlockImagePaywall?: () => void, onUnlockImagePaywall?: () => void,
onUnlockVoiceMessage?: () => void, onUnlockVoiceMessage?: () => void,
) { ) {
@@ -94,7 +90,6 @@ function renderMessagesWithDateHeaders(
) : ( ) : (
<MessageBubble <MessageBubble
key={item.key} key={item.key}
id={item.message.id}
content={item.message.content} content={item.message.content}
imageUrl={item.message.imageUrl} imageUrl={item.message.imageUrl}
imagePaywalled={item.message.imagePaywalled} imagePaywalled={item.message.imagePaywalled}
@@ -104,10 +99,6 @@ function renderMessagesWithDateHeaders(
lockReason={item.message.lockReason} lockReason={item.message.lockReason}
lockedPrivate={item.message.lockedPrivate} lockedPrivate={item.message.lockedPrivate}
privateMessageHint={item.message.privateMessageHint} privateMessageHint={item.message.privateMessageHint}
isUnlockingPrivate={
item.message.id != null &&
item.message.id === unlockingPrivateMessageId
}
onUnlockPrivateMessage={onUnlockPrivateMessage} onUnlockPrivateMessage={onUnlockPrivateMessage}
onUnlockImagePaywall={onUnlockImagePaywall} onUnlockImagePaywall={onUnlockImagePaywall}
onUnlockVoiceMessage={onUnlockVoiceMessage} onUnlockVoiceMessage={onUnlockVoiceMessage}
+1 -9
View File
@@ -15,7 +15,6 @@ import { MessageContent } from "./message-content";
import styles from "./chat-area.module.css"; import styles from "./chat-area.module.css";
export interface MessageBubbleProps { export interface MessageBubbleProps {
id?: string;
content: string; content: string;
imageUrl?: string | null; imageUrl?: string | null;
imagePaywalled?: boolean; imagePaywalled?: boolean;
@@ -25,14 +24,12 @@ export interface MessageBubbleProps {
lockReason?: string | null; lockReason?: string | null;
lockedPrivate?: boolean | null; lockedPrivate?: boolean | null;
privateMessageHint?: string | null; privateMessageHint?: string | null;
isUnlockingPrivate?: boolean; onUnlockPrivateMessage?: () => void;
onUnlockPrivateMessage?: (messageId: string) => void;
onUnlockImagePaywall?: () => void; onUnlockImagePaywall?: () => void;
onUnlockVoiceMessage?: () => void; onUnlockVoiceMessage?: () => void;
} }
export function MessageBubble({ export function MessageBubble({
id,
content, content,
imageUrl, imageUrl,
imagePaywalled, imagePaywalled,
@@ -42,7 +39,6 @@ export function MessageBubble({
lockReason, lockReason,
lockedPrivate, lockedPrivate,
privateMessageHint, privateMessageHint,
isUnlockingPrivate,
onUnlockPrivateMessage, onUnlockPrivateMessage,
onUnlockImagePaywall, onUnlockImagePaywall,
onUnlockVoiceMessage, onUnlockVoiceMessage,
@@ -55,7 +51,6 @@ export function MessageBubble({
<MessageAvatar isFromAI={true} /> <MessageAvatar isFromAI={true} />
<div style={{ width: 8 }} /> <div style={{ width: 8 }} />
<MessageContent <MessageContent
messageId={id}
content={content} content={content}
imageUrl={imageUrl} imageUrl={imageUrl}
imagePaywalled={imagePaywalled} imagePaywalled={imagePaywalled}
@@ -65,7 +60,6 @@ export function MessageBubble({
lockReason={lockReason} lockReason={lockReason}
lockedPrivate={lockedPrivate} lockedPrivate={lockedPrivate}
privateMessageHint={privateMessageHint} privateMessageHint={privateMessageHint}
isUnlockingPrivate={isUnlockingPrivate}
onUnlockPrivateMessage={onUnlockPrivateMessage} onUnlockPrivateMessage={onUnlockPrivateMessage}
onUnlockImagePaywall={onUnlockImagePaywall} onUnlockImagePaywall={onUnlockImagePaywall}
onUnlockVoiceMessage={onUnlockVoiceMessage} onUnlockVoiceMessage={onUnlockVoiceMessage}
@@ -79,7 +73,6 @@ export function MessageBubble({
<div className={styles.bubbleRowUser} aria-label="User message"> <div className={styles.bubbleRowUser} aria-label="User message">
<div style={{ width: 43 }} /> {/* 占位 */} <div style={{ width: 43 }} /> {/* 占位 */}
<MessageContent <MessageContent
messageId={id}
content={content} content={content}
imageUrl={imageUrl} imageUrl={imageUrl}
imagePaywalled={imagePaywalled} imagePaywalled={imagePaywalled}
@@ -89,7 +82,6 @@ export function MessageBubble({
lockReason={lockReason} lockReason={lockReason}
lockedPrivate={lockedPrivate} lockedPrivate={lockedPrivate}
privateMessageHint={privateMessageHint} privateMessageHint={privateMessageHint}
isUnlockingPrivate={isUnlockingPrivate}
onUnlockPrivateMessage={onUnlockPrivateMessage} onUnlockPrivateMessage={onUnlockPrivateMessage}
onUnlockImagePaywall={onUnlockImagePaywall} onUnlockImagePaywall={onUnlockImagePaywall}
onUnlockVoiceMessage={onUnlockVoiceMessage} onUnlockVoiceMessage={onUnlockVoiceMessage}
+4 -12
View File
@@ -5,7 +5,6 @@ import { TextBubble } from "./text-bubble";
import { VoiceBubble } from "./voice-bubble"; import { VoiceBubble } from "./voice-bubble";
export interface MessageContentProps { export interface MessageContentProps {
messageId?: string;
content: string; content: string;
imageUrl?: string | null; imageUrl?: string | null;
imagePaywalled?: boolean; imagePaywalled?: boolean;
@@ -15,8 +14,7 @@ export interface MessageContentProps {
lockReason?: string | null; lockReason?: string | null;
lockedPrivate?: boolean | null; lockedPrivate?: boolean | null;
privateMessageHint?: string | null; privateMessageHint?: string | null;
isUnlockingPrivate?: boolean; onUnlockPrivateMessage?: () => void;
onUnlockPrivateMessage?: (messageId: string) => void;
onUnlockImagePaywall?: () => void; onUnlockImagePaywall?: () => void;
onUnlockVoiceMessage?: () => void; onUnlockVoiceMessage?: () => void;
} }
@@ -24,7 +22,6 @@ export interface MessageContentProps {
const IMAGE_PLACEHOLDER = "[图片]"; const IMAGE_PLACEHOLDER = "[图片]";
export function MessageContent({ export function MessageContent({
messageId,
content, content,
imageUrl, imageUrl,
imagePaywalled, imagePaywalled,
@@ -34,7 +31,6 @@ export function MessageContent({
lockReason, lockReason,
lockedPrivate, lockedPrivate,
privateMessageHint, privateMessageHint,
isUnlockingPrivate = false,
onUnlockPrivateMessage, onUnlockPrivateMessage,
onUnlockImagePaywall, onUnlockImagePaywall,
onUnlockVoiceMessage, onUnlockVoiceMessage,
@@ -63,12 +59,8 @@ export function MessageContent({
{isLockedPrivateMessage ? ( {isLockedPrivateMessage ? (
<PrivateMessageCard <PrivateMessageCard
hint={privateMessageHint} hint={privateMessageHint}
isUnlocking={isUnlockingPrivate} isUnlocking={false}
onUnlock={ onUnlock={onUnlockPrivateMessage}
messageId
? () => onUnlockPrivateMessage?.(messageId)
: undefined
}
/> />
) : ( ) : (
<> <>
@@ -85,7 +77,7 @@ export function MessageContent({
isFromAI={isFromAI} isFromAI={isFromAI}
locked={isLockedVoiceMessage} locked={isLockedVoiceMessage}
hint={privateMessageHint} hint={privateMessageHint}
isUnlocking={isUnlockingPrivate} isUnlocking={false}
onUnlock={ onUnlock={
isLockedVoiceMessage isLockedVoiceMessage
? onUnlockVoiceMessage ? onUnlockVoiceMessage
@@ -39,7 +39,6 @@ function makeChatState(overrides: Partial<ChatState> = {}): ChatState {
upgradeReason: null, upgradeReason: null,
upgradeHint: null, upgradeHint: null,
upgradeDetail: null, upgradeDetail: null,
unlockingPrivateMessageId: null,
isLoadingMore: false, isLoadingMore: false,
hasMore: true, hasMore: true,
historyOffset: 0, historyOffset: 0,
@@ -3,7 +3,6 @@ import { createActor, fromCallback, fromPromise, waitFor } from "xstate";
import { import {
ChatSendResponse, ChatSendResponse,
UnlockPrivateResponse,
type UiMessage, type UiMessage,
} from "@/data/dto/chat"; } from "@/data/dto/chat";
import { chatMachine } from "@/stores/chat/chat-machine"; import { chatMachine } from "@/stores/chat/chat-machine";
@@ -29,11 +28,6 @@ interface SendMessageHttpOutput {
reply: UiMessage | null; reply: UiMessage | null;
} }
interface UnlockPrivateOutput {
messageId: string;
response: UnlockPrivateResponse;
}
function makeChatSendResponse(): ChatSendResponse { function makeChatSendResponse(): ChatSendResponse {
return ChatSendResponse.from({ return ChatSendResponse.from({
reply: "", reply: "",
@@ -56,7 +50,6 @@ function makeChatSendResponse(): ChatSendResponse {
function createTestChatMachine( function createTestChatMachine(
options: { options: {
historyMessages?: UiMessage[]; historyMessages?: UiMessage[];
unlockedContent?: string;
} = {}, } = {},
) { ) {
return chatMachine.provide({ return chatMachine.provide({
@@ -97,21 +90,6 @@ function createTestChatMachine(
}); });
return () => undefined; return () => undefined;
}), }),
unlockPrivateMessage: fromPromise<
UnlockPrivateOutput,
{ messageId: string }
>(async ({ input }) => ({
messageId: input.messageId,
response: UnlockPrivateResponse.from({
unlocked: true,
content: options.unlockedContent ?? "unlocked",
showUpgrade: false,
paywallTriggered: false,
privateFreeLimit: 0,
privateUsedToday: 0,
reason: "ok",
}),
})),
}, },
}); });
} }
@@ -167,49 +145,6 @@ describe("chatMachine transitions", () => {
actor.stop(); actor.stop();
}); });
it("unlocks a locked private message and clears unlocking state", async () => {
const actor = createActor(
createTestChatMachine({
historyMessages: [
{
id: "private-1",
content: "",
isFromAI: true,
date: "2026-06-25",
locked: true,
lockReason: "private_message",
isPrivate: true,
lockedPrivate: true,
privateMessageHint: "A private message is waiting.",
},
],
unlockedContent: "Here is the unlocked private message.",
}),
).start();
actor.send({ type: "ChatUserLogin", token: "token" });
await waitFor(actor, (snapshot) =>
snapshot.matches({ userSession: "ready" }),
);
actor.send({ type: "ChatUnlockPrivateMessage", messageId: "private-1" });
await waitFor(
actor,
(snapshot) =>
snapshot.context.messages[0]?.content ===
"Here is the unlocked private message.",
);
const [message] = actor.getSnapshot().context.messages;
expect(message.content).toBe("Here is the unlocked private message.");
expect(message.locked).toBe(false);
expect(message.lockedPrivate).toBe(false);
expect(message.privateMessageHint).toBeNull();
expect(actor.getSnapshot().context.unlockingPrivateMessageId).toBeNull();
actor.stop();
});
it("allows multiple messages to be queued without leaving ready state", async () => { it("allows multiple messages to be queued without leaving ready state", async () => {
const actor = createActor(createTestChatMachine()).start(); const actor = createActor(createTestChatMachine()).start();
@@ -278,21 +213,6 @@ describe("chatMachine transitions", () => {
}); });
return () => undefined; return () => undefined;
}), }),
unlockPrivateMessage: fromPromise<
UnlockPrivateOutput,
{ messageId: string }
>(async ({ input }) => ({
messageId: input.messageId,
response: UnlockPrivateResponse.from({
unlocked: true,
content: "unlocked",
showUpgrade: false,
paywallTriggered: false,
privateFreeLimit: 0,
privateUsedToday: 0,
reason: "ok",
}),
})),
}, },
}); });
const actor = createActor(machine).start(); const actor = createActor(machine).start();
-2
View File
@@ -25,7 +25,6 @@ interface ChatState {
upgradeReason: MachineContext["upgradeReason"]; upgradeReason: MachineContext["upgradeReason"];
upgradeHint: MachineContext["upgradeHint"]; upgradeHint: MachineContext["upgradeHint"];
upgradeDetail: MachineContext["upgradeDetail"]; upgradeDetail: MachineContext["upgradeDetail"];
unlockingPrivateMessageId: MachineContext["unlockingPrivateMessageId"];
isLoadingMore: boolean; isLoadingMore: boolean;
hasMore: boolean; hasMore: boolean;
historyOffset: number; historyOffset: number;
@@ -52,7 +51,6 @@ export function ChatProvider({ children }: ChatProviderProps) {
upgradeReason: state.context.upgradeReason, upgradeReason: state.context.upgradeReason,
upgradeHint: state.context.upgradeHint, upgradeHint: state.context.upgradeHint,
upgradeDetail: state.context.upgradeDetail, upgradeDetail: state.context.upgradeDetail,
unlockingPrivateMessageId: state.context.unlockingPrivateMessageId,
isLoadingMore: state.context.isLoadingMore, isLoadingMore: state.context.isLoadingMore,
hasMore: state.context.hasMore, hasMore: state.context.hasMore,
historyOffset: state.context.historyOffset, historyOffset: state.context.historyOffset,
+2 -3
View File
@@ -8,8 +8,8 @@
* chat 机器不感知鉴权。 * chat 机器不感知鉴权。
* *
* 鉴权生命周期事件(本轮新增): * 鉴权生命周期事件(本轮新增):
* - `ChatGuestLogin`:游客进入 /chat —— 拉服务器端首屏,不连 WS * - `ChatGuestLogin`:游客进入 /chat —— 拉服务器端首屏
* - `ChatUserLogin`:其他登录用户进入 /chat —— 拉服务器端首屏,不连 WS * - `ChatUserLogin`:其他登录用户进入 /chat —— 拉服务器端首屏
* - `ChatLogout`:正式登录用户登出 —— 清消息 * - `ChatLogout`:正式登录用户登出 —— 清消息
* *
* 设计:所有历史 / 配额相关副作用全部通过派发事件给 chat 机器处理, * 设计:所有历史 / 配额相关副作用全部通过派发事件给 chat 机器处理,
@@ -23,7 +23,6 @@ export type ChatEvent =
// 业务事件 // 业务事件
| { type: "ChatSendMessage"; content: string } | { type: "ChatSendMessage"; content: string }
| { type: "ChatSendImage"; imageBase64: string } | { type: "ChatSendImage"; imageBase64: string }
| { type: "ChatUnlockPrivateMessage"; messageId: string }
| { type: "ChatLoadMoreHistory" } | { type: "ChatLoadMoreHistory" }
| { type: "ChatQueuedSendStarted" } | { type: "ChatQueuedSendStarted" }
| { | {
-35
View File
@@ -73,41 +73,6 @@ export const loadMoreHistoryActor = fromPromise<
}; };
}); });
export const unlockPrivateMessageActor = fromPromise<
{
messageId: string;
response: import("@/data/dto/chat").UnlockPrivateResponse;
},
{ messageId: string }
>(async ({ input }) => {
const result = await chatRepo.unlockPrivateMessage(input.messageId);
if (Result.isErr(result)) {
log.error("[chat-machine] unlockPrivateMessageActor failed", {
messageId: input.messageId,
error: result.error,
});
throw result.error;
}
if (result.data.unlocked && result.data.content != null) {
const localResult = await chatRepo.markPrivateMessageUnlockedInLocal(
input.messageId,
result.data.content,
);
if (Result.isErr(localResult)) {
log.error("[chat-machine] unlockPrivateMessageActor local sync failed", {
messageId: input.messageId,
error: localResult.error,
});
}
}
return {
messageId: input.messageId,
response: result.data,
};
});
export const httpMessageQueueActor = fromCallback<ChatEvent>( export const httpMessageQueueActor = fromCallback<ChatEvent>(
({ sendBack, receive }) => { ({ sendBack, receive }) => {
return createMessageQueueActor(sendBack, receive); return createMessageQueueActor(sendBack, receive);
+9 -116
View File
@@ -2,10 +2,10 @@
* Chat 状态机(XState v5 * Chat 状态机(XState v5
* *
* 鉴权解耦(事件驱动): * 鉴权解耦(事件驱动):
* chat 机器不感知鉴权 / 不管 WebSocket —— 由 <ChatAuthSync /> 派生 loginStatus * chat 机器不感知鉴权 —— 由 <ChatAuthSync /> 派生 loginStatus
* ChatAuthSync 派发登录态生命周期事件: * ChatAuthSync 派发登录态生命周期事件:
* - `ChatGuestLogin` → 游客会话(断 WS = 不连) * - `ChatGuestLogin` → 游客会话
* - `ChatUserLogin { token }` → 其他登录用户会话(不连 WS * - `ChatUserLogin { token }` → 其他登录用户会话
* - `ChatLogout` → 正式登录用户登出 * - `ChatLogout` → 正式登录用户登出
* *
* 登录态流转约束: * 登录态流转约束:
@@ -15,8 +15,8 @@
* *
* 状态结构(parent state 模式): * 状态结构(parent state 模式):
* - `idle`:屏没挂 / 登出 * - `idle`:屏没挂 / 登出
* - `guestSession`parent):游客会话 —— 不 invoke WS * - `guestSession`parent):游客会话
* - `userSession`parent):其他登录用户会话 —— 不 invoke WS * - `userSession`parent):其他登录用户会话
* *
* init 任务: * init 任务:
* - guestSession.initializingloadHistory * - guestSession.initializingloadHistory
@@ -50,7 +50,6 @@ import {
sendMessageHttpActor, sendMessageHttpActor,
loadMoreHistoryActor, loadMoreHistoryActor,
httpMessageQueueActor, httpMessageQueueActor,
unlockPrivateMessageActor,
} from "./chat-machine.actors"; } from "./chat-machine.actors";
const log = new Logger("StoresChatChatMachine"); const log = new Logger("StoresChatChatMachine");
@@ -73,7 +72,6 @@ export const chatMachine = setup({
sendMessageHttp: sendMessageHttpActor, sendMessageHttp: sendMessageHttpActor,
loadMoreHistory: loadMoreHistoryActor, loadMoreHistory: loadMoreHistoryActor,
httpMessageQueue: httpMessageQueueActor, httpMessageQueue: httpMessageQueueActor,
unlockPrivateMessage: unlockPrivateMessageActor,
}, },
actions: { actions: {
enqueueMessage: sendTo("messageQueue", ({ event }) => event), enqueueMessage: sendTo("messageQueue", ({ event }) => event),
@@ -195,7 +193,7 @@ export const chatMachine = setup({
}; };
}), }),
appendSocketErrorMessage: assign(({ context }) => { appendQueuedSendErrorMessage: assign(({ context }) => {
const messages = [ const messages = [
...context.messages, ...context.messages,
{ {
@@ -213,69 +211,6 @@ export const chatMachine = setup({
if (event.type !== "ChatQueuedHttpDone") return {}; if (event.type !== "ChatQueuedHttpDone") return {};
return applyHttpSendOutput(context, event.output); return applyHttpSendOutput(context, event.output);
}), }),
setUnlockingPrivateMessage: assign(({ event }) => {
if (event.type !== "ChatUnlockPrivateMessage") return {};
return {
unlockingPrivateMessageId: event.messageId,
upgradePromptVisible: false,
upgradeReason: null,
upgradeHint: null,
upgradeDetail: null,
};
}),
applyUnlockPrivateOutput: assign(({ context, event }) => {
if (!("output" in event)) return {};
const output = event.output as unknown as {
messageId: string;
response: import("@/data/dto/chat").UnlockPrivateResponse;
};
const { messageId, response } = output;
if (response.unlocked && response.content != null) {
return {
messages: context.messages.map((message) =>
message.id === messageId
? {
...message,
content: response.content ?? message.content,
locked: false,
lockedPrivate: false,
privateMessageHint: null,
isPrivate: message.isPrivate ?? true,
}
: message,
),
unlockingPrivateMessageId: null,
upgradePromptVisible: false,
upgradeReason: null,
upgradeHint: null,
upgradeDetail: null,
};
}
if (response.showUpgrade) {
return {
unlockingPrivateMessageId: null,
upgradePromptVisible: true,
upgradeReason: "private_message",
upgradeHint: null,
upgradeDetail: {
usedToday: response.privateUsedToday,
limit: response.privateFreeLimit,
},
};
}
return {
unlockingPrivateMessageId: null,
};
}),
clearUnlockingPrivateMessage: assign({
unlockingPrivateMessageId: null,
}),
}, },
}).createMachine({ }).createMachine({
id: "chat", id: "chat",
@@ -312,7 +247,7 @@ export const chatMachine = setup({
actions: "applyQueuedHttpOutput", actions: "applyQueuedHttpOutput",
}, },
ChatQueuedSendError: { ChatQueuedSendError: {
actions: "appendSocketErrorMessage", actions: "appendQueuedSendErrorMessage",
}, },
}, },
initial: "initializing", initial: "initializing",
@@ -354,11 +289,7 @@ export const chatMachine = setup({
actions: "appendGuestUserImage", actions: "appendGuestUserImage",
target: "sending", target: "sending",
}, },
ChatUnlockPrivateMessage: { // 游客不支持翻页历史,发送消息统一走 HTTP 队列。
actions: "setUnlockingPrivateMessage",
target: "unlockingPrivate",
},
// 删除 ChatLoadMoreHistory / WS handlers —— 游客无服务端 history,也不连接 WS
}, },
}, },
sending: { sending: {
@@ -379,23 +310,6 @@ export const chatMachine = setup({
}, },
}, },
}, },
unlockingPrivate: {
invoke: {
src: "unlockPrivateMessage",
input: ({ event }) => ({
messageId:
event.type === "ChatUnlockPrivateMessage" ? event.messageId : "",
}),
onDone: {
target: "ready",
actions: "applyUnlockPrivateOutput",
},
onError: {
target: "ready",
actions: "clearUnlockingPrivateMessage",
},
},
},
}, },
}, },
@@ -421,7 +335,7 @@ export const chatMachine = setup({
actions: "applyQueuedHttpOutput", actions: "applyQueuedHttpOutput",
}, },
ChatQueuedSendError: { ChatQueuedSendError: {
actions: "appendSocketErrorMessage", actions: "appendQueuedSendErrorMessage",
}, },
}, },
initial: "initializing", initial: "initializing",
@@ -460,10 +374,6 @@ export const chatMachine = setup({
ChatLoadMoreHistory: { ChatLoadMoreHistory: {
target: "loadingMore", target: "loadingMore",
}, },
ChatUnlockPrivateMessage: {
actions: "setUnlockingPrivateMessage",
target: "unlockingPrivate",
},
}, },
}, },
sendingViaHttp: { sendingViaHttp: {
@@ -503,23 +413,6 @@ export const chatMachine = setup({
}, },
}, },
}, },
unlockingPrivate: {
invoke: {
src: "unlockPrivateMessage",
input: ({ event }) => ({
messageId:
event.type === "ChatUnlockPrivateMessage" ? event.messageId : "",
}),
onDone: {
target: "ready",
actions: "applyUnlockPrivateOutput",
},
onError: {
target: "ready",
actions: "clearUnlockingPrivateMessage",
},
},
},
}, },
}, },
}, },
+1 -3
View File
@@ -5,7 +5,7 @@ export interface ChatState {
isReplyingAI: boolean; isReplyingAI: boolean;
pendingReplyCount: number; pendingReplyCount: number;
upgradePromptVisible: boolean; upgradePromptVisible: boolean;
upgradeReason: "daily_limit" | "private_message" | "image" | null; upgradeReason: "daily_limit" | "image" | null;
upgradeHint: string | null; upgradeHint: string | null;
upgradeDetail: upgradeDetail:
| { | {
@@ -15,7 +15,6 @@ export interface ChatState {
limit?: number; limit?: number;
} }
| null; | null;
unlockingPrivateMessageId: string | null;
isLoadingMore: boolean; isLoadingMore: boolean;
hasMore: boolean; hasMore: boolean;
historyOffset: number; historyOffset: number;
@@ -34,7 +33,6 @@ export const initialState: ChatState = {
upgradeReason: null, upgradeReason: null,
upgradeHint: null, upgradeHint: null,
upgradeDetail: null, upgradeDetail: null,
unlockingPrivateMessageId: null,
isLoadingMore: false, isLoadingMore: false,
hasMore: true, hasMore: true,
historyOffset: 0, historyOffset: 0,