diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index c52546f..374035d 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -4,7 +4,16 @@ 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 } from "@/lib/api"; +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 { @@ -132,11 +141,7 @@ export default function ChatPage() { }); }, [isConnected, user, allUsers, currentRoom, joinRoom, setCurrentRoom]); - - - - - + // Fetch messages for current room useEffect(() => { if (!currentRoom) return; @@ -151,7 +156,6 @@ export default function ChatPage() { createdAt: msg.createdAt, })); - // Replace only this room's messages setMessages((prev) => { const others = prev.filter((m) => m.roomId !== currentRoom); return [...others, ...normalized]; @@ -162,9 +166,6 @@ export default function ChatPage() { }); }, [currentRoom, setMessages]); - - - // Fetch all users useEffect(() => { if (!user) return; @@ -177,6 +178,122 @@ export default function ChatPage() { .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; @@ -294,14 +411,12 @@ export default function ChatPage() { try { const updated = await addParticipant(convId, userId); - // Normalize participants: if they're plain ID strings, hydrate with names from allUsers 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 }; } - // Already an object — but name might be missing, try to fill it in if (!p.name) { const found = allUsers.find((u) => u._id === (p._id ?? p)); return found ? { _id: found._id, name: found.name } : p; @@ -346,14 +461,11 @@ export default function ChatPage() { } }; - 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", @@ -391,7 +503,6 @@ export default function ChatPage() { const selectedRoom = rooms.find((r) => r.id === currentRoom); - // Participants for manage dropdown (current selected group room) const manageParticipants = selectedRoom?.participants ?? []; const addableUsers = allUsers.filter( (u) => !manageParticipants.some((p: any) => (p._id ?? p) === u._id) @@ -399,7 +510,7 @@ export default function ChatPage() { return (
- {/* ================= LEFT SIDEBAR ================= */} + { /* LEFT SIDEBAR */}
{/* HEADER */}
@@ -461,13 +572,13 @@ export default function ChatPage() { {user.username}
- {/* TABS — only Chats & People */} + {/* TABS */}
- {/* ================= RIGHT CHAT ================= */} + {/* RIGHT CHAT */}
{/* HEADER */}
@@ -760,7 +871,6 @@ export default function ChatPage() { return (
- {/* DATE SEPARATOR */} {showDate && (
@@ -769,7 +879,6 @@ export default function ChatPage() {
)} - {/* MESSAGE */}
{!isMe && ( @@ -785,8 +894,6 @@ export default function ChatPage() { }`} >
{msg.text}
- - {/* TIME */}
{formatTime(msg.createdAt!)}
diff --git a/src/lib/api.ts b/src/lib/api.ts index 1915b7e..136ab65 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -81,4 +81,66 @@ export async function fetchMessages(convId: string): Promise { const res = await fetch(`${API_BASE_URL}/messages/${convId}`); if (!res.ok) throw new Error("Failed to fetch messages"); return res.json(); +} + +export function subscribeToSSE( + userId: string, + handlers: { + onGroupInvite?: (data: { conversation: any }) => void; + onGroupRemoved?: (data: { conversation: any }) => void; + } +): () => void { + const url = `${API_BASE_URL}/conversations/stream?userId=${userId}`; + console.log("[SSE] Opening EventSource:", url); + + const es = new EventSource(url); + + es.onopen = () => { + console.log("[SSE] Connection OPENED. readyState:", es.readyState); + }; + + // Catch-all: any unnamed event (i.e. events without `event:` field in the stream) + es.onmessage = (e: MessageEvent) => { + console.warn("[SSE] Received UNNAMED message (no event type). This means the backend is NOT setting the event name correctly."); + console.warn("[SSE] Unnamed message data:", e.data); + }; + + es.addEventListener("groupInvite", (e: MessageEvent) => { + console.log("[SSE] Received named event: groupInvite"); + console.log("[SSE] groupInvite raw data:", e.data); + try { + const parsed = JSON.parse(e.data); + console.log("[SSE] groupInvite parsed:", parsed); + handlers.onGroupInvite?.(parsed); + } catch (err) { + console.error("[SSE] Failed to parse groupInvite data:", err); + } + }); + + es.addEventListener("groupRemoved", (e: MessageEvent) => { + console.log("[SSE] Received named event: groupRemoved"); + console.log("[SSE] groupRemoved raw data:", e.data); + try { + const parsed = JSON.parse(e.data); + console.log("[SSE] groupRemoved parsed:", parsed); + handlers.onGroupRemoved?.(parsed); + } catch (err) { + console.error("[SSE] Failed to parse groupRemoved data:", err); + } + }); + + es.onerror = (err) => { + console.warn("[SSE] Connection error. readyState:", es.readyState, "| 0=CONNECTING, 1=OPEN, 2=CLOSED"); + console.warn("[SSE] Error object:", err); + if (es.readyState === EventSource.CLOSED) { + console.error("[SSE] Connection CLOSED permanently. Server may have rejected the request."); + } else { + console.log("[SSE] Will auto-retry (readyState=CONNECTING)..."); + } + }; + + return () => { + console.log("[SSE] Closing EventSource for userId:", userId); + es.close(); + }; } \ No newline at end of file