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 { 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 (
|
||||
<div className="flex h-screen">
|
||||
{/* ================= LEFT SIDEBAR ================= */}
|
||||
{ /* LEFT SIDEBAR */}
|
||||
<div className="w-1/3 border-r flex flex-col bg-background">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between p-4">
|
||||
@ -461,13 +572,13 @@ export default function ChatPage() {
|
||||
<span className="font-medium text-foreground">{user.username}</span>
|
||||
</div>
|
||||
|
||||
{/* TABS — only Chats & People */}
|
||||
{/* TABS */}
|
||||
<div className="flex gap-4 px-4 text-sm font-medium border-b pb-2">
|
||||
<button
|
||||
onClick={() => setActiveTab("rooms")}
|
||||
className={`pb-1 ${activeTab === "rooms"
|
||||
? "text-primary border-b-2 border-primary"
|
||||
: "text-muted-foreground"
|
||||
? "text-primary border-b-2 border-primary"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
Chats
|
||||
@ -475,8 +586,8 @@ export default function ChatPage() {
|
||||
<button
|
||||
onClick={() => setActiveTab("users")}
|
||||
className={`pb-1 ${activeTab === "users"
|
||||
? "text-primary border-b-2 border-primary"
|
||||
: "text-muted-foreground"
|
||||
? "text-primary border-b-2 border-primary"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
People
|
||||
@ -587,7 +698,7 @@ export default function ChatPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ================= RIGHT CHAT ================= */}
|
||||
{/* RIGHT CHAT */}
|
||||
<div className="w-2/3 flex flex-col">
|
||||
{/* HEADER */}
|
||||
<div className="p-4 border-b font-semibold flex items-center justify-between">
|
||||
@ -760,7 +871,6 @@ export default function ChatPage() {
|
||||
|
||||
return (
|
||||
<div key={msg._id || idx}>
|
||||
{/* DATE SEPARATOR */}
|
||||
{showDate && (
|
||||
<div className="flex justify-center my-4">
|
||||
<span className="text-xs bg-muted px-3 py-1 rounded-full text-muted-foreground">
|
||||
@ -769,7 +879,6 @@ export default function ChatPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* MESSAGE */}
|
||||
<div className={`flex ${isMe ? "justify-end" : "justify-start"}`}>
|
||||
<div className="flex flex-col max-w-xs">
|
||||
{!isMe && (
|
||||
@ -785,8 +894,6 @@ export default function ChatPage() {
|
||||
}`}
|
||||
>
|
||||
<div>{msg.text}</div>
|
||||
|
||||
{/* TIME */}
|
||||
<div className="text-[10px] text-right opacity-70 mt-1">
|
||||
{formatTime(msg.createdAt!)}
|
||||
</div>
|
||||
|
||||
@ -81,4 +81,66 @@ export async function fetchMessages(convId: string): Promise<any[]> {
|
||||
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();
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user