Files
dumpsterChat/web/src/stores/conversation.ts
T
hobokenchicken 08dfc4e400 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.
2026-07-02 09:28:28 -04:00

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,
],
},
}));
},
}));