import { create } from 'zustand'; import { ChatApi } from '../infrastructure/chatApi'; import { useAuthStore } from '../../auth/application/authStore'; import type { Chat, ChatMember, Message, TypingUser } from '../../../core/domain/types'; interface ChatState { chats: Chat[]; activeChat: string | null; messages: Record; pinnedMessages: Record; typingUsers: TypingUser[]; replyTo: Message | null; editingMessage: Message | null; isLoadingChats: boolean; isLoadingMessages: boolean; searchQuery: string; drafts: Record; hasMoreMessages: Record; setActiveChat: (chatId: string | null) => void; setSearchQuery: (query: string) => void; setDraft: (chatId: string, text: string) => void; getDraft: (chatId: string) => string; loadChats: () => Promise; loadMessages: (chatId: string, reset?: boolean, isHistory?: boolean) => Promise; addMessage: (message: Message) => void; updateMessage: (message: Message) => void; removeMessage: (messageId: string, chatId: string) => void; removeMessages: (messageIds: string[], chatId: string) => void; hideMessages: (messageIds: string[], chatId: string) => void; addReaction: (messageId: string, chatId: string, userId: string, username: string, emoji: string) => void; removeReaction: (messageId: string, chatId: string, userId: string, emoji: string) => void; markRead: (chatId: string, userId: string, lastReadSequenceId: number) => void; markAllAsRead: (chatId: string) => void; addTypingUser: (chatId: string, userId: string) => void; removeTypingUser: (chatId: string, userId: string) => void; updateUserOnlineStatus: (userId: string, isOnline: boolean, lastSeen?: string) => void; setReplyTo: (message: Message | null) => void; setEditingMessage: (message: Message | null) => void; addChat: (chat: Chat) => void; updateChat: (chat: Chat) => void; removeChat: (chatId: string) => void; clearMessages: (chatId: string) => void; setPinnedMessage: (chatId: string, message: Message) => void; removePinnedMessage: (chatId: string, messageId: string, newPinned?: Message[] | null) => void; jumpToMessage: (chatId: string, sequenceId: number) => Promise; clearStore: () => void; } export const useChatStore = create((set, get) => ({ chats: [], activeChat: null, messages: {}, pinnedMessages: {}, typingUsers: [], replyTo: null, editingMessage: null, isLoadingChats: false, isLoadingMessages: false, searchQuery: '', drafts: JSON.parse(localStorage.getItem('knot_drafts') || '{}'), hasMoreMessages: {}, setActiveChat: (chatId) => set((state) => ({ activeChat: chatId, replyTo: null, editingMessage: null, chats: chatId ? state.chats.map((c) => c.id === chatId ? { ...c, unreadCount: 0 } : c) : state.chats, })), setSearchQuery: (query) => set({ searchQuery: query }), setDraft: (chatId, text) => { set((state) => { const drafts = { ...state.drafts }; if (text.trim()) { drafts[chatId] = text; } else { delete drafts[chatId]; } localStorage.setItem('knot_drafts', JSON.stringify(drafts)); return { drafts }; }); }, getDraft: (chatId) => { return get().drafts[chatId] || ''; }, loadChats: async () => { try { set({ isLoadingChats: true }); const chats = await ChatApi.getChats(); // Auto-create favorites chat if not present if (!chats.some((c: any) => c.type === 'favorites')) { try { const favChat = await ChatApi.getOrCreateFavorites(); chats.unshift(favChat); } catch { } } // Extract pinned messages from chats const pinnedMessages: Record = {}; for (const chat of chats) { if (chat.pinnedMessages && chat.pinnedMessages.length > 0) { pinnedMessages[chat.id] = chat.pinnedMessages.map((pm: any) => pm.message); } } set({ chats, pinnedMessages, isLoadingChats: false }); } catch (error: any) { console.error('Load chats error:', error); set({ isLoadingChats: false }); const { addNotification } = (await import('../../../core/application/stores/notificationStore')).useNotificationStore.getState(); addNotification('error', error.message || 'Failed to load chats'); } }, loadMessages: async (chatId, reset = false, isHistory = false) => { try { const state = get(); if (!reset && !isHistory && typeof state.hasMoreMessages[chatId] !== 'undefined') return; if (!reset && isHistory && state.messages[chatId] && state.hasMoreMessages[chatId] === false) return; if (state.isLoadingMessages) return; set({ isLoadingMessages: true }); const currentMessages = state.messages[chatId] || []; const cursor = !reset && currentMessages.length > 0 ? currentMessages[0].sequenceId.toString() : undefined; const fetched = await ChatApi.getMessages(chatId, cursor); set((state) => { // Merge fetched messages with any that arrived via socket const existing = reset ? [] : (state.messages[chatId] || []); const fetchedIds = new Set(fetched.map(m => m.id)); const socketOnly = existing.filter(m => !fetchedIds.has(m.id)); const merged = [...fetched, ...socketOnly].sort((a, b) => { const tDiff = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); return tDiff !== 0 ? tDiff : a.sequenceId - b.sequenceId; }); return { messages: { ...state.messages, [chatId]: merged }, hasMoreMessages: { ...state.hasMoreMessages, [chatId]: fetched.length >= 50 }, isLoadingMessages: false, }; }); } catch (error: any) { console.error('Load messages error:', error); set({ isLoadingMessages: false }); const { addNotification } = (await import('../../../core/application/stores/notificationStore')).useNotificationStore.getState(); addNotification('error', error.message || 'Failed to load messages'); } }, addMessage: (message) => { const userId = useAuthStore.getState().user?.id; set((state) => { const chatMessages = state.messages[message.chatId] || []; if (chatMessages.some((m) => m.id === message.id)) return state; const updatedMessages = { ...state.messages, [message.chatId]: [...chatMessages, message], }; const updatedChats = state.chats.map((chat) => { if (chat.id === message.chatId) { return { ...chat, messages: [message], unreadCount: (chat.id === state.activeChat || message.senderId === userId) ? chat.unreadCount : chat.unreadCount + 1, }; } return chat; }); updatedChats.sort((a, b) => { const aPin = a.members?.find((m) => m.user?.id === userId)?.isPinned ? 1 : 0; const bPin = b.members?.find((m) => m.user?.id === userId)?.isPinned ? 1 : 0; if (aPin !== bPin) return bPin - aPin; const aTime = a.messages[0]?.createdAt || a.createdAt; const bTime = b.messages[0]?.createdAt || b.createdAt; return new Date(bTime).getTime() - new Date(aTime).getTime(); }); return { messages: updatedMessages, chats: updatedChats }; }); }, updateMessage: (message) => { set((state) => { const chatMessages = state.messages[message.chatId] || []; const updatedMessages = chatMessages.map((m) => (m.id === message.id ? { ...m, ...message } : m)); const updatedChats = state.chats.map((chat) => { if (chat.id === message.chatId) { return { ...chat, messages: chat.messages?.map((m) => (m.id === message.id ? { ...m, ...message } : m)), }; } return chat; }); return { messages: { ...state.messages, [message.chatId]: updatedMessages, }, chats: updatedChats, }; }); }, removeMessage: (messageId, chatId) => { set((state) => { const chatMessages = state.messages[chatId] || []; const updatedMessages = chatMessages.map((m) => m.id === messageId ? { ...m, isDeleted: true, content: null } : m ); // Find the latest non-deleted message to show in sidebar const latestVisible = updatedMessages .filter(m => !m.isDeleted) .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0]; const updatedChats = state.chats.map((chat) => { if (chat.id === chatId) { // If the deleted message was the last message shown, replace with previous one const currentLast = chat.messages?.[0]; if (currentLast?.id === messageId) { return { ...chat, messages: latestVisible ? [latestVisible] : [{ ...currentLast, isDeleted: true, content: null }], }; } } return chat; }); return { messages: { ...state.messages, [chatId]: updatedMessages, }, chats: updatedChats, }; }); }, removeMessages: (messageIds, chatId) => { const idsSet = new Set(messageIds); set((state) => { const chatMessages = state.messages[chatId] || []; const updatedMessages = chatMessages.map((m) => idsSet.has(m.id) ? { ...m, isDeleted: true, content: null } : m ); const latestVisible = updatedMessages .filter(m => !m.isDeleted) .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0]; const updatedChats = state.chats.map((chat) => { if (chat.id === chatId) { const currentLast = chat.messages?.[0]; if (currentLast && idsSet.has(currentLast.id)) { return { ...chat, messages: latestVisible ? [latestVisible] : [{ ...currentLast, isDeleted: true, content: null }], }; } } return chat; }); return { messages: { ...state.messages, [chatId]: updatedMessages }, chats: updatedChats, }; }); }, hideMessages: (messageIds, chatId) => { const idsSet = new Set(messageIds); set((state) => { const chatMessages = state.messages[chatId] || []; const updatedMessages = chatMessages.filter((m) => !idsSet.has(m.id)); const latestVisible = updatedMessages .filter(m => !m.isDeleted) .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0]; const updatedChats = state.chats.map((chat) => { if (chat.id === chatId) { const currentLast = chat.messages?.[0]; if (currentLast && idsSet.has(currentLast.id)) { return { ...chat, messages: latestVisible ? [latestVisible] : [], }; } } return chat; }); return { messages: { ...state.messages, [chatId]: updatedMessages }, chats: updatedChats, }; }); }, addReaction: (messageId, chatId, userId, username, emoji) => { console.log('[ChatStore] addReaction called:', { messageId, chatId, userId, username, emoji }); set((state) => { const chatMessages = state.messages[chatId] || []; const updateMsg = (m: Message) => { if (m.id === messageId) { const reactions = m.reactions || []; const exists = reactions.some((r) => r.userId === userId && r.emoji === emoji); if (exists) { console.log('[ChatStore] Reaction already exists, skipping'); return m; } console.log('[ChatStore] Adding reaction to message:', m.id); return { ...m, reactions: [ ...reactions, { id: `${messageId}-${userId}-${emoji}`, emoji, userId, user: { id: userId, username, displayName: username } }, ], }; } return m; }; const updatedMessages = chatMessages.map(updateMsg); const updatedChats = state.chats.map((chat) => { if (chat.id === chatId) { return { ...chat, messages: chat.messages?.map(updateMsg), }; } return chat; }); console.log('[ChatStore] State updated for chatId:', chatId); return { messages: { ...state.messages, [chatId]: updatedMessages, }, chats: updatedChats, }; }); }, removeReaction: (messageId, chatId, userId, emoji) => { console.log('[ChatStore] removeReaction called:', { messageId, chatId, userId, emoji }); set((state) => { const chatMessages = state.messages[chatId] || []; const updateMsg = (m: Message) => { if (m.id === messageId) { console.log('[ChatStore] Removing reaction from message:', m.id); return { ...m, reactions: (m.reactions || []).filter((r) => !(r.userId === userId && r.emoji === emoji)), }; } return m; }; const updatedMessages = chatMessages.map(updateMsg); const updatedChats = state.chats.map((chat) => { if (chat.id === chatId) { return { ...chat, messages: chat.messages?.map(updateMsg), }; } return chat; }); console.log('[ChatStore] State updated for chatId:', chatId); return { messages: { ...state.messages, [chatId]: updatedMessages, }, chats: updatedChats, }; }); }, markRead: (chatId, userId, lastReadSequenceId) => { const currentUserId = useAuthStore.getState().user?.id; set((state) => { const chatMessages = state.messages[chatId] || []; let newlyReadCount = 0; const updateMsg = (m: Message) => { if (m.sequenceId <= lastReadSequenceId) { const alreadyRead = m.readBy?.some((r) => r.userId === userId); if (alreadyRead) return m; if (userId === currentUserId && m.senderId !== currentUserId) newlyReadCount++; return { ...m, readBy: [...(m.readBy || []), { userId }] }; } return m; }; const updatedMessages = chatMessages.map(updateMsg); const updatedChats = state.chats.map((chat) => { if (chat.id === chatId) { const updatedLastMessages = chat.messages?.map(updateMsg); if (userId === currentUserId) { return { ...chat, messages: updatedLastMessages, unreadCount: Math.max(0, (chat.unreadCount || 0) - newlyReadCount) }; } return { ...chat, messages: updatedLastMessages }; } return chat; }); return { messages: { ...state.messages, [chatId]: updatedMessages, }, chats: updatedChats, }; }); }, markAllAsRead: (chatId) => { set((state) => { const updatedChats = state.chats.map((chat) => { if (chat.id === chatId) { return { ...chat, unreadCount: 0 }; } return chat; }); return { chats: updatedChats }; }); }, addTypingUser: (chatId, userId) => { set((state) => { const exists = state.typingUsers.some((t) => t.chatId === chatId && t.userId === userId); if (exists) return state; return { typingUsers: [...state.typingUsers, { chatId, userId }] }; }); }, removeTypingUser: (chatId, userId) => { set((state) => ({ typingUsers: state.typingUsers.filter((t) => !(t.chatId === chatId && t.userId === userId)), })); }, updateUserOnlineStatus: (userId, isOnline, lastSeen) => { set((state) => ({ chats: state.chats.map((chat) => ({ ...chat, members: chat.members.map((m) => m.user.id === userId ? { ...m, user: { ...m.user, isOnline, lastSeen: lastSeen || m.user.lastSeen } } : m ), })), })); }, setReplyTo: (message) => set({ replyTo: message, editingMessage: null }), setEditingMessage: (message) => set({ editingMessage: message, replyTo: null }), addChat: (chat) => { set((state) => { const existing = state.chats.find((c) => c.id === chat.id); const messagesFromState = state.messages[chat.id] || []; const messagesToUse = messagesFromState.length > 0 ? messagesFromState : (chat.messages || []); let unreadCount = chat.unreadCount || 0; if (!existing && messagesFromState.length > 0) { const userId = useAuthStore.getState().user?.id; unreadCount = messagesFromState.filter((m) => m.senderId !== userId && !m.readBy?.some(r => r.userId === userId)).length; } const updatedChat = { ...chat, messages: messagesToUse.length > 0 ? [messagesToUse[messagesToUse.length - 1]] : [], unreadCount }; if (existing) { return { chats: state.chats.map((c) => (c.id === chat.id ? { ...c, ...updatedChat } : c)), }; } return { chats: [updatedChat, ...state.chats] }; }); }, updateChat: (chat) => { set((state) => ({ chats: state.chats.map((c) => (c.id === chat.id ? { ...c, ...chat } : c)), })); }, removeChat: (chatId) => { set((state) => ({ chats: state.chats.filter((c) => c.id !== chatId), activeChat: state.activeChat === chatId ? null : state.activeChat, messages: (() => { const m = { ...state.messages }; delete m[chatId]; return m; })(), })); }, clearMessages: (chatId) => { set((state) => ({ messages: { ...state.messages, [chatId]: [] }, chats: state.chats.map((c) => c.id === chatId ? { ...c, messages: [] } : c ), })); }, setPinnedMessage: (chatId, message) => { set((state) => { const existing = state.pinnedMessages[chatId] || []; if (existing.some(m => m.id === message.id)) return state; return { pinnedMessages: { ...state.pinnedMessages, [chatId]: [...existing, message] }, }; }); }, removePinnedMessage: (chatId, messageId, newPinned?) => { set((state) => { const updated = { ...state.pinnedMessages }; if (newPinned) { updated[chatId] = newPinned; } else { const filtered = (updated[chatId] || []).filter(m => m.id !== messageId); if (filtered.length === 0) delete updated[chatId]; else updated[chatId] = filtered; } return { pinnedMessages: updated }; }); }, jumpToMessage: async (chatId, sequenceId) => { try { set({ isLoadingMessages: true }); const fetched = await ChatApi.getMessages(chatId, undefined, sequenceId, 50); set((state) => ({ messages: { ...state.messages, [chatId]: fetched }, // Since we jumped, we assume there is more history to load above hasMoreMessages: { ...state.hasMoreMessages, [chatId]: true }, isLoadingMessages: false, })); } catch (error: any) { console.error('Jump to message error:', error); set({ isLoadingMessages: false }); const { addNotification } = (await import('../../../core/application/stores/notificationStore')).useNotificationStore.getState(); addNotification('error', error.message || 'Failed to jump to message'); } }, clearStore: () => { set({ chats: [], activeChat: null, messages: {}, pinnedMessages: {}, typingUsers: [], replyTo: null, editingMessage: null, }); }, }));