Compare commits

..

2 Commits

Author SHA1 Message Date
sumona-banerjeee
2fcb8e1b68 added the p2pinvite for the starts a chat 2026-04-20 17:47:19 +05:30
sumona-banerjeee
6698a9eeed new branch 2026-04-20 14:56:32 +05:30
2 changed files with 229 additions and 160 deletions

View File

@ -3,7 +3,11 @@
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 {
useSocket,
type ChatMessage,
type RoomNotice,
} from "@/context/SocketContext";
import {
fetchConversations,
createConversation,
@ -55,10 +59,12 @@ export default function ChatPage() {
p2pReady,
setMessages,
clearP2PReady,
p2pInvite,
clearP2PInvite,
} = useSocket();
const [text, setText] = useState("");
const [roomSearch, setRoomSearch] = useState("");
// const [roomSearch, setRoomSearch] = useState("");
const [userSearch, setUserSearch] = useState("");
const [activeTab, setActiveTab] = useState<"rooms" | "users">("rooms");
const [showCreateDropdown, setShowCreateDropdown] = useState(false);
@ -81,7 +87,7 @@ export default function ChatPage() {
return debounced;
};
const debouncedRoomSearch = useDebounce(roomSearch, 300);
// const debouncedRoomSearch = useDebounce(roomSearch, 300);
const debouncedUserSearch = useDebounce(userSearch, 300);
// Auth guard
@ -99,33 +105,33 @@ export default function ChatPage() {
.then((data) => {
const loadedRooms = Array.isArray(data)
? data.map((c) => {
const isGroup = c.type === "group";
let roomName = c.name;
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;
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;
}
roomName = otherUserName;
}
return {
id: c._id || c.id,
name: roomName || "Unnamed Group",
isGroup,
participants: c.participants || [],
};
})
return {
id: c._id || c.id,
name: roomName || "Unnamed Group",
isGroup,
participants: c.participants || [],
};
})
: [];
setRooms(loadedRooms);
@ -179,122 +185,124 @@ export default function ChatPage() {
}, [user]);
// ─── SSE: real-time group invite / removal ───────────────────────────────
useEffect(() => {
if (!user?.token) return;
useEffect(() => {
if (!user?.token) return;
console.log("[SSE HOOK] Initializing authenticated SSE for user:", user.username);
console.log(
"[SSE HOOK] Initializing authenticated SSE for user:",
user.username,
);
const cleanup = subscribeToSSE(user.token, {
// Someone added this user to a group conversation
onGroupInvite: ({ conversation }) => {
console.log("[SSE HOOK] onGroupInvite TRIGGERED");
const cleanup = subscribeToSSE(user.token, {
// 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;
if (!conversation) {
console.warn("[SSE HOOK] groupInvite missing conversation payload");
return;
}
console.log("[SSE HOOK] adding new room to sidebar");
return [...prev, newRoom];
});
console.log("[SSE HOOK] groupInvite payload:", conversation);
console.log("[SSE HOOK] joining room:", convId);
joinRoom(convId);
},
const convId: string = conversation._id ?? conversation.id;
const isGroup = conversation.type === "group";
// This user was removed from a group
onGroupRemoved: ({ conversation }) => {
console.log("[SSE HOOK] onGroupRemoved TRIGGERED");
console.log("[SSE HOOK] convId:", convId, "isGroup:", isGroup);
if (!conversation) {
console.warn("[SSE HOOK] groupRemoved missing conversation payload");
return;
}
// Resolve display name
let roomName = conversation.name || "Unnamed Group";
console.log("[SSE HOOK] groupRemoved payload:", conversation);
if (!isGroup) {
const other = (conversation.participants ?? []).find(
(p: any) => (p._id ?? p) !== user.userId,
);
roomName = other?.name ?? "Direct Message";
}
const roomId = conversation._id ?? conversation.id;
console.log("[SSE HOOK] roomName resolved:", roomName);
console.log("[SSE HOOK] removing roomId:", roomId);
// 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 },
);
// Leave socket room
leaveRoom(roomId);
console.log("[SSE HOOK] left room:", roomId);
console.log("[SSE HOOK] participants:", participants);
// Remove from sidebar
setRooms((prev) => {
const filtered = prev.filter((r) => r.id !== roomId);
console.log("[SSE HOOK] updated rooms after removal:", filtered);
return filtered;
});
const newRoom: Room = {
id: convId,
name: roomName,
isGroup,
participants,
};
// Clear selection if the removed room was active
if (currentRoom === roomId) {
console.log("[SSE HOOK] clearing current room selection");
setCurrentRoom(null);
}
console.log("[SSE HOOK] newRoom created:", newRoom);
// 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;
});
},
});
// 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;
}
return cleanup;
}, [user?.token, joinRoom, leaveRoom, setCurrentRoom, setMessages]);
console.log("[SSE HOOK] adding new room to sidebar");
return [...prev, newRoom];
});
console.log("[SSE HOOK] joining room:", convId);
joinRoom(convId);
},
// React to P2P Ready event
// 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?.token, joinRoom, leaveRoom, setCurrentRoom, setMessages]);
// React to P2P Ready event (When YOU start a chat with someone)
useEffect(() => {
if (!p2pReady) return;
const targetUser = allUsers.find((u) => u._id === p2pReady.targetUserId);
@ -304,7 +312,12 @@ useEffect(() => {
if (prev.find((r) => r.id === p2pReady.roomId)) return prev;
return [
...prev,
{ id: p2pReady.roomId, name: roomName, isGroup: false, participants: [] },
{
id: p2pReady.roomId,
name: roomName,
isGroup: false,
participants: [],
},
];
});
@ -312,6 +325,31 @@ useEffect(() => {
clearP2PReady();
}, [p2pReady, allUsers, clearP2PReady]);
// React to P2P Invite event (When SOMEONE ELSE starts a chat with you)
// This completely replaces polling and allows instant updates via Socket.IO
useEffect(() => {
if (!p2pInvite) return;
const targetUser = allUsers.find((u) => u._id === p2pInvite.fromUserId);
const roomName = targetUser?.name || "Direct Message";
setRooms((prev) => {
if (prev.find((r) => r.id === p2pInvite.roomId)) return prev;
return [
...prev,
{
id: p2pInvite.roomId,
name: roomName,
isGroup: false,
participants: [],
},
];
});
// We do NOT call setActiveTab("rooms") here because we don't want to
// interrupt User 2's screen just because User 1 sent an invite.
clearP2PInvite();
}, [p2pInvite, allUsers, clearP2PInvite]);
// Scroll to bottom on new messages
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
@ -339,23 +377,23 @@ useEffect(() => {
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Filtered rooms
const filteredRooms = useMemo(() => {
// Filtered rooms - temporarily disabled
/* const filteredRooms = useMemo(() => {
if (!debouncedRoomSearch.trim()) return rooms;
return rooms.filter((r) =>
r.name.toLowerCase().includes(debouncedRoomSearch.toLowerCase())
r.name.toLowerCase().includes(debouncedRoomSearch.toLowerCase()),
);
}, [rooms, debouncedRoomSearch]);
}, [rooms, debouncedRoomSearch]); */
// Messages for current room
const roomMessages = useMemo(
() => messages.filter((m) => m.roomId === currentRoom),
[messages, currentRoom]
[messages, currentRoom],
);
const roomNotices = useMemo(
() => notices.filter((n) => n.roomId === currentRoom),
[notices, currentRoom]
[notices, currentRoom],
);
// Handlers
@ -396,7 +434,7 @@ useEffect(() => {
participants: created.participants || [],
};
setRooms((prev) =>
prev.find((r) => r.id === newRoom.id) ? prev : [...prev, newRoom]
prev.find((r) => r.id === newRoom.id) ? prev : [...prev, newRoom],
);
setNewRoomName("");
setShowCreateDropdown(false);
@ -415,7 +453,9 @@ useEffect(() => {
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 };
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));
@ -426,8 +466,8 @@ useEffect(() => {
setRooms((prev) =>
prev.map((r) =>
r.id === convId ? { ...r, participants: hydrated } : r
)
r.id === convId ? { ...r, participants: hydrated } : r,
),
);
} catch (err: any) {
console.error("Add participant failed:", err.message);
@ -442,7 +482,9 @@ useEffect(() => {
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 };
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));
@ -453,8 +495,8 @@ useEffect(() => {
setRooms((prev) =>
prev.map((r) =>
r.id === convId ? { ...r, participants: hydrated } : r
)
r.id === convId ? { ...r, participants: hydrated } : r,
),
);
} catch (err: any) {
console.error("Remove participant failed:", err.message);
@ -505,12 +547,12 @@ useEffect(() => {
const manageParticipants = selectedRoom?.participants ?? [];
const addableUsers = allUsers.filter(
(u) => !manageParticipants.some((p: any) => (p._id ?? p) === u._id)
(u) => !manageParticipants.some((p: any) => (p._id ?? p) === u._id),
);
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">
@ -576,19 +618,21 @@ useEffect(() => {
<div className="flex gap-4 px-4 text-sm font-medium border-b pb-2">
<button
onClick={() => setActiveTab("rooms")}
className={`pb-1 ${activeTab === "rooms"
className={`pb-1 ${
activeTab === "rooms"
? "text-primary border-b-2 border-primary"
: "text-muted-foreground"
}`}
}`}
>
Chats
</button>
<button
onClick={() => setActiveTab("users")}
className={`pb-1 ${activeTab === "users"
className={`pb-1 ${
activeTab === "users"
? "text-primary border-b-2 border-primary"
: "text-muted-foreground"
}`}
}`}
>
People
</button>
@ -597,22 +641,22 @@ useEffect(() => {
{/* ROOM LIST */}
{activeTab === "rooms" && (
<div className="flex flex-col flex-1 min-h-0">
<div className="p-4">
{/* <div className="p-4">
<Input
placeholder="Search rooms..."
placeholder="Search conversation..."
value={roomSearch}
onChange={(e) => setRoomSearch(e.target.value)}
className="rounded-full bg-muted"
/>
</div>
<div className="flex-1 overflow-y-auto px-2 space-y-1">
{filteredRooms.length === 0 && (
</div> */}
<div className="flex-1 overflow-y-auto px-2 py-4 space-y-1">
{rooms.length === 0 && (
<p className="p-4 text-sm text-muted-foreground">
No conversations yet. Start a chat from People tab!
</p>
)}
{filteredRooms.map((room) => {
{rooms.map((room) => {
const isActive = currentRoom === room.id;
return (
<div
@ -622,8 +666,9 @@ useEffect(() => {
${isActive ? "bg-primary text-primary-foreground" : "hover:bg-muted"}`}
>
<div
className={`w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-full ${isActive ? "bg-white/20" : "bg-primary/10"
}`}
className={`w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-full ${
isActive ? "bg-white/20" : "bg-primary/10"
}`}
>
{room.isGroup ? (
<Users
@ -673,7 +718,7 @@ useEffect(() => {
!debouncedUserSearch.trim() ||
u.name
?.toLowerCase()
.includes(debouncedUserSearch.toLowerCase())
.includes(debouncedUserSearch.toLowerCase()),
)
.map((u) => (
<div
@ -866,8 +911,7 @@ useEffect(() => {
lastDate = msgDate;
const isMe =
msg.senderId === user?.userId ||
msg.senderId === mySocketId;
msg.senderId === user?.userId || msg.senderId === mySocketId;
return (
<div key={msg._id || idx}>
@ -879,7 +923,9 @@ useEffect(() => {
</div>
)}
<div className={`flex ${isMe ? "justify-end" : "justify-start"}`}>
<div
className={`flex ${isMe ? "justify-end" : "justify-start"}`}
>
<div className="flex flex-col max-w-xs">
{!isMe && (
<span className="text-xs text-muted-foreground mb-1 ml-1">
@ -888,10 +934,11 @@ useEffect(() => {
)}
<div
className={`px-4 py-2 rounded-xl ${isMe
className={`px-4 py-2 rounded-xl ${
isMe
? "bg-primary text-primary-foreground"
: "bg-muted"
}`}
}`}
>
<div>{msg.text}</div>
<div className="text-[10px] text-right opacity-70 mt-1">

View File

@ -38,6 +38,13 @@ export interface P2PReadyData {
message: string;
}
// P2P Invite
export interface P2PInviteData {
roomId: string;
fromUserId: string;
message: string;
}
//context value
interface SocketContextValue {
@ -61,6 +68,9 @@ interface SocketContextValue {
p2pReady: P2PReadyData | null;
clearP2PReady: () => void;
p2pInvite: P2PInviteData | null;
clearP2PInvite: () => void;
}
//context
@ -81,6 +91,7 @@ export function SocketProvider({ children }: { children: ReactNode }) {
const [notices, setNotices] = useState<RoomNotice[]>([]);
const [currentRoom, setCurrentRoom] = useState<string | null>(null);
const [p2pReady, setP2pReady] = useState<P2PReadyData | null>(null);
const [p2pInvite, setP2pInvite] = useState<P2PInviteData | null>(null);
//socket connection
@ -153,6 +164,11 @@ export function SocketProvider({ children }: { children: ReactNode }) {
setCurrentRoom(data.roomId);
});
socket.on("p2pInvite", (data: P2PInviteData) => {
console.log("[Socket] P2P Invite:", data.message);
setP2pInvite(data);
});
//new message
socket.on("roomMessage", (data: ChatMessage) => {
@ -206,6 +222,10 @@ export function SocketProvider({ children }: { children: ReactNode }) {
setP2pReady(null);
}, []);
const clearP2PInvite = useCallback(() => {
setP2pInvite(null);
}, []);
//provider
return (
@ -226,6 +246,8 @@ export function SocketProvider({ children }: { children: ReactNode }) {
clearMessages,
p2pReady,
clearP2PReady,
p2pInvite,
clearP2PInvite,
}}
>
{children}