upd
This commit is contained in:
+3
-3
@@ -167,8 +167,8 @@ function App() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="sm:mt-16 mt-8 grid sm:grid-cols-2 lg:gap-4 sm:gap-3 gap-2">
|
||||
<div
|
||||
<div className="sm:mt-16 mt-8 grid sm:grid-cols-1 lg:gap-4 sm:gap-3 gap-2">
|
||||
{/* <div
|
||||
className="group relative sm:h-full h-[264px] bg-gray-700 bg-no-repeat bg-center bg-cover"
|
||||
style={{
|
||||
backgroundImage: `url("/images/cards/upside.jpg")`,
|
||||
@@ -200,7 +200,7 @@ function App() {
|
||||
<ArrowRightIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="grid lg:grid-cols-2 lg:gap-4 sm:gap-3 gap-2">
|
||||
{i18n.language === "ru" ? (
|
||||
|
||||
@@ -1,418 +0,0 @@
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import { FullScreen, useFullScreenHandle } from "react-full-screen";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import ky from "ky";
|
||||
import { Socket, io } from "socket.io-client";
|
||||
import userAgentParser from "ua-parser-js";
|
||||
import { Player } from "./components/Player";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import ShareIcon from "./components/icons/ShareIcon";
|
||||
import ModalContainer from "./components/ModalContainer";
|
||||
import useModalStore from "./stores/useModalStore";
|
||||
import ShareModal from "./components/modals/ShareModal";
|
||||
import { Transition } from "react-transition-group";
|
||||
import FullscreenIcon from "./components/icons/FullscreenIcon";
|
||||
import WindowedModeIcon from "./components/icons/WindowedModeIcon";
|
||||
import QRIcon from "./components/icons/QRIcon";
|
||||
import QRCodeModal from "./components/modals/QRCodeModal";
|
||||
import PersonsIcon from "./components/icons/PersonsIcon";
|
||||
import UsersManagementModal from "./components/modals/UsersManagementModal";
|
||||
import useStreamUserStore from "./stores/useStreamUserStore";
|
||||
import { ToastContainer, toast } from "react-toastify";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
// import UserIcon from "./components/icons/UserIcon";
|
||||
// import HandOnIcon from "./components/icons/HandOnIcon";
|
||||
import AlertIcon from "./components/icons/AlertIcon";
|
||||
import useSocketStore from "./stores/useSocketStore";
|
||||
import { LiveKitRoom, RoomAudioRenderer } from "@livekit/components-react";
|
||||
import Chat from "./components/Chat";
|
||||
// import AFKTimerModal from "./components/modals/AFKTimerModal";
|
||||
import { differenceInMilliseconds, format, parseISO } from "date-fns";
|
||||
import HandOnIcon from "./components/icons/HandOnIcon";
|
||||
import { useMobileOrientation } from "react-device-detect";
|
||||
import RotateDeviceIcon from "./components/icons/RotateDeviceIcon";
|
||||
|
||||
function StreamPage() {
|
||||
const { t } = useTranslation();
|
||||
const handleFullScreen = useFullScreenHandle();
|
||||
const [streamUrl, setStreamUrl] = useState<string>("");
|
||||
const [isStreamEnded, setIsStreamEnded] = useState<boolean>(false);
|
||||
const [isStreamLoaded, setStreamLoaded] = useState<boolean>(false);
|
||||
const [me, setMe] = useState<any>({});
|
||||
const meRef = useRef<any>({});
|
||||
meRef.current = me;
|
||||
const [modal, setModal] = useModalStore((state) => [
|
||||
state.modal,
|
||||
state.setModal,
|
||||
]);
|
||||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const roomId = params.id;
|
||||
const livekitServerUrl = "wss://livekit.stream.graff.tech";
|
||||
const [token, setToken] = useState<string>();
|
||||
const [socket, setSocket] = useSocketStore((state) => [
|
||||
state.socket,
|
||||
state.setSocket,
|
||||
]);
|
||||
const [users, setUsers] = useStreamUserStore((state) => [
|
||||
state.users,
|
||||
state.setUsers,
|
||||
]);
|
||||
const [isShowChat, setIsShowChat] = useState<boolean>(false);
|
||||
const [stopwatch, setStopwatch] = useState<string>();
|
||||
const { orientation } = useMobileOrientation();
|
||||
|
||||
async function getToken() {
|
||||
if (!socket) return;
|
||||
|
||||
const { token }: any = await ky
|
||||
.get(
|
||||
`https://coord.graff.tech/getToken?roomName=${roomId}&participantName=${socket.id}`
|
||||
)
|
||||
.json();
|
||||
|
||||
setToken(token);
|
||||
}
|
||||
|
||||
function updateStartDate(date: Date) {
|
||||
const diffMs = differenceInMilliseconds(new Date(), date);
|
||||
setStopwatch(format(diffMs, "mm:ss"));
|
||||
|
||||
setTimeout(() => {
|
||||
updateStartDate(date);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
const activeSession: any = await ky
|
||||
.get(`${import.meta.env.VITE_COORD_URL}/active_sessions/${params.id}`)
|
||||
.json();
|
||||
|
||||
if (activeSession) {
|
||||
setStreamUrl(
|
||||
`wss://${activeSession.location}.sess.stream.graff.tech/${activeSession.server}/${activeSession.cirrusPort}/`
|
||||
);
|
||||
|
||||
updateStartDate(parseISO(activeSession.createdAt));
|
||||
|
||||
setStreamLoaded(true);
|
||||
} else {
|
||||
setIsStreamEnded(true);
|
||||
}
|
||||
}
|
||||
|
||||
function update(socketId: string, data: { [key: string]: any }) {
|
||||
if (!socket) return;
|
||||
|
||||
socket.emit("update", socketId, data);
|
||||
}
|
||||
|
||||
function kick(socketId: string) {
|
||||
if (!socket) return;
|
||||
|
||||
socket.emit("kick", socketId);
|
||||
}
|
||||
|
||||
function toastWarn(text: string) {
|
||||
toast.warn(text, {
|
||||
position: "top-center",
|
||||
autoClose: 2000,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
theme: "dark",
|
||||
icon: <AlertIcon />,
|
||||
closeButton: false,
|
||||
});
|
||||
}
|
||||
|
||||
// function toastUser(text: string) {
|
||||
// toast.info(text, {
|
||||
// position: "top-center",
|
||||
// autoClose: 2000,
|
||||
// hideProgressBar: true,
|
||||
// closeOnClick: true,
|
||||
// pauseOnHover: true,
|
||||
// draggable: true,
|
||||
// theme: "dark",
|
||||
// icon: <UserIcon />,
|
||||
// closeButton: false,
|
||||
// });
|
||||
// }
|
||||
|
||||
function toastHandOn(text: string) {
|
||||
toast.info(text, {
|
||||
position: "top-center",
|
||||
autoClose: 2000,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
theme: "dark",
|
||||
icon: <HandOnIcon />,
|
||||
closeButton: false,
|
||||
});
|
||||
}
|
||||
|
||||
async function checkIsActiveSession() {
|
||||
const activeSession: any = await ky
|
||||
.get(`${import.meta.env.VITE_COORD_URL}/active_sessions/${params.id}`)
|
||||
.json();
|
||||
|
||||
if (!activeSession) {
|
||||
setIsStreamEnded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isStreamEnded) {
|
||||
setTimeout(() => {
|
||||
checkIsActiveSession();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
connect();
|
||||
|
||||
const socket: Socket = io(import.meta.env.VITE_COORD_URL, {
|
||||
query: {
|
||||
roomId,
|
||||
admin: searchParams.has("admin", "true"),
|
||||
},
|
||||
});
|
||||
|
||||
socket.on("connect", () => {
|
||||
setSocket(socket);
|
||||
});
|
||||
|
||||
socket.on("join", (_socketId, sockets) => {
|
||||
setUsers(sockets);
|
||||
toastHandOn(t("notification.newMember"));
|
||||
});
|
||||
|
||||
socket.on("update", (_socketId, _data, sockets) => {
|
||||
setUsers(sockets);
|
||||
});
|
||||
|
||||
socket.on("kick", () => {
|
||||
socket.close();
|
||||
window.close();
|
||||
});
|
||||
|
||||
socket.on("leave", (socketId) => {
|
||||
setUsers(
|
||||
useStreamUserStore
|
||||
.getState()
|
||||
.users.filter((user: any) => user.id !== socketId)
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("notification", (type) => {
|
||||
if (type === "afk-timer") {
|
||||
// setModal(<AFKTimerModal />);
|
||||
}
|
||||
});
|
||||
|
||||
checkIsActiveSession();
|
||||
|
||||
return () => {
|
||||
socket.off("connect");
|
||||
socket.off("join");
|
||||
socket.off("update");
|
||||
socket.off("kick");
|
||||
socket.off("leave");
|
||||
socket.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
|
||||
getToken();
|
||||
}, [socket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (socket) {
|
||||
setMe(users.find((user: any) => user.id === socket.id));
|
||||
}
|
||||
}, [users]);
|
||||
|
||||
useEffect(() => {
|
||||
if (me && me.allowControl && !me.admin) {
|
||||
toastHandOn(t("notification.controlReceived"));
|
||||
}
|
||||
}, [me]);
|
||||
|
||||
return (
|
||||
<FullScreen handle={handleFullScreen} className="h-screen text-[#F2F2F2]">
|
||||
{!isStreamEnded ? (
|
||||
isStreamLoaded ? (
|
||||
<>
|
||||
{orientation === "landscape" ? (
|
||||
<>{streamUrl && <Player ss={streamUrl} />}</>
|
||||
) : (
|
||||
<div className="absolute top-0 left-0 w-full h-full flex flex-col justify-center items-center gap-2">
|
||||
<RotateDeviceIcon />
|
||||
<Trans i18nKey="rotateDevice">Поверните устройство</Trans>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute top-0 left-0 w-full h-full flex justify-center items-center gap-4">
|
||||
<p className="flex items-center gap-4">
|
||||
<img
|
||||
src="/icons/Loader.png"
|
||||
alt=""
|
||||
className="animate-spin w-6 h-6"
|
||||
/>
|
||||
<Trans i18nKey="streamWaiting">Ожидание потока</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="absolute top-0 left-0 w-full h-full flex justify-center items-center">
|
||||
<p className="text-2xl font-gilroy">
|
||||
<Trans i18nKey="streamEnded">Трансляция была завершена</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{me && !me.allowControl && (
|
||||
<>
|
||||
{new userAgentParser(me.ua).getDevice().type === "mobile" ? (
|
||||
<div
|
||||
className="absolute top-0 left-0 w-full h-full"
|
||||
onTouchStart={() => toastWarn(t("notification.getAccess"))}
|
||||
></div>
|
||||
) : (
|
||||
<div
|
||||
className="absolute top-0 left-0 w-full h-full"
|
||||
onMouseDown={() => toastWarn(t("notification.getAccess"))}
|
||||
></div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="absolute top-0 left-0 min-h-screen flex flex-col justify-center transition-all">
|
||||
<div className="flex flex-col gap-4 lg:p-4 p-2">
|
||||
{new userAgentParser().getDevice().model !== "iPhone" && (
|
||||
<>
|
||||
{!handleFullScreen.active ? (
|
||||
<button
|
||||
onClick={() => handleFullScreen.enter()}
|
||||
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90"
|
||||
>
|
||||
<FullscreenIcon />
|
||||
<span className="absolute left-12 top-[50%] -translate-y-[50%] invisible group-hover:visible opacity-0 group-hover:opacity-100 text-xs transition-all px-2 py-1 bg-[#131313] rounded">
|
||||
<Trans i18nKey={"fullscreenMode"}>
|
||||
Полноэкранный режим
|
||||
</Trans>
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleFullScreen.exit()}
|
||||
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90"
|
||||
>
|
||||
<WindowedModeIcon />
|
||||
<span className="absolute left-12 top-[50%] -translate-y-[50%] invisible group-hover:visible opacity-0 group-hover:opacity-100 text-xs transition-all px-2 py-1 bg-[#131313] rounded">
|
||||
<Trans i18nKey={"windowedMode"}>Оконный режим</Trans>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setModal(<QRCodeModal />)}
|
||||
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90"
|
||||
>
|
||||
<QRIcon />
|
||||
<span className="absolute left-12 top-[50%] -translate-y-[50%] invisible group-hover:visible opacity-0 group-hover:opacity-100 text-xs transition-all px-2 py-1 bg-[#131313] rounded">
|
||||
<Trans i18nKey={"inviteByQRCode"}>Пригласить по QR коду</Trans>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setModal(<ShareModal />)}
|
||||
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90"
|
||||
>
|
||||
<ShareIcon />
|
||||
<span className="absolute left-12 top-[50%] -translate-y-[50%] invisible group-hover:visible opacity-0 group-hover:opacity-100 text-xs transition-all px-2 py-1 bg-[#131313] rounded">
|
||||
<Trans i18nKey={"inviteByLink"}>Пригласить по ссылке</Trans>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
setModal(
|
||||
<UsersManagementModal
|
||||
me={me}
|
||||
handleUpdate={update}
|
||||
handleKick={kick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90"
|
||||
>
|
||||
<PersonsIcon />
|
||||
<span className="absolute left-12 top-[50%] -translate-y-[50%] invisible group-hover:visible opacity-0 group-hover:opacity-100 text-xs transition-all px-2 py-1 bg-[#131313] rounded">
|
||||
<Trans i18nKey={"members"}>Участники</Trans>
|
||||
</span>
|
||||
<div className="absolute px-2 py-1 text-xs flex flex-col justify-center items-center rounded-full -top-2 -right-2 bg-[#131313] -z-10">
|
||||
{users.length}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* <button
|
||||
onClick={() => setIsShowChat(true)}
|
||||
className="relative group outline-none bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90 flex justify-center items-center"
|
||||
>
|
||||
<ChatIcon className="w-5 h-5" />
|
||||
<span className="absolute left-12 top-[50%] -translate-y-[50%] invisible group-hover:visible opacity-0 group-hover:opacity-100 text-xs transition-all px-2 py-1 bg-[#131313] rounded">
|
||||
<Trans i18nKey={"chat"}>Чат</Trans>
|
||||
</span>
|
||||
</button> */}
|
||||
|
||||
<LiveKitRoom
|
||||
video={true}
|
||||
audio={true}
|
||||
token={token}
|
||||
serverUrl={livekitServerUrl}
|
||||
>
|
||||
<RoomAudioRenderer />
|
||||
</LiveKitRoom>
|
||||
|
||||
<div className="relative group outline-none w-10 h-10 bg-[#131313] rounded-full shadow-lg shadow-[#131313] p-2 opacity-90 flex justify-center items-center">
|
||||
<p className="text-xs">{stopwatch}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Transition in={isShowChat} timeout={200} mountOnEnter unmountOnExit>
|
||||
{(state) => (
|
||||
<div className="absolute right-0 bottom-0">
|
||||
<Chat className={state} handleClose={() => setIsShowChat(false)} />
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
in={modal ? true : false}
|
||||
timeout={200}
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
>
|
||||
{(state) => <ModalContainer className={state} />}
|
||||
</Transition>
|
||||
|
||||
<ToastContainer />
|
||||
</FullScreen>
|
||||
);
|
||||
}
|
||||
|
||||
export default StreamPage;
|
||||
@@ -0,0 +1,86 @@
|
||||
import { format } from "date-fns";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
// import useSocketStore from "../stores/useSocketStore";
|
||||
import CloseIcon from "./icons/CloseIcon";
|
||||
import SendChatIcon from "./icons/SendChatIcon";
|
||||
import Button from "./ui/Button";
|
||||
import useStreamUserStore from "../stores/useStreamUserStore";
|
||||
import { FormEvent, useRef, useState } from "react";
|
||||
import IMessage from "../types/IMessage";
|
||||
import useStateRef from "react-usestateref";
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function Chat2({ onClose }: Props) {
|
||||
// const { socket } = useSocketStore();
|
||||
const { me, users } = useStreamUserStore();
|
||||
const [messages] = useStateRef<IMessage[]>([]);
|
||||
const messagesRef = useRef<HTMLDivElement>(null);
|
||||
const [messageText, setMessageText] = useState<string>("");
|
||||
const messageTextRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
function sendMessage(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`chat relative h-full flex flex-col w-[296px] bg-white`}>
|
||||
<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={onClose}
|
||||
/>
|
||||
</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 === me?.id)?.name === message.name
|
||||
? "bg-[#C4DDF5]"
|
||||
: "bg-[#F0F1F2]"
|
||||
}`}
|
||||
>
|
||||
{users.find((user) => user.id === me?.id)?.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>
|
||||
);
|
||||
}
|
||||
|
||||
export default Chat2;
|
||||
@@ -3,9 +3,15 @@ import useModalStore from "../stores/useModalStore";
|
||||
function ModalContainer2() {
|
||||
const { modal } = useModalStore();
|
||||
|
||||
if (modal) {
|
||||
return <div className="fixed top-0 left-0 w-full h-full">{modal}</div>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`fixed top-0 left-0 w-full h-full ${
|
||||
!modal ? "hidden" : ""
|
||||
}`}
|
||||
>
|
||||
{modal}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalContainer2;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Config,
|
||||
AllSettings,
|
||||
PixelStreaming,
|
||||
} from "@epicgames-ps/lib-pixelstreamingfrontend-ue5.5";
|
||||
} from "@epicgames-ps/lib-pixelstreamingfrontend-ue5.3";
|
||||
|
||||
export interface PixelStreamingWrapperProps {
|
||||
initialSettings?: Partial<AllSettings>;
|
||||
|
||||
@@ -4,7 +4,7 @@ interface TooltipProps {
|
||||
|
||||
function Tooltip({ text }: TooltipProps) {
|
||||
return (
|
||||
<div className="group-hover:opacity-100 opacity-0 transition-opacity absolute top-[calc(100%+12px)] -left-1.5 bg-[#111C26] rounded-lg px-4 py-2 z-10 pointer-events-none whitespace-nowrap">
|
||||
<div className="absolute top-[calc(100%+12px)] -left-1.5 z-20 group-hover:opacity-100 opacity-0 transition-opacity bg-[#111C26] rounded-lg px-4 py-2 pointer-events-none whitespace-nowrap">
|
||||
<svg
|
||||
width="12"
|
||||
height="10"
|
||||
|
||||
@@ -47,7 +47,7 @@ function Video({ mediaStream, muted, user }: Props) {
|
||||
<div className="relative">
|
||||
<video
|
||||
ref={remoteVideoRef}
|
||||
className={`aspect-video w-[216px] h-[162px] rounded-lg object-cover bg-gray-500 ring-2 ${
|
||||
className={`aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg object-cover bg-gray-500 ring-2 ${
|
||||
!_muted && isSpeaking ? "ring-green-500" : "ring-transparent"
|
||||
}`}
|
||||
playsInline
|
||||
|
||||
@@ -31,7 +31,7 @@ function InviteModal() {
|
||||
}
|
||||
|
||||
return (
|
||||
<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 sm:border-none border-l border-[#DAE0E5] flex flex-col lg:items-center lg:justify-center">
|
||||
<div className="relative 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 sm:border-none 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">
|
||||
|
||||
@@ -30,7 +30,7 @@ function SetNameModal({ onAction }: Props) {
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full bg-opacity-50 backdrop-blur-2xl">
|
||||
<div className="bg-white p-12 rounded-lg space-y-6">
|
||||
<div className="bg-white sm:p-12 p-6 sm:rounded-lg space-y-6 sm:h-auto h-full sm:w-auto w-full flex flex-col max-sm:justify-center">
|
||||
<p className="text-2xl font-semibold">Здравствуйте!</p>
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold">Представьтесь, пожалуйста</p>
|
||||
@@ -46,6 +46,7 @@ function SetNameModal({ onAction }: Props) {
|
||||
onChange={handleChangeName}
|
||||
autoFocus={!name}
|
||||
required
|
||||
className="max-sm:w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@@ -54,10 +55,11 @@ function SetNameModal({ onAction }: Props) {
|
||||
type="submit"
|
||||
large
|
||||
onClick={handleClickNoName}
|
||||
className="max-sm:w-full"
|
||||
>
|
||||
Не указывать
|
||||
</Button>
|
||||
<Button type="submit" large>
|
||||
<Button type="submit" large className="max-sm:w-full">
|
||||
Продолжить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ interface InputProps {
|
||||
autoFocus?: boolean;
|
||||
required?: boolean;
|
||||
value?: string;
|
||||
className?: string;
|
||||
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
@@ -15,13 +16,14 @@ function Input({
|
||||
autoFocus,
|
||||
required,
|
||||
value,
|
||||
className,
|
||||
onChange,
|
||||
}: InputProps) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
className="bg-white border border-[#DAE0E5] w-[296px] h-10 px-2 py-2.5 rounded-lg text-sm outline-none"
|
||||
className={`bg-white border border-[#DAE0E5] w-[296px] h-10 px-2 py-2.5 rounded-lg text-sm outline-none ${className}`}
|
||||
autoFocus={autoFocus}
|
||||
required={required}
|
||||
value={value}
|
||||
|
||||
@@ -31,7 +31,3 @@
|
||||
width: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
/* LiveKit */
|
||||
|
||||
|
||||
|
||||
+123
-83
@@ -9,7 +9,7 @@ import useStateRef from "react-usestateref";
|
||||
import Peer from "peerjs";
|
||||
import useIsAudioActive from "use-is-audio-active";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { io } from "socket.io-client";
|
||||
import IRemoteStream from "../types/IRemoteStream";
|
||||
import Video from "../components/Video";
|
||||
import ModalContainer2 from "../components/ModalContainer2";
|
||||
@@ -25,7 +25,7 @@ import MicroOffIcon from "../components/icons/MicroOffIcon";
|
||||
import CameraOnIcon from "../components/icons/CameraOnIcon";
|
||||
import CameraOffIcon from "../components/icons/CameraOffIcon";
|
||||
import { Trans } from "react-i18next";
|
||||
import { isIOS } from "react-device-detect";
|
||||
import { isIOS, useMobileOrientation } from "react-device-detect";
|
||||
import WindowIcon from "../components/icons/WindowIcon";
|
||||
import FullscreenIcon from "../components/icons/FullscreenIcon";
|
||||
import ShareIcon from "../components/icons/ShareIcon";
|
||||
@@ -34,6 +34,12 @@ import InviteModal from "../components/modals/stream/InviteModal";
|
||||
import Tooltip from "../components/Tooltip";
|
||||
import { toast, ToastContainer } from "react-toastify";
|
||||
import LoadingModal from "../components/modals/stream/LoadingModal";
|
||||
import useSocketStore from "../stores/useSocketStore";
|
||||
import useStreamUserStore from "../stores/useStreamUserStore";
|
||||
import Chat2 from "../components/Chat2";
|
||||
import Rotate64Icon from "../components/icons/Rotate64Icon";
|
||||
import Draggable from "react-draggable";
|
||||
// import ChatIcon from "../components/icons/ChatIcon";
|
||||
|
||||
// import MoreIcon from "../components/icons/MoreIcon";
|
||||
|
||||
@@ -57,11 +63,10 @@ function StreamPage3() {
|
||||
const isSpeaking = useIsAudioActive({
|
||||
source: localStream.getTracks().length ? localStream : null,
|
||||
});
|
||||
const [users, setUsers, usersRef] = useStateRef<IUser[]>([]);
|
||||
const [me, setMe, meRef] = useStateRef<IUser>();
|
||||
const { me, users, setMe, setUsers } = useStreamUserStore();
|
||||
const isCallInit = useRef<boolean>(false);
|
||||
const [roomId] = useState<string>(params.id!);
|
||||
const [socket, setSocket] = useState<Socket>();
|
||||
const { socket, setSocket } = useSocketStore();
|
||||
const { setModal } = useModalStore();
|
||||
const { name } = useStreamStore();
|
||||
const [isMicEnabled, setIsMicEnabled] = useState(true);
|
||||
@@ -72,6 +77,8 @@ function StreamPage3() {
|
||||
const [isFullscreen, { toggleFullscreen }] = useFullscreen(fullscreenRef);
|
||||
const [isVideoInitialized, setIsVideoInitialized] = useState<boolean>(false);
|
||||
const [step, setStep] = useState<number>(1);
|
||||
const [isShowChat, setIsShowChat] = useState<boolean>(false);
|
||||
const { isPortrait } = useMobileOrientation();
|
||||
|
||||
async function startCall(remotePeerId: string) {
|
||||
if (!peerInstance) return;
|
||||
@@ -187,20 +194,20 @@ function StreamPage3() {
|
||||
isCallInit.current = true;
|
||||
}
|
||||
|
||||
setUsers(users);
|
||||
setMe(users.find((user) => user.id === userId));
|
||||
setUsers(users);
|
||||
});
|
||||
|
||||
socket.on("request-control", (userId) => {
|
||||
const user = usersRef.current.find((user) => user.id === userId);
|
||||
const user = users.find((user) => user.id === userId);
|
||||
|
||||
if (user?.id === meRef.current?.id || !meRef.current?.isAdmin) return;
|
||||
if (user?.id === me?.id || !me?.isAdmin) return;
|
||||
|
||||
toast.info(`${user?.name} запрашивает разрешение на управление`);
|
||||
});
|
||||
|
||||
socket.on("transfer-control", (userId) => {
|
||||
if (meRef.current?.id !== userId) return;
|
||||
if (me?.id !== userId) return;
|
||||
|
||||
toast.info(`Вы получили разрешение на управление`);
|
||||
});
|
||||
@@ -336,16 +343,20 @@ function StreamPage3() {
|
||||
return (
|
||||
<div
|
||||
ref={fullscreenRef}
|
||||
className="h-[100dvh] flex flex-col bg-[#111C26] overflow-hidden"
|
||||
className="h-[100dvh] flex lg:flex-col bg-[#111C26] overflow-hidden"
|
||||
>
|
||||
{isEnded === false ? (
|
||||
{isEnded === false && (
|
||||
<>
|
||||
<div className="flex items-center bg-white h-12 px-6">
|
||||
<div className="pr-6">
|
||||
<img src="/images/logo24.svg" alt="" />
|
||||
<div className="flex max-lg:flex-col items-center justify-between bg-white lg:h-12 lg:w-auto w-12 lg:px-6 max-lg:py-2">
|
||||
<div className="lg:pr-6">
|
||||
<img
|
||||
src="/images/logo24.svg"
|
||||
alt=""
|
||||
className="lg:block hidden"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="lg:flex hidden items-center gap-2">
|
||||
<div className="relative w-6 h-6 bg-[#E6ECF2] rounded-full flex items-center justify-center">
|
||||
<p className="text-xs font-semibold">
|
||||
{name[0]?.toUpperCase()}
|
||||
@@ -356,7 +367,7 @@ function StreamPage3() {
|
||||
</div>
|
||||
<p className="text-xs">{name}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex max-lg:flex-col items-center gap-2">
|
||||
<div className="relative group">
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -416,47 +427,58 @@ function StreamPage3() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="h-4 w-px bg-[#DAE0E5]"></div>
|
||||
{users.map((user) => {
|
||||
if (user.id !== userId) {
|
||||
return (
|
||||
<div key={user.id} className="flex items-center gap-2">
|
||||
<div className="relative w-6 h-6 bg-[#E6ECF2] rounded-full flex items-center justify-center">
|
||||
<p className="text-xs font-semibold">
|
||||
{name[0]?.toUpperCase()}
|
||||
</p>
|
||||
{user?.isControlAllowed && (
|
||||
<div className="absolute bottom-0 right-0 bg-[#49A1F5] w-2 h-2 rounded-full border border-white"></div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs">{user.name}</p>
|
||||
<div className="h-4 w-px bg-[#DAE0E5] lg:block hidden"></div>
|
||||
<div className="lg:flex hidden">
|
||||
{users.map((user) => {
|
||||
if (user.id !== userId) {
|
||||
return (
|
||||
<div key={user.id} className="flex items-center gap-2">
|
||||
<div className="relative w-6 h-6 bg-[#E6ECF2] rounded-full flex items-center justify-center">
|
||||
<p className="text-xs font-semibold">
|
||||
{name[0]?.toUpperCase()}
|
||||
</p>
|
||||
{user?.isControlAllowed && (
|
||||
<div className="absolute bottom-0 right-0 bg-[#49A1F5] w-2 h-2 rounded-full border border-white"></div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs">{user.name}</p>
|
||||
|
||||
{me?.isAdmin && me?.isControlAllowed && (
|
||||
<div className="relative">
|
||||
{/* <Button
|
||||
{me?.isAdmin && me?.isControlAllowed && (
|
||||
<div className="relative">
|
||||
{/* <Button
|
||||
variant="secondary"
|
||||
icon={<MoreIcon />}
|
||||
onlyIcon
|
||||
/> */}
|
||||
{/* <div className="absolute"> */}
|
||||
<div className="relative group">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={<HandOnIcon />}
|
||||
onlyIcon
|
||||
onClick={() => transferControl(user.id)}
|
||||
/>
|
||||
<Tooltip text={"Передать управление"} />
|
||||
{/* <div className="absolute"> */}
|
||||
<div className="relative group">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={<HandOnIcon />}
|
||||
onlyIcon
|
||||
onClick={() => transferControl(user.id)}
|
||||
/>
|
||||
<Tooltip text={"Передать управление"} />
|
||||
</div>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 ml-auto">
|
||||
<div className="flex max-lg:flex-col gap-2 lg:ml-auto">
|
||||
{/* <div className="relative group">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={<ChatIcon />}
|
||||
onlyIcon
|
||||
onClick={() => setIsShowChat((prev) => !prev)}
|
||||
/>
|
||||
<Tooltip text={isShowChat ? "Скрыть чат" : "Показать чат"} />
|
||||
</div> */}
|
||||
<div className="relative group">
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -498,46 +520,64 @@ function StreamPage3() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{!users.find((user) => user.id === userId)?.isControlAllowed && (
|
||||
<div
|
||||
className="absolute top-0 left-0 w-full h-full"
|
||||
onClick={() =>
|
||||
toast.warn("Необходимо запросить разрешение на управление")
|
||||
}
|
||||
></div>
|
||||
)}
|
||||
{step !== 1 &&
|
||||
!users.find((user) => user.id === userId)?.isControlAllowed && (
|
||||
<div
|
||||
className="absolute top-0 left-0 w-full h-full"
|
||||
onClick={() =>
|
||||
toast.warn("Необходимо запросить разрешение на управление")
|
||||
}
|
||||
></div>
|
||||
)}
|
||||
|
||||
<div className="absolute top-2 left-2 space-y-2">
|
||||
<div className={`relative ${!permission ? "hidden" : ""}`}>
|
||||
<video
|
||||
ref={localVideoRef}
|
||||
className={`aspect-video w-[216px] h-[162px] rounded-lg object-cover bg-gray-500 -scale-x-100 ring-2 ${
|
||||
isMicEnabled && isSpeaking
|
||||
? "ring-green-500"
|
||||
: "ring-transparent"
|
||||
}`}
|
||||
playsInline
|
||||
autoPlay
|
||||
muted
|
||||
></video>
|
||||
<div className="absolute bottom-0 p-2">
|
||||
<p className="text-sm text-white">{name}</p>
|
||||
<Draggable
|
||||
defaultClassName="cursor-grab"
|
||||
defaultClassNameDragging="cursor-grabbing"
|
||||
>
|
||||
<div className="absolute top-2 lg:left-2 max-lg:right-2 space-y-2">
|
||||
<div className={`relative ${!permission ? "hidden" : ""}`}>
|
||||
<video
|
||||
ref={localVideoRef}
|
||||
className={`aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg object-cover bg-gray-500 -scale-x-100 ring-2 ${
|
||||
isMicEnabled && isSpeaking
|
||||
? "ring-green-500"
|
||||
: "ring-transparent"
|
||||
}`}
|
||||
playsInline
|
||||
autoPlay
|
||||
muted
|
||||
></video>
|
||||
<div className="absolute bottom-0 p-2">
|
||||
<p className="text-sm text-white">{name}</p>
|
||||
</div>
|
||||
</div>
|
||||
{remoteStreams.map(({ peerId, mediaStream }) => (
|
||||
<Video
|
||||
key={peerId}
|
||||
mediaStream={mediaStream}
|
||||
muted={!permission}
|
||||
user={users.find((user) => user.peerId === peerId)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{remoteStreams.map(({ peerId, mediaStream }) => (
|
||||
<Video
|
||||
key={peerId}
|
||||
mediaStream={mediaStream}
|
||||
muted={!permission}
|
||||
user={users.find((user) => user.peerId === peerId)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Draggable>
|
||||
|
||||
{isShowChat && <Chat2 onClose={() => setIsShowChat(false)} />}
|
||||
</div>
|
||||
|
||||
{isPortrait && (
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-white flex flex-col items-center justify-center gap-2">
|
||||
<Rotate64Icon />
|
||||
<p className="font-semibold">Поверните устройство</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ModalContainer2 />
|
||||
<ToastContainer />
|
||||
</>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{isEnded === true && (
|
||||
<div className="flex-1 flex items-center justify-center p-8">
|
||||
<p className="text-2xl text-white font-gilroy text-center">
|
||||
<Trans i18nKey={"demonstrationCompleted"}>
|
||||
|
||||
@@ -4,13 +4,17 @@ import { devtools } from "zustand/middleware";
|
||||
import IUser from "../types/IUser";
|
||||
|
||||
interface StreamUserState {
|
||||
me: IUser | undefined;
|
||||
users: IUser[];
|
||||
setMe: (me: IUser | undefined) => void;
|
||||
setUsers: (users: IUser[]) => void;
|
||||
}
|
||||
|
||||
const useStreamUserStore = create<StreamUserState>()(
|
||||
devtools((set) => ({
|
||||
me: undefined,
|
||||
users: [],
|
||||
setMe: (me) => set({ me }),
|
||||
setUsers: (users) => set({ users }),
|
||||
}))
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user