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 { 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 { import {
fetchConversations, fetchConversations,
createConversation, createConversation,
@ -55,10 +59,12 @@ export default function ChatPage() {
p2pReady, p2pReady,
setMessages, setMessages,
clearP2PReady, clearP2PReady,
p2pInvite,
clearP2PInvite,
} = useSocket(); } = useSocket();
const [text, setText] = useState(""); const [text, setText] = useState("");
const [roomSearch, setRoomSearch] = useState(""); // const [roomSearch, setRoomSearch] = useState("");
const [userSearch, setUserSearch] = useState(""); const [userSearch, setUserSearch] = useState("");
const [activeTab, setActiveTab] = useState<"rooms" | "users">("rooms"); const [activeTab, setActiveTab] = useState<"rooms" | "users">("rooms");
const [showCreateDropdown, setShowCreateDropdown] = useState(false); const [showCreateDropdown, setShowCreateDropdown] = useState(false);
@ -81,7 +87,7 @@ export default function ChatPage() {
return debounced; return debounced;
}; };
const debouncedRoomSearch = useDebounce(roomSearch, 300); // const debouncedRoomSearch = useDebounce(roomSearch, 300);
const debouncedUserSearch = useDebounce(userSearch, 300); const debouncedUserSearch = useDebounce(userSearch, 300);
// Auth guard // Auth guard
@ -106,12 +112,12 @@ export default function ChatPage() {
let otherUserName = "Unknown User"; let otherUserName = "Unknown User";
if (typeof c.participants?.[0] === "object") { if (typeof c.participants?.[0] === "object") {
const otherUser = c.participants.find( const otherUser = c.participants.find(
(p: any) => p._id !== user.userId (p: any) => p._id !== user.userId,
); );
otherUserName = otherUser?.name || otherUserName; otherUserName = otherUser?.name || otherUserName;
} else { } else {
const otherUserId = c.participants?.find( const otherUserId = c.participants?.find(
(id: string) => id !== user.userId (id: string) => id !== user.userId,
); );
const otherUser = allUsers.find((u) => u._id === otherUserId); const otherUser = allUsers.find((u) => u._id === otherUserId);
otherUserName = otherUser?.name || otherUserName; otherUserName = otherUser?.name || otherUserName;
@ -182,7 +188,10 @@ export default function ChatPage() {
useEffect(() => { useEffect(() => {
if (!user?.token) return; 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, { const cleanup = subscribeToSSE(user.token, {
// Someone added this user to a group conversation // Someone added this user to a group conversation
@ -206,7 +215,7 @@ useEffect(() => {
if (!isGroup) { if (!isGroup) {
const other = (conversation.participants ?? []).find( const other = (conversation.participants ?? []).find(
(p: any) => (p._id ?? p) !== user.userId (p: any) => (p._id ?? p) !== user.userId,
); );
roomName = other?.name ?? "Direct Message"; roomName = other?.name ?? "Direct Message";
} }
@ -219,7 +228,7 @@ useEffect(() => {
).map((p: any) => ).map((p: any) =>
typeof p === "object" typeof p === "object"
? { _id: p._id, name: p.name ?? p._id } ? { _id: p._id, name: p.name ?? p._id }
: { _id: p, name: p } : { _id: p, name: p },
); );
console.log("[SSE HOOK] participants:", participants); console.log("[SSE HOOK] participants:", participants);
@ -293,8 +302,7 @@ useEffect(() => {
return cleanup; return cleanup;
}, [user?.token, joinRoom, leaveRoom, setCurrentRoom, setMessages]); }, [user?.token, joinRoom, leaveRoom, setCurrentRoom, setMessages]);
// React to P2P Ready event (When YOU start a chat with someone)
// React to P2P Ready event
useEffect(() => { useEffect(() => {
if (!p2pReady) return; if (!p2pReady) return;
const targetUser = allUsers.find((u) => u._id === p2pReady.targetUserId); const targetUser = allUsers.find((u) => u._id === p2pReady.targetUserId);
@ -304,7 +312,12 @@ useEffect(() => {
if (prev.find((r) => r.id === p2pReady.roomId)) return prev; if (prev.find((r) => r.id === p2pReady.roomId)) return prev;
return [ return [
...prev, ...prev,
{ id: p2pReady.roomId, name: roomName, isGroup: false, participants: [] }, {
id: p2pReady.roomId,
name: roomName,
isGroup: false,
participants: [],
},
]; ];
}); });
@ -312,6 +325,31 @@ useEffect(() => {
clearP2PReady(); clearP2PReady();
}, [p2pReady, allUsers, 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 // Scroll to bottom on new messages
useEffect(() => { useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
@ -339,23 +377,23 @@ useEffect(() => {
return () => document.removeEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside);
}, []); }, []);
// Filtered rooms // Filtered rooms - temporarily disabled
const filteredRooms = useMemo(() => { /* const filteredRooms = useMemo(() => {
if (!debouncedRoomSearch.trim()) return rooms; if (!debouncedRoomSearch.trim()) return rooms;
return rooms.filter((r) => return rooms.filter((r) =>
r.name.toLowerCase().includes(debouncedRoomSearch.toLowerCase()) r.name.toLowerCase().includes(debouncedRoomSearch.toLowerCase()),
); );
}, [rooms, debouncedRoomSearch]); }, [rooms, debouncedRoomSearch]); */
// Messages for current room // Messages for current room
const roomMessages = useMemo( const roomMessages = useMemo(
() => messages.filter((m) => m.roomId === currentRoom), () => messages.filter((m) => m.roomId === currentRoom),
[messages, currentRoom] [messages, currentRoom],
); );
const roomNotices = useMemo( const roomNotices = useMemo(
() => notices.filter((n) => n.roomId === currentRoom), () => notices.filter((n) => n.roomId === currentRoom),
[notices, currentRoom] [notices, currentRoom],
); );
// Handlers // Handlers
@ -396,7 +434,7 @@ useEffect(() => {
participants: created.participants || [], participants: created.participants || [],
}; };
setRooms((prev) => setRooms((prev) =>
prev.find((r) => r.id === newRoom.id) ? prev : [...prev, newRoom] prev.find((r) => r.id === newRoom.id) ? prev : [...prev, newRoom],
); );
setNewRoomName(""); setNewRoomName("");
setShowCreateDropdown(false); setShowCreateDropdown(false);
@ -415,7 +453,9 @@ useEffect(() => {
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 };
} }
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));
@ -426,8 +466,8 @@ useEffect(() => {
setRooms((prev) => setRooms((prev) =>
prev.map((r) => prev.map((r) =>
r.id === convId ? { ...r, participants: hydrated } : r r.id === convId ? { ...r, participants: hydrated } : r,
) ),
); );
} catch (err: any) { } catch (err: any) {
console.error("Add participant failed:", err.message); console.error("Add participant failed:", err.message);
@ -442,7 +482,9 @@ useEffect(() => {
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 };
} }
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));
@ -453,8 +495,8 @@ useEffect(() => {
setRooms((prev) => setRooms((prev) =>
prev.map((r) => prev.map((r) =>
r.id === convId ? { ...r, participants: hydrated } : r r.id === convId ? { ...r, participants: hydrated } : r,
) ),
); );
} catch (err: any) { } catch (err: any) {
console.error("Remove participant failed:", err.message); console.error("Remove participant failed:", err.message);
@ -505,7 +547,7 @@ useEffect(() => {
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),
); );
return ( return (
@ -576,7 +618,8 @@ useEffect(() => {
<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")}
className={`pb-1 ${activeTab === "rooms" className={`pb-1 ${
activeTab === "rooms"
? "text-primary border-b-2 border-primary" ? "text-primary border-b-2 border-primary"
: "text-muted-foreground" : "text-muted-foreground"
}`} }`}
@ -585,7 +628,8 @@ useEffect(() => {
</button> </button>
<button <button
onClick={() => setActiveTab("users")} onClick={() => setActiveTab("users")}
className={`pb-1 ${activeTab === "users" className={`pb-1 ${
activeTab === "users"
? "text-primary border-b-2 border-primary" ? "text-primary border-b-2 border-primary"
: "text-muted-foreground" : "text-muted-foreground"
}`} }`}
@ -597,22 +641,22 @@ useEffect(() => {
{/* ROOM LIST */} {/* ROOM LIST */}
{activeTab === "rooms" && ( {activeTab === "rooms" && (
<div className="flex flex-col flex-1 min-h-0"> <div className="flex flex-col flex-1 min-h-0">
<div className="p-4"> {/* <div className="p-4">
<Input <Input
placeholder="Search rooms..." placeholder="Search conversation..."
value={roomSearch} value={roomSearch}
onChange={(e) => setRoomSearch(e.target.value)} onChange={(e) => setRoomSearch(e.target.value)}
className="rounded-full bg-muted" className="rounded-full bg-muted"
/> />
</div> </div> */}
<div className="flex-1 overflow-y-auto px-2 space-y-1"> <div className="flex-1 overflow-y-auto px-2 py-4 space-y-1">
{filteredRooms.length === 0 && ( {rooms.length === 0 && (
<p className="p-4 text-sm text-muted-foreground"> <p className="p-4 text-sm text-muted-foreground">
No conversations yet. Start a chat from People tab! No conversations yet. Start a chat from People tab!
</p> </p>
)} )}
{filteredRooms.map((room) => { {rooms.map((room) => {
const isActive = currentRoom === room.id; const isActive = currentRoom === room.id;
return ( return (
<div <div
@ -622,7 +666,8 @@ useEffect(() => {
${isActive ? "bg-primary text-primary-foreground" : "hover:bg-muted"}`} ${isActive ? "bg-primary text-primary-foreground" : "hover:bg-muted"}`}
> >
<div <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 ? ( {room.isGroup ? (
@ -673,7 +718,7 @@ useEffect(() => {
!debouncedUserSearch.trim() || !debouncedUserSearch.trim() ||
u.name u.name
?.toLowerCase() ?.toLowerCase()
.includes(debouncedUserSearch.toLowerCase()) .includes(debouncedUserSearch.toLowerCase()),
) )
.map((u) => ( .map((u) => (
<div <div
@ -866,8 +911,7 @@ useEffect(() => {
lastDate = msgDate; lastDate = msgDate;
const isMe = const isMe =
msg.senderId === user?.userId || msg.senderId === user?.userId || msg.senderId === mySocketId;
msg.senderId === mySocketId;
return ( return (
<div key={msg._id || idx}> <div key={msg._id || idx}>
@ -879,7 +923,9 @@ useEffect(() => {
</div> </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"> <div className="flex flex-col max-w-xs">
{!isMe && ( {!isMe && (
<span className="text-xs text-muted-foreground mb-1 ml-1"> <span className="text-xs text-muted-foreground mb-1 ml-1">
@ -888,7 +934,8 @@ useEffect(() => {
)} )}
<div <div
className={`px-4 py-2 rounded-xl ${isMe className={`px-4 py-2 rounded-xl ${
isMe
? "bg-primary text-primary-foreground" ? "bg-primary text-primary-foreground"
: "bg-muted" : "bg-muted"
}`} }`}

View File

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