import { create } from "zustand"; import { api } from "../lib/api.ts"; import { type Reaction } from "./message.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; reactions?: Reaction[]; } interface ConversationState { conversations: Conversation[]; activeConversationId: string | null; messagesByConversation: Record; isLoading: boolean; isLoadingOlder: boolean; hasMoreByConversation: Record; error: string | null; fetchConversations: () => Promise; createConversation: (userIds: string[]) => Promise; setActiveConversation: (id: string | null) => void; fetchMessages: (conversationId: string, before?: string) => Promise; fetchOlderMessages: (conversationId: string) => Promise; sendMessage: (conversationId: string, content: string) => Promise; addMessage: (message: ConversationMessage) => void; updateMessage: (message: ConversationMessage) => void; deleteMessage: (conversationId: string, messageId: string) => void; addReaction: (conversationId: string, messageId: string, emoji: string, userId: string) => void; removeReaction: (conversationId: string, messageId: string, emoji: string, userId: string) => void; } export const useConversationStore = create((set, get) => ({ conversations: [], activeConversationId: null, messagesByConversation: {}, isLoading: false, isLoadingOlder: false, hasMoreByConversation: {}, error: null, fetchConversations: async () => { try { const conversations = await api.get("/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("/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( `/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; // ponytail: existing is now oldest-first, so existing[0] is the true oldest const oldestId = existing[0].id; set({ isLoadingOlder: true }); try { const older = await api.get( `/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) => { // Add locally so the message appears immediately even if WS lags. // addMessage dedupes, so a late WS event won't double it. const message = await api.post( `/conversations/${conversationId}/messages`, { content }, ); // ponytail: local append as fallback for WS MESSAGE_CREATE; remove if WS reliability improves get().addMessage(message); return message; }, addMessage: (message) => { set((state) => { const existing = state.messagesByConversation[message.conversation_id] || []; if (existing.some((m) => m.id === message.id)) { return state; } return { messagesByConversation: { ...state.messagesByConversation, [message.conversation_id]: [...existing, message] .sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()), }, }; }); }, updateMessage: (message) => { set((state) => { const convMsgs = state.messagesByConversation[message.conversation_id] || []; return { messagesByConversation: { ...state.messagesByConversation, [message.conversation_id]: convMsgs.map((m) => (m.id === message.id ? message : m)), }, }; }); }, deleteMessage: (conversationId, messageId) => { set((state) => { const convMsgs = state.messagesByConversation[conversationId] || []; return { messagesByConversation: { ...state.messagesByConversation, [conversationId]: convMsgs.filter((m) => m.id !== messageId), }, }; }); }, addReaction: (conversationId, messageId, emoji, userId) => { set((state) => { const messages = state.messagesByConversation[conversationId]; if (!messages) return state; const newMessages = messages.map((m) => { if (m.id !== messageId) return m; const reactions = [...(m.reactions || [])]; const existing = reactions.find((r) => r.emoji === emoji); if (existing) { if (!existing.users.includes(userId)) { existing.users.push(userId); existing.count++; } } else { reactions.push({ emoji, count: 1, users: [userId] }); } return { ...m, reactions }; }); return { messagesByConversation: { ...state.messagesByConversation, [conversationId]: newMessages, }, }; }); }, removeReaction: (conversationId, messageId, emoji, userId) => { set((state) => { const messages = state.messagesByConversation[conversationId]; if (!messages) return state; const newMessages = messages.map((m) => { if (m.id !== messageId) return m; let reactions = [...(m.reactions || [])]; const existing = reactions.find((r) => r.emoji === emoji); if (existing) { existing.users = existing.users.filter((id) => id !== userId); existing.count--; if (existing.count <= 0) { reactions = reactions.filter((r) => r.emoji !== emoji); } } return { ...m, reactions }; }); return { messagesByConversation: { ...state.messagesByConversation, [conversationId]: newMessages, }, }; }); }, }));