diff --git a/client/src/pages/LiveClassRoom.jsx b/client/src/pages/LiveClassRoom.jsx index 49d265b..355b87a 100644 --- a/client/src/pages/LiveClassRoom.jsx +++ b/client/src/pages/LiveClassRoom.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, useCallback } from "react"; +import { useState, useEffect, useRef, useCallback, memo } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { useAuth } from "../context/AuthContext"; import { getSocket } from "../socket"; @@ -14,36 +14,79 @@ const ICE_CONFIG = { }; const REACTIONS = ["πŸ‘", "❀️", "πŸ˜‚", "πŸŽ‰", "πŸ”₯", "πŸ‘"]; - -// ─── small helpers ──────────────────────────────────────────────────────────── +const TILE_COLORS = [ + "bg-violet-600", + "bg-blue-600", + "bg-emerald-600", + "bg-amber-600", + "bg-pink-600", + "bg-indigo-600", + "bg-teal-600", +]; + +function tileColor(name = "?") { + return TILE_COLORS[(name.charCodeAt(0) || 0) % TILE_COLORS.length]; +} function timeAgo(date) { const s = Math.floor((Date.now() - new Date(date)) / 1000); if (s < 60) return "just now"; - if (s < 3600) return `${Math.floor(s / 60)}m ago`; - return `${Math.floor(s / 3600)}h ago`; + if (s < 3600) return `${Math.floor(s / 60)}m`; + return `${Math.floor(s / 3600)}h`; } -function Avatar({ name = "?" }) { - const cols = [ - "bg-violet-500", - "bg-blue-500", - "bg-emerald-500", - "bg-amber-500", - "bg-pink-500", - "bg-indigo-500", - ]; +// ── Participant tile (student camera or avatar) ──────────────────────────────── +const ParticipantTile = memo(function ParticipantTile({ + stream, + name, + muted = false, + label, + isSmall = false, + indicator, +}) { + const videoRef = useRef(null); + useEffect(() => { + if (videoRef.current) videoRef.current.srcObject = stream ?? null; + }, [stream]); + return (
- {name[0].toUpperCase()} + {stream ? ( +
); -} +}); -// Reusable toolbar icon-button +// ── Control button (Google Meet style) ─────────────────────────────────────── function CtrlBtn({ onClick, active, @@ -52,31 +95,32 @@ function CtrlBtn({ title, label, children, + wide, }) { return ( ); } -// ─── main page ──────────────────────────────────────────────────────────────── +// ─── Main classroom page ────────────────────────────────────────────────────── export default function LiveClassRoom() { const { id } = useParams(); const { user } = useAuth(); @@ -84,64 +128,68 @@ export default function LiveClassRoom() { const isTeacher = user.role === "teacher"; const socket = getSocket(user.id); - // ── class / chat data ─────────────────────────────────────────────────────── + // ── class / chat state ────────────────────────────────────────────────────── const [liveClass, setLiveClass] = useState(null); const [comments, setComments] = useState([]); const [questions, setQuestions] = useState([]); + const [chatOpen, setChatOpen] = useState(true); const [panelTab, setPanelTab] = useState("chat"); const [commentText, setCommentText] = useState(""); - const [replyTo, setReplyTo] = useState(null); // { id, userName } + const [replyTo, setReplyTo] = useState(null); const [questionText, setQuestionText] = useState(""); const chatEndRef = useRef(null); - // ── room presence ─────────────────────────────────────────────────────────── - const [reactions, setReactions] = useState([]); // { id, emoji, name, x } - const [raisedHands, setRaisedHands] = useState({}); // userId β†’ name + // ── presence state ────────────────────────────────────────────────────────── + const [reactions, setReactions] = useState([]); + const [raisedHands, setRaisedHands] = useState({}); const [participantCount, setParticipantCount] = useState(0); const [isHandRaised, setIsHandRaised] = useState(false); const [showReactPicker, setShowReactPicker] = useState(false); const [confirmEnd, setConfirmEnd] = useState(false); - // ── media flags ───────────────────────────────────────────────────────────── + // ── teacher media state ───────────────────────────────────────────────────── const [micOn, setMicOn] = useState(false); const [cameraOn, setCameraOn] = useState(false); const [screenSharing, setScreenSharing] = useState(false); - // student-side: what the teacher is currently broadcasting - const [teacherHasScreen, setTeacherHasScreen] = useState(false); - const [streamActive, setStreamActive] = useState(false); - - // ── recording ─────────────────────────────────────────────────────────────── const [isRecording, setIsRecording] = useState(false); const [uploadingRec, setUploadingRec] = useState(false); const [recDone, setRecDone] = useState(false); + // ── student media state ───────────────────────────────────────────────────── + const [studentMicOn, setStudentMicOn] = useState(false); + const [studentCamOn, setStudentCamOn] = useState(false); + const [streamActive, setStreamActive] = useState(false); + const [teacherHasScreen, setTeacherHasScreen] = useState(false); + + // ── student tiles (teacher's view of all connected students) ──────────────── + // Map + const [studentTiles, setStudentTiles] = useState(new Map()); + // ── media stream refs ─────────────────────────────────────────────────────── const cameraStreamRef = useRef(null); // teacher: getUserMedia stream const screenStreamRef = useRef(null); // teacher: getDisplayMedia stream - const studentMicRef = useRef(null); // student: mic-only getUserMedia stream - // When teacher's screen track arrives before remoteScreenRef DOM node exists, - // we hold the MediaStream here and mount it in a useEffect. - const pendingScreenRef = useRef(null); - - // ── WebRTC refs ───────────────────────────────────────────────────────────── - // Teacher keeps one RTCPeerConnection per viewer - const peerConnsRef = useRef(new Map()); // viewerSocketId β†’ PC - const screenSendersRef = useRef(new Map()); // viewerSocketId β†’ RTCRtpSender for screen track - // Student keeps one RTCPeerConnection to teacher - const peerConnRef = useRef(null); - const makingOfferRef = useRef(false); // guard against simultaneous renegotiations + const studentMicStreamRef = useRef(null); // student: mic stream + const studentCamStreamRef = useRef(null); // student: camera stream + const pendingScreenRef = useRef(null); // student: pending screen stream before mount // ── video element refs ────────────────────────────────────────────────────── - const localCameraRef = useRef(null); // teacher: camera