Files
stream.graff.tech-new/client/src/components/SessionUsersPanel.tsx
T
mikhail_lanskikh 28091d732a Enhance UI components and functionality across the application
- Updated ActionsSidebarWrapper to accept a ref for improved positioning.
- Enhanced SessionUsersPanel with new props for participant management, including mute and disable video functionalities.
- Added vertical positioning option to ActionsPopover for better alignment.
- Modified QRCodePopup and SharePopup to include a second argument in setPopup for type differentiation.
- Refactored ControlButton and Tooltip components for improved accessibility and styling.
- Updated UserCamera to integrate ActionsPopover for participant controls, enhancing user interaction.
- Improved PopoverWrapper to handle dynamic positioning based on parent element.
- Adjusted UserDevicesControls for better layout consistency and responsiveness.
- Enhanced popup management in the popupStore to track popup types.
2025-11-06 17:15:30 +05:00

272 lines
9.2 KiB
TypeScript

import UserCamera from "./ui/UserCamera";
import UserDevicesControls from "./ui/UserDevicesControls";
import DraggableContainer from "./DraggableContainer";
import { useWebRTC } from "../hooks/useWebRTC";
import clsx from "clsx";
import { useEffect, useRef, useState } from "react";
import type { Session } from "../types/Session";
import { getGuestId } from "../lib/guestId";
import { useMe } from "../hooks/useAuth";
// for test cameras grid
const LOCAL_CAMERAS_COUNT = 1;
interface SessionUsersPanelProps {
roomId: string;
autoJoin?: boolean;
mode?: "full" | "mini";
session?: Session;
}
function SessionUsersPanel({
roomId,
autoJoin = false,
mode = "full",
session,
}: SessionUsersPanelProps) {
const {
localStream,
participants,
isAudioMuted: isLocalAudioMuted,
isVideoMuted: isLocalVideoMuted,
toggleAudio,
toggleVideo,
updateSpeakingState,
muteParticipant,
disableParticipantVideo,
currentUserId,
} = useWebRTC(roomId, autoJoin);
const hasLocalStream = localStream !== null;
const { data: user } = useMe();
// Определяем, является ли локальный пользователь организатором сессии
const isLocalUserOrganizer = session
? !!(session.userId && user?.id === session.userId) ||
!!(session.guestId && getGuestId() === session.guestId)
: false;
// Функция для определения, является ли конкретный участник организатором
const isParticipantOrganizer = (participantId: string) => {
if (!session) return false;
return (
(session.userId && participantId === session.userId) ||
(session.guestId && participantId === session.guestId)
);
};
// Логируем изменения hasLocalStream для отладки
useEffect(() => {
console.log(
`[SessionUsersPanel] hasLocalStream changed to: ${hasLocalStream}`
);
}, [hasLocalStream]);
// State для хранения состояния speaking
const [localSpeaking, setLocalSpeaking] = useState<boolean>(false);
const lastSentSpeakingRef = useRef<boolean>(false);
const speakingStateTimeoutRef = useRef<NodeJS.Timeout | null>(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(() => {
// Отправляем только если состояние действительно изменилось
if (lastSentSpeakingRef.current === localSpeaking) {
return;
}
// Очищаем предыдущий таймер
if (speakingStateTimeoutRef.current) {
clearTimeout(speakingStateTimeoutRef.current);
}
// Отправляем состояние с задержкой (debounce 300ms)
speakingStateTimeoutRef.current = setTimeout(() => {
lastSentSpeakingRef.current = localSpeaking;
updateSpeakingState?.(localSpeaking);
speakingStateTimeoutRef.current = null;
}, 300);
return () => {
if (speakingStateTimeoutRef.current) {
clearTimeout(speakingStateTimeoutRef.current);
}
};
}, [localSpeaking, updateSpeakingState]);
// Вычисляем количество камер для grid
const activeCamerasCount =
(localStream ? LOCAL_CAMERAS_COUNT : 0) +
participants.filter(
(p) => p.stream != null && p.stream.getTracks().length > 0
).length;
// Определяем количество колонок в зависимости от количества камер
const getDesktopGridColumns = (count: number): number => {
if (count <= 2) return 1;
if (count <= 4) return 2;
if (count <= 9) return 3;
return 4;
};
const getMobilePortraitGridColumns = (count: number): number => {
if (count <= 3) return 1;
if (count <= 12) return 2;
return 3;
};
const getMobileLandscapeGridColumns = (count: number): number => {
if (count <= 3) return count;
if (count <= 6) return 3;
return 4;
};
const gridColumns =
windowDimensions.width >= 1440
? getDesktopGridColumns(activeCamerasCount)
: windowDimensions.height / windowDimensions.width < 1
? getMobileLandscapeGridColumns(activeCamerasCount)
: getMobilePortraitGridColumns(activeCamerasCount);
return (
<DraggableContainer
enableSnapping={mode === "mini"}
enabled={mode === "mini"}
autoAlign={true}
initialCorner={
windowDimensions.width >= 640 ? "bottom-right" : "top-right"
}
padding="1.111vw"
className={clsx(
"z-[100] 2xl:gap-[0.556vw] gap-2",
mode === "mini"
? "flex"
: `2xl:p-[2.778vw_5vw_5vw] p-[12px_12px_72px] w-full 2xl:h-dvh min-h-fulla grid relative 2xl:bg-black auto-rows-fr`,
gridColumns === 1
? "grid-cols-1"
: gridColumns === 2
? "grid-cols-2"
: gridColumns === 3
? "grid-cols-3"
: "grid-cols-4"
)}
>
{mode === "full" && <div className="fixed bg-black inset-0 2xl:hidden" />}
{localStream &&
Array.from({ length: LOCAL_CAMERAS_COUNT }).map((_, index) => (
<UserCamera
key={index}
mode={mode}
name="Вы"
isMuted={isLocalAudioMuted}
isVideoOff={isLocalVideoMuted}
hasControl={false}
isAdmin={isLocalUserOrganizer}
isLocal={true}
mediaStream={localStream}
onSpeakingChange={handleSpeakingChange}
isLocalUserOrganizer={isLocalUserOrganizer}
participantId={currentUserId}
className={clsx(
mode === "full" &&
(activeCamerasCount <= 2 ? "m-auto" : "min-w-full min-h-full")
)}
/>
))}
{/* Камеры удаленных участников - показываем только если есть поток с активными треками */}
{participants
.filter(
(participant) =>
participant.stream != null &&
participant.stream.getTracks().length > 0
)
.map((participant) => (
<UserCamera
className={clsx(
mode === "full" &&
(activeCamerasCount <= 2 ? "m-auto" : "min-w-full min-h-full")
)}
key={participant.id}
mode={mode}
name={participant.name}
isMuted={participant.isMuted || false}
isVideoOff={participant.isVideoOff || false}
isSpeaking={participant.isSpeaking}
hasControl={false}
isAdmin={isParticipantOrganizer(participant.id) || undefined}
mediaStream={participant.stream}
hasLocalMediaPermission={hasLocalStream}
isLocalUserOrganizer={isLocalUserOrganizer}
participantId={participant.id}
onMuteParticipant={muteParticipant}
onDisableParticipantVideo={disableParticipantVideo}
/>
))}
<UserDevicesControls
mode={mode}
toggleAudio={toggleAudio}
toggleVideo={toggleVideo}
isAudioMuted={isLocalAudioMuted}
isVideoMuted={isLocalVideoMuted}
hasLocalStream={hasLocalStream}
/>
</DraggableContainer>
);
}
export default SessionUsersPanel;