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
This commit is contained in:
@@ -24,6 +24,9 @@ export function MessageList() {
|
|||||||
const activeChatId = useChatStore((s) => s.activeChatId);
|
const activeChatId = useChatStore((s) => s.activeChatId);
|
||||||
const messagesByChat = useChatStore((s) => s.messagesByChat);
|
const messagesByChat = useChatStore((s) => s.messagesByChat);
|
||||||
const typingByChat = useChatStore((s) => s.typingByChat);
|
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 unreadBoundaryByChat = useChatStore((s) => s.unreadBoundaryByChat);
|
||||||
const chats = useChatStore((s) => s.chats);
|
const chats = useChatStore((s) => s.chats);
|
||||||
const setReplyToMessage = useChatStore((s) => s.setReplyToMessage);
|
const setReplyToMessage = useChatStore((s) => s.setReplyToMessage);
|
||||||
@@ -61,6 +64,8 @@ export function MessageList() {
|
|||||||
}, [chats, forwardQuery]);
|
}, [chats, forwardQuery]);
|
||||||
const unreadBoundaryCount = activeChatId ? (unreadBoundaryByChat[activeChatId] ?? 0) : 0;
|
const unreadBoundaryCount = activeChatId ? (unreadBoundaryByChat[activeChatId] ?? 0) : 0;
|
||||||
const unreadBoundaryIndex = unreadBoundaryCount > 0 ? Math.max(0, messages.length - unreadBoundaryCount) : -1;
|
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(
|
const selectedMessages = useMemo(
|
||||||
() => messages.filter((m) => selectedIds.has(m.id)),
|
() => messages.filter((m) => selectedIds.has(m.id)),
|
||||||
[messages, selectedIds]
|
[messages, selectedIds]
|
||||||
@@ -254,6 +259,17 @@ export function MessageList() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="tg-scrollbar flex-1 overflow-auto px-3 py-4 md:px-6">
|
<div className="tg-scrollbar flex-1 overflow-auto px-3 py-4 md:px-6">
|
||||||
|
{hasMore ? (
|
||||||
|
<div className="mb-3 flex justify-center">
|
||||||
|
<button
|
||||||
|
className="rounded-full border border-slate-700/80 bg-slate-900/70 px-3 py-1 text-xs text-slate-300 hover:bg-slate-800 disabled:opacity-60"
|
||||||
|
disabled={isLoadingMore}
|
||||||
|
onClick={() => void loadMoreMessages(chatId)}
|
||||||
|
>
|
||||||
|
{isLoadingMore ? "Loading..." : "Load older messages"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{messages.map((message, messageIndex) => {
|
{messages.map((message, messageIndex) => {
|
||||||
const own = message.sender_id === me?.id;
|
const own = message.sender_id === me?.id;
|
||||||
const replySource = message.reply_to_message_id ? messagesMap.get(message.reply_to_message_id) : null;
|
const replySource = message.reply_to_message_id ? messagesMap.get(message.reply_to_message_id) : null;
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ interface ChatState {
|
|||||||
activeChatId: number | null;
|
activeChatId: number | null;
|
||||||
messagesByChat: Record<number, Message[]>;
|
messagesByChat: Record<number, Message[]>;
|
||||||
draftsByChat: Record<number, string>;
|
draftsByChat: Record<number, string>;
|
||||||
|
hasMoreByChat: Record<number, boolean>;
|
||||||
|
loadingMoreByChat: Record<number, boolean>;
|
||||||
typingByChat: Record<number, number[]>;
|
typingByChat: Record<number, number[]>;
|
||||||
replyToByChat: Record<number, Message | null>;
|
replyToByChat: Record<number, Message | null>;
|
||||||
unreadBoundaryByChat: Record<number, number>;
|
unreadBoundaryByChat: Record<number, number>;
|
||||||
loadChats: (query?: string) => Promise<void>;
|
loadChats: (query?: string) => Promise<void>;
|
||||||
setActiveChatId: (chatId: number | null) => void;
|
setActiveChatId: (chatId: number | null) => void;
|
||||||
loadMessages: (chatId: number) => Promise<void>;
|
loadMessages: (chatId: number) => Promise<void>;
|
||||||
|
loadMoreMessages: (chatId: number) => Promise<void>;
|
||||||
prependMessage: (chatId: number, message: Message) => boolean;
|
prependMessage: (chatId: number, message: Message) => boolean;
|
||||||
addOptimisticMessage: (params: {
|
addOptimisticMessage: (params: {
|
||||||
chatId: number;
|
chatId: number;
|
||||||
@@ -48,6 +51,8 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
activeChatId: null,
|
activeChatId: null,
|
||||||
messagesByChat: {},
|
messagesByChat: {},
|
||||||
draftsByChat: {},
|
draftsByChat: {},
|
||||||
|
hasMoreByChat: {},
|
||||||
|
loadingMoreByChat: {},
|
||||||
typingByChat: {},
|
typingByChat: {},
|
||||||
replyToByChat: {},
|
replyToByChat: {},
|
||||||
unreadBoundaryByChat: {},
|
unreadBoundaryByChat: {},
|
||||||
@@ -71,6 +76,14 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
...state.unreadBoundaryByChat,
|
...state.unreadBoundaryByChat,
|
||||||
[chatId]: unreadCount
|
[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))
|
chats: state.chats.map((chat) => (chat.id === chatId ? { ...chat, unread_count: 0 } : chat))
|
||||||
}));
|
}));
|
||||||
const lastMessage = sorted[sorted.length - 1];
|
const lastMessage = sorted[sorted.length - 1];
|
||||||
@@ -78,6 +91,43 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
void updateMessageStatus(chatId, lastMessage.id, "message_read");
|
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) => {
|
prependMessage: (chatId, message) => {
|
||||||
const old = get().messagesByChat[chatId] ?? [];
|
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))) {
|
if (old.some((m) => m.id === message.id || (message.client_message_id && m.client_message_id === message.client_message_id))) {
|
||||||
|
|||||||
Reference in New Issue
Block a user