Compare commits

..

No commits in common. "feat/feature" and "main" have entirely different histories.

2 changed files with 153 additions and 222 deletions

View File

@ -3,11 +3,7 @@
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 { import { useSocket, type ChatMessage, type RoomNotice } from "@/context/SocketContext";
useSocket,
type ChatMessage,
type RoomNotice,
} from "@/context/SocketContext";
import { import {
fetchConversations, fetchConversations,
createConversation, createConversation,
@ -59,12 +55,10 @@ 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);
@ -87,7 +81,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
@ -112,12 +106,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;
@ -188,10 +182,7 @@ export default function ChatPage() {
useEffect(() => { useEffect(() => {
if (!user?.token) return; if (!user?.token) return;
console.log( console.log("[SSE HOOK] Initializing authenticated SSE for user:", user.username);
"[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
@ -215,7 +206,7 @@ export default function ChatPage() {
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";
} }
@ -228,7 +219,7 @@ export default function ChatPage() {
).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);
@ -302,7 +293,8 @@ export default function ChatPage() {
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);
@ -312,12 +304,7 @@ export default function ChatPage() {
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: [],
},
]; ];
}); });
@ -325,31 +312,6 @@ export default function ChatPage() {
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" });
@ -377,23 +339,23 @@ export default function ChatPage() {
return () => document.removeEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside);
}, []); }, []);
// Filtered rooms - temporarily disabled // Filtered rooms
/* 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
@ -434,7 +396,7 @@ export default function ChatPage() {
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);
@ -453,9 +415,7 @@ export default function ChatPage() {
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 return found ? { _id: found._id, name: found.name } : { _id: p, name: p };
? { _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));
@ -466,8 +426,8 @@ export default function ChatPage() {
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);
@ -482,9 +442,7 @@ export default function ChatPage() {
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 return found ? { _id: found._id, name: found.name } : { _id: p, name: p };
? { _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));
@ -495,8 +453,8 @@ export default function ChatPage() {
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);
@ -547,7 +505,7 @@ export default function ChatPage() {
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 (
@ -618,8 +576,7 @@ export default function ChatPage() {
<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 ${ className={`pb-1 ${activeTab === "rooms"
activeTab === "rooms"
? "text-primary border-b-2 border-primary" ? "text-primary border-b-2 border-primary"
: "text-muted-foreground" : "text-muted-foreground"
}`} }`}
@ -628,8 +585,7 @@ export default function ChatPage() {
</button> </button>
<button <button
onClick={() => setActiveTab("users")} onClick={() => setActiveTab("users")}
className={`pb-1 ${ className={`pb-1 ${activeTab === "users"
activeTab === "users"
? "text-primary border-b-2 border-primary" ? "text-primary border-b-2 border-primary"
: "text-muted-foreground" : "text-muted-foreground"
}`} }`}
@ -641,22 +597,22 @@ export default function ChatPage() {
{/* 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 conversation..." placeholder="Search rooms..."
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 py-4 space-y-1"> <div className="flex-1 overflow-y-auto px-2 space-y-1">
{rooms.length === 0 && ( {filteredRooms.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>
)} )}
{rooms.map((room) => { {filteredRooms.map((room) => {
const isActive = currentRoom === room.id; const isActive = currentRoom === room.id;
return ( return (
<div <div
@ -666,8 +622,7 @@ export default function ChatPage() {
${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 ${ className={`w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-full ${isActive ? "bg-white/20" : "bg-primary/10"
isActive ? "bg-white/20" : "bg-primary/10"
}`} }`}
> >
{room.isGroup ? ( {room.isGroup ? (
@ -718,7 +673,7 @@ export default function ChatPage() {
!debouncedUserSearch.trim() || !debouncedUserSearch.trim() ||
u.name u.name
?.toLowerCase() ?.toLowerCase()
.includes(debouncedUserSearch.toLowerCase()), .includes(debouncedUserSearch.toLowerCase())
) )
.map((u) => ( .map((u) => (
<div <div
@ -911,7 +866,8 @@ export default function ChatPage() {
lastDate = msgDate; lastDate = msgDate;
const isMe = const isMe =
msg.senderId === user?.userId || msg.senderId === mySocketId; msg.senderId === user?.userId ||
msg.senderId === mySocketId;
return ( return (
<div key={msg._id || idx}> <div key={msg._id || idx}>
@ -923,9 +879,7 @@ export default function ChatPage() {
</div> </div>
)} )}
<div <div className={`flex ${isMe ? "justify-end" : "justify-start"}`}>
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">
@ -934,8 +888,7 @@ export default function ChatPage() {
)} )}
<div <div
className={`px-4 py-2 rounded-xl ${ className={`px-4 py-2 rounded-xl ${isMe
isMe
? "bg-primary text-primary-foreground" ? "bg-primary text-primary-foreground"
: "bg-muted" : "bg-muted"
}`} }`}

View File

@ -38,13 +38,6 @@ 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 {
@ -68,9 +61,6 @@ interface SocketContextValue {
p2pReady: P2PReadyData | null; p2pReady: P2PReadyData | null;
clearP2PReady: () => void; clearP2PReady: () => void;
p2pInvite: P2PInviteData | null;
clearP2PInvite: () => void;
} }
//context //context
@ -91,7 +81,6 @@ 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
@ -164,11 +153,6 @@ 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) => {
@ -222,10 +206,6 @@ export function SocketProvider({ children }: { children: ReactNode }) {
setP2pReady(null); setP2pReady(null);
}, []); }, []);
const clearP2PInvite = useCallback(() => {
setP2pInvite(null);
}, []);
//provider //provider
return ( return (
@ -246,8 +226,6 @@ export function SocketProvider({ children }: { children: ReactNode }) {
clearMessages, clearMessages,
p2pReady, p2pReady,
clearP2PReady, clearP2PReady,
p2pInvite,
clearP2PInvite,
}} }}
> >
{children} {children}