Reorganize web folder structurally
This commit is contained in:
@@ -0,0 +1,549 @@
|
||||
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<string, Message[]>;
|
||||
pinnedMessages: Record<string, Message>;
|
||||
typingUsers: TypingUser[];
|
||||
replyTo: Message | null;
|
||||
editingMessage: Message | null;
|
||||
isLoadingChats: boolean;
|
||||
isLoadingMessages: boolean;
|
||||
searchQuery: string;
|
||||
drafts: Record<string, string>;
|
||||
hasMoreMessages: Record<string, boolean>;
|
||||
|
||||
setActiveChat: (chatId: string | null) => void;
|
||||
setSearchQuery: (query: string) => void;
|
||||
setDraft: (chatId: string, text: string) => void;
|
||||
getDraft: (chatId: string) => string;
|
||||
loadChats: () => Promise<void>;
|
||||
loadMessages: (chatId: string, reset?: boolean, isHistory?: boolean) => Promise<void>;
|
||||
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, messageIds: string[]) => 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;
|
||||
clearStore: () => void;
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatState>((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<string, Message> = {};
|
||||
for (const chat of chats) {
|
||||
if (chat.pinnedMessages && chat.pinnedMessages.length > 0) {
|
||||
pinnedMessages[chat.id] = chat.pinnedMessages[0].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].createdAt : 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) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
);
|
||||
return {
|
||||
messages: { ...state.messages, [chatId]: merged },
|
||||
hasMoreMessages: { ...state.hasMoreMessages, [chatId]: fetched.length === 100 },
|
||||
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 ? message : m));
|
||||
|
||||
const updatedChats = state.chats.map((chat) => {
|
||||
if (chat.id === message.chatId) {
|
||||
return {
|
||||
...chat,
|
||||
messages: chat.messages?.map((m) => (m.id === message.id ? 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, messageIds) => {
|
||||
const currentUserId = useAuthStore.getState().user?.id;
|
||||
set((state) => {
|
||||
const chatMessages = state.messages[chatId] || [];
|
||||
let newlyReadCount = 0;
|
||||
const updateMsg = (m: Message) => {
|
||||
if (messageIds.includes(m.id)) {
|
||||
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) => ({
|
||||
pinnedMessages: { ...state.pinnedMessages, [chatId]: message },
|
||||
}));
|
||||
},
|
||||
|
||||
removePinnedMessage: (chatId, _messageId, newPinned) => {
|
||||
set((state) => {
|
||||
const updated = { ...state.pinnedMessages };
|
||||
if (newPinned) {
|
||||
updated[chatId] = newPinned;
|
||||
} else {
|
||||
delete updated[chatId];
|
||||
}
|
||||
return { pinnedMessages: updated };
|
||||
});
|
||||
},
|
||||
|
||||
clearStore: () => {
|
||||
set({
|
||||
chats: [],
|
||||
activeChat: null,
|
||||
messages: {},
|
||||
pinnedMessages: {},
|
||||
typingUsers: [],
|
||||
replyTo: null,
|
||||
editingMessage: null,
|
||||
});
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user