diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx
index b74cd6ae..2e11e18b 100644
--- a/src/app/chat/chat-screen.tsx
+++ b/src/app/chat/chat-screen.tsx
@@ -49,12 +49,6 @@ export function ChatScreen() {
state.upgradePromptVisible && state.upgradeReason === "daily_limit";
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 chatPaywallSubscriptionUrl = ROUTE_BUILDERS.subscription("vip", {
returnTo: "chat",
@@ -129,7 +123,7 @@ export function ChatScreen() {
UrlLauncherUtil.openUrlWithExternalBrowser(ROUTES.splash);
}
- function handleUnlockPrivateMessage(): void {
+ function openChatPaywallSubscription(): void {
if (isGuest) {
router.push(ROUTE_BUILDERS.authWithRedirect(chatPaywallSubscriptionUrl));
return;
@@ -137,28 +131,20 @@ export function ChatScreen() {
router.push(chatPaywallSubscriptionUrl);
}
+ function handleUnlockPrivateMessage(): void {
+ openChatPaywallSubscription();
+ }
+
function handleUnlockImagePaywall(): void {
- if (isGuest) {
- router.push(ROUTE_BUILDERS.authWithRedirect(chatPaywallSubscriptionUrl));
- return;
- }
- router.push(chatPaywallSubscriptionUrl);
+ openChatPaywallSubscription();
}
function handleMessageLimitUnlock(): void {
- if (isGuest) {
- router.push(ROUTE_BUILDERS.authWithRedirect(chatPaywallSubscriptionUrl));
- return;
- }
- router.push(chatPaywallSubscriptionUrl);
+ openChatPaywallSubscription();
}
function handleUnlockVoiceMessage(): void {
- if (isGuest) {
- router.push(ROUTE_BUILDERS.authWithRedirect(chatPaywallSubscriptionUrl));
- return;
- }
- router.push(chatPaywallSubscriptionUrl);
+ openChatPaywallSubscription();
}
return (
@@ -183,19 +169,11 @@ export function ChatScreen() {
messages={state.messages}
isReplyingAI={state.isReplyingAI}
isGuest={isGuest}
- unlockingPrivateMessageId={state.unlockingPrivateMessageId}
onUnlockPrivateMessage={handleUnlockPrivateMessage}
onUnlockImagePaywall={handleUnlockImagePaywall}
onUnlockVoiceMessage={handleUnlockVoiceMessage}
/>
- {showPrivatePaywallBanner ? (
-
- ) : null}
-
{showMessageLimitBanner ? (
void;
+ onUnlockPrivateMessage?: () => void;
onUnlockImagePaywall?: () => void;
onUnlockVoiceMessage?: () => void;
}
@@ -35,7 +34,6 @@ export interface ChatAreaProps {
export function ChatArea({
messages,
isReplyingAI,
- unlockingPrivateMessageId,
onUnlockPrivateMessage,
onUnlockImagePaywall,
onUnlockVoiceMessage,
@@ -60,7 +58,6 @@ export function ChatArea({
{renderMessagesWithDateHeaders(
messages,
- unlockingPrivateMessageId,
onUnlockPrivateMessage,
onUnlockImagePaywall,
onUnlockVoiceMessage,
@@ -74,8 +71,7 @@ export function ChatArea({
/** 渲染消息列表(按日期分组插入分隔条) */
function renderMessagesWithDateHeaders(
messages: readonly UiMessage[],
- unlockingPrivateMessageId?: string | null,
- onUnlockPrivateMessage?: (messageId: string) => void,
+ onUnlockPrivateMessage?: () => void,
onUnlockImagePaywall?: () => void,
onUnlockVoiceMessage?: () => void,
) {
@@ -94,7 +90,6 @@ function renderMessagesWithDateHeaders(
) : (
void;
+ onUnlockPrivateMessage?: () => void;
onUnlockImagePaywall?: () => void;
onUnlockVoiceMessage?: () => void;
}
export function MessageBubble({
- id,
content,
imageUrl,
imagePaywalled,
@@ -42,7 +39,6 @@ export function MessageBubble({
lockReason,
lockedPrivate,
privateMessageHint,
- isUnlockingPrivate,
onUnlockPrivateMessage,
onUnlockImagePaywall,
onUnlockVoiceMessage,
@@ -55,7 +51,6 @@ export function MessageBubble({
{/* 占位 */}
void;
+ onUnlockPrivateMessage?: () => void;
onUnlockImagePaywall?: () => void;
onUnlockVoiceMessage?: () => void;
}
@@ -24,7 +22,6 @@ export interface MessageContentProps {
const IMAGE_PLACEHOLDER = "[图片]";
export function MessageContent({
- messageId,
content,
imageUrl,
imagePaywalled,
@@ -34,7 +31,6 @@ export function MessageContent({
lockReason,
lockedPrivate,
privateMessageHint,
- isUnlockingPrivate = false,
onUnlockPrivateMessage,
onUnlockImagePaywall,
onUnlockVoiceMessage,
@@ -63,12 +59,8 @@ export function MessageContent({
{isLockedPrivateMessage ? (
onUnlockPrivateMessage?.(messageId)
- : undefined
- }
+ isUnlocking={false}
+ onUnlock={onUnlockPrivateMessage}
/>
) : (
<>
@@ -85,7 +77,7 @@ export function MessageContent({
isFromAI={isFromAI}
locked={isLockedVoiceMessage}
hint={privateMessageHint}
- isUnlocking={isUnlockingPrivate}
+ isUnlocking={false}
onUnlock={
isLockedVoiceMessage
? onUnlockVoiceMessage
diff --git a/src/stores/chat/__tests__/chat-machine.helpers.test.ts b/src/stores/chat/__tests__/chat-machine.helpers.test.ts
index e7f3655c..296f8ada 100644
--- a/src/stores/chat/__tests__/chat-machine.helpers.test.ts
+++ b/src/stores/chat/__tests__/chat-machine.helpers.test.ts
@@ -39,7 +39,6 @@ function makeChatState(overrides: Partial = {}): ChatState {
upgradeReason: null,
upgradeHint: null,
upgradeDetail: null,
- unlockingPrivateMessageId: null,
isLoadingMore: false,
hasMore: true,
historyOffset: 0,
diff --git a/src/stores/chat/__tests__/chat-machine.transitions.test.ts b/src/stores/chat/__tests__/chat-machine.transitions.test.ts
index 5c63db98..4aeb7666 100644
--- a/src/stores/chat/__tests__/chat-machine.transitions.test.ts
+++ b/src/stores/chat/__tests__/chat-machine.transitions.test.ts
@@ -3,7 +3,6 @@ import { createActor, fromCallback, fromPromise, waitFor } from "xstate";
import {
ChatSendResponse,
- UnlockPrivateResponse,
type UiMessage,
} from "@/data/dto/chat";
import { chatMachine } from "@/stores/chat/chat-machine";
@@ -29,11 +28,6 @@ interface SendMessageHttpOutput {
reply: UiMessage | null;
}
-interface UnlockPrivateOutput {
- messageId: string;
- response: UnlockPrivateResponse;
-}
-
function makeChatSendResponse(): ChatSendResponse {
return ChatSendResponse.from({
reply: "",
@@ -56,7 +50,6 @@ function makeChatSendResponse(): ChatSendResponse {
function createTestChatMachine(
options: {
historyMessages?: UiMessage[];
- unlockedContent?: string;
} = {},
) {
return chatMachine.provide({
@@ -97,21 +90,6 @@ function createTestChatMachine(
});
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();
});
- 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 () => {
const actor = createActor(createTestChatMachine()).start();
@@ -278,21 +213,6 @@ describe("chatMachine transitions", () => {
});
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();
diff --git a/src/stores/chat/chat-context.tsx b/src/stores/chat/chat-context.tsx
index 114595ab..c66fe1b7 100644
--- a/src/stores/chat/chat-context.tsx
+++ b/src/stores/chat/chat-context.tsx
@@ -25,7 +25,6 @@ interface ChatState {
upgradeReason: MachineContext["upgradeReason"];
upgradeHint: MachineContext["upgradeHint"];
upgradeDetail: MachineContext["upgradeDetail"];
- unlockingPrivateMessageId: MachineContext["unlockingPrivateMessageId"];
isLoadingMore: boolean;
hasMore: boolean;
historyOffset: number;
@@ -52,7 +51,6 @@ export function ChatProvider({ children }: ChatProviderProps) {
upgradeReason: state.context.upgradeReason,
upgradeHint: state.context.upgradeHint,
upgradeDetail: state.context.upgradeDetail,
- unlockingPrivateMessageId: state.context.unlockingPrivateMessageId,
isLoadingMore: state.context.isLoadingMore,
hasMore: state.context.hasMore,
historyOffset: state.context.historyOffset,
diff --git a/src/stores/chat/chat-events.ts b/src/stores/chat/chat-events.ts
index 9a1dd65b..0d20a311 100644
--- a/src/stores/chat/chat-events.ts
+++ b/src/stores/chat/chat-events.ts
@@ -8,8 +8,8 @@
* chat 机器不感知鉴权。
*
* 鉴权生命周期事件(本轮新增):
- * - `ChatGuestLogin`:游客进入 /chat —— 拉服务器端首屏,不连 WS
- * - `ChatUserLogin`:其他登录用户进入 /chat —— 拉服务器端首屏,不连 WS
+ * - `ChatGuestLogin`:游客进入 /chat —— 拉服务器端首屏
+ * - `ChatUserLogin`:其他登录用户进入 /chat —— 拉服务器端首屏
* - `ChatLogout`:正式登录用户登出 —— 清消息
*
* 设计:所有历史 / 配额相关副作用全部通过派发事件给 chat 机器处理,
@@ -23,7 +23,6 @@ export type ChatEvent =
// 业务事件
| { type: "ChatSendMessage"; content: string }
| { type: "ChatSendImage"; imageBase64: string }
- | { type: "ChatUnlockPrivateMessage"; messageId: string }
| { type: "ChatLoadMoreHistory" }
| { type: "ChatQueuedSendStarted" }
| {
diff --git a/src/stores/chat/chat-machine.actors.ts b/src/stores/chat/chat-machine.actors.ts
index 944ed397..e88419e4 100644
--- a/src/stores/chat/chat-machine.actors.ts
+++ b/src/stores/chat/chat-machine.actors.ts
@@ -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(
({ sendBack, receive }) => {
return createMessageQueueActor(sendBack, receive);
diff --git a/src/stores/chat/chat-machine.ts b/src/stores/chat/chat-machine.ts
index c6b5c744..c37c4c6e 100644
--- a/src/stores/chat/chat-machine.ts
+++ b/src/stores/chat/chat-machine.ts
@@ -2,10 +2,10 @@
* Chat 状态机(XState v5)
*
* 鉴权解耦(事件驱动):
- * chat 机器不感知鉴权 / 不管 WebSocket —— 由 派生 loginStatus
+ * chat 机器不感知鉴权 —— 由 派生 loginStatus
* ChatAuthSync 派发登录态生命周期事件:
- * - `ChatGuestLogin` → 游客会话(断 WS = 不连)
- * - `ChatUserLogin { token }` → 其他登录用户会话(不连 WS)
+ * - `ChatGuestLogin` → 游客会话
+ * - `ChatUserLogin { token }` → 其他登录用户会话
* - `ChatLogout` → 正式登录用户登出
*
* 登录态流转约束:
@@ -15,8 +15,8 @@
*
* 状态结构(parent state 模式):
* - `idle`:屏没挂 / 登出
- * - `guestSession`(parent):游客会话 —— 不 invoke WS
- * - `userSession`(parent):其他登录用户会话 —— 不 invoke WS
+ * - `guestSession`(parent):游客会话
+ * - `userSession`(parent):其他登录用户会话
*
* init 任务:
* - guestSession.initializing:loadHistory
@@ -50,7 +50,6 @@ import {
sendMessageHttpActor,
loadMoreHistoryActor,
httpMessageQueueActor,
- unlockPrivateMessageActor,
} from "./chat-machine.actors";
const log = new Logger("StoresChatChatMachine");
@@ -73,7 +72,6 @@ export const chatMachine = setup({
sendMessageHttp: sendMessageHttpActor,
loadMoreHistory: loadMoreHistoryActor,
httpMessageQueue: httpMessageQueueActor,
- unlockPrivateMessage: unlockPrivateMessageActor,
},
actions: {
enqueueMessage: sendTo("messageQueue", ({ event }) => event),
@@ -195,7 +193,7 @@ export const chatMachine = setup({
};
}),
- appendSocketErrorMessage: assign(({ context }) => {
+ appendQueuedSendErrorMessage: assign(({ context }) => {
const messages = [
...context.messages,
{
@@ -213,69 +211,6 @@ export const chatMachine = setup({
if (event.type !== "ChatQueuedHttpDone") return {};
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({
id: "chat",
@@ -312,7 +247,7 @@ export const chatMachine = setup({
actions: "applyQueuedHttpOutput",
},
ChatQueuedSendError: {
- actions: "appendSocketErrorMessage",
+ actions: "appendQueuedSendErrorMessage",
},
},
initial: "initializing",
@@ -354,11 +289,7 @@ export const chatMachine = setup({
actions: "appendGuestUserImage",
target: "sending",
},
- ChatUnlockPrivateMessage: {
- actions: "setUnlockingPrivateMessage",
- target: "unlockingPrivate",
- },
- // 删除 ChatLoadMoreHistory / WS handlers —— 游客无服务端 history,也不连接 WS
+ // 游客不支持翻页历史,发送消息统一走 HTTP 队列。
},
},
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",
},
ChatQueuedSendError: {
- actions: "appendSocketErrorMessage",
+ actions: "appendQueuedSendErrorMessage",
},
},
initial: "initializing",
@@ -460,10 +374,6 @@ export const chatMachine = setup({
ChatLoadMoreHistory: {
target: "loadingMore",
},
- ChatUnlockPrivateMessage: {
- actions: "setUnlockingPrivateMessage",
- target: "unlockingPrivate",
- },
},
},
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",
- },
- },
- },
},
},
},
diff --git a/src/stores/chat/chat-state.ts b/src/stores/chat/chat-state.ts
index 54f727e6..26efd731 100644
--- a/src/stores/chat/chat-state.ts
+++ b/src/stores/chat/chat-state.ts
@@ -5,7 +5,7 @@ export interface ChatState {
isReplyingAI: boolean;
pendingReplyCount: number;
upgradePromptVisible: boolean;
- upgradeReason: "daily_limit" | "private_message" | "image" | null;
+ upgradeReason: "daily_limit" | "image" | null;
upgradeHint: string | null;
upgradeDetail:
| {
@@ -15,7 +15,6 @@ export interface ChatState {
limit?: number;
}
| null;
- unlockingPrivateMessageId: string | null;
isLoadingMore: boolean;
hasMore: boolean;
historyOffset: number;
@@ -34,7 +33,6 @@ export const initialState: ChatState = {
upgradeReason: null,
upgradeHint: null,
upgradeDetail: null,
- unlockingPrivateMessageId: null,
isLoadingMore: false,
hasMore: true,
historyOffset: 0,