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:
@@ -32,20 +32,25 @@ interface ConversationState {
|
||||
activeConversationId: string | null;
|
||||
messagesByConversation: Record<string, ConversationMessage[]>;
|
||||
isLoading: boolean;
|
||||
isLoadingOlder: boolean;
|
||||
hasMoreByConversation: Record<string, boolean>;
|
||||
error: string | null;
|
||||
fetchConversations: () => Promise<void>;
|
||||
createConversation: (userIds: string[]) => Promise<Conversation>;
|
||||
setActiveConversation: (id: string | null) => void;
|
||||
fetchMessages: (conversationId: string, before?: string) => Promise<void>;
|
||||
fetchOlderMessages: (conversationId: string) => Promise<void>;
|
||||
sendMessage: (conversationId: string, content: string) => Promise<ConversationMessage>;
|
||||
addMessage: (message: ConversationMessage) => void;
|
||||
}
|
||||
|
||||
export const useConversationStore = create<ConversationState>((set, _get) => ({
|
||||
export const useConversationStore = create<ConversationState>((set, get) => ({
|
||||
conversations: [],
|
||||
activeConversationId: null,
|
||||
messagesByConversation: {},
|
||||
isLoading: false,
|
||||
isLoadingOlder: false,
|
||||
hasMoreByConversation: {},
|
||||
error: null,
|
||||
|
||||
fetchConversations: async () => {
|
||||
@@ -77,10 +82,15 @@ export const useConversationStore = create<ConversationState>((set, _get) => ({
|
||||
const messages = await api.get<ConversationMessage[]>(
|
||||
`/conversations/${conversationId}/messages${params}`,
|
||||
);
|
||||
const list = Array.isArray(messages) ? messages : [];
|
||||
set((state) => ({
|
||||
messagesByConversation: {
|
||||
...state.messagesByConversation,
|
||||
[conversationId]: Array.isArray(messages) ? messages : [],
|
||||
[conversationId]: list,
|
||||
},
|
||||
hasMoreByConversation: {
|
||||
...state.hasMoreByConversation,
|
||||
[conversationId]: list.length >= 50,
|
||||
},
|
||||
isLoading: false,
|
||||
}));
|
||||
@@ -89,6 +99,34 @@ export const useConversationStore = create<ConversationState>((set, _get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
fetchOlderMessages: async (conversationId) => {
|
||||
const state = get();
|
||||
if (state.isLoadingOlder || state.hasMoreByConversation[conversationId] === false) return;
|
||||
const existing = state.messagesByConversation[conversationId] || [];
|
||||
if (existing.length === 0) return;
|
||||
const oldestId = existing[0].id;
|
||||
set({ isLoadingOlder: true });
|
||||
try {
|
||||
const older = await api.get<ConversationMessage[]>(
|
||||
`/conversations/${conversationId}/messages?before=${encodeURIComponent(oldestId)}`,
|
||||
);
|
||||
const list = Array.isArray(older) ? older : [];
|
||||
set((state) => ({
|
||||
messagesByConversation: {
|
||||
...state.messagesByConversation,
|
||||
[conversationId]: [...list, ...existing],
|
||||
},
|
||||
hasMoreByConversation: {
|
||||
...state.hasMoreByConversation,
|
||||
[conversationId]: list.length >= 50,
|
||||
},
|
||||
isLoadingOlder: false,
|
||||
}));
|
||||
} catch {
|
||||
set({ isLoadingOlder: false });
|
||||
}
|
||||
},
|
||||
|
||||
sendMessage: async (conversationId, content) => {
|
||||
const message = await api.post<ConversationMessage>(
|
||||
`/conversations/${conversationId}/messages`,
|
||||
|
||||
@@ -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)}`,
|
||||
|
||||
Reference in New Issue
Block a user