refactor(chat): split machine by business flow
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
applyHistoryLoadedOutput,
|
||||
applyNetworkHistoryLoadedOutput,
|
||||
countLockedHistoryMessages,
|
||||
shouldPromptUnlockHistory,
|
||||
} from "../chat-machine.helpers";
|
||||
import { baseChatMachineSetup } from "./setup";
|
||||
|
||||
const applyLocalHistoryLoadedAction = baseChatMachineSetup.assign(
|
||||
({ event }) => {
|
||||
if (event.type !== "ChatLocalHistoryLoaded") return {};
|
||||
return applyHistoryLoadedOutput(event.output);
|
||||
},
|
||||
);
|
||||
|
||||
const applyNetworkHistoryLoadedAction = baseChatMachineSetup.assign(
|
||||
({ context, event }) => {
|
||||
if (event.type !== "ChatNetworkHistoryLoaded") return {};
|
||||
return applyNetworkHistoryLoadedOutput(context, event.output);
|
||||
},
|
||||
);
|
||||
|
||||
const applyNetworkHistoryLoadedAndClearPaymentAction =
|
||||
baseChatMachineSetup.assign(({ context, event }) => {
|
||||
if (event.type !== "ChatNetworkHistoryLoaded") return {};
|
||||
return {
|
||||
...applyNetworkHistoryLoadedOutput(context, event.output),
|
||||
paymentUnlockPending: false,
|
||||
};
|
||||
});
|
||||
|
||||
const showUnlockHistoryPromptFromNetworkAction = baseChatMachineSetup.assign(
|
||||
({ context, event }) => {
|
||||
if (event.type !== "ChatNetworkHistoryLoaded") return {};
|
||||
const nextHistory = applyNetworkHistoryLoadedOutput(context, event.output);
|
||||
return {
|
||||
...nextHistory,
|
||||
paymentUnlockPending: false,
|
||||
unlockHistoryPromptVisible: true,
|
||||
lockedHistoryCount: countLockedHistoryMessages(nextHistory.messages),
|
||||
unlockHistoryError: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const markHistoryLoadFailedAction = baseChatMachineSetup.assign({
|
||||
historyLoaded: true,
|
||||
});
|
||||
|
||||
export const historyMachineSetup = baseChatMachineSetup.extend({
|
||||
actions: {
|
||||
applyLocalHistoryLoaded: applyLocalHistoryLoadedAction,
|
||||
applyNetworkHistoryLoaded: applyNetworkHistoryLoadedAction,
|
||||
applyNetworkHistoryLoadedAndClearPayment:
|
||||
applyNetworkHistoryLoadedAndClearPaymentAction,
|
||||
showUnlockHistoryPromptFromNetwork:
|
||||
showUnlockHistoryPromptFromNetworkAction,
|
||||
markHistoryLoadFailed: markHistoryLoadFailedAction,
|
||||
},
|
||||
});
|
||||
|
||||
export const guestInitializingState = historyMachineSetup.createStateConfig({
|
||||
on: {
|
||||
ChatLocalHistoryLoaded: {
|
||||
target: "ready",
|
||||
actions: "applyLocalHistoryLoaded",
|
||||
},
|
||||
ChatNetworkHistoryLoaded: {
|
||||
target: "ready",
|
||||
actions: "applyNetworkHistoryLoaded",
|
||||
},
|
||||
ChatHistoryLoadFailed: {
|
||||
target: "ready",
|
||||
actions: "markHistoryLoadFailed",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const userInitializingState = historyMachineSetup.createStateConfig({
|
||||
on: {
|
||||
ChatLocalHistoryLoaded: {
|
||||
target: "ready",
|
||||
actions: "applyLocalHistoryLoaded",
|
||||
},
|
||||
ChatNetworkHistoryLoaded: [
|
||||
{
|
||||
guard: ({ context, event }) =>
|
||||
event.type === "ChatNetworkHistoryLoaded" &&
|
||||
context.paymentUnlockPending &&
|
||||
shouldPromptUnlockHistory(event.output.messages),
|
||||
target: "ready",
|
||||
actions: "showUnlockHistoryPromptFromNetwork",
|
||||
},
|
||||
{
|
||||
target: "ready",
|
||||
actions: "applyNetworkHistoryLoadedAndClearPayment",
|
||||
},
|
||||
],
|
||||
ChatHistoryLoadFailed: {
|
||||
target: "ready",
|
||||
actions: "markHistoryLoadFailed",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,233 @@
|
||||
import { type DoneActorEvent, type ErrorActorEvent } from "xstate";
|
||||
|
||||
import { todayString } from "@/utils/date";
|
||||
import { Logger } from "@/utils/logger";
|
||||
|
||||
import {
|
||||
applyHttpSendOutput,
|
||||
beginPendingReply,
|
||||
finishPendingReply,
|
||||
type HttpSendOutput,
|
||||
} from "../chat-machine.helpers";
|
||||
import { historyMachineSetup } from "./history-flow";
|
||||
import { createChatActorActionSetup } from "./setup";
|
||||
|
||||
const sendLog = new Logger("StoresChatChatSendFlow");
|
||||
const mediaLog = new Logger("StoresChatChatMediaFlow");
|
||||
|
||||
const enqueueMessageAction = historyMachineSetup.sendTo(
|
||||
"messageQueue",
|
||||
({ event }) => event,
|
||||
);
|
||||
|
||||
const appendGuestUserMessageAction = historyMachineSetup.assign(
|
||||
({ context, event }) => {
|
||||
if (event.type !== "ChatSendMessage") return {};
|
||||
|
||||
const today = todayString();
|
||||
sendLog.debug("[chat-machine] appendGuestUserMessage", {
|
||||
contentLength: event.content.length,
|
||||
contentPreview: event.content.slice(0, 50),
|
||||
quotaMode: "server controlled",
|
||||
isReplyingAI: context.isReplyingAI,
|
||||
});
|
||||
|
||||
return {
|
||||
messages: [
|
||||
...context.messages,
|
||||
{
|
||||
content: event.content,
|
||||
isFromAI: false,
|
||||
date: today,
|
||||
},
|
||||
],
|
||||
upgradePromptVisible: false,
|
||||
upgradeReason: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const appendUserMessageAction = historyMachineSetup.assign(
|
||||
({ context, event }) => {
|
||||
if (event.type !== "ChatSendMessage") return {};
|
||||
|
||||
const today = todayString();
|
||||
sendLog.debug("[chat-machine] appendUserMessage", {
|
||||
contentLength: event.content.length,
|
||||
contentPreview: event.content.slice(0, 50),
|
||||
isReplyingAI: context.isReplyingAI,
|
||||
});
|
||||
|
||||
return {
|
||||
messages: [
|
||||
...context.messages,
|
||||
{
|
||||
content: event.content,
|
||||
isFromAI: false,
|
||||
date: today,
|
||||
},
|
||||
],
|
||||
upgradePromptVisible: false,
|
||||
upgradeReason: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const appendQueuedSendErrorMessageAction = historyMachineSetup.assign(
|
||||
({ context }) => {
|
||||
const messages = [
|
||||
...context.messages,
|
||||
{
|
||||
content: "Something went wrong. Try sending again?",
|
||||
isFromAI: true,
|
||||
date: todayString(),
|
||||
},
|
||||
];
|
||||
return { messages, ...finishPendingReply(context) };
|
||||
},
|
||||
);
|
||||
|
||||
const markQueuedSendStartedAction = historyMachineSetup.assign(
|
||||
({ context }) => beginPendingReply(context),
|
||||
);
|
||||
|
||||
const applyQueuedHttpOutputAction = historyMachineSetup.assign(
|
||||
({ context, event }) => {
|
||||
if (event.type !== "ChatQueuedHttpDone") return {};
|
||||
return applyHttpSendOutput(context, event.output);
|
||||
},
|
||||
);
|
||||
|
||||
const clearUpgradePromptAction = historyMachineSetup.assign(() => ({
|
||||
upgradePromptVisible: false,
|
||||
upgradeReason: null,
|
||||
canSendMessage: true,
|
||||
requiredCredits: 0,
|
||||
shortfallCredits: 0,
|
||||
}));
|
||||
|
||||
const appendGuestUserImageAction = historyMachineSetup.assign(
|
||||
({ context, event }) => {
|
||||
if (event.type !== "ChatSendImage") return {};
|
||||
|
||||
const today = todayString();
|
||||
mediaLog.debug("[chat-machine] appendGuestUserImage", {
|
||||
oldMessagesCount: context.messages.length,
|
||||
quotaMode: "server controlled",
|
||||
});
|
||||
return {
|
||||
messages: [
|
||||
...context.messages,
|
||||
{
|
||||
content: "[Image]",
|
||||
isFromAI: false,
|
||||
date: today,
|
||||
imageUrl: event.imageBase64,
|
||||
},
|
||||
],
|
||||
...beginPendingReply(context),
|
||||
upgradePromptVisible: false,
|
||||
upgradeReason: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const appendUserImageAction = historyMachineSetup.assign(
|
||||
({ context, event }) => {
|
||||
if (event.type !== "ChatSendImage") return {};
|
||||
|
||||
const today = todayString();
|
||||
mediaLog.debug("[chat-machine] appendUserImage", {
|
||||
oldMessagesCount: context.messages.length,
|
||||
});
|
||||
|
||||
return {
|
||||
messages: [
|
||||
...context.messages,
|
||||
{
|
||||
content: "[Image]",
|
||||
isFromAI: false,
|
||||
date: today,
|
||||
imageUrl: event.imageBase64,
|
||||
},
|
||||
],
|
||||
...beginPendingReply(context),
|
||||
upgradePromptVisible: false,
|
||||
upgradeReason: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const sendMachineSetup = historyMachineSetup.extend({
|
||||
actions: {
|
||||
enqueueMessage: enqueueMessageAction,
|
||||
appendGuestUserMessage: appendGuestUserMessageAction,
|
||||
appendUserMessage: appendUserMessageAction,
|
||||
appendQueuedSendErrorMessage: appendQueuedSendErrorMessageAction,
|
||||
markQueuedSendStarted: markQueuedSendStartedAction,
|
||||
applyQueuedHttpOutput: applyQueuedHttpOutputAction,
|
||||
clearUpgradePrompt: clearUpgradePromptAction,
|
||||
appendGuestUserImage: appendGuestUserImageAction,
|
||||
appendUserImage: appendUserImageAction,
|
||||
},
|
||||
});
|
||||
|
||||
const sendMessageDoneActionSetup =
|
||||
createChatActorActionSetup<DoneActorEvent<HttpSendOutput>>();
|
||||
const actorErrorActionSetup = createChatActorActionSetup<ErrorActorEvent>();
|
||||
|
||||
const applyHttpSendOutputAction = sendMessageDoneActionSetup.assign(
|
||||
({ context, event }) => applyHttpSendOutput(context, event.output),
|
||||
);
|
||||
|
||||
const markHttpSendFailedAction = actorErrorActionSetup.assign({
|
||||
isReplyingAI: false,
|
||||
});
|
||||
|
||||
export const guestReadyState = sendMachineSetup.createStateConfig({
|
||||
on: {
|
||||
ChatSendMessage: {
|
||||
actions: ["appendGuestUserMessage", "enqueueMessage"],
|
||||
guard: ({ context, event }) =>
|
||||
context.canSendMessage && event.content.trim().length > 0,
|
||||
},
|
||||
ChatSendImage: {
|
||||
actions: "appendGuestUserImage",
|
||||
target: "sending",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const guestSendingState = sendMachineSetup.createStateConfig({
|
||||
invoke: {
|
||||
src: "sendMessageHttp",
|
||||
input: ({ event }) => ({
|
||||
content: event.type === "ChatSendMessage" ? event.content : "",
|
||||
}),
|
||||
onDone: {
|
||||
target: "ready",
|
||||
actions: applyHttpSendOutputAction,
|
||||
},
|
||||
onError: {
|
||||
target: "ready",
|
||||
actions: markHttpSendFailedAction,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const userSendingViaHttpState = sendMachineSetup.createStateConfig({
|
||||
invoke: {
|
||||
src: "sendMessageHttp",
|
||||
input: ({ event }) => ({
|
||||
content: event.type === "ChatSendMessage" ? event.content : "",
|
||||
}),
|
||||
onDone: {
|
||||
target: "ready",
|
||||
actions: applyHttpSendOutputAction,
|
||||
},
|
||||
onError: {
|
||||
target: "ready",
|
||||
actions: markHttpSendFailedAction,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,232 @@
|
||||
import {
|
||||
createChatPromotionState,
|
||||
shouldPromptUnlockHistory,
|
||||
} from "../chat-machine.helpers";
|
||||
import { initialState } from "../chat-state";
|
||||
import {
|
||||
guestInitializingState,
|
||||
userInitializingState,
|
||||
} from "./history-flow";
|
||||
import {
|
||||
guestReadyState,
|
||||
guestSendingState,
|
||||
userSendingViaHttpState,
|
||||
} from "./send-flow";
|
||||
import {
|
||||
unlockingHistoryState,
|
||||
unlockingMessageState,
|
||||
unlockMachineSetup,
|
||||
} from "./unlock-flow";
|
||||
|
||||
const startGuestSessionAction = unlockMachineSetup.assign(
|
||||
({ context }) => ({
|
||||
...initialState,
|
||||
promotion: context.promotion,
|
||||
}),
|
||||
);
|
||||
|
||||
const startUserSessionAction = unlockMachineSetup.assign(
|
||||
({ context }) => ({
|
||||
...initialState,
|
||||
promotion: context.promotion,
|
||||
}),
|
||||
);
|
||||
|
||||
const clearChatSessionAction = unlockMachineSetup.assign(() => ({
|
||||
...initialState,
|
||||
}));
|
||||
|
||||
const injectPromotionAction = unlockMachineSetup.assign(({ event }) => {
|
||||
if (event.type !== "ChatPromotionInjected") return {};
|
||||
return {
|
||||
promotion: createChatPromotionState(
|
||||
event.promotion,
|
||||
event.messageId,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const clearPromotionAction = unlockMachineSetup.assign({ promotion: null });
|
||||
|
||||
export const chatMachineSetup = unlockMachineSetup.extend({
|
||||
actions: {
|
||||
startGuestSession: startGuestSessionAction,
|
||||
startUserSession: startUserSessionAction,
|
||||
clearChatSession: clearChatSessionAction,
|
||||
injectPromotion: injectPromotionAction,
|
||||
clearPromotion: clearPromotionAction,
|
||||
},
|
||||
});
|
||||
|
||||
const idleState = chatMachineSetup.createStateConfig({
|
||||
on: {
|
||||
ChatGuestLogin: {
|
||||
target: "#chat.guestSession",
|
||||
actions: "startGuestSession",
|
||||
},
|
||||
ChatUserLogin: {
|
||||
target: "#chat.userSession",
|
||||
actions: "startUserSession",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const guestSessionState = chatMachineSetup.createStateConfig({
|
||||
invoke: [
|
||||
{
|
||||
id: "messageQueue",
|
||||
src: "httpMessageQueue",
|
||||
},
|
||||
{
|
||||
id: "loadHistory",
|
||||
src: "loadHistory",
|
||||
},
|
||||
],
|
||||
on: {
|
||||
ChatUserLogin: {
|
||||
target: "#chat.userSession",
|
||||
actions: "startUserSession",
|
||||
},
|
||||
ChatNetworkHistoryLoaded: {
|
||||
actions: "applyNetworkHistoryLoaded",
|
||||
},
|
||||
ChatHistoryLoadFailed: {
|
||||
actions: "markHistoryLoadFailed",
|
||||
},
|
||||
ChatQueuedSendStarted: {
|
||||
actions: "markQueuedSendStarted",
|
||||
},
|
||||
ChatQueuedHttpDone: {
|
||||
actions: "applyQueuedHttpOutput",
|
||||
},
|
||||
ChatQueuedSendError: {
|
||||
actions: "appendQueuedSendErrorMessage",
|
||||
},
|
||||
},
|
||||
initial: "initializing",
|
||||
states: {
|
||||
initializing: guestInitializingState,
|
||||
ready: guestReadyState,
|
||||
sending: guestSendingState,
|
||||
},
|
||||
});
|
||||
|
||||
const userReadyState = chatMachineSetup.createStateConfig({
|
||||
on: {
|
||||
ChatSendMessage: {
|
||||
guard: ({ context, event }) =>
|
||||
context.canSendMessage && event.content.trim().length > 0,
|
||||
actions: ["appendUserMessage", "enqueueMessage"],
|
||||
},
|
||||
ChatSendImage: {
|
||||
actions: "appendUserImage",
|
||||
},
|
||||
ChatUnlockMessageRequested: {
|
||||
guard: ({ event }) => event.messageId.trim().length > 0,
|
||||
target: "unlockingMessage",
|
||||
actions: "markUnlockMessageStarted",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSessionState = chatMachineSetup.createStateConfig({
|
||||
invoke: [
|
||||
{
|
||||
id: "messageQueue",
|
||||
src: "httpMessageQueue",
|
||||
},
|
||||
{
|
||||
id: "loadHistory",
|
||||
src: "loadHistory",
|
||||
},
|
||||
],
|
||||
on: {
|
||||
ChatGuestLogin: {
|
||||
target: "#chat.guestSession",
|
||||
actions: "startGuestSession",
|
||||
},
|
||||
ChatLogout: {
|
||||
target: "#chat.idle",
|
||||
actions: "clearChatSession",
|
||||
},
|
||||
ChatNetworkHistoryLoaded: [
|
||||
{
|
||||
guard: ({ context, event }) =>
|
||||
event.type === "ChatNetworkHistoryLoaded" &&
|
||||
context.paymentUnlockPending &&
|
||||
shouldPromptUnlockHistory(event.output.messages),
|
||||
target: ".ready",
|
||||
actions: "showUnlockHistoryPromptFromNetwork",
|
||||
},
|
||||
{
|
||||
guard: ({ context }) => context.paymentUnlockPending,
|
||||
target: ".ready",
|
||||
actions: "applyNetworkHistoryLoadedAndClearPayment",
|
||||
},
|
||||
{
|
||||
guard: ({ context }) =>
|
||||
!context.isUnlockingHistory && !context.isUnlockingMessage,
|
||||
actions: "applyNetworkHistoryLoaded",
|
||||
},
|
||||
],
|
||||
ChatHistoryLoadFailed: {
|
||||
actions: "markHistoryLoadFailed",
|
||||
},
|
||||
ChatQueuedSendStarted: {
|
||||
actions: "markQueuedSendStarted",
|
||||
},
|
||||
ChatQueuedHttpDone: {
|
||||
actions: "applyQueuedHttpOutput",
|
||||
},
|
||||
ChatQueuedSendError: {
|
||||
actions: "appendQueuedSendErrorMessage",
|
||||
},
|
||||
ChatPaymentSucceeded: [
|
||||
{
|
||||
guard: ({ context }) =>
|
||||
context.historyLoaded &&
|
||||
shouldPromptUnlockHistory(context.messages),
|
||||
actions: ["clearUpgradePrompt", "showUnlockHistoryPrompt"],
|
||||
},
|
||||
{
|
||||
guard: ({ context }) => !context.historyLoaded,
|
||||
actions: ["clearUpgradePrompt", "markPaymentUnlockPending"],
|
||||
},
|
||||
{
|
||||
guard: ({ context }) => context.historyLoaded,
|
||||
actions: ["clearUpgradePrompt", "dismissUnlockHistoryPrompt"],
|
||||
},
|
||||
],
|
||||
ChatUnlockHistoryConfirmed: {
|
||||
target: ".unlockingHistory",
|
||||
actions: "markUnlockHistoryStarted",
|
||||
},
|
||||
ChatUnlockHistoryDismissed: {
|
||||
actions: "dismissUnlockHistoryPrompt",
|
||||
},
|
||||
ChatUnlockPaywallNavigationConsumed: {
|
||||
actions: "clearUnlockPaywallRequest",
|
||||
},
|
||||
},
|
||||
initial: "initializing",
|
||||
states: {
|
||||
initializing: userInitializingState,
|
||||
ready: userReadyState,
|
||||
sendingViaHttp: userSendingViaHttpState,
|
||||
unlockingHistory: unlockingHistoryState,
|
||||
unlockingMessage: unlockingMessageState,
|
||||
},
|
||||
});
|
||||
|
||||
export const chatRootStateConfig = chatMachineSetup.createStateConfig({
|
||||
initial: "idle",
|
||||
on: {
|
||||
ChatPromotionInjected: { actions: "injectPromotion" },
|
||||
ChatPromotionCleared: { actions: "clearPromotion" },
|
||||
},
|
||||
states: {
|
||||
idle: idleState,
|
||||
guestSession: guestSessionState,
|
||||
userSession: userSessionState,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { setup, type ActionFunction, type EventObject } from "xstate";
|
||||
|
||||
import type { ChatEvent } from "../chat-events";
|
||||
import {
|
||||
httpMessageQueueActor,
|
||||
loadHistoryActor,
|
||||
sendMessageHttpActor,
|
||||
unlockHistoryActor,
|
||||
unlockMessageActor,
|
||||
} from "../chat-machine.actors";
|
||||
import type { ChatState } from "../chat-state";
|
||||
|
||||
export const baseChatMachineSetup = setup({
|
||||
types: {
|
||||
context: {} as ChatState,
|
||||
events: {} as ChatEvent,
|
||||
},
|
||||
actors: {
|
||||
loadHistory: loadHistoryActor,
|
||||
sendMessageHttp: sendMessageHttpActor,
|
||||
httpMessageQueue: httpMessageQueueActor,
|
||||
unlockHistory: unlockHistoryActor,
|
||||
unlockMessage: unlockMessageActor,
|
||||
},
|
||||
});
|
||||
|
||||
type ChatActorAction<TEvent extends EventObject> = ActionFunction<
|
||||
ChatState,
|
||||
TEvent,
|
||||
ChatEvent,
|
||||
undefined,
|
||||
never,
|
||||
never,
|
||||
never,
|
||||
never,
|
||||
never
|
||||
>;
|
||||
|
||||
export function createChatActorActionSetup<TEvent extends EventObject>() {
|
||||
const actorActionSetup = setup({
|
||||
types: {
|
||||
context: {} as ChatState,
|
||||
events: {} as TEvent,
|
||||
},
|
||||
});
|
||||
return {
|
||||
assign(
|
||||
assignment: Parameters<typeof actorActionSetup.assign>[0],
|
||||
): ChatActorAction<TEvent> {
|
||||
// Actor lifecycle events are expression events, not public machine events.
|
||||
return actorActionSetup.assign(assignment) as unknown as ChatActorAction<TEvent>;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import { type DoneActorEvent } from "xstate";
|
||||
|
||||
import {
|
||||
applyPromotionUnlockOutput,
|
||||
applySingleUnlockOutput,
|
||||
countLockedHistoryMessages,
|
||||
type UnlockMessageOutput,
|
||||
} from "../chat-machine.helpers";
|
||||
import type { UnlockHistoryOutput } from "../chat-unlock-flow";
|
||||
import { sendMachineSetup } from "./send-flow";
|
||||
import { createChatActorActionSetup } from "./setup";
|
||||
|
||||
const markPaymentUnlockPendingAction = sendMachineSetup.assign(() => ({
|
||||
paymentUnlockPending: true,
|
||||
unlockHistoryError: null,
|
||||
}));
|
||||
|
||||
const showUnlockHistoryPromptAction = sendMachineSetup.assign(
|
||||
({ context }) => ({
|
||||
paymentUnlockPending: false,
|
||||
unlockHistoryPromptVisible: true,
|
||||
lockedHistoryCount: countLockedHistoryMessages(context.messages),
|
||||
unlockHistoryError: null,
|
||||
}),
|
||||
);
|
||||
|
||||
const dismissUnlockHistoryPromptAction = sendMachineSetup.assign(() => ({
|
||||
paymentUnlockPending: false,
|
||||
unlockHistoryPromptVisible: false,
|
||||
unlockHistoryError: null,
|
||||
}));
|
||||
|
||||
const markUnlockHistoryStartedAction = sendMachineSetup.assign(() => ({
|
||||
paymentUnlockPending: false,
|
||||
unlockHistoryPromptVisible: false,
|
||||
isUnlockingHistory: true,
|
||||
unlockHistoryError: null,
|
||||
}));
|
||||
|
||||
const markUnlockHistoryFailedAction = sendMachineSetup.assign(() => ({
|
||||
isUnlockingHistory: false,
|
||||
unlockHistoryPromptVisible: true,
|
||||
unlockHistoryError: "Failed to unlock messages. Please try again.",
|
||||
}));
|
||||
|
||||
const markUnlockMessageStartedAction = sendMachineSetup.assign(
|
||||
({ event }) => {
|
||||
if (event.type !== "ChatUnlockMessageRequested") return {};
|
||||
return {
|
||||
isUnlockingMessage: true,
|
||||
unlockingMessageId: event.messageId,
|
||||
unlockingMessageKind: event.kind,
|
||||
unlockingRemoteMessageId:
|
||||
event.remoteMessageId ?? (event.lockType ? null : event.messageId),
|
||||
unlockingLockType: event.lockType ?? null,
|
||||
unlockingClientLockId: event.clientLockId ?? null,
|
||||
unlockMessageError: null,
|
||||
unlockPaywallRequest: null,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const applyUnlockMessageSucceededAction = sendMachineSetup.assign(
|
||||
({ context, event }) => {
|
||||
const { output } = event as unknown as DoneActorEvent<UnlockMessageOutput>;
|
||||
return {
|
||||
messages: applySingleUnlockOutput(context.messages, output),
|
||||
promotion: applyPromotionUnlockOutput(context.promotion, output),
|
||||
isUnlockingMessage: false,
|
||||
unlockingMessageId: null,
|
||||
unlockingMessageKind: null,
|
||||
unlockingRemoteMessageId: null,
|
||||
unlockingLockType: null,
|
||||
unlockingClientLockId: null,
|
||||
unlockMessageError: null,
|
||||
unlockPaywallRequest: null,
|
||||
creditBalance: output.response.creditBalance,
|
||||
creditsCharged: output.response.creditsCharged,
|
||||
requiredCredits: 0,
|
||||
shortfallCredits: 0,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const requestUnlockPaymentFromOutputAction = sendMachineSetup.assign(
|
||||
({ context, event }) => {
|
||||
const { output } = event as unknown as DoneActorEvent<UnlockMessageOutput>;
|
||||
return {
|
||||
promotion: applyPromotionUnlockOutput(context.promotion, output),
|
||||
isUnlockingMessage: false,
|
||||
unlockingMessageId: null,
|
||||
unlockingMessageKind: null,
|
||||
unlockingRemoteMessageId: null,
|
||||
unlockingLockType: null,
|
||||
unlockingClientLockId: null,
|
||||
unlockMessageError: output.response.reason,
|
||||
unlockPaywallRequest: {
|
||||
displayMessageId:
|
||||
output.response.messageId || output.displayMessageId,
|
||||
...(output.response.messageId || output.request.messageId
|
||||
? {
|
||||
messageId:
|
||||
output.response.messageId || output.request.messageId,
|
||||
}
|
||||
: {}),
|
||||
kind: context.unlockingMessageKind ?? "private",
|
||||
...(output.request.lockType
|
||||
? { lockType: output.request.lockType }
|
||||
: {}),
|
||||
...(output.request.clientLockId
|
||||
? { clientLockId: output.request.clientLockId }
|
||||
: {}),
|
||||
...(context.promotion?.message.id === output.displayMessageId
|
||||
? { promotion: context.promotion.session }
|
||||
: {}),
|
||||
reason: output.response.reason,
|
||||
creditBalance: output.response.creditBalance,
|
||||
requiredCredits: output.response.requiredCredits,
|
||||
shortfallCredits: output.response.shortfallCredits,
|
||||
},
|
||||
creditBalance: output.response.creditBalance,
|
||||
requiredCredits: output.response.requiredCredits,
|
||||
shortfallCredits: output.response.shortfallCredits,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const requestUnlockPaymentFromErrorAction = sendMachineSetup.assign(
|
||||
({ context }) => ({
|
||||
isUnlockingMessage: false,
|
||||
unlockMessageError: "unlock_failed",
|
||||
unlockPaywallRequest:
|
||||
context.unlockingMessageId && context.unlockingMessageKind
|
||||
? {
|
||||
displayMessageId: context.unlockingMessageId,
|
||||
...(context.unlockingRemoteMessageId
|
||||
? { messageId: context.unlockingRemoteMessageId }
|
||||
: {}),
|
||||
kind: context.unlockingMessageKind,
|
||||
...(context.unlockingLockType
|
||||
? { lockType: context.unlockingLockType }
|
||||
: {}),
|
||||
...(context.unlockingClientLockId
|
||||
? { clientLockId: context.unlockingClientLockId }
|
||||
: {}),
|
||||
...(context.promotion?.message.id === context.unlockingMessageId
|
||||
? { promotion: context.promotion.session }
|
||||
: {}),
|
||||
reason: "unlock_failed",
|
||||
creditBalance: context.creditBalance,
|
||||
requiredCredits: context.requiredCredits,
|
||||
shortfallCredits: context.shortfallCredits,
|
||||
}
|
||||
: null,
|
||||
unlockingMessageId: null,
|
||||
unlockingMessageKind: null,
|
||||
unlockingRemoteMessageId: null,
|
||||
unlockingLockType: null,
|
||||
unlockingClientLockId: null,
|
||||
}),
|
||||
);
|
||||
|
||||
const clearUnlockPaywallRequestAction = sendMachineSetup.assign(() => ({
|
||||
unlockPaywallRequest: null,
|
||||
}));
|
||||
|
||||
export const unlockMachineSetup = sendMachineSetup.extend({
|
||||
actions: {
|
||||
markPaymentUnlockPending: markPaymentUnlockPendingAction,
|
||||
showUnlockHistoryPrompt: showUnlockHistoryPromptAction,
|
||||
dismissUnlockHistoryPrompt: dismissUnlockHistoryPromptAction,
|
||||
markUnlockHistoryStarted: markUnlockHistoryStartedAction,
|
||||
markUnlockHistoryFailed: markUnlockHistoryFailedAction,
|
||||
markUnlockMessageStarted: markUnlockMessageStartedAction,
|
||||
applyUnlockMessageSucceeded: applyUnlockMessageSucceededAction,
|
||||
requestUnlockPaymentFromOutput: requestUnlockPaymentFromOutputAction,
|
||||
requestUnlockPaymentFromError: requestUnlockPaymentFromErrorAction,
|
||||
clearUnlockPaywallRequest: clearUnlockPaywallRequestAction,
|
||||
},
|
||||
});
|
||||
|
||||
const unlockHistoryDoneActionSetup =
|
||||
createChatActorActionSetup<DoneActorEvent<UnlockHistoryOutput>>();
|
||||
|
||||
const applyUnlockHistoryOutputAction =
|
||||
unlockHistoryDoneActionSetup.assign(({ event }) => {
|
||||
const lockedHistoryCount = countLockedHistoryMessages(
|
||||
event.output.messages,
|
||||
);
|
||||
const insufficientBalance =
|
||||
!event.output.unlocked &&
|
||||
event.output.reason === "insufficient_balance";
|
||||
return {
|
||||
messages: event.output.messages,
|
||||
historyLoaded: true,
|
||||
paymentUnlockPending: false,
|
||||
unlockHistoryPromptVisible: insufficientBalance,
|
||||
lockedHistoryCount,
|
||||
isUnlockingHistory: false,
|
||||
unlockHistoryError: insufficientBalance
|
||||
? `Insufficient credits. You still need ${event.output.shortfallCredits} credits.`
|
||||
: null,
|
||||
};
|
||||
});
|
||||
|
||||
export const unlockingHistoryState = unlockMachineSetup.createStateConfig({
|
||||
invoke: {
|
||||
id: "unlockHistory",
|
||||
src: "unlockHistory",
|
||||
onDone: {
|
||||
target: "ready",
|
||||
actions: applyUnlockHistoryOutputAction,
|
||||
},
|
||||
onError: {
|
||||
target: "ready",
|
||||
actions: "markUnlockHistoryFailed",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const unlockingMessageState = unlockMachineSetup.createStateConfig({
|
||||
invoke: {
|
||||
id: "unlockMessage",
|
||||
src: "unlockMessage",
|
||||
input: ({ context }) => ({
|
||||
displayMessageId: context.unlockingMessageId ?? "",
|
||||
...(context.unlockingRemoteMessageId
|
||||
? { messageId: context.unlockingRemoteMessageId }
|
||||
: {}),
|
||||
...(context.unlockingLockType
|
||||
? { lockType: context.unlockingLockType }
|
||||
: {}),
|
||||
...(context.unlockingClientLockId
|
||||
? { clientLockId: context.unlockingClientLockId }
|
||||
: {}),
|
||||
}),
|
||||
onDone: [
|
||||
{
|
||||
guard: ({ event }) => event.output.response.unlocked,
|
||||
target: "ready",
|
||||
actions: "applyUnlockMessageSucceeded",
|
||||
},
|
||||
{
|
||||
target: "ready",
|
||||
actions: "requestUnlockPaymentFromOutput",
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: "ready",
|
||||
actions: "requestUnlockPaymentFromError",
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user