From 71d04723375fcfab7d54d8f4a518251648516265 Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 02:52:01 +0300 Subject: [PATCH] feat(web-chat): add message history pagination - add loadMoreMessages with before_id cursor in chat store - track hasMore/loading state per chat - add 'Load older messages' control in message list --- web/src/components/MessageList.tsx | 16 ++++++++++ web/src/store/chatStore.ts | 50 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/web/src/components/MessageList.tsx b/web/src/components/MessageList.tsx index 5dcd3a4..5d77118 100644 --- a/web/src/components/MessageList.tsx +++ b/web/src/components/MessageList.tsx @@ -24,6 +24,9 @@ export function MessageList() { const activeChatId = useChatStore((s) => s.activeChatId); const messagesByChat = useChatStore((s) => s.messagesByChat); const typingByChat = useChatStore((s) => s.typingByChat); + const hasMoreByChat = useChatStore((s) => s.hasMoreByChat); + const loadingMoreByChat = useChatStore((s) => s.loadingMoreByChat); + const loadMoreMessages = useChatStore((s) => s.loadMoreMessages); const unreadBoundaryByChat = useChatStore((s) => s.unreadBoundaryByChat); const chats = useChatStore((s) => s.chats); const setReplyToMessage = useChatStore((s) => s.setReplyToMessage); @@ -61,6 +64,8 @@ export function MessageList() { }, [chats, forwardQuery]); const unreadBoundaryCount = activeChatId ? (unreadBoundaryByChat[activeChatId] ?? 0) : 0; const unreadBoundaryIndex = unreadBoundaryCount > 0 ? Math.max(0, messages.length - unreadBoundaryCount) : -1; + const hasMore = Boolean(activeChatId && hasMoreByChat[activeChatId]); + const isLoadingMore = Boolean(activeChatId && loadingMoreByChat[activeChatId]); const selectedMessages = useMemo( () => messages.filter((m) => selectedIds.has(m.id)), [messages, selectedIds] @@ -254,6 +259,17 @@ export function MessageList() { ) : null}
+ {hasMore ? ( +
+ +
+ ) : null} {messages.map((message, messageIndex) => { const own = message.sender_id === me?.id; const replySource = message.reply_to_message_id ? messagesMap.get(message.reply_to_message_id) : null; diff --git a/web/src/store/chatStore.ts b/web/src/store/chatStore.ts index 8a261bf..e22f902 100644 --- a/web/src/store/chatStore.ts +++ b/web/src/store/chatStore.ts @@ -7,12 +7,15 @@ interface ChatState { activeChatId: number | null; messagesByChat: Record; draftsByChat: Record; + hasMoreByChat: Record; + loadingMoreByChat: Record; typingByChat: Record; replyToByChat: Record; unreadBoundaryByChat: Record; loadChats: (query?: string) => Promise; setActiveChatId: (chatId: number | null) => void; loadMessages: (chatId: number) => Promise; + loadMoreMessages: (chatId: number) => Promise; prependMessage: (chatId: number, message: Message) => boolean; addOptimisticMessage: (params: { chatId: number; @@ -48,6 +51,8 @@ export const useChatStore = create((set, get) => ({ activeChatId: null, messagesByChat: {}, draftsByChat: {}, + hasMoreByChat: {}, + loadingMoreByChat: {}, typingByChat: {}, replyToByChat: {}, unreadBoundaryByChat: {}, @@ -71,6 +76,14 @@ export const useChatStore = create((set, get) => ({ ...state.unreadBoundaryByChat, [chatId]: unreadCount }, + hasMoreByChat: { + ...state.hasMoreByChat, + [chatId]: messages.length >= 50 + }, + loadingMoreByChat: { + ...state.loadingMoreByChat, + [chatId]: false + }, chats: state.chats.map((chat) => (chat.id === chatId ? { ...chat, unread_count: 0 } : chat)) })); const lastMessage = sorted[sorted.length - 1]; @@ -78,6 +91,43 @@ export const useChatStore = create((set, get) => ({ void updateMessageStatus(chatId, lastMessage.id, "message_read"); } }, + loadMoreMessages: async (chatId) => { + if (get().loadingMoreByChat[chatId]) { + return; + } + const existing = get().messagesByChat[chatId] ?? []; + if (!existing.length) { + await get().loadMessages(chatId); + return; + } + const oldestId = existing[0]?.id; + if (!oldestId) { + return; + } + set((state) => ({ + loadingMoreByChat: { ...state.loadingMoreByChat, [chatId]: true } + })); + try { + const older = await getMessages(chatId, oldestId); + const olderSorted = [...older].reverse(); + const knownIds = new Set(existing.map((m) => m.id)); + const merged = [...olderSorted.filter((m) => !knownIds.has(m.id)), ...existing]; + set((state) => ({ + messagesByChat: { + ...state.messagesByChat, + [chatId]: merged + }, + hasMoreByChat: { + ...state.hasMoreByChat, + [chatId]: older.length >= 50 + } + })); + } finally { + set((state) => ({ + loadingMoreByChat: { ...state.loadingMoreByChat, [chatId]: false } + })); + } + }, prependMessage: (chatId, message) => { const old = get().messagesByChat[chatId] ?? []; if (old.some((m) => m.id === message.id || (message.client_message_id && m.client_message_id === message.client_message_id))) {