Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions frontend/src/components/ChatOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState, useRef, useEffect } from "react";
import { io, Socket } from "socket.io-client";
import { toast } from "sonner";

const SOCKET_SERVER_URL = "http://localhost:3000";

Expand All @@ -8,9 +9,9 @@ interface ChatOverlayProps {
userName?: string;
}

const ChatOverlay: React.FC<ChatOverlayProps> = ({
roomId = "global",
userName = "Anonymous"
const ChatOverlay: React.FC<ChatOverlayProps> = ({
roomId = "global",
userName = "Anonymous"
}) => {
const [open, setOpen] = useState(false);
const [messages, setMessages] = useState<{ user: string; text: string; time?: Date }[]>([]);
Expand All @@ -25,7 +26,11 @@ const ChatOverlay: React.FC<ChatOverlayProps> = ({
socketRef.current.on("chat-message", (msg) => {
setMessages((prev) => [...prev, msg]);
if (!open) {
setUnreadCount(prev => prev + 1);
setUnreadCount((prev) => prev + 1);
toast.info(`New message from ${msg.user}`, {
description: msg.text,
duration: 2500,
});
}
});
socketRef.current.on("chat-history", (history) => {
Expand Down Expand Up @@ -53,6 +58,9 @@ const ChatOverlay: React.FC<ChatOverlayProps> = ({
if (input.trim() === "" || !socketRef.current) return;
const msg = { roomId, user: userName, text: input };
socketRef.current.emit("chat-message", msg);
toast.success("Message sent!", {
duration: 1200,
});
setInput("");
};

Expand All @@ -61,7 +69,10 @@ const ChatOverlay: React.FC<ChatOverlayProps> = ({
{!open && (
<button
className="fixed bottom-6 right-6 bg-green-600 dark:bg-green-500 text-white rounded-full w-14 h-14 flex items-center justify-center shadow-lg hover:bg-green-700 dark:hover:bg-green-600 transition-all z-50"
onClick={() => setOpen(true)}
onClick={() => {
setOpen(true);
toast("Chat opened.");
}}
aria-label="Open chat"
>
💬
Expand All @@ -75,18 +86,30 @@ const ChatOverlay: React.FC<ChatOverlayProps> = ({
{open && (
<div className="fixed bottom-6 right-6 w-80 max-w-[90vw] bg-white dark:bg-gray-900 shadow-2xl rounded-xl flex flex-col z-50 animate-fade-in-up border border-gray-200 dark:border-gray-800">
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800">
<span className="font-semibold text-gray-900 dark:text-gray-100">In-Call Chat - {roomId}</span>
<button onClick={() => setOpen(false)} className="text-gray-500 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-500">
<span className="font-semibold text-gray-900 dark:text-gray-100">
In-Call Chat - {roomId}
</span>
<button
onClick={() => {
setOpen(false);
toast("Chat closed.");
}}
className="text-gray-500 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-500"
>
×
</button>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-2 h-72 bg-gray-50 dark:bg-gray-950">
{messages.map((msg, idx) => (
<div key={idx} className="flex flex-col">
<div className="flex items-center gap-2">
<span className="text-xs text-green-700 dark:text-green-400 font-bold">{msg.user}</span>
<span className="text-xs text-green-700 dark:text-green-400 font-bold">
{msg.user}
</span>
<span className="text-xs text-gray-500 dark:text-gray-400">
{msg.time ? new Date(msg.time).toLocaleTimeString() : ""}
{msg.time
? new Date(msg.time).toLocaleTimeString()
: ""}
</span>
</div>
<span className="bg-green-100 dark:bg-green-900/30 text-gray-900 dark:text-gray-100 rounded px-2 py-1 text-sm w-fit max-w-[85%]">{msg.text}</span>
Expand Down
49 changes: 38 additions & 11 deletions frontend/src/components/ConnectionQualityIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React, { useState } from "react";
import { WifiOff, SignalLow, SignalMedium, SignalHigh, Info } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { ConnectionQuality, ConnectionQualityStats } from "../hooks/useConnectionQuality";
import { toast } from "sonner";
import { ConnectionQuality, ConnectionQualityStats } from "../hooks/useConnectionQuality.js";

interface ConnectionQualityIndicatorProps {
quality: ConnectionQuality;
stats?: ConnectionQualityStats;
showLabel?: boolean;
className?: string;
compact?: boolean;
compact?: boolean;
}

const ConnectionQualityIndicator: React.FC<ConnectionQualityIndicatorProps> = ({
Expand All @@ -29,7 +30,7 @@ const ConnectionQualityIndicator: React.FC<ConnectionQualityIndicatorProps> = ({
inactiveColor: "bg-gray-700/40",
textColor: "text-green-500",
label: "Excellent",
bars: [true, true, true, true],
bars: [true, true, true, true],
};
case "good": // 3 bars
return {
Expand All @@ -38,7 +39,7 @@ const ConnectionQualityIndicator: React.FC<ConnectionQualityIndicatorProps> = ({
inactiveColor: "bg-gray-700/40",
textColor: "text-green-400",
label: "Good",
bars: [true, true, true, false],
bars: [true, true, true, false],
};
case "fair": // 2 bars
return {
Expand All @@ -47,7 +48,7 @@ const ConnectionQualityIndicator: React.FC<ConnectionQualityIndicatorProps> = ({
inactiveColor: "bg-gray-700/40",
textColor: "text-yellow-500",
label: "Fair",
bars: [true, true, false, false],
bars: [true, true, false, false],
};
case "poor": // 1 bar
return {
Expand All @@ -56,7 +57,7 @@ const ConnectionQualityIndicator: React.FC<ConnectionQualityIndicatorProps> = ({
inactiveColor: "bg-gray-700/40",
textColor: "text-red-500",
label: "Poor",
bars: [true, false, false, false],
bars: [true, false, false, false],
};
default: // 0 bars
return {
Expand All @@ -65,14 +66,40 @@ const ConnectionQualityIndicator: React.FC<ConnectionQualityIndicatorProps> = ({
inactiveColor: "bg-gray-700/40",
textColor: "text-gray-400",
label: "Unknown",
bars: [false, false, false, false],
bars: [false, false, false, false],
};
}
};

const config = getQualityConfig();
const Icon = config.icon;
const barHeights = [6, 9, 12, 15];
const barHeights = [6, 9, 12, 15];

const prevQuality = React.useRef<ConnectionQuality | null>(null);
React.useEffect(() => {
if (prevQuality.current === quality) return;
prevQuality.current = quality;
switch (quality) {
case "poor":
toast.error("Your connection is poor. Expect lag or interruptions.", {
duration: 2500,
});
break;
case "fair":
toast.warning("Your connection is fair. Performance may vary.", {
duration: 2500,
});
break;
case "good":
toast("Your connection is good.", { duration: 2000 });
break;
case "excellent":
toast.success("Excellent connection!", { duration: 2000 });
break;
default:
toast("Connection quality unknown.", { duration: 2000 });
}
}, [quality]);

const formatMetric = (value?: number, unit: string = "") => {
if (value === undefined) return "N/A";
Expand Down Expand Up @@ -142,7 +169,7 @@ const ConnectionQualityIndicator: React.FC<ConnectionQualityIndicatorProps> = ({
className={`w-1 rounded-sm transition-all duration-200 ${
active ? config.barColor : config.inactiveColor
}`}
style={{
style={{
height: `${barHeights[index]}px`,
}}
/>
Expand Down Expand Up @@ -186,9 +213,9 @@ const ConnectionQualityIndicator: React.FC<ConnectionQualityIndicatorProps> = ({
className={`w-1.5 rounded-sm transition-all duration-200 ${
active ? config.barColor : config.inactiveColor
}`}
style={{
style={{
height: `${barHeights[index]}px`,
}}
}}
/>
))}
</div>
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/pages/CreateRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { React, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "../components/ui/button.js";
import axios from "axios";
import { toast } from "sonner";

export default function CreateRoom() {
const [roomName, setRoomName] = useState("");
Expand All @@ -12,7 +13,7 @@ export default function CreateRoom() {

const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
if (!roomName.trim()) return alert("Please enter a room name!");
if (!roomName.trim()) return toast.message("please enter the room name")

setLoading(true);
try {
Expand All @@ -26,12 +27,12 @@ export default function CreateRoom() {
}
);

alert("Room created successfully!");
toast.success("Room created sucessfully");
navigate(`/lobby/${res.data._id}`);

} catch (err: any) {
console.error(err);
alert(err.response?.data?.message || "Failed to create room.");
toast.error(err.response?.data?.message ||"Failed to create room.")
} finally {
setLoading(false);
}
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/pages/InRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { useParams, useNavigate } from "react-router-dom";
import { io, Socket } from "socket.io-client";
import { Mic, MicOff, Video, VideoOff, PhoneOff, Users, MessageSquare } from "lucide-react";
import { motion } from "framer-motion";
import { HotKeys } from "react-hotkeys";
import { HotKeys } from "react-hotkeys";
import { toast } from "sonner";
import { useConnectionQuality } from "../hooks/useConnectionQuality.js";
import API_ENDPOINTS from "../lib/apiConfig.js";
import ConnectionQualityIndicator from "../components/ConnectionQualityIndicator.js";
import { API_ENDPOINTS } from "../lib/apiConfig.js";

const keyMap = {
TOGGLE_MIC: "ctrl+m",
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/pages/JoinRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
import { Button } from "../components/ui/button.js";
import axios from "axios";
import PreJoinPreview from "../components/PreJoinPreview.js";
import { toast } from "sonner"

export default function JoinRoom() {
const [roomName, setRoomName] = useState("");
Expand Down Expand Up @@ -32,11 +33,11 @@ export default function JoinRoom() {
console.error("Media access denied:", err);

if (err.name === "NotAllowedError") {
alert(
toast.error(
"You blocked the camera/mic.\n\nPlease enable permissions:\n1. Click lock icon in URL bar\n2. Open Site Settings\n3. Set Camera & Microphone to Allow\n4. Reload the page"
);
} else {
alert("Unable to access camera/mic: " + err.message);
toast.error("Unable to access camera/mic: " + err.message);
}
}
};
Expand All @@ -63,15 +64,17 @@ export default function JoinRoom() {
}
);

alert("Joined room successfully!");
toast.success("Joined room successfully!");
// Optional: Stop preview stream before entering actual call


// 🛑 SAFE STOP (prevents getTracks() crash)
mediaStream.getTracks().forEach((track) => track.stop());

navigate(`/room/${roomName}`);
} catch (err: any) {
console.error("Join room error:", err);
alert(err.response?.data?.message || err.message || "Failed to join room.");
toast.error(err.response?.data?.message || err.message || "Failed to join room.");
setShowPreview(false);
} finally {
setLoading(false);
Expand Down Expand Up @@ -116,3 +119,4 @@ export default function JoinRoom() {
</div>
);
}

Loading