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
89 changes: 89 additions & 0 deletions frontend/src/components/PermissionErrorModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { AlertTriangle, RefreshCw, X } from "lucide-react";

interface PermissionErrorModalProps {
isOpen: boolean;
message: string;
onRetry: () => void;
onClose: () => void;
}

const PermissionErrorModal: React.FC<PermissionErrorModalProps> = ({
isOpen,
message,
onRetry,
onClose,
}) => {
return (
<AnimatePresence>
{isOpen && (
<motion.div
className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-9999"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{/* Modal Card */}
<motion.div
initial={{ scale: 0.85, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
transition={{ type: "spring", damping: 14 }}
className="bg-white rounded-2xl shadow-xl p-6 w-[90%] max-w-md"
>
{/* Header */}
<div className="flex justify-between items-center mb-3">
<div className="flex items-center gap-2 text-red-600 font-semibold text-lg">
<AlertTriangle size={22} />
Device Permission Error
</div>
<button onClick={onClose}>
<X size={22} className="text-gray-500 hover:text-gray-700" />
</button>
</div>

{/* Message */}
<p className="text-gray-800 leading-relaxed">{message}</p>

{/* Actions */}
<div className="mt-5 flex flex-col gap-3">

{/* Retry Button */}
<button
onClick={onRetry}
className="flex items-center justify-center gap-2 bg-red-600 hover:bg-red-700 text-white py-2.5 rounded-xl font-medium transition-all"
>
<RefreshCw size={18} />
Retry Access
</button>

{/* Browser Permission Links */}
<div className="text-sm text-gray-700">
<p className="font-semibold mb-1">Grant permissions in browser:</p>

<a
href="chrome://settings/content/camera"
target="_blank"
className="block underline text-blue-600"
>
Open Camera Permissions
</a>

<a
href="chrome://settings/content/microphone"
target="_blank"
className="block underline text-blue-600 mt-1"
>
Open Microphone Permissions
</a>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
};

export default PermissionErrorModal;
114 changes: 93 additions & 21 deletions frontend/src/pages/InRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ interface ChatMessage {
const InRoom: React.FC = () => {
const { roomName } = useParams<{ roomName: string }>();
const navigate = useNavigate();
const [permissionError, setPermissionError] = useState<string | null>(null);
const [showPermissionModal, setShowPermissionModal] = useState(false);


const [micOn, setMicOn] = useState(true);
const [videoOn, setVideoOn] = useState(true);
Expand Down Expand Up @@ -233,32 +236,50 @@ const InRoom: React.FC = () => {
}, [roomName, userName]);

// Initialize media stream
useEffect(() => {
const initMedia = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
mediaStreamRef.current = stream;
if (localVideoRef.current) localVideoRef.current.srcObject = stream;
setMicOn(stream.getAudioTracks().some((t) => t.enabled));
setVideoOn(stream.getVideoTracks().some((t) => t.enabled));
} catch (err) {
console.error("Error accessing camera/mic:", err);
alert("Please allow camera and microphone permissions.");
setMicOn(false);
setVideoOn(false);
const initMedia = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
});

// Success
setPermissionError(null);
setShowPermissionModal(false);

// Save stream to your refs/states
mediaStreamRef.current = stream;
setMicOn(true);
setVideoOn(true);

// Attach tracks, send to peer, whatever your flow is
if (localVideoRef.current) {
localVideoRef.current.srcObject = stream;
}
};

initMedia();
} catch (err: any) {
console.error("Error accessing camera/mic:", err);

return () => {
mediaStreamRef.current?.getTracks().forEach((track) => track.stop());
};
let msg = "Camera/Microphone access was denied. Please allow permissions.";

if (err.name === "NotAllowedError") {
msg = "You blocked camera/mic access for this site. Please enable it from browser settings.";
} else if (err.name === "NotFoundError") {
msg = "No camera or microphone was found on your device.";
}

setPermissionError(msg);
setShowPermissionModal(true);

setMicOn(false);
setVideoOn(false);
}
};
useEffect(() => {
initMedia();
}, []);


// When local media becomes available and a peer is present, ensure tracks are added
useEffect(() => {
// If we have a pc and a media stream, make sure tracks are added
Expand Down Expand Up @@ -452,6 +473,55 @@ const InRoom: React.FC = () => {
</div>
);
}
const permissionModal = showPermissionModal && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-gray-900 text-white w-full max-w-md p-6 rounded-2xl shadow-xl border border-gray-700"
>
<h2 className="text-xl font-semibold mb-3">Permissions Required</h2>

<p className="text-gray-300 mb-4 text-sm leading-relaxed">
{permissionError}
</p>

{/* Instructions when user BLOCKED permissions */}
{permissionError?.includes("blocked") && (
<div className="text-sm text-gray-400 mb-4">
<p className="mb-2">To enable camera/mic permissions:</p>
<ul className="list-disc ml-5 space-y-1">
<li>Click the lock icon in the URL bar</li>
<li>Open "Site Settings"</li>
<li>Set Camera and Microphone to "Allow"</li>
<li>Reload the page</li>
</ul>
</div>
)}

<div className="flex justify-end gap-3 mt-6">
<button
onClick={() => setShowPermissionModal(false)}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-sm"
>
Close
</button>

<button
onClick={() => {
setShowPermissionModal(false);
initMedia(); // 🔥 This re-triggers browser permission prompt
}}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 rounded-lg text-sm font-medium"
>
Retry
</button>
</div>
</motion.div>
</div>
);


return (
<HotKeys keyMap={keyMap} handlers={handlers}>
Expand All @@ -477,6 +547,8 @@ const InRoom: React.FC = () => {
</header>

{/* Main Video Area */}
{permissionModal}

<div className="flex-1 flex flex-col md:flex-row">
{/* Video Grid */}
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-4 p-4">
Expand Down
43 changes: 36 additions & 7 deletions frontend/src/pages/JoinRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,40 @@ export default function JoinRoom() {
const API_BASE = "http://localhost:3000/api/rooms";

// 🔹 Step 1: Handle room join initiation
const handleJoinClick = (e: React.FormEvent) => {
const handleJoinClick = async (e: React.FormEvent) => {
e.preventDefault();

if (!roomName.trim()) return alert("Please enter a room name!");
// Instead of joining immediately, show preview first
setShowPreview(true);

// ⬇️ PREVIEW SHOULD ONLY SHOW IF MEDIA PERMISSION IS SUCCESSFUL
try {
const tempStream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
});

setStream(tempStream);
setShowPreview(true);
} catch (err: any) {
console.error("Media access denied:", err);

if (err.name === "NotAllowedError") {
alert(
"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);
}
}
};

// 🔹 Step 2: After preview confirmation, actually join the backend room
const handleConfirmJoin = async (mediaStream: MediaStream) => {
if (!mediaStream) {
alert("Camera or microphone not available!");
return;
}

setStream(mediaStream);
setLoading(true);

Expand All @@ -39,8 +64,10 @@ export default function JoinRoom() {
);

alert("Joined room successfully!");
// Optional: Stop preview stream before entering actual call
mediaStream.getTracks().forEach(track => track.stop());

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

navigate(`/room/${roomName}`);
} catch (err: any) {
console.error("Join room error:", err);
Expand All @@ -52,10 +79,10 @@ export default function JoinRoom() {
};

// 🔹 Step 3: Conditional render — preview or join form
if (showPreview) {
if (showPreview && stream) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-900 dark:bg-gray-950">
<PreJoinPreview onJoin={handleConfirmJoin} />
<PreJoinPreview onJoin={handleConfirmJoin} initialStream={stream} />
</div>
);
}
Expand All @@ -67,6 +94,7 @@ export default function JoinRoom() {
<h2 className="text-3xl font-bold text-green-600 dark:text-green-500 text-center mb-6">
Join a Room
</h2>

<form onSubmit={handleJoinClick} className="space-y-4">
<input
type="text"
Expand All @@ -75,6 +103,7 @@ export default function JoinRoom() {
placeholder="Enter room name"
className="w-full border border-gray-300 dark:border-gray-700 rounded-lg px-4 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-400 dark:focus:ring-green-500"
/>

<Button
type="submit"
disabled={loading}
Expand Down
Loading