diff --git a/client/.env b/client/.env
index 0f22f67..5e374be 100644
--- a/client/.env
+++ b/client/.env
@@ -1,4 +1,4 @@
-VITE_API_URL=http://localhost:3000
-VITE_WEBRTC_URL=http://localhost:3001
-# VITE_API_URL=https://stream.graff.estate/api
-# VITE_WEBRTC_URL=https://stream.graff.estate
\ No newline at end of file
+# VITE_API_URL=http://localhost:3000
+# VITE_WEBRTC_URL=http://localhost:3001
+VITE_API_URL=https://stream.graff.estate/api
+VITE_WEBRTC_URL=https://stream.graff.estate
\ No newline at end of file
diff --git a/client/src/components/DraggableContainer.tsx b/client/src/components/DraggableContainer.tsx
index f50da2d..6f93a6d 100644
--- a/client/src/components/DraggableContainer.tsx
+++ b/client/src/components/DraggableContainer.tsx
@@ -496,7 +496,7 @@ export default function DraggableContainer({
// Если компонент отключен, просто рендерим children без стилей и логики
if (!enabled) {
- return <>{children}>;
+ return
{children}
;
}
return (
diff --git a/client/src/components/SessionUsersPanel.tsx b/client/src/components/SessionUsersPanel.tsx
index e8db0ca..fb556b2 100644
--- a/client/src/components/SessionUsersPanel.tsx
+++ b/client/src/components/SessionUsersPanel.tsx
@@ -5,6 +5,8 @@ import { useWebRTC } from "../hooks/useWebRTC";
import clsx from "clsx";
import { useEffect, useRef, useState } from "react";
+const LOCAL_CAMERAS_COUNT = 10;
+
interface SessionUsersPanelProps {
roomId: string;
autoJoin?: boolean;
@@ -33,11 +35,59 @@ function SessionUsersPanel({
const lastSentSpeakingRef = useRef(false);
const speakingStateTimeoutRef = useRef(null);
+ // State для отслеживания размеров и ориентации экрана
+ const [windowDimensions, setWindowDimensions] = useState({
+ width: window.innerWidth,
+ height: window.innerHeight,
+ });
+
// Callback для получения изменений состояния speaking от UserCamera
const handleSpeakingChange = (isSpeaking: boolean) => {
setLocalSpeaking(isSpeaking);
};
+ // useEffect для отслеживания изменения ориентации и размеров экрана
+ useEffect(() => {
+ const handleResize = () => {
+ setWindowDimensions({
+ width: window.innerWidth,
+ height: window.innerHeight,
+ });
+ };
+
+ const handleOrientationChange = () => {
+ // Небольшая задержка для корректного получения новых размеров после поворота
+ setTimeout(() => {
+ setWindowDimensions({
+ width: window.innerWidth,
+ height: window.innerHeight,
+ });
+ }, 100);
+ };
+
+ // Слушаем событие resize (срабатывает при изменении размера окна)
+ window.addEventListener("resize", handleResize);
+
+ // Слушаем событие orientationchange (срабатывает при повороте устройства)
+ window.addEventListener("orientationchange", handleOrientationChange);
+
+ // Также слушаем изменения в screen.orientation API (современный способ)
+ if (screen.orientation) {
+ screen.orientation.addEventListener("change", handleOrientationChange);
+ }
+
+ return () => {
+ window.removeEventListener("resize", handleResize);
+ window.removeEventListener("orientationchange", handleOrientationChange);
+ if (screen.orientation) {
+ screen.orientation.removeEventListener(
+ "change",
+ handleOrientationChange
+ );
+ }
+ };
+ }, []);
+
// useEffect для throttle и отправки состояния speaking через socket
useEffect(() => {
// Отправляем только если состояние действительно изменилось
@@ -66,45 +116,80 @@ function SessionUsersPanel({
// Вычисляем количество камер для grid
const activeCamerasCount =
- (localStream ? 8 : 0) +
+ (localStream ? LOCAL_CAMERAS_COUNT : 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 => {
+ const getDesktopGridColumns = (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;
+ return 4;
};
- const gridColumns = getGridColumns(activeCamerasCount);
+ const getMobilePortraitGridColumns = (count: number): number => {
+ if (count <= 3) return 1;
+ if (count <= 12) return 2;
+ return 3;
+ };
- // Вычисляем количество рядов для правильного расчета высоты
- const gridRows = Math.ceil(activeCamerasCount / gridColumns);
+ const getMobileLandscapeGridColumns = (count: number): number => {
+ if (count <= 3) return count;
+ if (count <= 6) return 3;
+ return 4;
+ };
- // Рендерим камеры
- const camerasContent = (
- <>
- {/* Локальная камера пользователя - показываем только если есть разрешение */}
- {localStream && (
- console.log("Toggle control")}
- onSpeakingChange={handleSpeakingChange}
- />
+ const gridColumns =
+ windowDimensions.width >= 1440
+ ? getDesktopGridColumns(activeCamerasCount)
+ : windowDimensions.height / windowDimensions.width < 1
+ ? getMobileLandscapeGridColumns(activeCamerasCount)
+ : getMobilePortraitGridColumns(activeCamerasCount);
+
+ return (
+ = 640 ? "bottom-right" : "top-right"
+ }
+ padding="1.111vw"
+ className={clsx(
+ "z-[999] 2xl:gap-[0.556vw] gap-2",
+ mode === "full"
+ ? "flex"
+ : `2xl:p-[5vw] p-4 w-full 2xl:h-dvh max-2xl:portrait:max-h-[calc(100dvh-17.778vw)] max-2xl:landscape:max-h-[calc(100dvh-8.75vw)] grid grid-cols-${gridColumns}`
)}
+ >
+ {localStream &&
+ Array.from({ length: LOCAL_CAMERAS_COUNT }).map((_, index) => (
+ console.log("Toggle control")}
+ onSpeakingChange={handleSpeakingChange}
+ className={clsx(
+ mode === "mini" &&
+ (activeCamerasCount <= 2
+ ? "m-auto"
+ : activeCamerasCount > 12
+ ? "!aspect-square w-full"
+ : "w-full")
+ )}
+ />
+ ))}
{/* Камеры удаленных участников - показываем только если есть поток с активными треками */}
{participants
@@ -115,6 +200,14 @@ function SessionUsersPanel({
)
.map((participant) => (
12
+ ? "!aspect-square w-full"
+ : "w-full")
+ )}
key={participant.id}
mode={mode}
name={participant.id}
@@ -141,47 +234,32 @@ function SessionUsersPanel({
isVideoMuted={isLocalVideoMuted}
hasLocalStream={hasLocalStream}
/>
- >
+
);
-
- // Для режима 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 (
-
- );
+ // return (
+ //
+ //
+ // {camerasContent}
+ //
+ //
+ // );
}
export default SessionUsersPanel;
diff --git a/client/src/components/ui/ControlsPopover.tsx b/client/src/components/ui/ControlsPopover.tsx
index ba20557..df68eca 100644
--- a/client/src/components/ui/ControlsPopover.tsx
+++ b/client/src/components/ui/ControlsPopover.tsx
@@ -45,7 +45,9 @@ function ControlsPopover({ session }: ControlsPopoverProps) {
function handleClickOpenSharePopup() {
setIsOpened(false);
- setPopup();
+ setPopup(
+
+ );
}
function handleClickOpenSettingsModal() {
@@ -54,7 +56,7 @@ function ControlsPopover({ session }: ControlsPopoverProps) {
}
return (
-
+
(localSpeaking);
-
+
useEffect(() => {
- if (isLocal && onSpeakingChange && prevLocalSpeakingRef.current !== localSpeaking) {
+ if (
+ isLocal &&
+ onSpeakingChange &&
+ prevLocalSpeakingRef.current !== localSpeaking
+ ) {
prevLocalSpeakingRef.current = localSpeaking;
onSpeakingChange(localSpeaking);
}
@@ -276,7 +280,7 @@ export default function UserCamera({
className={clsx(
mode === "full"
? "aspect-square h-fit group 2xl:rounded-[1.667vw] rounded-2xl relative flex-shrink-0 pointer-events-auto 2xl:hover:w-[10.833vw] 2xl:w-[6.944vw] sm:w-[15.625vw] w-[27.778vw] overflow-hidden"
- : "aspect-video 2xl:rounded-[2.222vw] rounded-[32px] overflow-hidden group relative flex-shrink-0 pointer-events-auto w-full h-full object-contain",
+ : "aspect-video 2xl:rounded-[2.222vw] rounded-[32px] overflow-hidden group relative pointer-events-auto max-w-full h-full self-center object-cover",
isLocal && "order-last",
isVideoOff ? "bg-green-500" : "bg-yellow-500/10",
className
@@ -290,7 +294,9 @@ export default function UserCamera({
window.innerWidth >= 1536 ? "0.069vw" : "1px"
} rgba(255, 255, 255, 0.3)`,
transition:
- "box-shadow 0.1s ease-out, width 0.3s, background-color 0.3s",
+ mode === "full"
+ ? "box-shadow 0.1s ease-out, width 0.3s, background-color 0.3s"
+ : undefined,
}}
onClick={handleVideoClick}
>
diff --git a/client/src/pages/SessionPage.tsx b/client/src/pages/SessionPage.tsx
index 38d9efc..d4834a3 100644
--- a/client/src/pages/SessionPage.tsx
+++ b/client/src/pages/SessionPage.tsx
@@ -85,7 +85,6 @@ function SessionPage() {
const session = sessionData?.session;
function handleChatOpen() {
- console.log("handleChatOpen");
setPopup();
}
@@ -99,7 +98,7 @@ function SessionPage() {
setPopup();
}
- const [mode, setMode] = useState<"full" | "mini">("full");
+ const [mode, setMode] = useState<"full" | "mini">("mini");
function toggleMode() {
setMode(mode === "full" ? "mini" : "full");
@@ -161,9 +160,8 @@ function SessionPage() {
return (
{/* Pixel Streaming - показывается только когда сессия активна */}
@@ -206,7 +204,7 @@ function SessionPage() {
)}
-
+
{mode === "mini" ? : }