added the p2p and group with the fetch data from jwt token
This commit is contained in:
parent
6fa6415bfa
commit
04128f0205
10
package-lock.json
generated
10
package-lock.json
generated
@ -12,6 +12,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.38.0",
|
"framer-motion": "^12.38.0",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
@ -8053,6 +8054,15 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jwt-decode": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.38.0",
|
"framer-motion": "^12.38.0",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
"next": "16.2.4",
|
"next": "16.2.4",
|
||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ 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 { fetchConversations, createConversation } from "@/lib/api";
|
import { fetchConversations, createConversation, fetchAllUsers, ConversationType, addParticipant, removeParticipant } from "@/lib/api";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@ -18,6 +18,9 @@ import {
|
|||||||
Wifi,
|
Wifi,
|
||||||
WifiOff,
|
WifiOff,
|
||||||
Users,
|
Users,
|
||||||
|
User as UserIcon,
|
||||||
|
UserMinus,
|
||||||
|
UserPlus,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@ -26,22 +29,17 @@ import {
|
|||||||
} from "@/components/ui/avatar";
|
} from "@/components/ui/avatar";
|
||||||
import { useDebounce } from "@/hooks/useDebounce";
|
import { useDebounce } from "@/hooks/useDebounce";
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* Types */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
interface Room {
|
interface Room {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
isGroup: boolean;
|
isGroup: boolean;
|
||||||
|
participants: { _id: string; name: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* Backend rooms loaded dynamically */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* ChatPage */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user, isLoading: authLoading, logout } = useAuth();
|
const { user, isLoading: authLoading, logout } = useAuth();
|
||||||
@ -51,71 +49,143 @@ export default function ChatPage() {
|
|||||||
joinRoom,
|
joinRoom,
|
||||||
leaveRoom,
|
leaveRoom,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
startP2P,
|
||||||
messages,
|
messages,
|
||||||
notices,
|
notices,
|
||||||
currentRoom,
|
currentRoom,
|
||||||
setCurrentRoom,
|
setCurrentRoom,
|
||||||
clearMessages,
|
clearMessages,
|
||||||
|
p2pReady,
|
||||||
|
clearP2PReady,
|
||||||
} = useSocket();
|
} = useSocket();
|
||||||
|
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [search, setSearch] = useState("");
|
const [roomSearch, setRoomSearch] = useState("");
|
||||||
const [activeTab, setActiveTab] = useState<"rooms" | "create">("rooms");
|
const [userSearch, setUserSearch] = useState("");
|
||||||
|
const [activeTab, setActiveTab] = useState<"rooms" | "users" | "create">("rooms");
|
||||||
const [newRoomName, setNewRoomName] = useState("");
|
const [newRoomName, setNewRoomName] = useState("");
|
||||||
const [rooms, setRooms] = useState<Room[]>([]);
|
const [rooms, setRooms] = useState<Room[]>([]);
|
||||||
|
const [allUsers, setAllUsers] = useState<any[]>([]);
|
||||||
|
|
||||||
const debouncedSearch = useDebounce(search, 300);
|
const debouncedRoomSearch = useDebounce(roomSearch, 300);
|
||||||
|
const debouncedUserSearch = useDebounce(userSearch, 300);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [managingRoom, setManagingRoom] = useState<Room | null>(null);
|
||||||
|
|
||||||
// ── Auth guard ────────────────────────────────────────────────────
|
// Auth guard
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authLoading && !user) {
|
if (!authLoading && !user) {
|
||||||
router.push("/login");
|
router.push("/login");
|
||||||
}
|
}
|
||||||
}, [authLoading, user, router]);
|
}, [authLoading, user, router]);
|
||||||
|
|
||||||
// ── Fetch rooms & Auto-join ─────────────────────────────────────
|
// Fetch rooms & Auto-join
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isConnected) return;
|
// Guard: wait for everything needed
|
||||||
|
if (!isConnected || !user?.userId || allUsers.length === 0) return;
|
||||||
|
|
||||||
// Fetch conversations from API
|
fetchConversations(user.userId)
|
||||||
fetchConversations()
|
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
// Map backend returned data to Room format
|
const loadedRooms = Array.isArray(data)
|
||||||
const loadedRooms = Array.isArray(data) ? data.map(c => ({
|
? data.map((c) => {
|
||||||
id: c._id || c.id || "unknown",
|
const isGroup = c.type === "group";
|
||||||
name: c.name || "Unnamed Group",
|
|
||||||
isGroup: c.type === "group" || c.isGroup || true
|
let roomName = c.name;
|
||||||
})) : [];
|
|
||||||
|
// FIX: derive name for P2P
|
||||||
|
if (!isGroup) {
|
||||||
|
let otherUserName = "Unknown User";
|
||||||
|
|
||||||
|
// Case 1: participants are objects
|
||||||
|
if (typeof c.participants?.[0] === "object") {
|
||||||
|
const otherUser = c.participants.find(
|
||||||
|
(p: any) => p._id !== user.userId
|
||||||
|
);
|
||||||
|
otherUserName = otherUser?.name || otherUserName;
|
||||||
|
}
|
||||||
|
// Case 2: participants are IDs
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: c._id || c.id,
|
||||||
|
name: roomName || "Unnamed Group",
|
||||||
|
isGroup,
|
||||||
|
participants: c.participants || [],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
setRooms(loadedRooms);
|
setRooms(loadedRooms);
|
||||||
|
|
||||||
// Auto-join first room
|
// Auto-join first room (only once)
|
||||||
if (!currentRoom && loadedRooms.length > 0) {
|
if (!currentRoom && loadedRooms.length > 0) {
|
||||||
const firstRoom = loadedRooms[0];
|
const firstRoom = loadedRooms[0];
|
||||||
setCurrentRoom(firstRoom.id);
|
setCurrentRoom(firstRoom.id);
|
||||||
joinRoom(firstRoom.id);
|
joinRoom(firstRoom.id);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
console.error("Failed to fetch conversations", err);
|
console.error("Failed to fetch conversations", err.message);
|
||||||
});
|
});
|
||||||
}, [isConnected, currentRoom, joinRoom, setCurrentRoom]);
|
}, [isConnected, user, allUsers, currentRoom, joinRoom, setCurrentRoom]);
|
||||||
|
|
||||||
// ── Scroll to bottom on new messages ──────────────────────────────
|
// Fetch all users for P2P tab
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) return;
|
||||||
|
fetchAllUsers()
|
||||||
|
.then((data) => {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
setAllUsers(data.filter((u: any) => u.name !== user.username));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
// ── React to P2P Ready event
|
||||||
|
useEffect(() => {
|
||||||
|
if (!p2pReady) return;
|
||||||
|
|
||||||
|
const targetUser = allUsers.find((u) => u._id === p2pReady.targetUserId);
|
||||||
|
const roomName = targetUser?.name || "Direct Message";
|
||||||
|
|
||||||
|
// Add the new P2P room to sidebar if not already there
|
||||||
|
setRooms((prev) => {
|
||||||
|
if (prev.find((r) => r.id === p2pReady.roomId)) return prev;
|
||||||
|
return [...prev, { id: p2pReady.roomId, name: roomName, isGroup: false }];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Switch to rooms tab
|
||||||
|
setActiveTab("rooms");
|
||||||
|
clearP2PReady();
|
||||||
|
}, [p2pReady, allUsers, clearP2PReady]);
|
||||||
|
|
||||||
|
// ── Scroll to bottom on new messages
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
}, [messages, notices]);
|
}, [messages, notices]);
|
||||||
|
|
||||||
// ── Filtered rooms ────────────────────────────────────────────────
|
// ── Filtered rooms
|
||||||
const filteredRooms = useMemo(() => {
|
const filteredRooms = useMemo(() => {
|
||||||
if (!debouncedSearch.trim()) return rooms;
|
if (!debouncedRoomSearch.trim()) return rooms;
|
||||||
return rooms.filter((r) =>
|
return rooms.filter((r) =>
|
||||||
r.name.toLowerCase().includes(debouncedSearch.toLowerCase())
|
r.name.toLowerCase().includes(debouncedRoomSearch.toLowerCase())
|
||||||
);
|
);
|
||||||
}, [rooms, debouncedSearch]);
|
}, [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]
|
||||||
@ -126,7 +196,7 @@ export default function ChatPage() {
|
|||||||
[notices, currentRoom]
|
[notices, currentRoom]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Handlers ──────────────────────────────────────────────────────
|
// ── Handlers
|
||||||
const handleSelectRoom = (room: Room) => {
|
const handleSelectRoom = (room: Room) => {
|
||||||
if (currentRoom === room.id) return;
|
if (currentRoom === room.id) return;
|
||||||
|
|
||||||
@ -155,20 +225,20 @@ export default function ChatPage() {
|
|||||||
const handleCreateRoom = async () => {
|
const handleCreateRoom = async () => {
|
||||||
const name = newRoomName.trim();
|
const name = newRoomName.trim();
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
|
console.log(user);
|
||||||
try {
|
try {
|
||||||
// Actually create the room on the backend
|
const created = await createConversation({
|
||||||
const created = await createConversation({ name, type: "group", participants: [user?.username || "unknown"] });
|
name,
|
||||||
const newRoom: Room = {
|
type: ConversationType.GROUP,
|
||||||
id: created._id || created.id || name.toLowerCase().replace(/\s+/g, "-"),
|
participants: [user!.userId], // send userId not username
|
||||||
name: created.name || name,
|
|
||||||
isGroup: true
|
|
||||||
};
|
|
||||||
|
|
||||||
setRooms((prev) => {
|
|
||||||
if (prev.find((r) => r.id === newRoom.id)) return prev;
|
|
||||||
return [...prev, newRoom];
|
|
||||||
});
|
});
|
||||||
|
const newRoom: Room = {
|
||||||
|
id: created._id || created.id,
|
||||||
|
name: created.name || name,
|
||||||
|
isGroup: true,
|
||||||
|
participants: created.participants || [],
|
||||||
|
};
|
||||||
|
setRooms(prev => prev.find(r => r.id === newRoom.id) ? prev : [...prev, newRoom]);
|
||||||
setNewRoomName("");
|
setNewRoomName("");
|
||||||
setActiveTab("rooms");
|
setActiveTab("rooms");
|
||||||
handleSelectRoom(newRoom);
|
handleSelectRoom(newRoom);
|
||||||
@ -177,6 +247,42 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleAddParticipant = async (convId: string, userId: string) => {
|
||||||
|
try {
|
||||||
|
const updated = await addParticipant(convId, userId);
|
||||||
|
setRooms(prev => prev.map(r =>
|
||||||
|
r.id === convId
|
||||||
|
? { ...r, participants: updated.participants || r.participants }
|
||||||
|
: r
|
||||||
|
));
|
||||||
|
// refresh managingRoom state
|
||||||
|
setManagingRoom(prev => prev?.id === convId ? { ...prev, participants: updated.participants || prev.participants } : prev);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Add participant failed:", err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveParticipant = async (convId: string, userId: string) => {
|
||||||
|
try {
|
||||||
|
const updated = await removeParticipant(convId, userId);
|
||||||
|
setRooms(prev => prev.map(r =>
|
||||||
|
r.id === convId
|
||||||
|
? { ...r, participants: updated.participants || r.participants }
|
||||||
|
: r
|
||||||
|
));
|
||||||
|
setManagingRoom(prev => prev?.id === convId ? { ...prev, participants: updated.participants || prev.participants } : prev);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Remove participant failed:", err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleStartP2P = (targetUser: any) => {
|
||||||
|
if (!targetUser?._id) return;
|
||||||
|
startP2P(targetUser._id);
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
if (currentRoom) leaveRoom(currentRoom);
|
if (currentRoom) leaveRoom(currentRoom);
|
||||||
clearMessages();
|
clearMessages();
|
||||||
@ -237,26 +343,33 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TABS */}
|
{/* TABS */}
|
||||||
<div className="flex gap-6 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"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Rooms
|
Chats
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab("users")}
|
||||||
|
className={`pb-1 ${activeTab === "users"
|
||||||
|
? "text-primary border-b-2 border-primary"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
People
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("create")}
|
onClick={() => setActiveTab("create")}
|
||||||
className={`pb-1 ${
|
className={`pb-1 ${activeTab === "create"
|
||||||
activeTab === "create"
|
|
||||||
? "text-primary border-b-2 border-primary"
|
? "text-primary border-b-2 border-primary"
|
||||||
: "text-muted-foreground"
|
: "text-muted-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Create
|
Group
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -281,64 +394,213 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* SEARCH */}
|
{/* ROOM LIST */}
|
||||||
|
{activeTab === "rooms" && (
|
||||||
|
<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 rooms..."
|
||||||
value={search}
|
value={roomSearch}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setRoomSearch(e.target.value)}
|
||||||
className="rounded-full bg-muted"
|
className="rounded-full bg-muted"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ROOM LIST */}
|
|
||||||
<div className="flex-1 overflow-y-auto px-2 space-y-1">
|
<div className="flex-1 overflow-y-auto px-2 space-y-1">
|
||||||
{filteredRooms.length === 0 && (
|
{filteredRooms.length === 0 && (
|
||||||
<p className="p-4 text-sm text-muted-foreground">
|
<p className="p-4 text-sm text-muted-foreground">
|
||||||
No rooms found
|
No conversations yet. Start a chat from People tab!
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{filteredRooms.map((room) => {
|
{filteredRooms.map((room) => {
|
||||||
const isActive = currentRoom === room.id;
|
const isActive = currentRoom === room.id;
|
||||||
const unreadCount = messages.filter(
|
|
||||||
(m) => m.roomId === room.id && m.senderId !== mySocketId
|
|
||||||
).length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div key={room.id} className="flex items-center gap-1 pr-2">
|
||||||
<div
|
<div
|
||||||
key={room.id}
|
|
||||||
onClick={() => handleSelectRoom(room)}
|
onClick={() => handleSelectRoom(room)}
|
||||||
className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer transition
|
className={`flex items-center gap-3 p-3 rounded-xl cursor-pointer transition flex-1 min-w-0
|
||||||
${
|
${isActive
|
||||||
isActive
|
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary text-primary-foreground"
|
||||||
: "hover:bg-muted"
|
: "hover:bg-muted"
|
||||||
}
|
}`}
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`w-10 h-10 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 ? (
|
||||||
<Users className={`w-5 h-5 ${isActive ? "text-white" : "text-primary"}`} />
|
<Users className={`w-5 h-5 ${isActive ? "text-white" : "text-primary"}`} />
|
||||||
) : (
|
) : (
|
||||||
<Hash className={`w-5 h-5 ${isActive ? "text-white" : "text-primary"}`} />
|
<UserIcon className={`w-5 h-5 ${isActive ? "text-white" : "text-primary"}`} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
<h3 className="font-semibold truncate">{room.name}</h3>
|
<h3 className="font-semibold truncate">{room.name}</h3>
|
||||||
<p className="text-xs opacity-70 truncate">
|
<p className="text-xs opacity-70 truncate">
|
||||||
#{room.id}
|
{room.isGroup ? `${room.participants?.length ?? 0} members` : "Direct Message"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Manage button — group rooms only */}
|
||||||
|
{room.isGroup && (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setManagingRoom(managingRoom?.id === room.id ? null : room)
|
||||||
|
}
|
||||||
|
className={`flex-shrink-0 p-2 rounded-lg transition ${managingRoom?.id === room.id
|
||||||
|
? "bg-primary/10 text-primary"
|
||||||
|
: "text-muted-foreground hover:bg-muted"
|
||||||
|
}`}
|
||||||
|
title="Manage participants"
|
||||||
|
>
|
||||||
|
<Users className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ── Manage participants panel ── */}
|
||||||
|
{managingRoom && (
|
||||||
|
<div className="border-t mx-2 mb-2 rounded-xl bg-muted/40 p-3 space-y-3 max-h-72 overflow-y-auto">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-xs font-semibold truncate">
|
||||||
|
{managingRoom.name}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setManagingRoom(null)}
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Current members */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||||
|
Members ({managingRoom.participants?.length ?? 0})
|
||||||
|
</p>
|
||||||
|
{(managingRoom.participants ?? []).map((p: any) => {
|
||||||
|
const pid = p._id ?? p;
|
||||||
|
const pname = p.name ?? pid;
|
||||||
|
const isSelf = pid === user?.userId;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={pid}
|
||||||
|
className="flex items-center justify-between gap-2 py-1"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<div className="w-7 h-7 flex-shrink-0 flex items-center justify-center rounded-full bg-primary/10">
|
||||||
|
<UserIcon className="w-3.5 h-3.5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm truncate">
|
||||||
|
{pname}
|
||||||
|
{isSelf && (
|
||||||
|
<span className="text-muted-foreground text-xs ml-1">(you)</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{!isSelf && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveParticipant(managingRoom.id, pid)}
|
||||||
|
className="flex-shrink-0 p-1 rounded-md text-destructive hover:bg-destructive/10 transition"
|
||||||
|
title={`Remove ${pname}`}
|
||||||
|
>
|
||||||
|
<UserMinus className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add people */}
|
||||||
|
{(() => {
|
||||||
|
const addable = allUsers.filter(
|
||||||
|
(u) =>
|
||||||
|
!(managingRoom.participants ?? []).some(
|
||||||
|
(p: any) => (p._id ?? p) === u._id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (addable.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs text-muted-foreground uppercase tracking-wide">
|
||||||
|
Add people
|
||||||
|
</p>
|
||||||
|
{addable.map((u) => (
|
||||||
|
<div
|
||||||
|
key={u._id}
|
||||||
|
className="flex items-center justify-between gap-2 py-1"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<div className="w-7 h-7 flex-shrink-0 flex items-center justify-center rounded-full bg-primary/10">
|
||||||
|
<UserIcon className="w-3.5 h-3.5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm truncate">{u.name}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleAddParticipant(managingRoom.id, u._id)}
|
||||||
|
className="flex-shrink-0 p-1 rounded-md text-primary hover:bg-primary/10 transition"
|
||||||
|
title={`Add ${u.name}`}
|
||||||
|
>
|
||||||
|
<UserPlus className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* USERS / PEOPLE LIST (for starting P2P) */}
|
||||||
|
{activeTab === "users" && (
|
||||||
|
<div className="flex flex-col flex-1 min-h-0">
|
||||||
|
<div className="p-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Search people..."
|
||||||
|
value={userSearch}
|
||||||
|
onChange={(e) => setUserSearch(e.target.value)}
|
||||||
|
className="rounded-full bg-muted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto px-2 space-y-1">
|
||||||
|
{allUsers.length === 0 ? (
|
||||||
|
<p className="p-4 text-sm text-muted-foreground">
|
||||||
|
No other users found.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
allUsers
|
||||||
|
.filter((u) =>
|
||||||
|
!debouncedUserSearch.trim() ||
|
||||||
|
u.name?.toLowerCase().includes(debouncedUserSearch.toLowerCase())
|
||||||
|
)
|
||||||
|
.map((u) => (
|
||||||
|
<div
|
||||||
|
key={u._id}
|
||||||
|
onClick={() => handleStartP2P(u)}
|
||||||
|
className="flex items-center gap-3 p-3 rounded-xl cursor-pointer transition hover:bg-muted"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 flex items-center justify-center rounded-full bg-primary/10">
|
||||||
|
<UserIcon className="w-5 h-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<h3 className="font-semibold truncate">{u.name}</h3>
|
||||||
|
<p className="text-xs opacity-70 truncate">Click to chat</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ================= RIGHT CHAT ================= */}
|
{/* ================= RIGHT CHAT ================= */}
|
||||||
@ -347,13 +609,20 @@ export default function ChatPage() {
|
|||||||
<div className="p-4 border-b font-semibold flex items-center justify-between">
|
<div className="p-4 border-b font-semibold flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 flex items-center justify-center rounded-full bg-primary/10">
|
<div className="w-10 h-10 flex items-center justify-center rounded-full bg-primary/10">
|
||||||
|
{selectedRoom?.isGroup ? (
|
||||||
<Users className="w-5 h-5 text-primary" />
|
<Users className="w-5 h-5 text-primary" />
|
||||||
|
) : (
|
||||||
|
<UserIcon className="w-5 h-5 text-primary" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-lg">{selectedRoom?.name || "Select a room"}</span>
|
<span className="text-lg">{selectedRoom?.name || "Select a room"}</span>
|
||||||
{selectedRoom && (
|
{selectedRoom && (
|
||||||
<p className="text-xs text-muted-foreground font-normal">
|
<p className="text-xs text-muted-foreground font-normal">
|
||||||
#{selectedRoom.id} · {isConnected ? "Connected" : "Disconnected"}
|
{selectedRoom.isGroup
|
||||||
|
? `${selectedRoom.participants?.length ?? 0} members`
|
||||||
|
: "Direct Message"}{" "}
|
||||||
|
· {isConnected ? "Connected" : "Disconnected"}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -392,27 +661,21 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Interleave messages and notices by order */}
|
|
||||||
{roomMessages.map((msg, idx) => {
|
{roomMessages.map((msg, idx) => {
|
||||||
const isMe = msg.senderId === mySocketId;
|
const isMe = msg.senderId === mySocketId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`msg-${idx}`}
|
key={`msg-${idx}`}
|
||||||
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">
|
||||||
{/* Show sender name for others */}
|
|
||||||
{!isMe && (
|
{!isMe && (
|
||||||
<span className="text-xs text-muted-foreground mb-1 ml-1">
|
<span className="text-xs text-muted-foreground mb-1 ml-1">
|
||||||
{msg.username}
|
{msg.username}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`px-4 py-2 rounded-xl ${
|
className={`px-4 py-2 rounded-xl ${isMe ? "bg-primary text-primary-foreground" : "bg-muted"
|
||||||
isMe
|
|
||||||
? "bg-primary text-primary-foreground"
|
|
||||||
: "bg-muted"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{msg.text}
|
{msg.text}
|
||||||
@ -422,7 +685,6 @@ export default function ChatPage() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Room notices */}
|
|
||||||
{roomNotices.map((notice, idx) => (
|
{roomNotices.map((notice, idx) => (
|
||||||
<div key={`notice-${idx}`} className="flex justify-center">
|
<div key={`notice-${idx}`} className="flex justify-center">
|
||||||
<span className="text-xs text-muted-foreground bg-muted/50 px-3 py-1 rounded-full">
|
<span className="text-xs text-muted-foreground bg-muted/50 px-3 py-1 rounded-full">
|
||||||
@ -441,9 +703,7 @@ export default function ChatPage() {
|
|||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={
|
placeholder={
|
||||||
currentRoom
|
currentRoom ? `Message ${selectedRoom?.name ?? ""}...` : "Select a room first..."
|
||||||
? `Message #${currentRoom}...`
|
|
||||||
: "Select a room first..."
|
|
||||||
}
|
}
|
||||||
disabled={!currentRoom || !isConnected}
|
disabled={!currentRoom || !isConnected}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -10,14 +10,22 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { loginWithName } from "@/lib/api";
|
import { loginWithName } from "@/lib/api";
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
|
||||||
// Represents logged-in user data stored in app
|
// Represents logged-in user data
|
||||||
interface AuthUser {
|
interface AuthUser {
|
||||||
token: string;
|
token: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Values available globally from AuthContext
|
// JWT structure
|
||||||
|
interface DecodedToken {
|
||||||
|
sub: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context values
|
||||||
interface AuthContextValue {
|
interface AuthContextValue {
|
||||||
user: AuthUser | null;
|
user: AuthUser | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
@ -25,26 +33,19 @@ interface AuthContextValue {
|
|||||||
logout: () => void;
|
logout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context
|
// Create context
|
||||||
|
|
||||||
// Create authentication context
|
|
||||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
||||||
|
|
||||||
// Key used to store auth data in browser storage
|
// Storage key
|
||||||
const STORAGE_KEY = "chat_app_auth";
|
const STORAGE_KEY = "chat_app_auth";
|
||||||
|
|
||||||
// Provider
|
// Provider
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
// Stores current logged-in user
|
|
||||||
const [user, setUser] = useState<AuthUser | null>(null);
|
const [user, setUser] = useState<AuthUser | null>(null);
|
||||||
|
|
||||||
// Tracks whether auth state is still being loaded
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// Runs once when app loads → restores user from localStorage
|
// Restore user from localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
@ -52,13 +53,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
if (stored) {
|
if (stored) {
|
||||||
const parsed: AuthUser = JSON.parse(stored);
|
const parsed: AuthUser = JSON.parse(stored);
|
||||||
|
|
||||||
// Only set user if data is valid
|
if (parsed.token && parsed.username && parsed.userId) {
|
||||||
if (parsed.token && parsed.username) {
|
|
||||||
setUser(parsed);
|
setUser(parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// If corrupted data exists, remove it
|
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@ -66,32 +65,25 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// LOGIN
|
// LOGIN
|
||||||
|
const login = useCallback(async (name: string) => {
|
||||||
// Logs user in using API and saves data locally
|
|
||||||
const login = useCallback(
|
|
||||||
async (name: string) => {
|
|
||||||
const data = await loginWithName(name);
|
const data = await loginWithName(name);
|
||||||
|
|
||||||
|
// ✅ Decode token here
|
||||||
|
const decoded: DecodedToken = jwtDecode(data.token);
|
||||||
|
|
||||||
const authUser: AuthUser = {
|
const authUser: AuthUser = {
|
||||||
token: data.token,
|
token: data.token,
|
||||||
username: data.username,
|
username: decoded.username,
|
||||||
|
userId: decoded.sub, // ✅ extracted from token
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save user in localStorage for persistence
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(authUser));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(authUser));
|
||||||
|
|
||||||
// Update app state
|
|
||||||
setUser(authUser);
|
setUser(authUser);
|
||||||
|
|
||||||
// Redirect to chat page after login
|
|
||||||
router.push("/chat");
|
router.push("/chat");
|
||||||
},
|
}, [router]);
|
||||||
[router],
|
|
||||||
);
|
|
||||||
|
|
||||||
// LOGOUT
|
// LOGOUT
|
||||||
|
|
||||||
// Clears user session and redirects to login page
|
|
||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
@ -105,7 +97,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom hook to access auth state anywhere in app
|
// Hook
|
||||||
export function useAuth() {
|
export function useAuth() {
|
||||||
const ctx = useContext(AuthContext);
|
const ctx = useContext(AuthContext);
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,13 @@ export interface RoomNotice {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Data received when a P2P room is ready
|
||||||
|
export interface P2PReadyData {
|
||||||
|
roomId: string;
|
||||||
|
targetUserId: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Everything that the socket context will provide to the app
|
// Everything that the socket context will provide to the app
|
||||||
interface SocketContextValue {
|
interface SocketContextValue {
|
||||||
socket: Socket | null;
|
socket: Socket | null;
|
||||||
@ -39,6 +46,7 @@ interface SocketContextValue {
|
|||||||
joinRoom: (roomId: string) => void;
|
joinRoom: (roomId: string) => void;
|
||||||
leaveRoom: (roomId: string) => void;
|
leaveRoom: (roomId: string) => void;
|
||||||
sendMessage: (roomId: string, text: string) => void;
|
sendMessage: (roomId: string, text: string) => void;
|
||||||
|
startP2P: (targetUserId: string) => void;
|
||||||
|
|
||||||
// chat state stored globally
|
// chat state stored globally
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
@ -46,6 +54,10 @@ interface SocketContextValue {
|
|||||||
currentRoom: string | null;
|
currentRoom: string | null;
|
||||||
setCurrentRoom: (roomId: string | null) => void;
|
setCurrentRoom: (roomId: string | null) => void;
|
||||||
clearMessages: () => void;
|
clearMessages: () => void;
|
||||||
|
|
||||||
|
// P2P readiness (chat page reacts to this)
|
||||||
|
p2pReady: P2PReadyData | null;
|
||||||
|
clearP2PReady: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
//context
|
//context
|
||||||
@ -72,9 +84,11 @@ export function SocketProvider({ children }: { children: ReactNode }) {
|
|||||||
// System notices (join/leave/info messages)
|
// System notices (join/leave/info messages)
|
||||||
const [notices, setNotices] = useState<RoomNotice[]>([]);
|
const [notices, setNotices] = useState<RoomNotice[]>([]);
|
||||||
|
|
||||||
// Currently active chat room
|
|
||||||
const [currentRoom, setCurrentRoom] = useState<string | null>(null);
|
const [currentRoom, setCurrentRoom] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// P2P ready event data — consumed by chat page
|
||||||
|
const [p2pReady, setP2pReady] = useState<P2PReadyData | null>(null);
|
||||||
|
|
||||||
// Connect to socket when user logs in, disconnect when logged out
|
// Connect to socket when user logs in, disconnect when logged out
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user?.token) {
|
if (!user?.token) {
|
||||||
@ -150,6 +164,13 @@ export function SocketProvider({ children }: { children: ReactNode }) {
|
|||||||
setNotices((prev) => [...prev, data]);
|
setNotices((prev) => [...prev, data]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// P2P room ready — backend already did server-side join
|
||||||
|
socket.on("p2pReady", (data: P2PReadyData) => {
|
||||||
|
console.log("[Socket] P2P Ready:", data.message);
|
||||||
|
setP2pReady(data);
|
||||||
|
setCurrentRoom(data.roomId);
|
||||||
|
});
|
||||||
|
|
||||||
// New chat message received
|
// New chat message received
|
||||||
socket.on("roomMessage", (data: ChatMessage) => {
|
socket.on("roomMessage", (data: ChatMessage) => {
|
||||||
setMessages((prev) => [...prev, data]);
|
setMessages((prev) => [...prev, data]);
|
||||||
@ -186,12 +207,23 @@ export function SocketProvider({ children }: { children: ReactNode }) {
|
|||||||
socketRef.current?.emit("roomMessage", { roomId, text });
|
socketRef.current?.emit("roomMessage", { roomId, text });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Clear all messages and notices (reset chat state)
|
// Start a P2P conversation via socket
|
||||||
|
const startP2P = useCallback((targetUserId: string) => {
|
||||||
|
console.log(targetUserId);
|
||||||
|
socketRef.current?.emit("startP2P", { targetUserId });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Clear all messages and notices
|
||||||
const clearMessages = useCallback(() => {
|
const clearMessages = useCallback(() => {
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
setNotices([]);
|
setNotices([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Clear p2pReady after chat page has consumed it
|
||||||
|
const clearP2PReady = useCallback(() => {
|
||||||
|
setP2pReady(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SocketContext.Provider
|
<SocketContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@ -201,11 +233,14 @@ export function SocketProvider({ children }: { children: ReactNode }) {
|
|||||||
joinRoom,
|
joinRoom,
|
||||||
leaveRoom,
|
leaveRoom,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
startP2P,
|
||||||
messages,
|
messages,
|
||||||
notices,
|
notices,
|
||||||
currentRoom,
|
currentRoom,
|
||||||
setCurrentRoom,
|
setCurrentRoom,
|
||||||
clearMessages,
|
clearMessages,
|
||||||
|
p2pReady,
|
||||||
|
clearP2PReady,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -1,55 +1,85 @@
|
|||||||
// Base URL for the NestJS backend API
|
|
||||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL;
|
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
||||||
/**
|
export enum ConversationType {
|
||||||
* POST /auth/token
|
P2P = 'p2p',
|
||||||
* Authenticates (or creates) a user by name and returns a JWT token.
|
GROUP = 'group',
|
||||||
*/
|
}
|
||||||
export async function loginWithName(name: string): Promise<{ token: string; username: string }> {
|
|
||||||
|
export async function loginWithName(name: string): Promise<{ token: string; username: string; userId: string }> {
|
||||||
const res = await fetch(`${API_BASE_URL}/auth/token`, {
|
const res = await fetch(`${API_BASE_URL}/auth/token`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ name }),
|
body: JSON.stringify({ name }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = await res.json().catch(() => ({}));
|
const error = await res.json().catch(() => ({}));
|
||||||
throw new Error(error.message || "Login failed");
|
throw new Error(error.message || "Login failed");
|
||||||
}
|
}
|
||||||
|
return res.json(); // here our backend must and will return userId too
|
||||||
return res.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /users
|
|
||||||
* Fetches all registered users.
|
|
||||||
*/
|
|
||||||
export async function fetchAllUsers(): Promise<any[]> {
|
export async function fetchAllUsers(): Promise<any[]> {
|
||||||
const res = await fetch(`${API_BASE_URL}/users`);
|
const res = await fetch(`${API_BASE_URL}/users`);
|
||||||
if (!res.ok) throw new Error("Failed to fetch users");
|
if (!res.ok) throw new Error("Failed to fetch users");
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// takes userId as path param
|
||||||
* GET /conversations
|
export async function fetchConversations(userId: string): Promise<any[]> {
|
||||||
* Fetches all conversations.
|
const res = await fetch(`${API_BASE_URL}/conversations/${userId}`);
|
||||||
*/
|
|
||||||
export async function fetchConversations(): Promise<any[]> {
|
|
||||||
const res = await fetch(`${API_BASE_URL}/conversations`);
|
|
||||||
if (!res.ok) throw new Error("Failed to fetch conversations");
|
if (!res.ok) throw new Error("Failed to fetch conversations");
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Participants must be user IDs now
|
||||||
* POST /conversations
|
export async function createConversation(data: {
|
||||||
* Creates a new conversation.
|
name: string;
|
||||||
*/
|
type: ConversationType;
|
||||||
export async function createConversation(data: any): Promise<any> {
|
participants: string[]; // user IDs, not names
|
||||||
|
}): Promise<any> {
|
||||||
const res = await fetch(`${API_BASE_URL}/conversations`, {
|
const res = await fetch(`${API_BASE_URL}/conversations`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error("Failed to create conversation");
|
if (!res.ok) {
|
||||||
|
const errBody = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(errBody.message || "Failed to create conversation");
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function addParticipant(convId: string, userId: string): Promise<any> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/conversations/${convId}/add-participant`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ userId }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const errBody = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(errBody.message || "Failed to add participant");
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function removeParticipant(convId: string, userId: string): Promise<any> {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/conversations/${convId}/remove-participant`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ userId }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const errBody = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(errBody.message || "Failed to remove participant");
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMessages(roomId: string): Promise<any[]> {
|
||||||
|
if (!roomId) return [];
|
||||||
|
const res = await fetch(`${API_BASE_URL}/messages?roomId=${roomId}`);
|
||||||
|
if (!res.ok) return [];
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user