implemented the sse in background

This commit is contained in:
sumona-banerjeee 2026-04-17 19:53:19 +05:30
parent d2ea0ff9cb
commit 9b18cf5118
2 changed files with 196 additions and 27 deletions

View File

@ -4,7 +4,16 @@ import { useState, useEffect, useRef, useMemo } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useAuth } from "@/context/AuthContext"; import { useAuth } from "@/context/AuthContext";
import { useSocket, type ChatMessage, type RoomNotice } from "@/context/SocketContext"; 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 { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -132,11 +141,7 @@ export default function ChatPage() {
}); });
}, [isConnected, user, allUsers, currentRoom, joinRoom, setCurrentRoom]); }, [isConnected, user, allUsers, currentRoom, joinRoom, setCurrentRoom]);
// Fetch messages for current room
useEffect(() => { useEffect(() => {
if (!currentRoom) return; if (!currentRoom) return;
@ -151,7 +156,6 @@ export default function ChatPage() {
createdAt: msg.createdAt, createdAt: msg.createdAt,
})); }));
// Replace only this room's messages
setMessages((prev) => { setMessages((prev) => {
const others = prev.filter((m) => m.roomId !== currentRoom); const others = prev.filter((m) => m.roomId !== currentRoom);
return [...others, ...normalized]; return [...others, ...normalized];
@ -162,9 +166,6 @@ export default function ChatPage() {
}); });
}, [currentRoom, setMessages]); }, [currentRoom, setMessages]);
// Fetch all users // Fetch all users
useEffect(() => { useEffect(() => {
if (!user) return; if (!user) return;
@ -177,6 +178,122 @@ export default function ChatPage() {
.catch(console.error); .catch(console.error);
}, [user]); }, [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 // React to P2P Ready event
useEffect(() => { useEffect(() => {
if (!p2pReady) return; if (!p2pReady) return;
@ -294,14 +411,12 @@ export default function ChatPage() {
try { try {
const updated = await addParticipant(convId, userId); 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 rawParticipants: any[] = updated.participants || [];
const hydrated = rawParticipants.map((p: any) => { const hydrated = rawParticipants.map((p: any) => {
if (typeof p === "string") { if (typeof p === "string") {
const found = allUsers.find((u) => u._id === p); const found = allUsers.find((u) => u._id === p);
return found ? { _id: found._id, name: found.name } : { _id: p, name: 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) { if (!p.name) {
const found = allUsers.find((u) => u._id === (p._id ?? p)); const found = allUsers.find((u) => u._id === (p._id ?? p));
return found ? { _id: found._id, name: found.name } : p; return found ? { _id: found._id, name: found.name } : p;
@ -346,14 +461,11 @@ export default function ChatPage() {
} }
}; };
const formatDate = (dateStr: string) => { const formatDate = (dateStr: string) => {
const date = new Date(dateStr); const date = new Date(dateStr);
const today = new Date(); const today = new Date();
const isToday = date.toDateString() === today.toDateString(); const isToday = date.toDateString() === today.toDateString();
if (isToday) return "Today"; if (isToday) return "Today";
return date.toLocaleDateString([], { return date.toLocaleDateString([], {
day: "numeric", day: "numeric",
month: "short", month: "short",
@ -391,7 +503,6 @@ export default function ChatPage() {
const selectedRoom = rooms.find((r) => r.id === currentRoom); const selectedRoom = rooms.find((r) => r.id === currentRoom);
// Participants for manage dropdown (current selected group room)
const manageParticipants = selectedRoom?.participants ?? []; const manageParticipants = selectedRoom?.participants ?? [];
const addableUsers = allUsers.filter( const addableUsers = allUsers.filter(
(u) => !manageParticipants.some((p: any) => (p._id ?? p) === u._id) (u) => !manageParticipants.some((p: any) => (p._id ?? p) === u._id)
@ -399,7 +510,7 @@ export default function ChatPage() {
return ( return (
<div className="flex h-screen"> <div className="flex h-screen">
{/* ================= LEFT SIDEBAR ================= */} { /* LEFT SIDEBAR */}
<div className="w-1/3 border-r flex flex-col bg-background"> <div className="w-1/3 border-r flex flex-col bg-background">
{/* HEADER */} {/* HEADER */}
<div className="flex items-center justify-between p-4"> <div className="flex items-center justify-between p-4">
@ -461,7 +572,7 @@ export default function ChatPage() {
<span className="font-medium text-foreground">{user.username}</span> <span className="font-medium text-foreground">{user.username}</span>
</div> </div>
{/* TABS — only Chats & People */} {/* TABS */}
<div className="flex gap-4 px-4 text-sm font-medium border-b pb-2"> <div className="flex gap-4 px-4 text-sm font-medium border-b pb-2">
<button <button
onClick={() => setActiveTab("rooms")} onClick={() => setActiveTab("rooms")}
@ -587,7 +698,7 @@ export default function ChatPage() {
)} )}
</div> </div>
{/* ================= RIGHT CHAT ================= */} {/* RIGHT CHAT */}
<div className="w-2/3 flex flex-col"> <div className="w-2/3 flex flex-col">
{/* HEADER */} {/* HEADER */}
<div className="p-4 border-b font-semibold flex items-center justify-between"> <div className="p-4 border-b font-semibold flex items-center justify-between">
@ -760,7 +871,6 @@ export default function ChatPage() {
return ( return (
<div key={msg._id || idx}> <div key={msg._id || idx}>
{/* DATE SEPARATOR */}
{showDate && ( {showDate && (
<div className="flex justify-center my-4"> <div className="flex justify-center my-4">
<span className="text-xs bg-muted px-3 py-1 rounded-full text-muted-foreground"> <span className="text-xs bg-muted px-3 py-1 rounded-full text-muted-foreground">
@ -769,7 +879,6 @@ export default function ChatPage() {
</div> </div>
)} )}
{/* MESSAGE */}
<div className={`flex ${isMe ? "justify-end" : "justify-start"}`}> <div className={`flex ${isMe ? "justify-end" : "justify-start"}`}>
<div className="flex flex-col max-w-xs"> <div className="flex flex-col max-w-xs">
{!isMe && ( {!isMe && (
@ -785,8 +894,6 @@ export default function ChatPage() {
}`} }`}
> >
<div>{msg.text}</div> <div>{msg.text}</div>
{/* TIME */}
<div className="text-[10px] text-right opacity-70 mt-1"> <div className="text-[10px] text-right opacity-70 mt-1">
{formatTime(msg.createdAt!)} {formatTime(msg.createdAt!)}
</div> </div>

View File

@ -82,3 +82,65 @@ export async function fetchMessages(convId: string): Promise<any[]> {
if (!res.ok) throw new Error("Failed to fetch messages"); if (!res.ok) throw new Error("Failed to fetch messages");
return res.json(); 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();
};
}