28091d732a
- 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.
272 lines
9.2 KiB
TypeScript
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;
|