Files
stream.graff.tech-client/src/pages/StreamPage2.tsx
T
2024-07-05 20:08:41 +05:00

1235 lines
45 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/rules-of-hooks */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
import "./StreamPage2.css";
import { PixelStreamingWrapper } from "../components/PixelStreamingWrapper";
import { useParams, useSearchParams } from "react-router-dom";
import { FormEvent, useEffect, useRef } from "react";
import { Transition } from "react-transition-group";
import Button from "../components/ui/Button";
// import CloseIcon from "../components/icons/CloseIcon";
import HandOffIcon from "../components/icons/HandOffIcon";
// import MicroOffIcon from "../components/icons/MicroOffIcon";
import PersonsIcon from "../components/icons/PersonsIcon";
import MicroOnIcon from "../components/icons/MicroOnIcon";
// import MoreIcon from "../components/icons/MoreIcon";
import FullscreenIcon from "../components/icons/FullscreenIcon";
import WindowIcon from "../components/icons/WindowIcon";
import ChatIcon from "../components/icons/ChatIcon";
import ShareIcon from "../components/icons/ShareIcon";
// import GearIcon from "../components/icons/GearIcon";
import { useFullscreen } from "ahooks";
import api from "../utils/api";
import { Socket, io } from "socket.io-client";
import { v4 as uuidv4 } from "uuid";
import HandOnIcon from "../components/icons/HandOnIcon";
import User from "../components/User";
import Tooltip from "../components/Tooltip";
import { Bounce, ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import InfoBlueIcon from "../components/icons/InfoBlueIcon";
import CloseIcon from "../components/icons/CloseIcon";
import SendChatIcon from "../components/icons/SendChatIcon";
import UserIcon from "../components/icons/UserIcon";
import { isIOS, isMobile, useMobileOrientation } from "react-device-detect";
import LinkIcon from "../components/icons/LinkIcon";
import removeSpaces from "../utils/removeSpaces";
import MobileIcon from "../components/icons/MobileIcon";
import DesktopIcon from "../components/icons/DesktopIcon";
import { format } from "date-fns";
import IUser from "../types/IUser";
import IMessage from "../types/IMessage";
import InfoIcon from "../components/icons/InfoIcon";
import Rotate64Icon from "../components/icons/Rotate64Icon";
import { useClipboard } from "use-clipboard-copy";
import QRCode from "react-qr-code";
import Star12Icon from "../components/icons/Star12Icon";
import { Trans, useTranslation } from "react-i18next";
import Countdown from "react-countdown";
import useStreamStore from "../stores/useStreamStore";
import Peer from "peerjs";
import Video from "../components/Video";
import MicroOffIcon from "../components/icons/MicroOffIcon";
import useIsAudioActive from "use-is-audio-active";
import CameraOffIcon from "../components/icons/CameraOffIcon";
import CameraOnIcon from "../components/icons/CameraOnIcon";
import useState from "react-usestateref";
const renderer = ({ minutes, seconds }: any) => {
return (
<>
{String(minutes).padStart(2, "0")}:{String(seconds).padStart(2, "0")}
</>
);
};
interface IRemoteStream {
peerId: string;
mediaStream: MediaStream;
}
function StreamPage2() {
const { t, i18n } = useTranslation();
const { isPortrait } = useMobileOrientation();
const params = useParams();
const [searchParams] = useSearchParams();
const [socket, setSocket] = useState<Socket>();
const [wsUrl, setWsUrl] = useState<string>();
const [isEnded, setIsEnded] = useState<boolean>(false);
const { name, setName } = useStreamStore();
const userId = uuidv4();
const [step, setStep] = useState<number>(1);
const nameRef = useRef<HTMLInputElement>(null!);
const [users, setUsers] = useState<IUser[]>([]);
const fullscreenRef = useRef(null);
const [isFullscreen, { toggleFullscreen }] = useFullscreen(fullscreenRef);
const [isShowChat, setIsShowChat] = useState<boolean>(false);
const [isShowUsers, setIsShowUsers] = useState<boolean>(false);
const [isShowInviteModal, setIsShowInviteModal] = useState(false);
const [isVideoInitialized, setIsVideoInitialized] = useState<boolean>(false);
const [messageText, setMessageText] = useState("");
const [messages] = useState<IMessage[]>([]);
const messagesRef = useRef<HTMLDivElement>(null);
const messageTextRef = useRef<HTMLInputElement>(null);
const parentElementRef = useRef<HTMLDivElement>(null);
const clipboard = useClipboard();
const link = window.location.origin + window.location.pathname;
const [anyNewMessages, setAnyNewMessages] = useState(false);
const [endAt, setEndAt] = useState();
const localVideoRef = useRef<HTMLVideoElement>(null);
const [localStream, setLocalStream] = useState<MediaStream>(
new MediaStream()
);
const [remoteStreams, setRemoteStreams, remoteStreamsRef] = useState<
IRemoteStream[]
>([]);
const [peerId, setPeerId] = useState<string>("");
const [peerInstance, setPeerInstance] = useState<Peer>();
const [permission, setPermission] = useState<boolean>();
const isSpeaking = useIsAudioActive({
source: localStream.getTracks().length ? localStream : null,
});
const isCallInit = useRef<boolean>(false);
const [isVideoEnabled, setIsVideoEnabled] = useState<boolean>(false);
const [isAudioEnabled, setIsAudioEnabled] = useState<boolean>(false);
async function getLang() {
const { countryCode, error }: { countryCode: string; error: string } =
await api.get("getCountryCode").json();
if (!error && countryCode !== "RU") {
i18n.changeLanguage("en");
}
}
function handleClickClipboard() {
clipboard.copy();
toast.info("Ссылка скопирована в буфер обмена", {
icon: <InfoIcon className="text-blue-500" />,
position: "top-center",
autoClose: 3000,
hideProgressBar: true,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
}
async function sendMessage(e: FormEvent) {
e.preventDefault();
const text = removeSpaces(messageText);
if (text) {
const name = users.find((user) => user.id === userId)?.name;
socket?.emit("message", { name, text });
}
setMessageText("");
messageTextRef.current?.focus();
}
async function getActiveSession() {
const activeSession: any = await api
.get(`activeSessions/${params.id}`)
.json();
if (activeSession?.endAt) {
setEndAt(activeSession.endAt);
}
return activeSession;
}
async function checkSessionStatus() {
const activeSession = await getActiveSession();
if (!activeSession || activeSession.status === "error") {
setIsEnded(true);
return;
}
}
async function getWsUrl() {
const activeSession = await getActiveSession();
if (!activeSession || activeSession.status === "error") {
setIsEnded(true);
return;
}
setIsEnded(false);
setWsUrl(
`wss://${activeSession.location}.sess.stream.graff.tech/${activeSession.name}/${activeSession.cirrusPort}/`
);
checkSessionStatus();
}
function handleSetName(e: FormEvent) {
e.preventDefault();
if (!name) {
nameRef.current.focus();
return;
}
setStep(2);
}
function setNameGuest() {
i18n.language === "ru" ? setName("Гость") : setName("Guest");
setStep(2);
}
function toggleVideo() {
localStream.getVideoTracks().forEach((track) => {
track.enabled = !track.enabled;
if (!permission) return;
setIsVideoEnabled(track.enabled);
});
}
function toggleAudio() {
localStream.getAudioTracks().forEach((track) => {
track.enabled = !track.enabled;
if (!permission) return;
setIsAudioEnabled(track.enabled);
});
}
function transferControl(userId: string) {
socket?.emit("transferControl", userId);
}
function requestControl(userId: string) {
socket?.emit("requestControl", userId);
toast.info(`Вы запросили разрешение на управление`, {
icon: <InfoBlueIcon />,
position: "top-center",
autoClose: 3000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
});
}
function kickUser(userId: string) {
socket?.emit("kickUser", userId);
}
async function startCall(remotePeerId: string) {
if (!peerInstance) return;
console.log("startCall", remotePeerId);
const options = {
constraints: {
offerToReceiveVideo: true,
offerToReceiveAudio: true,
},
};
const call = peerInstance.call(remotePeerId, localStream, options as any);
let accept = true;
call.on("stream", (remoteStream) => {
if (!accept) return;
console.log("setRemoteStreams", remoteStream);
setRemoteStreams((prev) => [
...prev,
{ peerId: remotePeerId, mediaStream: remoteStream },
]);
accept = false;
});
}
async function getUserMedia() {
try {
const mediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
if (!localVideoRef.current) return;
localVideoRef.current.srcObject = mediaStream;
localVideoRef.current.onloadedmetadata = () => {
localVideoRef.current?.play();
};
setLocalStream(mediaStream);
setPermission(true);
console.log("setLocalStream mediaStream", mediaStream);
} catch (error) {
setPermission(false);
console.log("ERROR: ", error);
}
}
function initPeer() {
const peer = new Peer();
peer.on("open", (id) => {
setPeerId(id);
});
peer.on("call", (call) => {
call.answer(localStream || undefined);
let accept = true;
call.on("stream", (remoteStream) => {
if (!accept) return;
setRemoteStreams((prev) => [
...prev,
{ peerId: call.peer, mediaStream: remoteStream },
]);
accept = false;
});
});
setPeerInstance(peer);
}
function initSocket() {
const socket = io(import.meta.env.VITE_SOCKET_URL, {
transports: ["websocket"],
auth: {
roomId: params.id,
user: {
id: userId,
name: name,
device: isMobile ? "mobile" : "desktop",
isAdmin: searchParams.has("admin", true),
peerId,
},
},
});
socket.on("update", async (users: IUser[]) => {
console.log("isCallInit", isCallInit.current);
if (!isCallInit.current) {
for (const user of users) {
if (userId === user.id) continue;
await startCall(user.peerId);
}
isCallInit.current = true;
}
setUsers(users);
});
setSocket(socket);
setStep(3);
}
useEffect(() => {
console.log("users", users);
}, [users]);
function updateRemoteStreams() {
setTimeout(() => {
console.log("users", users);
const newRemoteStreams = remoteStreamsRef.current.filter((remoteStream) =>
users.some((user) => user.peerId === remoteStream.peerId)
);
setRemoteStreams(newRemoteStreams);
}, 500);
}
useEffect(() => {
getLang();
getWsUrl();
}, []);
useEffect(() => {
document.title = t("title");
}, [i18n.language]);
useEffect(() => {
if (!name) return;
setName(name.trim());
}, [name]);
useEffect(() => {
if (!isShowChat || messagesRef.current?.scrollTop === undefined) return;
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
}, [messages, isShowChat]);
useEffect(() => {
if (!messages.length || isShowChat) return;
setAnyNewMessages(true);
}, [messages]);
useEffect(() => {
if (isShowChat) {
setAnyNewMessages(false);
}
}, [isShowChat]);
// useEffect(() => {
// if (!peerId) return;
// const socket = io(import.meta.env.VITE_SOCKET_URL, {
// auth: {
// roomId: params.id,
// user: {
// id: userId,
// name: name,
// device: isMobile ? "mobile" : "desktop",
// isAdmin: searchParams.has("admin", true),
// peerId,
// },
// },
// });
// // TODO
// socket.on("message", (message: IMessage) => {
// setMessages((prev) => [...prev, message]);
// });
// socket.on("requestControl", (user: IUser) => {
// if (!usersRef.current.find((user) => user.id === userId)?.isAdmin) return;
// toast.info(`${user.name} запрашивает разрешение на управление`, {
// icon: <InfoBlueIcon />,
// position: "top-center",
// autoClose: 5000,
// hideProgressBar: false,
// closeOnClick: true,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
// });
// socket.on("transferControl", (user: IUser) => {
// if (user.id !== userId) return;
// toast.info(`Вы получили разрешение на управление`, {
// icon: <InfoBlueIcon />,
// position: "top-center",
// autoClose: 3000,
// hideProgressBar: false,
// closeOnClick: true,
// pauseOnHover: true,
// draggable: true,
// progress: undefined,
// theme: "light",
// transition: Bounce,
// });
// });
// socket.on("kickUser", (socketUserId: string) => {
// if (socketUserId === userId) {
// socket.disconnect();
// window.close();
// window.location.reload();
// }
// });
// setSocket(socket);
// }, [peerId]);
useEffect(() => {
if (!isMobile) return;
if (isShowUsers) {
setIsShowChat(false);
}
}, [isShowUsers]);
useEffect(() => {
if (!isMobile) return;
if (isShowChat) {
setIsShowUsers(false);
}
}, [isShowChat]);
useEffect(() => {
if (step === 3) {
setInterval(async () => {
await checkSessionStatus();
}, 1000);
}
}, [step]);
useEffect(() => {
if (permission === undefined) return;
initPeer();
}, [permission]);
useEffect(() => {
if (!peerId) return;
initSocket();
}, [peerId]);
useEffect(() => {
if (!users.length) return;
updateRemoteStreams();
}, [users.length]);
return (
<>
{isEnded === false ? (
<div
ref={fullscreenRef}
className="h-[100dvh] flex flex-col overflow-hidden touch-none"
>
<div className="top-header hidden h-12 bg-white px-6 py-3 lg:flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="mr-2">
<img src="/images/logo24.svg" alt="" />
</div>
<div className="flex items-center gap-2">
<div className="relative h-6 w-6 flex items-center justify-center bg-[#E6ECF2] font-semibold text-[10px] rounded-full">
{name && name[0].toUpperCase()}
{users.find((user) => user.id === userId)
?.isControlAllowed && (
<div className="w-2 h-2 border border-white bg-[#49A1F5] absolute right-0 bottom-0 rounded-full"></div>
)}
{users.find((user) => user.id === userId)?.isAdmin && (
<div className="absolute -top-[3px] -left-[3px] text-[#49A1F5]">
<Star12Icon />
</div>
)}
</div>
<p className="text-xs">{name}</p>
</div>
<div className="flex gap-2">
{users.find((user) => user.id === userId)?.isControlAllowed ? (
<Button variant="secondary" icon={<HandOnIcon />} onlyIcon />
) : users.find((user) => user.id === userId)?.isAdmin ? (
<Button
variant="secondary"
icon={<HandOffIcon />}
onlyIcon
onClick={() => transferControl(userId)}
className="group relative"
>
<Tooltip text="Вернуть управление" />
</Button>
) : (
<Button
variant="secondary"
icon={<HandOffIcon />}
onlyIcon
onClick={() => requestControl(userId)}
className="group relative"
>
<Tooltip text="Запросить управление" />
</Button>
)}
<Button
variant="secondary"
icon={isVideoEnabled ? <CameraOnIcon /> : <CameraOffIcon />}
onlyIcon
onClick={toggleVideo}
/>
<Button
variant="secondary"
icon={isAudioEnabled ? <MicroOnIcon /> : <MicroOffIcon />}
onlyIcon
onClick={toggleAudio}
/>
</div>
<div className="w-px h-4 bg-[#DAE0E5]"></div>
<div className="flex gap-6">
{users
.filter((user) => user.id !== userId)
.map(
(user, index) =>
index < 3 && (
<div key={user.id} className="flex items-center gap-2">
<div className="relative h-6 w-6 flex items-center justify-center bg-[#E6ECF2] font-semibold text-[10px] rounded-full">
{user.name[0].toUpperCase()}
{user.isControlAllowed && (
<div className="w-2 h-2 border border-white bg-[#49A1F5] absolute right-0 bottom-0 rounded-full"></div>
)}
{user.isAdmin && (
<div className="absolute -top-[3px] -left-[3px] text-[#49A1F5]">
<Star12Icon />
</div>
)}
</div>
<p className="text-xs">{user.name}</p>
<div className="text-[#CCC]">
{user.device === "mobile" ? (
<MobileIcon />
) : (
<DesktopIcon />
)}
</div>
{users.find((user) => user.id === userId)
?.isAdmin && (
<User
user={user}
onTransferControl={(userId) =>
transferControl(userId)
}
onKickUser={(userId) => {
kickUser(userId);
}}
/>
)}
</div>
)
)}
{users.length > 4 && (
<Button
variant="secondary"
icon={<PersonsIcon />}
onlyIcon
onClick={() => setIsShowUsers((prev) => !prev)}
/>
)}
</div>
</div>
<div id="right" className="flex items-center gap-4">
<div className="relative">
<Button
variant="secondary"
icon={<ChatIcon />}
onlyIcon
onClick={() => setIsShowChat((prev) => !prev)}
/>
{anyNewMessages && (
<div className="w-2 h-2 bg-[#49A1F5] border rounded-full animate-ping absolute top-[25%] right-[20%] pointer-events-none"></div>
)}
</div>
<div id="divider" className="w-px h-4 bg-[#DAE0E5]"></div>
<div className="flex gap-2">
{!isIOS && (
<Button
variant="secondary"
icon={isFullscreen ? <WindowIcon /> : <FullscreenIcon />}
onlyIcon
onClick={toggleFullscreen}
/>
)}
<Button
variant="secondary"
icon={<ShareIcon />}
onlyIcon
onClick={() => setIsShowInviteModal((prev) => !prev)}
/>
</div>
{endAt && searchParams.has("admin", "true") && (
<>
<div id="divider" className="w-px h-4 bg-[#DAE0E5]"></div>
<div className="flex items-center gap-4">
<p className="text-[#77828C] font-semibold text-sm">
До завершения сеанса
</p>
<p className="font-semibold text-sm w-10">
<Countdown date={new Date(endAt)} renderer={renderer} />
</p>
{/* <Button variant="secondary">Завершить</Button> */}
</div>
</>
)}
</div>
</div>
<div className="main flex-1 bg-white flex">
<div className="left-header lg:hidden flex flex-col justify-between gap-4 w-12 p-2">
<div className="flex flex-col items-center gap-4">
<div className="mt-2">
<img src="/images/logo24.svg" alt="" />
</div>
{!isIOS && (
<Button
variant="secondary"
icon={isFullscreen ? <WindowIcon /> : <FullscreenIcon />}
onlyIcon
onClick={toggleFullscreen}
/>
)}
</div>
<div className="flex flex-col items-center gap-2">
<Button
variant="secondary"
icon={<UserIcon />}
onlyIcon
onClick={() => setIsShowUsers((prev) => !prev)}
/>
<div className="relative">
<Button
variant="secondary"
icon={<ChatIcon />}
onlyIcon
onClick={() => setIsShowChat((prev) => !prev)}
/>
{anyNewMessages && (
<div className="w-2 h-2 bg-[#49A1F5] border rounded-full animate-ping absolute top-[25%] right-[20%] pointer-events-none"></div>
)}
</div>
<hr className="bg-[#DAE0E5] w-4" />
<Button
variant="secondary"
icon={isAudioEnabled ? <MicroOnIcon /> : <MicroOffIcon />}
onlyIcon
onClick={toggleAudio}
/>
</div>
<div className="">
<Button
variant="secondary"
icon={<ShareIcon />}
onlyIcon
onClick={() => setIsShowInviteModal((prev) => !prev)}
/>
</div>
</div>
<div className="relative w-full bg-[#111C26]">
{wsUrl && (
<PixelStreamingWrapper
initialSettings={{
AutoPlayVideo: true,
AutoConnect: true,
ss: wsUrl,
StartVideoMuted: true,
HoveringMouse: true,
WaitForStreamer: true,
}}
onVideoInitialized={() => setIsVideoInitialized(true)}
/>
)}
{!users.find((user) => user.id === userId)?.isControlAllowed && (
<div
className="absolute w-full h-full top-0 left-0"
onTouchStart={() =>
toast.error(
`Запросите доступ на управление у администратора!`,
{
icon: <InfoIcon className="text-red-500" />,
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
}
)
}
onMouseDown={() =>
toast.error(
`Запросите доступ на управление у администратора!`,
{
icon: <InfoIcon className="text-red-500" />,
position: "top-center",
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
theme: "light",
transition: Bounce,
}
)
}
></div>
)}
<div className="absolute top-2 lg:left-2 lg:right-auto right-2 flex flex-col gap-2">
<video
ref={localVideoRef}
className={`aspect-video bg-black rounded-lg -scale-x-100 object-cover ring-2 ${
isAudioEnabled && isSpeaking
? "ring-green-500"
: "ring-transparent"
} ${
permission
? "lg:w-[216px] lg:h-[162px] w-[112px] h-[84px]"
: "w-0 h-0"
}`}
playsInline
autoPlay
muted
/>
{remoteStreams.map(({ peerId, mediaStream }) => (
<Video
key={peerId}
mediaStream={mediaStream}
muted={!permission}
/>
))}
</div>
</div>
{(isShowUsers || isShowChat) && (
<div className="absolute right-0 bg-white min-w-[240px] lg:w-[296px] w-[240px] h-full lg:max-h-[calc(100dvh-48px)] flex flex-col border-t border-[#DAE0E5] shadow-2xl shadow-black">
{isShowUsers && (
<div
className={`users min-h-[50%] h-full flex flex-col ${
isShowChat ? "max-h-[calc(50vh-48px)]" : "max-h-[100dvh]"
}`}
>
<div className="flex items-center justify-between p-2 pl-4 border-b border-[#DAE0E5]">
<p className="text-sm font-semibold">
<Trans i18nKey={"members"}>Участники</Trans>
</p>
<Button
variant="tertiary"
icon={<CloseIcon />}
onlyIcon
onClick={() => setIsShowUsers((prev) => !prev)}
/>
</div>
<div className="flex-1 overflow-y-scroll overflow-x-hidden flex flex-col gap-4 p-4 border-b border-[#DAE0E5]">
{users.map((user) => (
<div key={user.id} className="flex items-center gap-2">
<div className="relative h-6 w-6 flex items-center justify-center bg-[#E6ECF2] font-semibold text-[10px] rounded-full">
{user.name[0].toUpperCase()}
{user.isControlAllowed && (
<div className="w-2 h-2 border border-white bg-[#49A1F5] absolute right-0 bottom-0 rounded-full"></div>
)}
{user.isAdmin && (
<div className="absolute -top-[3px] -left-[3px] text-[#49A1F5]">
<Star12Icon />
</div>
)}
</div>
<p className="text-xs">{user.name}</p>
<div className="text-[#CCC]">
{user.device === "mobile" ? (
<MobileIcon />
) : (
<DesktopIcon />
)}
</div>
<div className="ml-auto">
{users.find((user) => user.id === userId)
?.isAdmin && (
<User
className="top-[calc(100%+4px)] -translate-x-[calc(100%-42px)]"
user={user}
onTransferControl={(userId) =>
transferControl(userId)
}
onKickUser={(userId) => {
kickUser(userId);
}}
/>
)}
</div>
</div>
))}
</div>
<div className="px-4 py-2">
<Button
fullWidth
onClick={() => setIsShowInviteModal((prev) => !prev)}
>
<Trans i18nKey={"invite"}>Пригласить</Trans>
</Button>
</div>
</div>
)}
{isShowChat && (
<div
className={`chat min-h-[50%] h-full flex flex-col ${
isShowUsers ? "max-h-[calc(50vh-48px)]" : "max-h-[100dvh]"
}`}
>
<div className="flex items-center justify-between p-2 pl-4 border-b border-[#DAE0E5]">
<p className="text-sm font-semibold">
<Trans i18nKey={"chat"}>Чат</Trans>
</p>
<Button
variant="tertiary"
icon={<CloseIcon />}
onlyIcon
onClick={() => setIsShowChat((prev) => !prev)}
/>
</div>
<div
ref={messagesRef}
className="flex-1 overflow-y-scroll flex flex-col gap-1 p-3 border-b border-[#DAE0E5]"
>
{messages.map((message, index) => (
<div
key={index}
className={`text-sm p-2 flex flex-col gap-1 rounded-lg ${
users.find((user) => user.id === userId)?.name ===
message.name
? "bg-[#C4DDF5]"
: "bg-[#F0F1F2]"
}`}
>
{users.find((user) => user.id === userId)?.name !==
message.name && (
<p className="text-[#49A1F5] font-semibold">
{message.name}
</p>
)}
<p className="break-words">{message.text}</p>
<p className="text-[#767676] translate-x-1 translate-y-1 self-end leading-none">
{format(new Date(), "HH:mm")}
</p>
</div>
))}
</div>
<div className="p-3 pl-4">
<form
onSubmit={sendMessage}
className="flex items-center gap-3"
>
<input
ref={messageTextRef}
type="text"
placeholder={t("writeAMessage")}
className="w-full bg-transparent text-sm outline-none"
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
/>
<button
type="submit"
className="text-[#49A1F5] outline-none"
>
<SendChatIcon />
</button>
</form>
</div>
</div>
)}
</div>
)}
</div>
{isShowInviteModal && (
<div className="absolute top-0 lg:left-0 left-12 lg:w-full w-[calc(100vw-48px)] h-full lg:bg-black lg:bg-opacity-50 bg-white border-l border-[#DAE0E5] flex flex-col lg:items-center lg:justify-center">
<div className="bg-white flex flex-col lg:rounded-lg lg:w-[400px] lg:flex-none flex-1">
<div className="p-2 pl-6 flex items-center justify-between border-b border-[#DAE0E5]">
<p className="text-sm font-semibold">
<Trans i18nKey={"invite"}>Пригласить</Trans>
</p>
<Button
variant="tertiary"
icon={<CloseIcon />}
onlyIcon
onClick={() => setIsShowInviteModal(false)}
/>
</div>
<div className="py-4 px-6 flex-1 flex flex-col gap-8">
<div className="flex items-center lg:justify-between justify-center gap-8">
<QRCode
size={128}
value={link}
className="rounded-lg p-3 shadow-lg"
/>
<p className="font-semibold text-right">
<Trans i18nKey={"scanQRCode"}>
Отсканируйте QR-код,
<br />
чтобы присоедениться
<br />к демонстрации
</Trans>
</p>
</div>
<div className="">
<Button
icon={<LinkIcon />}
fullWidth
large
onClick={handleClickClipboard}
>
<Trans i18nKey={"copyLinkToConnect"}>
Скопировать ссылку для подключения
</Trans>
</Button>
<input ref={clipboard.target} type="hidden" value={link} />
</div>
<div className="">
<form
onSubmit={(e) => e.preventDefault()}
className="flex gap-2"
>
<input
type="email"
placeholder="Email"
className="text-sm bg-transparent border border-[#DAE0E5] rounded-lg px-2 pb-0.5 outline-none h-10 w-full"
/>
<Button type="submit" large className="" disabled>
<Trans i18nKey={"invite"}>Пригласить</Trans>
</Button>
</form>
</div>
{/* <div className="flex flex-col gap-2">
<p className="text-sm font-semibold">Участники</p>
<div className="flex flex-col gap-4 overflow-y-scroll max-h-[calc(100dvh-206px)]">
{users.map((user) => (
<div key={user.id} className="flex items-center gap-2">
<div className="h-8 w-8 bg-[#E6ECF2] rounded-full flex items-center justify-center">
<p className="text-xs font-semibold">
{user.name[0].toUpperCase()}
</p>
</div>
<p className="text-sm">{user.name}</p>
</div>
))}
</div>
</div> */}
</div>
</div>
</div>
)}
<Transition
in={step !== 3 || !isVideoInitialized}
timeout={300}
mountOnEnter
unmountOnExit
>
{(state) => (
<div
className={`absolute top-0 left-0 w-full h-full flex items-center justify-center bg-[#14161F] transition-all ${state} ${
step !== 3 || !isVideoInitialized
? "bg-opacity-50 backdrop-blur-2xl"
: "bg-opacity-0 backdrop-blur-none"
} `}
>
<Transition
in={step === 1}
timeout={300}
mountOnEnter
unmountOnExit
>
{(state) => (
<form
className={`absolute lg:w-[396px] lg:h-auto w-full h-full flex flex-col justify-center bg-white lg:rounded-lg lg:p-12 p-6 transition-opacity ${state}`}
onSubmit={handleSetName}
>
<div className="flex-1">
<div className="mb-6">
<p className="text-2xl font-semibold">
<Trans i18nKey={"hello"}>Здравствуйте!</Trans>
</p>
</div>
<div className="mb-6 flex flex-col gap-2">
<p className="font-semibold">
<Trans i18nKey={"pleaseIntroduceYourself"}>
Представьтесь, пожалуйста
</Trans>
</p>
<p className="text-[#77828C] text-sm">
<Trans i18nKey={"communicateWithYou"}>
Так мы будем знать, как к вам обращаться
</Trans>
</p>
</div>
<div className="mb-10">
<div className="flex flex-col gap-1">
<label className="text-[#77828C] text-xs">
<Trans i18nKey={"name"}>Имя</Trans>
</label>
<input
ref={nameRef}
type="text"
autoComplete="off"
autoFocus
className="rounded-lg px-2 py-2.5 bg-white border border-[#DAE0E5] hover:border-[#49A1F5] focus:border-[#49A1F5] outline-none transition-colors text-sm"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
</div>
</div>
<div className="flex gap-2">
<Button
type="button"
variant="secondary"
fullWidth
large
onClick={setNameGuest}
>
<Trans i18nKey={"skip"}>Не указывать</Trans>
</Button>
<Button
type="submit"
fullWidth
large
onClick={void handleSetName}
>
<Trans i18nKey={"сontinue"}>Продолжить</Trans>
</Button>
</div>
</form>
)}
</Transition>
<Transition
in={step === 2}
timeout={300}
mountOnEnter
unmountOnExit
>
{(state) => (
<div
className={`absolute lg:w-[396px] lg:h-auto w-full h-full flex flex-col justify-center bg-white lg:rounded-lg lg:p-12 p-6 transition-opacity ${state}`}
>
<div className="flex-1">
<div className="mb-6">
<p className="text-2xl font-semibold">
<Trans i18nKey={"partInDiscussion"}>
Хотите принять участие в обсуждении?
</Trans>
</p>
</div>
<div className="mb-6 flex items-center gap-6">
<div className="border border-[#DAE0E5] text-[#49A1F5] p-2 rounded-full">
<MicroOnIcon />
</div>
<p className="text-sm">
<Trans i18nKey={"allowMicrophoneUse"}>
Разрешите использование камеры и микрофона
</Trans>
</p>
</div>
<div className="mb-10">
<p className="text-[#77828C] text-xs">
<Trans i18nKey={"turnOffMicrophone"}>
Выключить камеру и микрофон можно в любой момент
</Trans>
</p>
</div>
</div>
<div className="flex gap-2">
{/* <Button
variant="secondary"
fullWidth
large
onClick={disallowMic}
>
<Trans i18nKey={"skip"}>Пропустить</Trans>
</Button> */}
<Button fullWidth large onClick={getUserMedia}>
<Trans i18nKey={"allow"}>Продолжить</Trans>
</Button>
</div>
</div>
)}
</Transition>
<Transition
in={step === 3 && !isVideoInitialized}
timeout={300}
mountOnEnter
unmountOnExit
>
{(state) => (
<div
className={`absolute bg-white rounded-lg p-12 w-[396px] transition-opacity ${state}`}
>
<div className="mb-6">
<p className="text-2xl font-semibold">
<Trans i18nKey={"pleaseWait"}>
Пожалуйста, подождите
</Trans>
</p>
</div>
<div className="flex items-center gap-2">
<img
src="/icons/LoaderPrimary.png"
alt=""
className="w-8 h-8 animate-spin"
/>
<p className="font-semibold text-[#49A1F5]">
<Trans i18nKey={"connection"}>Подключение</Trans>
</p>
</div>
</div>
)}
</Transition>
</div>
)}
</Transition>
<div ref={parentElementRef} className="hidden"></div>
{step === 3 && isMobile && isPortrait && (
<div className="absolute top-0 left-0 w-full h-full bg-white sm:hidden flex items-center justify-center">
<div className="flex flex-col items-center gap-2">
<Rotate64Icon />
<p className="font-semibold">
<Trans i18nKey={"rotateYourDevice"}>
Поверните устройство
</Trans>
</p>
</div>
</div>
)}
<ToastContainer />
</div>
) : (
<div className="h-[100dvh] flex items-center justify-center p-8">
<p className="text-2xl text-white font-gilroy text-center">
<Trans i18nKey={"demonstrationCompleted"}>
Данная демонстрация была завершена
</Trans>
</p>
</div>
)}
</>
);
}
export default StreamPage2;