Compare commits
2 Commits
main
...
feat/featu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fcb8e1b68 | ||
|
|
6698a9eeed |
@ -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
|
||||
@ -106,12 +112,12 @@ export default function ChatPage() {
|
||||
let otherUserName = "Unknown User";
|
||||
if (typeof c.participants?.[0] === "object") {
|
||||
const otherUser = c.participants.find(
|
||||
(p: any) => p._id !== user.userId
|
||||
(p: any) => p._id !== user.userId,
|
||||
);
|
||||
otherUserName = otherUser?.name || otherUserName;
|
||||
} else {
|
||||
const otherUserId = c.participants?.find(
|
||||
(id: string) => id !== user.userId
|
||||
(id: string) => id !== user.userId,
|
||||
);
|
||||
const otherUser = allUsers.find((u) => u._id === otherUserId);
|
||||
otherUserName = otherUser?.name || otherUserName;
|
||||
@ -182,7 +188,10 @@ export default function ChatPage() {
|
||||
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
|
||||
@ -206,7 +215,7 @@ useEffect(() => {
|
||||
|
||||
if (!isGroup) {
|
||||
const other = (conversation.participants ?? []).find(
|
||||
(p: any) => (p._id ?? p) !== user.userId
|
||||
(p: any) => (p._id ?? p) !== user.userId,
|
||||
);
|
||||
roomName = other?.name ?? "Direct Message";
|
||||
}
|
||||
@ -219,7 +228,7 @@ useEffect(() => {
|
||||
).map((p: any) =>
|
||||
typeof p === "object"
|
||||
? { _id: p._id, name: p.name ?? p._id }
|
||||
: { _id: p, name: p }
|
||||
: { _id: p, name: p },
|
||||
);
|
||||
|
||||
console.log("[SSE HOOK] participants:", participants);
|
||||
@ -293,8 +302,7 @@ useEffect(() => {
|
||||
return cleanup;
|
||||
}, [user?.token, joinRoom, leaveRoom, setCurrentRoom, setMessages]);
|
||||
|
||||
|
||||
// React to P2P Ready event
|
||||
// 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,7 +547,7 @@ 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 (
|
||||
@ -576,7 +618,8 @@ 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"
|
||||
}`}
|
||||
@ -585,7 +628,8 @@ useEffect(() => {
|
||||
</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"
|
||||
}`}
|
||||
@ -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,7 +666,8 @@ 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 ? (
|
||||
@ -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,7 +934,8 @@ 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"
|
||||
}`}
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user