Users Cameras; Devices controls;

This commit is contained in:
2025-10-15 17:06:05 +05:00
parent 728d727cd1
commit 2ade35a8ed
6 changed files with 276 additions and 7 deletions
@@ -0,0 +1,59 @@
import UserCamera from "./ui/UserCamera";
import UserDevicesControls from "./ui/UserDevicesControls";
export default function SessionUsersPanel() {
const users = [
{
id: 1,
name: "John Doe",
isSpeaking: true,
isMuted: false,
isVideoOff: false,
isControlDisabled: false,
isAdmin: true,
},
{
id: 2,
name: "Jane Doe",
isSpeaking: false,
isMuted: true,
isVideoOff: true,
isControlDisabled: true,
},
{
id: 3,
name: "Jim Doe",
isSpeaking: false,
isMuted: false,
isVideoOff: false,
isControlDisabled: false,
},
];
function handleMute(id: number) {
console.log(`Mute user ${id}`);
}
function handleVideoOff(id: number) {
console.log(`Video off user ${id}`);
}
function handleCanControl(id: number) {
console.log(`Can control user ${id}`);
}
return (
<div className="flex gap-4 items-end">
<div className="flex gap-4 w-max items-end">
{users.map((user) => (
<UserCamera
key={user.id}
onMute={() => handleMute(user.id)}
onVideoOff={() => handleVideoOff(user.id)}
onCanControl={() => handleCanControl(user.id)}
{...user}
/>
))}
</div>
<UserDevicesControls />
</div>
);
}
+1 -1
View File
@@ -9,7 +9,7 @@ export default function Admin({ className }: { className?: string }) {
className
)}
>
<div className="size-[0.694vw]">
<div className="size-[0.694vw] text-white">
<CrownIcon />
</div>
</div>
+2 -6
View File
@@ -23,18 +23,14 @@ export default function Tooltip({
showDelay = 500,
}: TooltipProps) {
const [isVisible, setIsVisible] = useState(false);
const [tooltipPosition, setTooltipPosition] = useState<TooltipPosition>({
top: 0,
left: 0,
transform: "none",
});
const [tooltipPosition, setTooltipPosition] = useState<TooltipPosition>({});
const tooltipWrapperRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!tooltipWrapperRef.current) return;
const current = tooltipWrapperRef.current;
current.addEventListener("mouseenter", () => setIsVisible(true));
current.addEventListener("mouseleave", () => setIsVisible(false));
return () => {
+160
View File
@@ -0,0 +1,160 @@
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import HandRaisedOffFilledIcon from "../icons/HandRaisedOffFilledIcon";
import HandRaisedFilledIcon from "../icons/HandRaisedFilledIcon";
import MicrophoneFilledIcon from "../icons/MicrophoneFilledIcon";
import VideoOffFilledIcon from "../icons/VideoOffFilledIcon";
import MicrophoneOffIcon from "../icons/MicrophoneOffIcon";
import VideoFilledIcon from "../icons/VideoFilledIcon";
import ControlButton from "./ControlButton";
import Admin from "../indicators/Admin";
import clsx from "clsx";
interface UserCameraControlsProps {
isMuted: boolean;
isVideoOff: boolean;
isControlDisabled: boolean;
onMute: () => void;
onVideoOff: () => void;
onCanControl: () => void;
}
interface UserCameraProps extends UserCameraControlsProps {
isAdmin?: boolean;
name?: string;
mediaStream?: string;
isSpeaking?: boolean;
}
export default function UserCamera({
isMuted,
isVideoOff,
isControlDisabled,
onMute,
onVideoOff,
onCanControl,
isSpeaking = false,
isAdmin = false,
name = "Гость",
mediaStream = "",
}: UserCameraProps) {
const [isHover, setIsHover] = useState(false);
return (
<motion.div
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
animate={{
width: isHover ? "10.833vw" : "6.944vw",
border: isSpeaking
? "0.139vw solid #7B60F3"
: "0.139vw solid #FFFFFF4D",
}}
className={clsx(
"aspect-square rounded-[1.667vw] bg-yellow-500 relative flex-shrink-0",
isAdmin && "order-last"
)}
>
{isAdmin && <Admin className="absolute top-0 right-0" />}
<AnimatePresence mode="wait">
{isHover && (
<motion.div
key="name"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="absolute whitespace-nowrap text-white button-s top-[0.556vw] left-1/2 translate-x-[-50%] px-[0.556vw] py-[0.278vw] rounded-full bg-[#14141440] backdrop-blur-[4px]"
>
{name}
</motion.div>
)}
</AnimatePresence>
<video
src={mediaStream}
className="size-full object-cover"
autoPlay
muted={isMuted}
playsInline
/>
<UserCameraControls
isMuted={isMuted}
isVideoOff={isVideoOff}
isControlDisabled={isControlDisabled}
isHover={isHover}
onMute={onMute}
onVideoOff={onVideoOff}
onCanControl={onCanControl}
/>
</motion.div>
);
}
function UserCameraControls({
isMuted,
isVideoOff,
isControlDisabled,
isHover,
onMute,
onVideoOff,
onCanControl,
}: UserCameraControlsProps & { isHover: boolean }) {
return (
<div className="absolute bottom-[0.278vw] left-1/2 translate-x-[-50%]">
<AnimatePresence mode="wait">
{isHover ? (
<motion.div
key="controls"
className="flex gap-[0.278vw] mb-[0.278vw]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<ControlButton
icon={isMuted ? <MicrophoneOffIcon /> : <MicrophoneFilledIcon />}
size={"small"}
enabled={!isMuted}
onClick={onMute}
/>
<ControlButton
icon={isVideoOff ? <VideoOffFilledIcon /> : <VideoFilledIcon />}
size={"small"}
enabled={!isVideoOff}
onClick={onVideoOff}
/>
<ControlButton
icon={
isControlDisabled ? (
<HandRaisedOffFilledIcon />
) : (
<HandRaisedFilledIcon />
)
}
size={"small"}
enabled={!isControlDisabled}
onClick={onCanControl}
/>
</motion.div>
) : (
<motion.div
key="controls-muted"
className="size-[1.667vw] bg-[#14141426] backdrop-blur-[4px] rounded-full flex items-center justify-center z-10"
initial={{ opacity: 0 }}
animate={{ opacity: isMuted ? 1 : 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<div className="size-[0.972vw] text-white flex items-center justify-center z-20">
<MicrophoneOffIcon />
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
@@ -0,0 +1,52 @@
import MicrophoneFilledIcon from "../icons/MicrophoneFilledIcon";
import ControlButton from "./ControlButton";
import VideoFilledIcon from "../icons/VideoFilledIcon";
import HandRaisedFilledIcon from "../icons/HandRaisedFilledIcon";
import CogFilledIcon from "../icons/CogFilledIcon";
import useModalStore from "../../store/modalStore";
import SettingsModal from "../modals/SettingsModal";
export default function UserDevicesControls() {
const { setModal } = useModalStore();
function ToggleAudioDevice() {
console.log("Mute device");
}
function ToggleVideoDevice() {
console.log("Video device");
}
function ToggleCanControl() {
console.log("Can control device");
}
function ToggleSettings() {
setModal(<SettingsModal />);
}
return (
<div className="hidden 2xl:flex aspect-square p-[0.556vw] flex-wrap justify-between items-center size-[6.944vw] rounded-[1.667vw] bg-[#00000040] shadow-[0_4px_40px_0_#0F10111A] backdrop-blur-[10px]">
<ControlButton
size="large"
icon={<MicrophoneFilledIcon />}
onClick={ToggleAudioDevice}
enabled={true}
/>
<ControlButton
size="large"
icon={<VideoFilledIcon />}
enabled={true}
onClick={ToggleVideoDevice}
/>
<ControlButton
size="large"
icon={<HandRaisedFilledIcon />}
onClick={ToggleCanControl}
enabled={true}
/>
<ControlButton
size="large"
icon={<CogFilledIcon />}
enabled={true}
onClick={ToggleSettings}
/>
</div>
);
}
+2
View File
@@ -8,6 +8,7 @@ import SettingsModal from "../components/modals/SettingsModal";
import useModalStore from "../store/modalStore";
import CogFilledIcon from "../components/icons/CogFilledIcon";
import ParticipantsPopup from "../components/popups/ParticipantsPopup";
import SessionUsersPanel from "../components/SessionUsersPanel";
function HomePage() {
const { data: user } = useMe();
@@ -38,6 +39,7 @@ function HomePage() {
<ShareFilledIcon />
</div>
</FloatingActionButton>
<SessionUsersPanel />
<FloatingActionButton
variant="default"