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