feat: scroll-to-top loads older messages

Both ChatArea and DMChat now detect when you scroll near the top
and fetch the next 50 older messages. Scroll position is preserved
so you don't lose your place. Works for both server channels and
DM conversations.
This commit is contained in:
2026-07-02 09:28:28 -04:00
parent cbfbcb2627
commit 08dfc4e400
4 changed files with 119 additions and 8 deletions
+40 -2
View File
@@ -32,8 +32,11 @@ export interface MessageState {
searchResultsByChannel: Record<string, SearchResultMessage[]>;
selectedMessageIds: Record<string, Set<string>>; // ponytail: per-channel bulk selection
isLoading: boolean;
isLoadingOlder: boolean;
hasMoreByChannel: Record<string, boolean>;
error: string | null;
fetchMessages: (channelId: string, before?: string) => Promise<void>;
fetchOlderMessages: (channelId: string) => Promise<void>;
sendMessage: (channelId: string, content: string, replyTo?: string) => Promise<Message>;
searchMessages: (channelId: string, query: string) => Promise<SearchResultMessage[]>;
addMessage: (message: Message) => void;
@@ -44,11 +47,13 @@ export interface MessageState {
clearSelectedMessages: (channelId: string) => void;
}
export const useMessageStore = create<MessageState>((set) => ({
export const useMessageStore = create<MessageState>((set, get) => ({
messagesByChannel: {},
searchResultsByChannel: {},
selectedMessageIds: {},
isLoading: false,
isLoadingOlder: false,
hasMoreByChannel: {},
error: null,
fetchMessages: async (channelId, before) => {
@@ -58,10 +63,15 @@ export const useMessageStore = create<MessageState>((set) => ({
const messages = await api.get<Message[]>(
`/channels/${channelId}/messages${params}`,
);
const list = Array.isArray(messages) ? messages : [];
set((state) => ({
messagesByChannel: {
...state.messagesByChannel,
[channelId]: Array.isArray(messages) ? messages : [],
[channelId]: list,
},
hasMoreByChannel: {
...state.hasMoreByChannel,
[channelId]: list.length >= 50,
},
isLoading: false,
}));
@@ -76,6 +86,34 @@ export const useMessageStore = create<MessageState>((set) => ({
}
},
fetchOlderMessages: async (channelId) => {
const state = get();
if (state.isLoadingOlder || state.hasMoreByChannel[channelId] === false) return;
const existing = state.messagesByChannel[channelId] || [];
if (existing.length === 0) return;
const oldestId = existing[0].id;
set({ isLoadingOlder: true });
try {
const older = await api.get<Message[]>(
`/channels/${channelId}/messages?before=${encodeURIComponent(oldestId)}`,
);
const list = Array.isArray(older) ? older : [];
set((state) => ({
messagesByChannel: {
...state.messagesByChannel,
[channelId]: [...list, ...existing],
},
hasMoreByChannel: {
...state.hasMoreByChannel,
[channelId]: list.length >= 50,
},
isLoadingOlder: false,
}));
} catch {
set({ isLoadingOlder: false });
}
},
searchMessages: async (channelId, query) => {
const results = await api.get<SearchResultMessage[]>(
`/channels/${channelId}/messages/search?q=${encodeURIComponent(query)}`,