| // src/components/messages/VideoCallModal.tsx | |
| import { useEffect } from "react"; | |
| import { | |
| Phone, PhoneOff, Mic, MicOff, Video, VideoOff, Minimize2, | |
| User, Clock, | |
| } from "lucide-react"; | |
| import { toast } from "react-toastify"; | |
| import type { UseVideoCallReturn } from "@/hooks/useVideoCall"; | |
| interface VideoCallModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| remoteUsername: string; | |
| currentUsername: string; | |
| mode: "outgoing" | "incoming" | "active"; | |
| onAccept?: () => void; | |
| onReject?: () => void; | |
| videoCall: UseVideoCallReturn; | |
| } | |
| export function VideoCallModal({ | |
| isOpen, | |
| onClose, | |
| remoteUsername, | |
| currentUsername, | |
| mode, | |
| onAccept, | |
| onReject, | |
| videoCall, | |
| }: VideoCallModalProps) { | |
| const { | |
| localVideoRef, | |
| remoteVideoRef, | |
| isMuted, | |
| isVideoOff, | |
| callDuration, | |
| toggleMute, | |
| toggleVideo, | |
| endCall, | |
| formatDuration, | |
| } = videoCall; | |
| // When modal closes, end the call (if active) | |
| useEffect(() => { | |
| if (!isOpen && mode === "active") { | |
| endCall(); | |
| } | |
| }, [isOpen, mode, endCall]); | |
| // Prevent accidental background scroll | |
| useEffect(() => { | |
| if (isOpen) { | |
| document.body.style.overflow = "hidden"; | |
| return () => { | |
| document.body.style.overflow = ""; | |
| }; | |
| } | |
| }, [isOpen]); | |
| if (!isOpen) return null; | |
| const handleEndCall = () => { | |
| endCall(); | |
| onClose(); | |
| }; | |
| return ( | |
| <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black"> | |
| {/* Remote video (full screen) */} | |
| <video | |
| ref={remoteVideoRef} | |
| autoPlay | |
| playsInline | |
| className="absolute inset-0 w-full h-full object-cover" | |
| /> | |
| {/* Local video (picture‑in‑picture) */} | |
| <div className="absolute bottom-6 right-6 w-40 h-28 sm:w-52 sm:h-36 rounded-2xl overflow-hidden border-2 border-white/30 shadow-2xl z-10 bg-gray-800"> | |
| <video | |
| ref={localVideoRef} | |
| autoPlay | |
| playsInline | |
| muted | |
| className="w-full h-full object-cover" | |
| /> | |
| {isVideoOff && ( | |
| <div className="absolute inset-0 bg-gray-900 flex items-center justify-center"> | |
| <User className="w-10 h-10 text-gray-500" /> | |
| </div> | |
| )} | |
| </div> | |
| {/* Status overlay */} | |
| <div className="absolute top-6 left-1/2 -translate-x-1/2 z-10"> | |
| {mode === "outgoing" && ( | |
| <div className="bg-gray-900/80 text-white px-6 py-3 rounded-2xl flex items-center gap-3 text-sm"> | |
| <div className="w-3 h-3 rounded-full bg-yellow-400 animate-pulse" /> | |
| Calling {remoteUsername}... | |
| </div> | |
| )} | |
| {mode === "incoming" && ( | |
| <div className="bg-gray-900/80 text-white px-6 py-3 rounded-2xl text-center"> | |
| <p className="text-lg font-semibold">{remoteUsername}</p> | |
| <p className="text-sm text-gray-300">is calling you...</p> | |
| </div> | |
| )} | |
| {mode === "active" && ( | |
| <div className="bg-gray-900/70 text-white px-4 py-2 rounded-full flex items-center gap-2 text-sm"> | |
| <Clock className="w-4 h-4" /> | |
| {formatDuration(callDuration)} | |
| </div> | |
| )} | |
| </div> | |
| {/* Caller info (when ringing) */} | |
| {(mode === "outgoing" || mode === "incoming") && ( | |
| <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10 text-center"> | |
| <div className="w-24 h-24 rounded-full bg-linear-to-br from-primary to-secondary flex items-center justify-center text-white text-3xl font-bold mx-auto mb-4 shadow-2xl"> | |
| {(remoteUsername[0] || "?").toUpperCase()} | |
| </div> | |
| <h2 className="text-white text-xl font-semibold">{remoteUsername}</h2> | |
| </div> | |
| )} | |
| {/* Controls */} | |
| <div className="absolute bottom-24 left-1/2 -translate-x-1/2 flex gap-4 z-10"> | |
| {mode === "incoming" ? ( | |
| <> | |
| <button | |
| onClick={() => { | |
| onReject?.(); | |
| onClose(); | |
| }} | |
| className="w-16 h-16 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center text-white shadow-xl transition" | |
| > | |
| <PhoneOff className="w-7 h-7" /> | |
| </button> | |
| <button | |
| onClick={() => onAccept?.()} | |
| className="w-16 h-16 rounded-full bg-green-500 hover:bg-green-600 flex items-center justify-center text-white shadow-xl transition animate-pulse" | |
| > | |
| <Phone className="w-7 h-7" /> | |
| </button> | |
| </> | |
| ) : ( | |
| <> | |
| <button | |
| onClick={toggleMute} | |
| className={`w-12 h-12 rounded-full flex items-center justify-center text-white shadow-lg transition ${ | |
| isMuted ? "bg-red-500" : "bg-gray-700 hover:bg-gray-600" | |
| }`} | |
| > | |
| {isMuted ? <MicOff className="w-5 h-5" /> : <Mic className="w-5 h-5" />} | |
| </button> | |
| <button | |
| onClick={handleEndCall} | |
| className="w-16 h-16 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center text-white shadow-xl transition" | |
| > | |
| <PhoneOff className="w-7 h-7" /> | |
| </button> | |
| <button | |
| onClick={toggleVideo} | |
| className={`w-12 h-12 rounded-full flex items-center justify-center text-white shadow-lg transition ${ | |
| isVideoOff ? "bg-red-500" : "bg-gray-700 hover:bg-gray-600" | |
| }`} | |
| > | |
| {isVideoOff ? <VideoOff className="w-5 h-5" /> : <Video className="w-5 h-5" />} | |
| </button> | |
| </> | |
| )} | |
| </div> | |
| {/* Minimize button (when call is active) */} | |
| {mode === "active" && ( | |
| <button | |
| onClick={onClose} | |
| className="absolute top-6 left-6 z-10 bg-gray-800/70 text-white p-2 rounded-full hover:bg-gray-700" | |
| > | |
| <Minimize2 className="w-5 h-5" /> | |
| </button> | |
| )} | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 6.05 kB
- Xet hash:
- c3d8bc6b36c7378aaf775094ee39709607214bc3c4e27bbed6724bed540c2451
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.