"use client"; import { useState, useEffect, useRef, useMemo } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; import { useSocket, type ChatMessage, type RoomNotice } from "@/context/SocketContext"; import { fetchConversations, createConversation, fetchAllUsers, ConversationType, addParticipant, removeParticipant, fetchMessages, subscribeToSSE, } from "@/lib/api"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Send, Phone, Video, Plus, LogOut, Loader2, Users, User as UserIcon, UserMinus, UserPlus, ChevronDown, } from "lucide-react"; interface Room { id: string; name: string; isGroup: boolean; participants: { _id: string; name: string }[]; } export default function ChatPage() { const router = useRouter(); const { user, isLoading: authLoading, logout } = useAuth(); const { isConnected, mySocketId, joinRoom, leaveRoom, sendMessage, startP2P, messages, notices, currentRoom, setCurrentRoom, clearMessages, p2pReady, setMessages, clearP2PReady, } = useSocket(); const [text, setText] = useState(""); const [roomSearch, setRoomSearch] = useState(""); const [userSearch, setUserSearch] = useState(""); const [activeTab, setActiveTab] = useState<"rooms" | "users">("rooms"); const [showCreateDropdown, setShowCreateDropdown] = useState(false); const [newRoomName, setNewRoomName] = useState(""); const [rooms, setRooms] = useState([]); const [allUsers, setAllUsers] = useState([]); const [showManageDropdown, setShowManageDropdown] = useState(false); const messagesEndRef = useRef(null); const dropdownRef = useRef(null); const manageDropdownRef = useRef(null); // Debounce helper const useDebounce = (value: string, delay: number) => { const [debounced, setDebounced] = useState(value); useEffect(() => { const t = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(t); }, [value, delay]); return debounced; }; const debouncedRoomSearch = useDebounce(roomSearch, 300); const debouncedUserSearch = useDebounce(userSearch, 300); // Auth guard useEffect(() => { if (!authLoading && !user) { router.push("/login"); } }, [authLoading, user, router]); // Fetch rooms & Auto-join useEffect(() => { if (!isConnected || !user?.userId || allUsers.length === 0) return; fetchConversations(user.userId) .then((data) => { const loadedRooms = Array.isArray(data) ? data.map((c) => { const isGroup = c.type === "group"; let roomName = c.name; if (!isGroup) { let otherUserName = "Unknown User"; if (typeof c.participants?.[0] === "object") { const otherUser = c.participants.find( (p: any) => p._id !== user.userId ); otherUserName = otherUser?.name || otherUserName; } else { const otherUserId = c.participants?.find( (id: string) => id !== user.userId ); const otherUser = allUsers.find((u) => u._id === otherUserId); otherUserName = otherUser?.name || otherUserName; } roomName = otherUserName; } return { id: c._id || c.id, name: roomName || "Unnamed Group", isGroup, participants: c.participants || [], }; }) : []; setRooms(loadedRooms); if (!currentRoom && loadedRooms.length > 0) { const firstRoom = loadedRooms[0]; setCurrentRoom(firstRoom.id); joinRoom(firstRoom.id); } }) .catch((err) => { console.error("Failed to fetch conversations", err.message); }); }, [isConnected, user, allUsers, currentRoom, joinRoom, setCurrentRoom]); // Fetch messages for current room useEffect(() => { if (!currentRoom) return; fetchMessages(currentRoom) .then((data) => { const normalized = data.map((msg: any) => ({ _id: msg._id, roomId: msg.conversationId, senderId: msg.senderId._id, username: msg.senderId.name, text: msg.text, createdAt: msg.createdAt, })); setMessages((prev) => { const others = prev.filter((m) => m.roomId !== currentRoom); return [...others, ...normalized]; }); }) .catch((err) => { console.error("Failed to fetch messages:", err); }); }, [currentRoom, setMessages]); // Fetch all users useEffect(() => { if (!user) return; fetchAllUsers() .then((data) => { if (Array.isArray(data)) { setAllUsers(data.filter((u: any) => u.name !== user.username)); } }) .catch(console.error); }, [user]); // ─── SSE: real-time group invite / removal ─────────────────────────────── useEffect(() => { if (!user?.userId) return; console.log("[SSE HOOK] Initializing for user:", user.userId); const cleanup = subscribeToSSE(user.userId, { // Someone added this user to a group conversation onGroupInvite: ({ conversation }) => { console.log("[SSE HOOK] onGroupInvite TRIGGERED"); if (!conversation) { console.warn("[SSE HOOK] groupInvite missing conversation payload"); return; } console.log("[SSE HOOK] groupInvite payload:", conversation); const convId: string = conversation._id ?? conversation.id; const isGroup = conversation.type === "group"; console.log("[SSE HOOK] convId:", convId, "isGroup:", isGroup); // Resolve display name let roomName = conversation.name || "Unnamed Group"; if (!isGroup) { const other = (conversation.participants ?? []).find( (p: any) => (p._id ?? p) !== user.userId ); roomName = other?.name ?? "Direct Message"; } console.log("[SSE HOOK] roomName resolved:", roomName); // Hydrate participants array const participants: { _id: string; name: string }[] = ( conversation.participants ?? [] ).map((p: any) => typeof p === "object" ? { _id: p._id, name: p.name ?? p._id } : { _id: p, name: p } ); console.log("[SSE HOOK] participants:", participants); const newRoom: Room = { id: convId, name: roomName, isGroup, participants, }; console.log("[SSE HOOK] newRoom created:", newRoom); // Add to sidebar if not already there setRooms((prev) => { const exists = prev.find((r) => r.id === convId); if (exists) { console.log("[SSE HOOK] room already exists, skipping add"); return prev; } console.log("[SSE HOOK] adding new room to sidebar"); return [...prev, newRoom]; }); console.log("[SSE HOOK] joining room:", convId); joinRoom(convId); }, // This user was removed from a group onGroupRemoved: ({ conversation }) => { console.log("[SSE HOOK] onGroupRemoved TRIGGERED"); if (!conversation) { console.warn("[SSE HOOK] groupRemoved missing conversation payload"); return; } console.log("[SSE HOOK] groupRemoved payload:", conversation); const roomId = conversation._id ?? conversation.id; console.log("[SSE HOOK] removing roomId:", roomId); // Leave socket room leaveRoom(roomId); console.log("[SSE HOOK] left room:", roomId); // Remove from sidebar setRooms((prev) => { const filtered = prev.filter((r) => r.id !== roomId); console.log("[SSE HOOK] updated rooms after removal:", filtered); return filtered; }); // Clear selection if the removed room was active if (currentRoom === roomId) { console.log("[SSE HOOK] clearing current room selection"); setCurrentRoom(null); } // Drop its messages from state setMessages((prev) => { const filtered = prev.filter((m) => m.roomId !== roomId); console.log("[SSE HOOK] messages cleaned for room:", roomId); return filtered; }); }, }); return cleanup; }, [user?.userId, joinRoom, leaveRoom, setCurrentRoom, setMessages]); // React to P2P Ready event useEffect(() => { if (!p2pReady) return; const targetUser = allUsers.find((u) => u._id === p2pReady.targetUserId); const roomName = targetUser?.name || "Direct Message"; setRooms((prev) => { if (prev.find((r) => r.id === p2pReady.roomId)) return prev; return [ ...prev, { id: p2pReady.roomId, name: roomName, isGroup: false, participants: [] }, ]; }); setActiveTab("rooms"); clearP2PReady(); }, [p2pReady, allUsers, clearP2PReady]); // Scroll to bottom on new messages useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, notices]); // Close create dropdown on outside click useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (!dropdownRef.current) return; if (dropdownRef.current.contains(e.target as Node)) return; setShowCreateDropdown(false); }; document.addEventListener("click", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); // Close manage dropdown on outside click useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (!manageDropdownRef.current) return; if (manageDropdownRef.current.contains(e.target as Node)) return; setShowManageDropdown(false); }; document.addEventListener("click", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); // Filtered rooms const filteredRooms = useMemo(() => { if (!debouncedRoomSearch.trim()) return rooms; return rooms.filter((r) => r.name.toLowerCase().includes(debouncedRoomSearch.toLowerCase()) ); }, [rooms, debouncedRoomSearch]); // Messages for current room const roomMessages = useMemo( () => messages.filter((m) => m.roomId === currentRoom), [messages, currentRoom] ); const roomNotices = useMemo( () => notices.filter((n) => n.roomId === currentRoom), [notices, currentRoom] ); // Handlers const handleSelectRoom = (room: Room) => { if (currentRoom === room.id) return; if (currentRoom) leaveRoom(currentRoom); setCurrentRoom(room.id); joinRoom(room.id); setShowManageDropdown(false); }; const handleSend = () => { if (!text.trim() || !currentRoom) return; sendMessage(currentRoom, text.trim()); setText(""); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const handleCreateRoom = async () => { const name = newRoomName.trim(); if (!name) return; try { const created = await createConversation({ name, type: ConversationType.GROUP, participants: [user!.userId], }); const newRoom: Room = { id: created._id || created.id, name: created.name || name, isGroup: true, participants: created.participants || [], }; setRooms((prev) => prev.find((r) => r.id === newRoom.id) ? prev : [...prev, newRoom] ); setNewRoomName(""); setShowCreateDropdown(false); setActiveTab("rooms"); handleSelectRoom(newRoom); } catch (err) { console.error("Error creating room", err); } }; const handleAddParticipant = async (convId: string, userId: string) => { try { const updated = await addParticipant(convId, userId); const rawParticipants: any[] = updated.participants || []; const hydrated = rawParticipants.map((p: any) => { if (typeof p === "string") { const found = allUsers.find((u) => u._id === p); return found ? { _id: found._id, name: found.name } : { _id: p, name: p }; } if (!p.name) { const found = allUsers.find((u) => u._id === (p._id ?? p)); return found ? { _id: found._id, name: found.name } : p; } return p; }); setRooms((prev) => prev.map((r) => r.id === convId ? { ...r, participants: hydrated } : r ) ); } catch (err: any) { console.error("Add participant failed:", err.message); } }; const handleRemoveParticipant = async (convId: string, userId: string) => { try { const updated = await removeParticipant(convId, userId); const rawParticipants: any[] = updated.participants || []; const hydrated = rawParticipants.map((p: any) => { if (typeof p === "string") { const found = allUsers.find((u) => u._id === p); return found ? { _id: found._id, name: found.name } : { _id: p, name: p }; } if (!p.name) { const found = allUsers.find((u) => u._id === (p._id ?? p)); return found ? { _id: found._id, name: found.name } : p; } return p; }); setRooms((prev) => prev.map((r) => r.id === convId ? { ...r, participants: hydrated } : r ) ); } catch (err: any) { console.error("Remove participant failed:", err.message); } }; const formatDate = (dateStr: string) => { const date = new Date(dateStr); const today = new Date(); const isToday = date.toDateString() === today.toDateString(); if (isToday) return "Today"; return date.toLocaleDateString([], { day: "numeric", month: "short", year: "numeric", }); }; const formatTime = (dateStr: string) => { const date = new Date(dateStr); return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }); }; const handleStartP2P = (targetUser: any) => { if (!targetUser?._id) return; startP2P(targetUser._id); }; const handleLogout = () => { if (currentRoom) leaveRoom(currentRoom); clearMessages(); logout(); }; // Loading / Auth guard if (authLoading || !user) { return (
); } const selectedRoom = rooms.find((r) => r.id === currentRoom); const manageParticipants = selectedRoom?.participants ?? []; const addableUsers = allUsers.filter( (u) => !manageParticipants.some((p: any) => (p._id ?? p) === u._id) ); return (
{ /* LEFT SIDEBAR */}
{/* HEADER */}

Chats

{/* Create Group Dropdown */}
{showCreateDropdown && (
setNewRoomName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleCreateRoom(); }} autoFocus />
)}
{/* Logout */}
{/* USER INFO */}
Logged in as{" "} {user.username}
{/* TABS */}
{/* ROOM LIST */} {activeTab === "rooms" && (
setRoomSearch(e.target.value)} className="rounded-full bg-muted" />
{filteredRooms.length === 0 && (

No conversations yet. Start a chat from People tab!

)} {filteredRooms.map((room) => { const isActive = currentRoom === room.id; return (
handleSelectRoom(room)} className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer transition ${isActive ? "bg-primary text-primary-foreground" : "hover:bg-muted"}`} >
{room.isGroup ? ( ) : ( )}

{room.name}

{room.isGroup ? `${room.participants?.length ?? 0} members` : "Direct Message"}

); })}
)} {/* PEOPLE LIST */} {activeTab === "users" && (
setUserSearch(e.target.value)} className="rounded-full bg-muted" />
{allUsers.length === 0 ? (

No other users found.

) : ( allUsers .filter( (u) => !debouncedUserSearch.trim() || u.name ?.toLowerCase() .includes(debouncedUserSearch.toLowerCase()) ) .map((u) => (
handleStartP2P(u)} className="flex items-center gap-3 p-3 rounded-xl cursor-pointer transition hover:bg-muted" >

{u.name}

Click to chat

)) )}
)}
{/* RIGHT CHAT */}
{/* HEADER */}
{selectedRoom?.isGroup ? ( ) : ( )}
{selectedRoom?.name || "Select a room"} {selectedRoom && (

{selectedRoom.isGroup ? `${selectedRoom.participants?.length ?? 0} members` : "Direct Message"}{" "} · {isConnected ? "Connected" : "Disconnected"}

)}
{/* Right-side action buttons */}
{/* Manage Participants — only for group rooms */} {selectedRoom?.isGroup && (
{showManageDropdown && (

{selectedRoom.name}

{/* Current members */}

Members ({manageParticipants.length})

{manageParticipants.map((p: any) => { const pid = p._id ?? p; const pname = p.name ?? pid; const isSelf = pid === user?.userId; return (
{pname} {isSelf && ( (you) )}
{!isSelf && ( )}
); })}
{/* Add people */} {addableUsers.length > 0 && (

Add people

{addableUsers.map((u) => (
{u.name}
))}
)}
)}
)}
{/* MESSAGES */}
{!currentRoom && (
Select a room to start chatting
)} {currentRoom && roomMessages.length === 0 && roomNotices.length === 0 && (
No messages yet. Say something!
)} {(() => { let lastDate = ""; return roomMessages.map((msg, idx) => { const msgDate = formatDate(msg.createdAt!); const showDate = msgDate !== lastDate; lastDate = msgDate; const isMe = msg.senderId === user?.userId || msg.senderId === mySocketId; return (
{showDate && (
{msgDate}
)}
{!isMe && ( {msg.username} )}
{msg.text}
{formatTime(msg.createdAt!)}
); }); })()} {roomNotices.map((notice, idx) => (
{notice.message}
))}
{/* INPUT */}
setText(e.target.value)} onKeyDown={handleKeyDown} placeholder={ currentRoom ? `Message ${selectedRoom?.name ?? ""}...` : "Select a room first..." } disabled={!currentRoom || !isConnected} />
); }