diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 453820e..80249e7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,6 +25,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-hook-form": "^7.65.0", + "react-hotkeys": "^2.0.0", "react-router-dom": "^7.9.4", "shadcn": "^3.4.1", "socket.io-client": "^4.8.1", @@ -160,7 +161,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1387,7 +1387,6 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -1471,7 +1470,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -3023,7 +3021,6 @@ "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3325,7 +3322,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -5004,6 +5000,18 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lucide-react": { "version": "0.545.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz", @@ -5678,7 +5686,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5732,6 +5739,17 @@ "node": ">=6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5880,7 +5898,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5890,7 +5907,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5914,6 +5930,24 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-hotkeys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-hotkeys/-/react-hotkeys-2.0.0.tgz", + "integrity": "sha512-3n3OU8vLX/pfcJrR3xJ1zlww6KS1kEJt0Whxc4FiGV+MJrQ1mYSYI3qS/11d2MJDFm8IhOXMTFQirfu6AVOF6Q==", + "license": "ISC", + "dependencies": { + "prop-types": "^15.6.1" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-router": { "version": "7.9.4", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", @@ -6944,7 +6978,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -7249,7 +7282,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index d37ea95..1ef3935 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-hook-form": "^7.65.0", + "react-hotkeys": "^2.0.0", "react-router-dom": "^7.9.4", "shadcn": "^3.4.1", "socket.io-client": "^4.8.1", diff --git a/frontend/src/pages/InRoom.tsx b/frontend/src/pages/InRoom.tsx index c6fad6a..251919f 100644 --- a/frontend/src/pages/InRoom.tsx +++ b/frontend/src/pages/InRoom.tsx @@ -1,110 +1,237 @@ import React, { useState, useEffect, useRef } from "react"; import { Mic, MicOff, Video, VideoOff, PhoneOff, Users, MessageSquare } from "lucide-react"; import { motion } from "framer-motion"; +import { HotKeys } from "react-hotkeys"; + +const keyMap = { + TOGGLE_MIC: "ctrl+m", + TOGGLE_VIDEO: "ctrl+v", + TOGGLE_CHAT: "ctrl+c", +}; const InRoom: React.FC<{ roomName: string }> = ({ roomName }) => { - const [micOn, setMicOn] = useState(true); - const [videoOn, setVideoOn] = useState(true); - const [showChat, setShowChat] = useState(false); - const localVideoRef = useRef(null); - const remoteVideoRef = useRef(null); - - useEffect(() => { - document.title = `${roomName} | PeerCall`; - // TODO: Setup WebRTC connection here in next step - }, [roomName]); - - return ( -
- {/* Header */} -
-

Room: {roomName}

-
- - 2 Participants -
-
- - {/* Main Video Area */} -
- {/* Video Grid */} -
-
-
-
-
-
- - {/* Chat Panel (toggle) */} - {showChat && ( - -
In-call Chat
-
-

No messages yet...

+ const [micOn, setMicOn] = useState(true); + const [videoOn, setVideoOn] = useState(true); + const [showChat, setShowChat] = useState(false); + const localVideoRef = useRef(null); + const remoteVideoRef = useRef(null); + const mediaStreamRef = useRef(null); + const [messages, setMessages] = useState<{ user: string; text: string; time?: string }[]>([]); + const [chatInput, setChatInput] = useState(""); + const chatEndRef = useRef(null); + + useEffect(() => { + document.title = `${roomName} | PeerCall`; + //initalize local media when joining the room + const initMedia = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: true, + }); + mediaStreamRef.current = stream; + if (localVideoRef.current) { + localVideoRef.current.srcObject = stream; + } + const hasAudio = stream.getAudioTracks().some((t) => t.enabled !== false); + const hasVideo = stream.getVideoTracks().some((t) => t.enabled !== false); + setMicOn(hasAudio); + setVideoOn(hasVideo); + } catch (err) { + console.error("Failed to get user media:", err);//due to denial or other unavailability issues + setMicOn(false); + setVideoOn(false); + } + }; + + initMedia(); + + return () => { + if (mediaStreamRef.current) { + mediaStreamRef.current.getTracks().forEach((t) => t.stop()); + mediaStreamRef.current = null; + } + }; + }, [roomName]); + + const handlers = {//to handle the keyboard events + TOGGLE_MIC: (e: KeyboardEvent) => { + e.preventDefault(); + toggleMic(); + }, + TOGGLE_VIDEO: (e: KeyboardEvent) => { + e.preventDefault(); + toggleVideo(); + }, + TOGGLE_CHAT: (e: KeyboardEvent) => { + e.preventDefault(); + toggleChat(); + }, + }; + const toggleMic = () => { + const stream = mediaStreamRef.current; + if (!stream) { + console.warn("No local media stream available to toggle mic"); + setMicOn((s) => !s); + return; + } + const audioTracks = stream.getAudioTracks(); + if (audioTracks.length === 0) { + console.warn("No audio tracks found"); + setMicOn(false); + return; + } + const enabled = !audioTracks[0].enabled; + audioTracks.forEach((t) => (t.enabled = enabled)); + setMicOn(enabled); + }; + + const toggleVideo = () => { + const stream = mediaStreamRef.current; + if (!stream) { + console.warn("No local media stream available to toggle video"); + setVideoOn((s) => !s); + return; + } + const videoTracks = stream.getVideoTracks(); + if (videoTracks.length === 0) { + console.warn("No video tracks found"); + setVideoOn(false); + return; + } + const enabled = !videoTracks[0].enabled; + videoTracks.forEach((t) => (t.enabled = enabled)); + setVideoOn(enabled); + }; + + const toggleChat = () => { + setShowChat((prev) => !prev); + }; + + useEffect(() => { + if(chatEndRef.current)chatEndRef.current.scrollIntoView({behavior:"smooth"}); + },[messages,showChat]); + + return ( + +
+ {/* Header */} +
+

Room: {roomName}

+
+ + 2 Participants +
+
+ + {/* Main Video Area */} +
+ {/* Video Grid */} +
+
+
+
+
+
+ + {/* Chat Panel */} + {showChat && ( + +
+ In-call Chat +
+
+ {messages.length === 0 ? ( +

No messages yet...

+ ) : ( +
+ {messages.map((msg, idx) => ( +
+
+ {msg.user} + {msg.time ? new Date(msg.time).toLocaleTimeString() : ""}
-
- - +
+ {msg.text}
- +
+ ))} +
+
)} -
- - {/* Controls */} -
- - - - - - - -
+
+
{ e.preventDefault(); + if (chatInput.trim() === "") return; setMessages((s) => [...s, { user: "You", text: chatInput.trim(), time: new Date().toISOString() }]); setChatInput(""); + }}> + setChatInput(e.target.value)} + placeholder="Type a message..." + className="flex-1 bg-gray-800 rounded-l-md px-3 py-2 text-sm focus:outline-none" + /> + +
+ + )}
- ); + + {/* Controls */} + +
+ + ); }; export default InRoom;