08dfc4e400
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.
159 lines
4.7 KiB
TypeScript
159 lines
4.7 KiB
TypeScript
import { create } from "zustand";
|
|
import { api } from "../lib/api.ts";
|
|
|
|
export interface ConversationMember {
|
|
id: string;
|
|
username: string;
|
|
display_name: string;
|
|
avatar: string;
|
|
}
|
|
|
|
export interface Conversation {
|
|
id: string;
|
|
type: "dm" | "group_dm";
|
|
name: string;
|
|
members: ConversationMember[];
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ConversationMessage {
|
|
id: string;
|
|
conversation_id: string;
|
|
author_id: string;
|
|
author_username: string;
|
|
author_display_name: string | null;
|
|
content: string;
|
|
created_at: string;
|
|
edited_at: string | null;
|
|
}
|
|
|
|
interface ConversationState {
|
|
conversations: Conversation[];
|
|
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) => ({
|
|
conversations: [],
|
|
activeConversationId: null,
|
|
messagesByConversation: {},
|
|
isLoading: false,
|
|
isLoadingOlder: false,
|
|
hasMoreByConversation: {},
|
|
error: null,
|
|
|
|
fetchConversations: async () => {
|
|
try {
|
|
const conversations = await api.get<Conversation[]>("/conversations");
|
|
set({ conversations: Array.isArray(conversations) ? conversations : [] });
|
|
} catch (error) {
|
|
set({
|
|
error: error instanceof Error ? error.message : "Failed to fetch conversations",
|
|
});
|
|
}
|
|
},
|
|
|
|
createConversation: async (userIds) => {
|
|
const conversation = await api.post<Conversation>("/conversations", { user_ids: userIds });
|
|
set((state) => ({
|
|
conversations: [conversation, ...state.conversations],
|
|
activeConversationId: conversation.id,
|
|
}));
|
|
return conversation;
|
|
},
|
|
|
|
setActiveConversation: (id) => set({ activeConversationId: id }),
|
|
|
|
fetchMessages: async (conversationId, before) => {
|
|
set({ isLoading: true });
|
|
try {
|
|
const params = before ? "?before=" + encodeURIComponent(before) : "";
|
|
const messages = await api.get<ConversationMessage[]>(
|
|
`/conversations/${conversationId}/messages${params}`,
|
|
);
|
|
const list = Array.isArray(messages) ? messages : [];
|
|
set((state) => ({
|
|
messagesByConversation: {
|
|
...state.messagesByConversation,
|
|
[conversationId]: list,
|
|
},
|
|
hasMoreByConversation: {
|
|
...state.hasMoreByConversation,
|
|
[conversationId]: list.length >= 50,
|
|
},
|
|
isLoading: false,
|
|
}));
|
|
} catch (error) {
|
|
set({ isLoading: false, error: error instanceof Error ? error.message : "Failed" });
|
|
}
|
|
},
|
|
|
|
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`,
|
|
{ content },
|
|
);
|
|
set((state) => ({
|
|
messagesByConversation: {
|
|
...state.messagesByConversation,
|
|
[conversationId]: [
|
|
...(state.messagesByConversation[conversationId] || []),
|
|
message,
|
|
],
|
|
},
|
|
}));
|
|
return message;
|
|
},
|
|
|
|
addMessage: (message) => {
|
|
set((state) => ({
|
|
messagesByConversation: {
|
|
...state.messagesByConversation,
|
|
[message.conversation_id]: [
|
|
...(state.messagesByConversation[message.conversation_id] || []),
|
|
message,
|
|
],
|
|
},
|
|
}));
|
|
},
|
|
}));
|