241 lines
8.5 KiB
TypeScript
241 lines
8.5 KiB
TypeScript
import ActionsSidebarWrapper from "../components/ActionsSidebarWrapper";
|
||
import ChatFilledIcon from "../components/icons/ChatFilledIcon";
|
||
import ExitFilledIcon from "../components/icons/ExitFilledIcon";
|
||
import FullscreenExitIcon from "../components/icons/FullscreenExitIcon";
|
||
import FullscreenIcon from "../components/icons/FullscreenIcon";
|
||
import MicrophoneFilledIcon from "../components/icons/MicrophoneFilledIcon";
|
||
import ShareFilledIcon from "../components/icons/ShareFilledIcon";
|
||
import UsersFilledIcon from "../components/icons/UsersFilledIcon";
|
||
import VideoOffFilledIcon from "../components/icons/VideoOffFilledIcon";
|
||
import FloatingActionButton from "../components/ui/FloatingActionButton";
|
||
import ParticipantsPopup from "../components/popups/ParticipantsPopup";
|
||
import usePopupStore from "../store/popupStore";
|
||
import ControlsPopover from "../components/ui/ControlsPopover";
|
||
import ChatPopup from "../components/popups/ChatPopup";
|
||
import SharePopup from "../components/popups/SharePopup";
|
||
import { useEffect, useState } from "react";
|
||
import { useNavigate, useParams } from "react-router";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { api } from "../lib/api";
|
||
import type { Session } from "../types/Session";
|
||
import { PixelStreamingWrapper } from "../components/PixelStreamingWrapper";
|
||
import WarningIcon from "../components/icons/WarningIcon";
|
||
import Button from "../components/ui/Button";
|
||
import LoaderIcon from "../components/icons/LoaderIcon";
|
||
import SessionUsersPanel from "../components/SessionUsersPanel";
|
||
|
||
function SessionPage() {
|
||
const { setPopup } = usePopupStore();
|
||
|
||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||
|
||
function toggleFullscreen() {
|
||
if (document.fullscreenElement) document.exitFullscreen();
|
||
else document.documentElement.requestFullscreen();
|
||
}
|
||
|
||
useEffect(() => {
|
||
document.addEventListener("fullscreenchange", () =>
|
||
setIsFullscreen(!!document.fullscreenElement)
|
||
);
|
||
return () =>
|
||
document.removeEventListener("fullscreenchange", () =>
|
||
setIsFullscreen(!!document.fullscreenElement)
|
||
);
|
||
}, []);
|
||
|
||
const { id } = useParams();
|
||
const navigate = useNavigate();
|
||
|
||
const {
|
||
data: sessionData,
|
||
isLoading,
|
||
error,
|
||
// refetch,
|
||
} = useQuery({
|
||
queryKey: ["session", id],
|
||
queryFn: async () => {
|
||
const response = await api.get(`sessions/${id}`).json<{
|
||
session: Session;
|
||
}>();
|
||
return response;
|
||
},
|
||
refetchInterval: (query) => {
|
||
// Автоматически обновляем каждые 2 секунды, если сессия в процессе запуска
|
||
const data = query.state.data;
|
||
if (
|
||
data?.session.status === "starting" ||
|
||
data?.session.status === "ending"
|
||
) {
|
||
return 2000;
|
||
}
|
||
return false;
|
||
},
|
||
});
|
||
|
||
const session = sessionData?.session;
|
||
|
||
function handleChatOpen() {
|
||
console.log("handleChatOpen");
|
||
setPopup(<ChatPopup />);
|
||
}
|
||
|
||
function handleParticipantsOpen() {
|
||
setPopup(<ParticipantsPopup session={session} />);
|
||
}
|
||
|
||
function handleShareOpen() {
|
||
setPopup(<SharePopup link={`${window.location.origin}/sessions/${id}`} />);
|
||
}
|
||
|
||
// Не перенаправляем автоматически - пользователи могут продолжать общаться
|
||
// useEffect(() => {
|
||
// if (session?.status === "ended") {
|
||
// const timer = setTimeout(() => {
|
||
// navigate("/test");
|
||
// }, 5000);
|
||
// return () => clearTimeout(timer);
|
||
// }
|
||
// }, [session?.status, navigate]);
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<div className="flex justify-center items-center min-h-screen bg-gray-50">
|
||
<div className="flex flex-col gap-4 items-center">
|
||
<div className="size-12 text-[#7B60F3] animate-spin">
|
||
<LoaderIcon />
|
||
</div>
|
||
<p className="text-gray-600 text-m">
|
||
Загрузка информации о сессии...
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error || !session) {
|
||
return (
|
||
<div className="flex justify-center items-center min-h-screen bg-gray-50">
|
||
<div className="p-8 w-full max-w-2xl bg-white rounded-lg shadow-md">
|
||
<div className="flex gap-4 items-start">
|
||
<div className="text-red-500 size-6">
|
||
<WarningIcon />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h1 className="mb-2 text-red-900 title-l">Сессия не найдена</h1>
|
||
<p className="mb-6 text-gray-600 text-m">
|
||
{error instanceof Error
|
||
? error.message
|
||
: "Не удалось загрузить информацию о сессии"}
|
||
</p>
|
||
<Button variant="primary" onClick={() => navigate("/test")}>
|
||
Вернуться назад
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="flex overflow-hidden relative order-3 w-screen h-screen bg-black justify-center_items-center">
|
||
{/* Pixel Streaming - показывается только когда сессия активна */}
|
||
{session.status === "started" &&
|
||
session.mode === "stream" &&
|
||
session.server?.localIp &&
|
||
session.playerPort && (
|
||
<div className="w-full h-full aspect-video">
|
||
<PixelStreamingWrapper
|
||
initialSettings={{
|
||
ss: `ws://${session.server.localIp}:${session.playerPort}`,
|
||
AutoPlayVideo: true,
|
||
AutoConnect: true,
|
||
StartVideoMuted: true,
|
||
HoveringMouse: true,
|
||
WaitForStreamer: true,
|
||
StreamerId: "DefaultStreamer",
|
||
}}
|
||
onVideoInitialized={() => {
|
||
console.log("Video initialized");
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Сообщение когда Pixel Streaming завершён, но WebRTC чат работает */}
|
||
{session.status === "ended" && (
|
||
<div className="flex flex-col gap-6 justify-center items-center w-full h-full">
|
||
<div className="flex flex-col gap-2 items-center">
|
||
<div className="text-2xl font-semibold text-white">
|
||
Сессия завершена
|
||
</div>
|
||
<div className="text-base text-gray-400">
|
||
Вы можете продолжать общаться через видеочат
|
||
</div>
|
||
</div>
|
||
<Button variant="primary" onClick={() => navigate("/test")}>
|
||
Покинуть сессию
|
||
</Button>
|
||
</div>
|
||
)}
|
||
|
||
<ActionsSidebarWrapper className="z-[99]">
|
||
<FloatingActionButton
|
||
className="max-2xl:hidden"
|
||
onClick={handleChatOpen}
|
||
>
|
||
<div className="size-[1.111vw] text-white">
|
||
<ChatFilledIcon />
|
||
</div>
|
||
</FloatingActionButton>
|
||
<FloatingActionButton
|
||
className="max-2xl:hidden"
|
||
onClick={handleParticipantsOpen}
|
||
>
|
||
<div className="size-[1.111vw] text-white">
|
||
<UsersFilledIcon />
|
||
</div>
|
||
</FloatingActionButton>
|
||
<FloatingActionButton
|
||
className="max-2xl:hidden"
|
||
onClick={handleShareOpen}
|
||
>
|
||
<div className="size-[1.111vw] text-white">
|
||
<ShareFilledIcon />
|
||
</div>
|
||
</FloatingActionButton>
|
||
<FloatingActionButton className="2xl:hidden">
|
||
<div className="text-white size-4">
|
||
<MicrophoneFilledIcon />
|
||
</div>
|
||
</FloatingActionButton>
|
||
<FloatingActionButton className="2xl:hidden">
|
||
<div className="text-white size-4">
|
||
<VideoOffFilledIcon />
|
||
</div>
|
||
</FloatingActionButton>
|
||
<FloatingActionButton
|
||
className="max-2xl:order-2"
|
||
onClick={toggleFullscreen}
|
||
>
|
||
<div className="2xl:size-[1.111vw] size-4 text-white">
|
||
{isFullscreen ? <FullscreenExitIcon /> : <FullscreenIcon />}
|
||
</div>
|
||
</FloatingActionButton>
|
||
<FloatingActionButton variant="critical" className="max-2xl:order-1">
|
||
<div className="2xl:size-[1.111vw] size-4 text-white">
|
||
<ExitFilledIcon />
|
||
</div>
|
||
</FloatingActionButton>
|
||
<ControlsPopover />
|
||
</ActionsSidebarWrapper>
|
||
|
||
{/* WebRTC видеочат - работает всегда, пока пользователь на странице */}
|
||
<SessionUsersPanel roomId={session.id} autoJoin={true} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default SessionPage;
|