diff --git a/client/src/components/SessionUsersPanel.tsx b/client/src/components/SessionUsersPanel.tsx index 97f5542..47313a1 100644 --- a/client/src/components/SessionUsersPanel.tsx +++ b/client/src/components/SessionUsersPanel.tsx @@ -2,16 +2,18 @@ import UserCamera from "./ui/UserCamera"; import UserDevicesControls from "./ui/UserDevicesControls"; import DraggableContainer from "./DraggableContainer"; import { useWebRTC } from "../hooks/useWebRTC"; -import { useCallback } from "react"; +import clsx from "clsx"; interface SessionUsersPanelProps { roomId: string; autoJoin?: boolean; + mode?: "full" | "mini"; } function SessionUsersPanel({ roomId, autoJoin = false, + mode = "full", }: SessionUsersPanelProps) { const { localStream, @@ -26,21 +28,38 @@ function SessionUsersPanel({ const hasLocalStream = localStream !== null; // Callback для отправки состояния speaking - const handleSpeakingChange = useCallback((isSpeaking: boolean) => { + const handleSpeakingChange = (isSpeaking: boolean) => { updateSpeakingState?.(isSpeaking); - }, [updateSpeakingState]); + }; - return ( - = 640 ? "bottom-right" : "top-right"} - padding="1.111vw" - className="flex gap-4" - > + // Вычисляем количество камер для grid + const activeCamerasCount = + (localStream ? 8 : 0) + + participants.filter( + (p) => p.stream != null && p.stream.getTracks().length > 0 + ).length; + // Определяем количество колонок в зависимости от количества камер + // 1-2 камеры: 1 колонка (друг под другом), 3-4: 2 колонки, 5-9: 3 колонки, 10-16: 4 колонки, 17-25: 5 колонок + const getGridColumns = (count: number): number => { + if (count <= 2) return 1; + if (count <= 4) return 2; + if (count <= 9) return 3; + if (count <= 16) return 4; + return 5; + }; + + const gridColumns = getGridColumns(activeCamerasCount); + + // Вычисляем количество рядов для правильного расчета высоты + const gridRows = Math.ceil(activeCamerasCount / gridColumns); + + // Рендерим камеры + const camerasContent = ( + <> {/* Локальная камера пользователя - показываем только если есть разрешение */} {localStream && ( ( console.log(`Mute user ${participant.id}`)} @@ -82,13 +102,53 @@ function SessionUsersPanel({ ))} - + + ); + + // Для режима full используем DraggableContainer + if (mode === "full") { + return ( + = 640 ? "bottom-right" : "top-right"} + padding="1.111vw" + className="z-[999] flex gap-4" + > + {camerasContent} + + ); + } + + // Для режима mini используем flex-обертку для центрирования и внутри grid + return ( +
+
+ {camerasContent} +
+
); } diff --git a/client/src/components/ui/UserCamera.tsx b/client/src/components/ui/UserCamera.tsx index 1672c1f..264b118 100644 --- a/client/src/components/ui/UserCamera.tsx +++ b/client/src/components/ui/UserCamera.tsx @@ -37,6 +37,8 @@ interface UserCameraProps { isSpeaking?: boolean; // Для удаленных участников - получаем по Socket.IO onSpeakingChange?: (isSpeaking: boolean) => void; // Для локального - отправляем изменения hasLocalMediaPermission?: boolean; // Есть ли у локального пользователя разрешение на медиа + mode: "full" | "mini"; + className?: string; } export default function UserCamera({ @@ -53,6 +55,8 @@ export default function UserCamera({ isSpeaking: remoteSpeaking, onSpeakingChange, hasLocalMediaPermission = false, + mode = "full", + className, }: UserCameraProps) { const ref = useRef(null); // Для удаленных участников: если у локального пользователя есть разрешение на медиа - unmute, иначе mute (для autoplay) @@ -243,9 +247,7 @@ export default function UserCamera({ const newMutedState = !isAudioMuted; setIsAudioMuted(newMutedState); console.log( - `[UserCamera] ${name} audio ${ - newMutedState ? "muted" : "unmuted" - }` + `[UserCamera] ${name} audio ${newMutedState ? "muted" : "unmuted"}` ); } }; @@ -268,9 +270,12 @@ export default function UserCamera({ return (
void; @@ -14,6 +15,7 @@ export interface UserDevicesControlsProps { isAudioMuted: boolean; isVideoMuted: boolean; hasLocalStream?: boolean; + mode?: "full" | "mini"; } export default function UserDevicesControls({ @@ -22,6 +24,7 @@ export default function UserDevicesControls({ isAudioMuted, isVideoMuted, hasLocalStream = true, + mode = "full", }: UserDevicesControlsProps) { const { setModal } = useModalStore(); @@ -30,7 +33,13 @@ export default function UserDevicesControls({ } return ( -
+