implemented the sse in background
This commit is contained in:
parent
d2ea0ff9cb
commit
9b18cf5118
@ -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>
|
||||||
|
|||||||
@ -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();
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user