feat(chat): implement pull-to-refresh functionality and pagination for chat history
This commit is contained in:
@@ -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