import { useEffect, useState, useRef } from "react"; import { useSelector, useDispatch } from "react-redux"; import { store } from "./store/store.js"; import "./App.css"; import Sidebar from "./components/Sidebar"; import RoomList from "./components/RoomList"; import ChatArea from "./components/ChatArea"; import MemberList from "./components/MemberList"; import CreateSpace from "./components/createspace/CreateSpace"; import CreateAgent from "./components/createspace/CreateAgent"; import CreateSpaceTips from "./components/createspace/CreateSpaceTips"; import CreateAgentTips from "./components/createspace/CreateAgentTips"; import ManageAgent from "./components/createspace/ManageAgent"; import ManageAgentTips from "./components/createspace/ManageAgentTips"; import LoginPage from "./pages/LoginPage"; import AppLoadingScreen from "./components/AppLoadingScreen"; import { initializeAuth } from "./store/slices/authSlice"; import { addMessage as addDMMessage, updateMessage as updateDMMessage, updateConversationLastMessage, incrementUnreadCount, addConversation, clearUnreadCount, setOnlineUsers, updateUserStatus, preloadAllData, } from "./store/slices/dmSlice"; import { addMessage } from "./store/slices/messageSlice"; import { updateRoomLastMessage, incrementRoomUnreadCount, addJoinedRoom, } from "./store/slices/spaceSlice"; import socketService from "./services/socket.service"; function App() { const dispatch = useDispatch(); const { activeView, activeSpace, activeRoom, searchQuery, isSettings } = useSelector((state) => state.app); const { isAuthenticated, initialized, loading, isLoggingOut } = useSelector( (state) => state.auth, ); const { appLoading, appLoadingPhase } = useSelector((state) => state.app); const [createTab, setCreateTab] = useState("space"); const [editingAgent, setEditingAgent] = useState(null); const [roomListCollapsed, setRoomListCollapsed] = useState(false); const [memberListCollapsed, setMemberListCollapsed] = useState(false); const [isCreatingRoom, setIsCreatingRoom] = useState(false); const [isRoomSettingsOpen, setIsRoomSettingsOpen] = useState(false); // 1. Auth initialization useEffect(() => { if (window.location.pathname !== "/") { window.history.replaceState(null, "", "/"); } if (!initialized && !loading) { dispatch(initializeAuth()); } }, [dispatch, initialized, loading]); // 2. Fetch ALL data once after auth is ready const preloadStartTime = useRef(null); useEffect(() => { if (!isAuthenticated || !initialized) return; preloadStartTime.current = performance.now(); dispatch(preloadAllData()); }, [isAuthenticated, initialized, dispatch]); // Log preload time when loading finishes useEffect(() => { if (!appLoading && preloadStartTime.current !== null) { const elapsed = Math.round(performance.now() - preloadStartTime.current); console.log(`%c[Preload] Tất cả data đã load xong trong ${elapsed}ms`, "color: #22c55e; font-weight: bold; font-size: 14px;"); preloadStartTime.current = null; } }, [appLoading]); // 3. Connect WebSocket when authenticated useEffect(() => { if (isAuthenticated) { socketService.connect(); } else { socketService.disconnect(); } return () => { socketService.disconnect(); }; }, [isAuthenticated]); // ============================================ // Global WebSocket DM Listener — REGISTERED ONCE ONLY // ============================================ useEffect(() => { if (!isAuthenticated) return; const handleNewDM = (data) => { if (!data?.id) return; const conversationId = data.conversation_id || data.conversationId; if (!conversationId) return; const now = Date.now(); const isOwn = String(data.sender_id) === String(store.getState().auth.user?.id); if (!isOwn && data.clientSentAt) { const receiveDelay = now - data.clientSentAt; console.log( `%c[DM Latency] RECEIVE | delay: ${receiveDelay}ms | from: ${data.sender?.display_name || data.sender_id} | msgId: ${data.id}`, "color: #3b82f6; font-weight: bold;", ); } const state = store.getState(); const currentConversations = state.dm.conversations; const currentActiveId = state.dm.activeConversationId; const currentUserId = state.auth.user?.id; const convExists = currentConversations.some((c) => c.id === conversationId); if (!convExists && data.conversation) { dispatch(addConversation(data.conversation)); } else if (!convExists && data.sender) { dispatch( addConversation({ id: conversationId, other_user: data.sender, last_message: { id: data.id, content: data.content, created_at: data.created_at || data.timestamp, }, unread_count: 0, created_at: data.created_at || data.timestamp, }), ); } if (!convExists) { socketService.joinDM(conversationId); } dispatch( addDMMessage({ conversationId, message: { id: data.id, conversation_id: conversationId, sender_id: data.sender_id, content: data.content, is_read: data.is_read ?? false, created_at: data.created_at || data.timestamp, sender: data.sender, tempId: data.tempId || data.temp_id, }, }), ); dispatch( updateConversationLastMessage({ conversationId, message: { id: data.id, content: data.content, created_at: data.created_at || data.timestamp, }, }), ); const isOwnMessage = String(data.sender_id) === String(currentUserId); if (conversationId !== currentActiveId && !isOwnMessage) { dispatch(incrementUnreadCount({ conversationId })); } }; const handleDmSent = (data) => { if (!data?.success || !data?.message) return; const msg = data.message; const conversationId = msg.conversation_id || msg.conversationId; const tempId = data.tempId || data.temp_id; if (!conversationId) return; dispatch( addDMMessage({ conversationId, message: { id: msg.id, conversation_id: conversationId, sender_id: msg.sender_id, content: msg.content, is_read: msg.is_read ?? false, created_at: msg.created_at || msg.timestamp, sender: msg.sender, tempId, }, }), ); dispatch( updateConversationLastMessage({ conversationId, message: { id: msg.id, content: msg.content, created_at: msg.created_at || msg.timestamp, }, }), ); }; const handleDmMarkedRead = (data) => { if (!data?.conversationId || !data?.messageId) return; dispatch( updateDMMessage({ conversationId: data.conversationId, messageId: data.messageId, updates: { is_read: true }, }), ); }; const handleConnected = (data) => { if (data?.onlineUsers) { dispatch(setOnlineUsers(data.onlineUsers)); } }; const handleUserStatusChanged = (data) => { if (!data?.userId) return; dispatch(updateUserStatus({ userId: data.userId, status: data.status })); }; const handleNewMessage = (data) => { console.log("[App] newMessage received:", JSON.stringify(data, null, 2)); const roomId = data.room_id || data.roomId; const content = data.content; const author = data.author; const senderId = data.user_id || data.senderId; const id = data.id; const tempId = data.tempId; if (!roomId || !content) return; console.log("[App] newMessage parsed:", { roomId, id, tempId, author, senderId }); const state = store.getState(); const currentActiveRoom = state.app.activeRoom; const currentUserId = state.auth.user?.id; const isOwnMessage = String(senderId) === String(currentUserId); // 1. Save message to Redux dispatch( addMessage({ roomId, message: { id: id || Date.now(), sender: { id: senderId, display_name: author?.display_name || author?.username || "Unknown", avatar_url: author?.avatar_url || null, }, sender_id: senderId, content, created_at: data.created_at || new Date().toISOString(), isPinned: data.is_pinned || false, isOwn: isOwnMessage, tempId, }, }), ); // 2. Update room's last_message in spaceSlice (for RoomList display) dispatch( updateRoomLastMessage({ roomId, message: { id: id || Date.now(), content, created_at: data.created_at || new Date().toISOString(), sender_id: senderId, sender: author, }, }), ); // 3. Increment unread count if not viewing this room and not own message if (roomId !== currentActiveRoom && !isOwnMessage) { dispatch(incrementRoomUnreadCount({ roomId })); } }; socketService.onConnected(handleConnected); socketService.onUserStatusChanged(handleUserStatusChanged); socketService.onNewDM(handleNewDM); socketService.onDmSent(handleDmSent); socketService.onDmMarkedRead(handleDmMarkedRead); socketService.onNewMessage(handleNewMessage); return () => { socketService.off("connected", handleConnected); socketService.off("userStatusChanged", handleUserStatusChanged); socketService.off("newDM", handleNewDM); socketService.off("dmSent", handleDmSent); socketService.off("dmMarkedRead", handleDmMarkedRead); socketService.off("newMessage", handleNewMessage); }; }, [isAuthenticated, dispatch]); const currentView = isSettings ? "settings" : activeView; // Not initialized yet (checking auth state) OR logging out if (!initialized || isLoggingOut) { return ; } if (!isAuthenticated) { return ; } // 🆕 Loading all data after login if (appLoading) { return ; } const handleEditAgent = (agent) => { setEditingAgent(agent); setCreateTab("editAgent"); }; const handleCancelEdit = () => { setEditingAgent(null); setCreateTab("manageAgent"); }; const renderCreateContent = () => { switch (createTab) { case "agent": return ; case "manageAgent": return ; case "editAgent": return ( ); default: return ; } }; const renderCreateTips = () => { switch (createTab) { case "agent": return ; case "manageAgent": return ; case "editAgent": return ; default: return ; } }; return (
{currentView === "createSpace" ? ( <> {renderCreateContent()} {renderCreateTips()} ) : ( <>
setIsCreatingRoom(true)} />
{/* Collapsed room list indicator */} {roomListCollapsed && (
setRoomListCollapsed(false)} onMouseEnter={(e) => { e.currentTarget.style.background = "var(--hover-primary)"; }} onMouseLeave={(e) => { e.currentTarget.style.background = "var(--bg-surface-secondary)"; }} title="Hiện danh sách room" >
)} setRoomListCollapsed((p) => !p)} onToggleMemberList={() => setMemberListCollapsed((p) => !p)} roomListCollapsed={roomListCollapsed} memberListCollapsed={memberListCollapsed} isCreatingRoom={isCreatingRoom} onCancelCreateRoom={() => setIsCreatingRoom(false)} isRoomSettingsOpen={isRoomSettingsOpen} onOpenRoomSettings={() => setIsRoomSettingsOpen(true)} onCloseRoomSettings={() => setIsRoomSettingsOpen(false)} /> {/* Collapsed member list indicator */} {memberListCollapsed && (
setMemberListCollapsed(false)} onMouseEnter={(e) => { e.currentTarget.style.background = "var(--hover-primary)"; }} onMouseLeave={(e) => { e.currentTarget.style.background = "var(--bg-surface-secondary)"; }} title="Hiện danh sách thành viên" >
)}
)}
); } export default App;