feat(chat): implement pull-to-refresh functionality and pagination for chat history
This commit is contained in:
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { ChatSendResponse } from "@/data/dto/chat";
|
||||
import type { ChatState } from "@/stores/chat/chat-state";
|
||||
import {
|
||||
applyNetworkHistoryLoadedOutput,
|
||||
applyHttpSendOutput,
|
||||
countLockedHistoryMessages,
|
||||
localMessagesToUi,
|
||||
@@ -40,6 +41,10 @@ function makeChatState(overrides: Partial<ChatState> = {}): ChatState {
|
||||
requiredCredits: 0,
|
||||
shortfallCredits: 0,
|
||||
historyLoaded: true,
|
||||
historyTotal: 0,
|
||||
historyLimit: 50,
|
||||
nextHistoryOffset: 0,
|
||||
isLoadingMoreHistory: false,
|
||||
paymentUnlockPending: false,
|
||||
unlockHistoryPromptVisible: false,
|
||||
lockedHistoryCount: 0,
|
||||
@@ -244,6 +249,21 @@ describe("applyHttpSendOutput", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("chat history pagination helpers", () => {
|
||||
it("falls back to the bounded history limit when the backend limit is zero", () => {
|
||||
const nextState = applyNetworkHistoryLoadedOutput(makeChatState(), {
|
||||
messages: [],
|
||||
localCount: 0,
|
||||
total: 120,
|
||||
limit: 0,
|
||||
});
|
||||
|
||||
expect(nextState.historyLimit).toBe(50);
|
||||
expect(nextState.nextHistoryOffset).toBe(50);
|
||||
expect(nextState.historyTotal).toBe(120);
|
||||
});
|
||||
});
|
||||
|
||||
describe("localMessagesToUi", () => {
|
||||
it("maps locked voice messages from history", () => {
|
||||
const [message] = localMessagesToUi([
|
||||
|
||||
@@ -4,8 +4,12 @@ import { createActor, fromCallback, waitFor } from "xstate";
|
||||
import type { UiMessage } from "@/data/dto/chat";
|
||||
import type { ChatEvent } from "@/stores/chat/chat-events";
|
||||
import { chatMachine } from "@/stores/chat/chat-machine";
|
||||
import type { LoadMoreHistoryActorEvent } from "@/stores/chat/machine/actors/history";
|
||||
|
||||
import { createTestChatMachine } from "./chat-machine.test-utils";
|
||||
import {
|
||||
createLoadHistoryCallback,
|
||||
createTestChatMachine,
|
||||
} from "./chat-machine.test-utils";
|
||||
|
||||
describe("chat history flow", () => {
|
||||
it("enters user ready after history is loaded", async () => {
|
||||
@@ -57,6 +61,8 @@ describe("chat history flow", () => {
|
||||
localOverwritten: true,
|
||||
localCount: 1,
|
||||
networkCount: 1,
|
||||
total: 1,
|
||||
limit: 50,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -87,4 +93,182 @@ describe("chat history flow", () => {
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("loads older pages until total is exhausted and keeps existing messages", async () => {
|
||||
const requests: LoadMoreHistoryActorEvent[] = [];
|
||||
const latestMessage: UiMessage = {
|
||||
id: "latest",
|
||||
content: "current unlocked message",
|
||||
isFromAI: true,
|
||||
date: "2026-07-15",
|
||||
};
|
||||
const machine = chatMachine.provide({
|
||||
actors: {
|
||||
loadHistory: createLoadHistoryCallback(
|
||||
[latestMessage],
|
||||
[latestMessage],
|
||||
{ total: 120, limit: 50 },
|
||||
),
|
||||
loadMoreHistory: fromCallback<LoadMoreHistoryActorEvent>(
|
||||
({ receive, sendBack }) => {
|
||||
receive((event) => {
|
||||
requests.push(event);
|
||||
if (event.offset === 50) {
|
||||
sendBack({
|
||||
type: "ChatOlderHistoryLoaded",
|
||||
output: {
|
||||
messages: [
|
||||
createHistoryMessage("older-50"),
|
||||
{ ...latestMessage, content: "stale duplicate" },
|
||||
],
|
||||
offset: event.offset,
|
||||
total: 120,
|
||||
limit: 50,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
sendBack({
|
||||
type: "ChatOlderHistoryLoaded",
|
||||
output: {
|
||||
messages: [createHistoryMessage("oldest-100")],
|
||||
offset: event.offset,
|
||||
total: 120,
|
||||
limit: 50,
|
||||
},
|
||||
});
|
||||
});
|
||||
return () => undefined;
|
||||
},
|
||||
),
|
||||
},
|
||||
});
|
||||
const actor = createActor(machine).start();
|
||||
|
||||
actor.send({ type: "ChatUserLogin", token: "token" });
|
||||
await waitFor(actor, (snapshot) =>
|
||||
snapshot.matches({ userSession: "ready" }),
|
||||
);
|
||||
|
||||
actor.send({ type: "ChatLoadMoreHistoryRequested" });
|
||||
await waitFor(
|
||||
actor,
|
||||
(snapshot) => snapshot.context.nextHistoryOffset === 100,
|
||||
);
|
||||
expect(requests[0]).toMatchObject({ offset: 50, limit: 50 });
|
||||
expect(actor.getSnapshot().context.messages).toMatchObject([
|
||||
{ id: "older-50" },
|
||||
{ id: "latest", content: "current unlocked message" },
|
||||
]);
|
||||
|
||||
actor.send({ type: "ChatLoadMoreHistoryRequested" });
|
||||
await waitFor(
|
||||
actor,
|
||||
(snapshot) => snapshot.context.nextHistoryOffset === 150,
|
||||
);
|
||||
actor.send({ type: "ChatLoadMoreHistoryRequested" });
|
||||
|
||||
expect(requests).toHaveLength(2);
|
||||
expect(actor.getSnapshot().context.nextHistoryOffset).toBeGreaterThanOrEqual(
|
||||
actor.getSnapshot().context.historyTotal,
|
||||
);
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("keeps the offset after a failed page and allows retrying", async () => {
|
||||
let requestCount = 0;
|
||||
const machine = chatMachine.provide({
|
||||
actors: {
|
||||
loadHistory: createLoadHistoryCallback([], [], {
|
||||
total: 75,
|
||||
limit: 50,
|
||||
}),
|
||||
loadMoreHistory: fromCallback<LoadMoreHistoryActorEvent>(
|
||||
({ receive, sendBack }) => {
|
||||
receive((event) => {
|
||||
requestCount += 1;
|
||||
if (requestCount === 1) {
|
||||
sendBack({
|
||||
type: "ChatOlderHistoryLoadFailed",
|
||||
error: new Error("network failed"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
sendBack({
|
||||
type: "ChatOlderHistoryLoaded",
|
||||
output: {
|
||||
messages: [createHistoryMessage("retry-message")],
|
||||
offset: event.offset,
|
||||
total: 75,
|
||||
limit: 50,
|
||||
},
|
||||
});
|
||||
});
|
||||
return () => undefined;
|
||||
},
|
||||
),
|
||||
},
|
||||
});
|
||||
const actor = createActor(machine).start();
|
||||
|
||||
actor.send({ type: "ChatGuestLogin" });
|
||||
await waitFor(actor, (snapshot) =>
|
||||
snapshot.matches({ guestSession: "ready" }),
|
||||
);
|
||||
actor.send({ type: "ChatLoadMoreHistoryRequested" });
|
||||
await waitFor(
|
||||
actor,
|
||||
(snapshot) =>
|
||||
requestCount === 1 && !snapshot.context.isLoadingMoreHistory,
|
||||
);
|
||||
expect(actor.getSnapshot().context.nextHistoryOffset).toBe(50);
|
||||
|
||||
actor.send({ type: "ChatLoadMoreHistoryRequested" });
|
||||
await waitFor(
|
||||
actor,
|
||||
(snapshot) => snapshot.context.nextHistoryOffset === 100,
|
||||
);
|
||||
expect(requestCount).toBe(2);
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("does not request another page when total does not exceed limit", async () => {
|
||||
let requested = false;
|
||||
const machine = chatMachine.provide({
|
||||
actors: {
|
||||
loadHistory: createLoadHistoryCallback([], [], {
|
||||
total: 50,
|
||||
limit: 50,
|
||||
}),
|
||||
loadMoreHistory: fromCallback<LoadMoreHistoryActorEvent>(
|
||||
({ receive }) => {
|
||||
receive(() => {
|
||||
requested = true;
|
||||
});
|
||||
return () => undefined;
|
||||
},
|
||||
),
|
||||
},
|
||||
});
|
||||
const actor = createActor(machine).start();
|
||||
|
||||
actor.send({ type: "ChatUserLogin", token: "token" });
|
||||
await waitFor(actor, (snapshot) =>
|
||||
snapshot.matches({ userSession: "ready" }),
|
||||
);
|
||||
actor.send({ type: "ChatLoadMoreHistoryRequested" });
|
||||
|
||||
expect(requested).toBe(false);
|
||||
expect(actor.getSnapshot().context.isLoadingMoreHistory).toBe(false);
|
||||
actor.stop();
|
||||
});
|
||||
});
|
||||
|
||||
function createHistoryMessage(id: string): UiMessage {
|
||||
return {
|
||||
id,
|
||||
content: `Message ${id}`,
|
||||
isFromAI: true,
|
||||
date: "2026-07-15",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -131,6 +131,10 @@ export function createTestChatMachine(
|
||||
export function createLoadHistoryCallback(
|
||||
messages: UiMessage[],
|
||||
networkMessages: UiMessage[] = messages,
|
||||
pagination: { total: number; limit: number } = {
|
||||
total: networkMessages.length,
|
||||
limit: 50,
|
||||
},
|
||||
) {
|
||||
return fromCallback<ChatEvent>(({ sendBack }) => {
|
||||
sendBack({
|
||||
@@ -147,6 +151,8 @@ export function createLoadHistoryCallback(
|
||||
localOverwritten: true,
|
||||
localCount: messages.length,
|
||||
networkCount: networkMessages.length,
|
||||
total: pagination.total,
|
||||
limit: pagination.limit,
|
||||
},
|
||||
});
|
||||
return () => undefined;
|
||||
|
||||
@@ -23,6 +23,8 @@ interface ChatState {
|
||||
upgradeReason: MachineContext["upgradeReason"];
|
||||
/** history 初始加载完成标志(local → network → save 跑完) —— UI 可用此显示 loading */
|
||||
historyLoaded: boolean;
|
||||
hasMoreHistory: boolean;
|
||||
isLoadingMoreHistory: boolean;
|
||||
unlockHistoryPromptVisible: boolean;
|
||||
lockedHistoryCount: number;
|
||||
isUnlockingHistory: boolean;
|
||||
@@ -76,6 +78,10 @@ function selectChatState(state: ChatSnapshot): SelectedChatState {
|
||||
upgradePromptVisible: state.context.upgradePromptVisible,
|
||||
upgradeReason: state.context.upgradeReason,
|
||||
historyLoaded: state.context.historyLoaded,
|
||||
hasMoreHistory:
|
||||
state.context.historyLoaded &&
|
||||
state.context.nextHistoryOffset < state.context.historyTotal,
|
||||
isLoadingMoreHistory: state.context.isLoadingMoreHistory,
|
||||
unlockHistoryPromptVisible: state.context.unlockHistoryPromptVisible,
|
||||
lockedHistoryCount: state.context.lockedHistoryCount,
|
||||
isUnlockingHistory: state.context.isUnlockingHistory,
|
||||
|
||||
@@ -33,6 +33,12 @@ export type ChatEvent =
|
||||
output: import("./chat-history-sync").NetworkHistorySyncOutput;
|
||||
}
|
||||
| { type: "ChatHistoryLoadFailed"; error: unknown }
|
||||
| { type: "ChatLoadMoreHistoryRequested" }
|
||||
| {
|
||||
type: "ChatOlderHistoryLoaded";
|
||||
output: import("./machine/actors/history").LoadMoreHistoryOutput;
|
||||
}
|
||||
| { type: "ChatOlderHistoryLoadFailed"; error: unknown }
|
||||
// 业务事件
|
||||
| { type: "ChatSendMessage"; content: string }
|
||||
| { type: "ChatSendImage"; imageBase64: string }
|
||||
|
||||
@@ -17,6 +17,8 @@ export type ReadAndSyncHistoryOutput = {
|
||||
localOverwritten: boolean;
|
||||
localCount: number;
|
||||
networkCount: number;
|
||||
total: number;
|
||||
limit: number;
|
||||
};
|
||||
|
||||
export type LocalHistorySnapshotOutput = {
|
||||
@@ -121,6 +123,8 @@ export async function syncNetworkHistory(
|
||||
localOverwritten,
|
||||
localCount,
|
||||
networkCount: networkUi.length,
|
||||
total: networkResult.data.total,
|
||||
limit: networkResult.data.limit,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,5 +148,7 @@ export async function readAndSyncHistory(): Promise<ReadAndSyncHistoryOutput> {
|
||||
localOverwritten: false,
|
||||
localCount: localSnapshot.localCount,
|
||||
networkCount: 0,
|
||||
total: 0,
|
||||
limit: CHAT_HISTORY_LIMIT,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ export interface ChatState {
|
||||
shortfallCredits: number;
|
||||
/** history 首屏可展示标志(本地快照加载完成即可进入 ready)。 */
|
||||
historyLoaded: boolean;
|
||||
historyTotal: number;
|
||||
historyLimit: number;
|
||||
nextHistoryOffset: number;
|
||||
isLoadingMoreHistory: boolean;
|
||||
paymentUnlockPending: boolean;
|
||||
unlockHistoryPromptVisible: boolean;
|
||||
lockedHistoryCount: number;
|
||||
@@ -61,6 +65,10 @@ export const initialState: ChatState = {
|
||||
requiredCredits: 0,
|
||||
shortfallCredits: 0,
|
||||
historyLoaded: false,
|
||||
historyTotal: 0,
|
||||
historyLimit: 50,
|
||||
nextHistoryOffset: 0,
|
||||
isLoadingMoreHistory: false,
|
||||
paymentUnlockPending: false,
|
||||
unlockHistoryPromptVisible: false,
|
||||
lockedHistoryCount: 0,
|
||||
|
||||
@@ -21,12 +21,88 @@ export function applyNetworkHistoryLoadedOutput(
|
||||
output: {
|
||||
messages: UiMessage[];
|
||||
localCount: number;
|
||||
total: number;
|
||||
limit: number;
|
||||
},
|
||||
): Pick<ChatState, "messages" | "historyLoaded"> {
|
||||
): Pick<
|
||||
ChatState,
|
||||
| "messages"
|
||||
| "historyLoaded"
|
||||
| "historyTotal"
|
||||
| "historyLimit"
|
||||
| "nextHistoryOffset"
|
||||
| "isLoadingMoreHistory"
|
||||
> {
|
||||
const localSnapshotSize = output.localCount === 0 ? 1 : output.localCount;
|
||||
const optimisticTail = context.messages.slice(localSnapshotSize);
|
||||
const historyLimit = normalizeHistoryLimit(output.limit);
|
||||
return {
|
||||
messages: [...output.messages, ...optimisticTail],
|
||||
historyLoaded: true,
|
||||
historyTotal: Math.max(0, output.total),
|
||||
historyLimit,
|
||||
nextHistoryOffset: historyLimit,
|
||||
isLoadingMoreHistory: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyOlderHistoryLoadedOutput(
|
||||
context: ChatState,
|
||||
output: {
|
||||
messages: UiMessage[];
|
||||
offset: number;
|
||||
total: number;
|
||||
limit: number;
|
||||
},
|
||||
): Pick<
|
||||
ChatState,
|
||||
| "messages"
|
||||
| "historyTotal"
|
||||
| "historyLimit"
|
||||
| "nextHistoryOffset"
|
||||
| "isLoadingMoreHistory"
|
||||
> {
|
||||
const historyLimit = normalizeHistoryLimit(
|
||||
output.limit,
|
||||
context.historyLimit,
|
||||
);
|
||||
return {
|
||||
messages: prependUniqueHistoryMessages(context.messages, output.messages),
|
||||
historyTotal: Math.max(0, output.total),
|
||||
historyLimit,
|
||||
nextHistoryOffset: output.offset + historyLimit,
|
||||
isLoadingMoreHistory: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function canLoadMoreHistory(context: ChatState): boolean {
|
||||
return (
|
||||
context.historyLoaded &&
|
||||
!context.isLoadingMoreHistory &&
|
||||
context.nextHistoryOffset < context.historyTotal
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeHistoryLimit(
|
||||
limit: number,
|
||||
fallback = CHAT_HISTORY_LIMIT,
|
||||
): number {
|
||||
return Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : fallback;
|
||||
}
|
||||
|
||||
export function prependUniqueHistoryMessages(
|
||||
currentMessages: readonly UiMessage[],
|
||||
olderMessages: readonly UiMessage[],
|
||||
): UiMessage[] {
|
||||
const existingIds = new Set(
|
||||
currentMessages.flatMap((message) => (message.id ? [message.id] : [])),
|
||||
);
|
||||
const pageIds = new Set<string>();
|
||||
const uniqueOlderMessages = olderMessages.filter((message) => {
|
||||
if (!message.id) return true;
|
||||
if (existingIds.has(message.id) || pageIds.has(message.id)) return false;
|
||||
pageIds.add(message.id);
|
||||
return true;
|
||||
});
|
||||
return [...uniqueOlderMessages, ...currentMessages];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { fromCallback } from "xstate";
|
||||
|
||||
import type { UiMessage } from "@/data/dto/chat";
|
||||
import { loadChatRepository } from "@/data/repositories/chat_repository_loader";
|
||||
import { Logger } from "@/utils/logger";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
import {
|
||||
readLocalHistorySnapshot,
|
||||
@@ -8,9 +11,24 @@ import {
|
||||
syncNetworkHistory,
|
||||
} from "../../chat-history-sync";
|
||||
import type { ChatEvent } from "../../chat-events";
|
||||
import { normalizeHistoryLimit } from "../../helper/history";
|
||||
import { localMessagesToUi } from "../../helper/message-mappers";
|
||||
|
||||
const log = new Logger("StoresChatChatHistoryFlow");
|
||||
|
||||
export interface LoadMoreHistoryActorEvent {
|
||||
type: "LoadMoreHistoryPageRequested";
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface LoadMoreHistoryOutput {
|
||||
messages: UiMessage[];
|
||||
offset: number;
|
||||
total: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export const loadHistoryActor = fromCallback<ChatEvent>(({ sendBack }) => {
|
||||
let cancelled = false;
|
||||
|
||||
@@ -42,3 +60,54 @@ export const loadHistoryActor = fromCallback<ChatEvent>(({ sendBack }) => {
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
|
||||
export const loadMoreHistoryActor =
|
||||
fromCallback<LoadMoreHistoryActorEvent>(({ receive, sendBack }) => {
|
||||
let cancelled = false;
|
||||
let running = false;
|
||||
|
||||
receive((event) => {
|
||||
if (running || cancelled) return;
|
||||
running = true;
|
||||
|
||||
void (async () => {
|
||||
const chatRepo = await loadChatRepository();
|
||||
const historyResult = await chatRepo.getHistory(
|
||||
event.limit,
|
||||
event.offset,
|
||||
);
|
||||
if (Result.isErr(historyResult)) throw historyResult.error;
|
||||
if (cancelled) return;
|
||||
|
||||
const response = historyResult.data;
|
||||
sendBack({
|
||||
type: "ChatOlderHistoryLoaded",
|
||||
output: {
|
||||
messages: localMessagesToUi(response.messages),
|
||||
offset: event.offset,
|
||||
total: response.total,
|
||||
limit: normalizeHistoryLimit(response.limit, event.limit),
|
||||
} satisfies LoadMoreHistoryOutput,
|
||||
});
|
||||
void resolveHistoryCacheIdentity().then((cacheIdentity) => {
|
||||
if (!cacheIdentity) return;
|
||||
void chatRepo.prefetchMediaForMessages(
|
||||
response.messages,
|
||||
cacheIdentity,
|
||||
);
|
||||
});
|
||||
})()
|
||||
.catch((error: unknown) => {
|
||||
if (cancelled) return;
|
||||
log.warn("[chat-machine] loadMoreHistoryActor failed", { error });
|
||||
sendBack({ type: "ChatOlderHistoryLoadFailed", error });
|
||||
})
|
||||
.finally(() => {
|
||||
running = false;
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import {
|
||||
applyOlderHistoryLoadedOutput,
|
||||
applyHistoryLoadedOutput,
|
||||
applyNetworkHistoryLoadedOutput,
|
||||
canLoadMoreHistory,
|
||||
} from "../helper/history";
|
||||
import {
|
||||
countLockedHistoryMessages,
|
||||
shouldPromptUnlockHistory,
|
||||
} from "../helper/unlock";
|
||||
import type { ChatState } from "../chat-state";
|
||||
import { baseChatMachineSetup } from "./setup";
|
||||
|
||||
const applyLocalHistoryLoadedAction = baseChatMachineSetup.assign(
|
||||
@@ -49,6 +52,30 @@ const markHistoryLoadFailedAction = baseChatMachineSetup.assign({
|
||||
historyLoaded: true,
|
||||
});
|
||||
|
||||
const markLoadMoreHistoryStartedAction = baseChatMachineSetup.assign({
|
||||
isLoadingMoreHistory: true,
|
||||
});
|
||||
|
||||
const requestOlderHistoryPageAction = baseChatMachineSetup.sendTo(
|
||||
"loadMoreHistory",
|
||||
({ context }) => ({
|
||||
type: "LoadMoreHistoryPageRequested",
|
||||
offset: context.nextHistoryOffset,
|
||||
limit: context.historyLimit,
|
||||
}),
|
||||
);
|
||||
|
||||
const applyOlderHistoryLoadedAction = baseChatMachineSetup.assign(
|
||||
({ context, event }) => {
|
||||
if (event.type !== "ChatOlderHistoryLoaded") return {};
|
||||
return applyOlderHistoryLoadedOutput(context, event.output);
|
||||
},
|
||||
);
|
||||
|
||||
const markOlderHistoryLoadFailedAction = baseChatMachineSetup.assign({
|
||||
isLoadingMoreHistory: false,
|
||||
});
|
||||
|
||||
export const historyMachineSetup = baseChatMachineSetup.extend({
|
||||
actions: {
|
||||
applyLocalHistoryLoaded: applyLocalHistoryLoadedAction,
|
||||
@@ -58,9 +85,27 @@ export const historyMachineSetup = baseChatMachineSetup.extend({
|
||||
showUnlockHistoryPromptFromNetwork:
|
||||
showUnlockHistoryPromptFromNetworkAction,
|
||||
markHistoryLoadFailed: markHistoryLoadFailedAction,
|
||||
markLoadMoreHistoryStarted: markLoadMoreHistoryStartedAction,
|
||||
requestOlderHistoryPage: requestOlderHistoryPageAction,
|
||||
applyOlderHistoryLoaded: applyOlderHistoryLoadedAction,
|
||||
markOlderHistoryLoadFailed: markOlderHistoryLoadFailedAction,
|
||||
},
|
||||
});
|
||||
|
||||
export const historyPaginationTransitions = {
|
||||
ChatLoadMoreHistoryRequested: {
|
||||
guard: ({ context }: { context: ChatState }) =>
|
||||
canLoadMoreHistory(context),
|
||||
actions: ["markLoadMoreHistoryStarted", "requestOlderHistoryPage"],
|
||||
},
|
||||
ChatOlderHistoryLoaded: {
|
||||
actions: "applyOlderHistoryLoaded",
|
||||
},
|
||||
ChatOlderHistoryLoadFailed: {
|
||||
actions: "markOlderHistoryLoadFailed",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const guestInitializingState = historyMachineSetup.createStateConfig({
|
||||
on: {
|
||||
ChatLocalHistoryLoaded: {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shouldPromptUnlockHistory } from "../helper/unlock";
|
||||
import { initialState } from "../chat-state";
|
||||
import {
|
||||
guestInitializingState,
|
||||
historyPaginationTransitions,
|
||||
userInitializingState,
|
||||
} from "./history-flow";
|
||||
import {
|
||||
@@ -79,8 +80,13 @@ const guestSessionState = chatMachineSetup.createStateConfig({
|
||||
id: "loadHistory",
|
||||
src: "loadHistory",
|
||||
},
|
||||
{
|
||||
id: "loadMoreHistory",
|
||||
src: "loadMoreHistory",
|
||||
},
|
||||
],
|
||||
on: {
|
||||
...historyPaginationTransitions,
|
||||
ChatUserLogin: {
|
||||
target: "#chat.userSession",
|
||||
actions: "startUserSession",
|
||||
@@ -137,8 +143,13 @@ const userSessionState = chatMachineSetup.createStateConfig({
|
||||
id: "loadHistory",
|
||||
src: "loadHistory",
|
||||
},
|
||||
{
|
||||
id: "loadMoreHistory",
|
||||
src: "loadMoreHistory",
|
||||
},
|
||||
],
|
||||
on: {
|
||||
...historyPaginationTransitions,
|
||||
ChatGuestLogin: {
|
||||
target: "#chat.guestSession",
|
||||
actions: "startGuestSession",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { setup, type ActionFunction, type EventObject } from "xstate";
|
||||
|
||||
import type { ChatEvent } from "../chat-events";
|
||||
import { loadHistoryActor } from "./actors/history";
|
||||
import {
|
||||
loadHistoryActor,
|
||||
loadMoreHistoryActor,
|
||||
} from "./actors/history";
|
||||
import {
|
||||
httpMessageQueueActor,
|
||||
sendMessageHttpActor,
|
||||
@@ -19,6 +22,7 @@ export const baseChatMachineSetup = setup({
|
||||
},
|
||||
actors: {
|
||||
loadHistory: loadHistoryActor,
|
||||
loadMoreHistory: loadMoreHistoryActor,
|
||||
sendMessageHttp: sendMessageHttpActor,
|
||||
httpMessageQueue: httpMessageQueueActor,
|
||||
unlockHistory: unlockHistoryActor,
|
||||
|
||||
Reference in New Issue
Block a user