Users Cameras; Devices controls;
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user