upd
This commit is contained in:
+21
-13
@@ -9,6 +9,7 @@ import useStreamUserStore from "../stores/useStreamUserStore";
|
|||||||
import { FormEvent, useEffect, useRef, useState } from "react";
|
import { FormEvent, useEffect, useRef, useState } from "react";
|
||||||
import useSocketStore from "../stores/useSocketStore";
|
import useSocketStore from "../stores/useSocketStore";
|
||||||
import useChatStore from "../stores/useChatStore";
|
import useChatStore from "../stores/useChatStore";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -26,6 +27,8 @@ function Chat2({ onClose }: Props) {
|
|||||||
function sendMessage(e: FormEvent) {
|
function sendMessage(e: FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!messageText || messageText === " ") return;
|
||||||
|
|
||||||
socket?.emit("message", messageText);
|
socket?.emit("message", messageText);
|
||||||
|
|
||||||
setMessageText("");
|
setMessageText("");
|
||||||
@@ -42,22 +45,24 @@ function Chat2({ onClose }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`chat relative h-[calc(100vh-48px)] flex flex-col w-[296px] bg-white`}
|
className={`chat lg:relative absolute right-0 lg:h-[calc(100dvh-48px)] h-dvh flex flex-col w-[296px] bg-white border-t border-[#DAE0E5]`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-2 pl-4 border-b border-[#DAE0E5]">
|
<div className="p-4 pb-2">
|
||||||
<p className="text-sm font-semibold">
|
<div className="flex items-center justify-between border-b border-[#DAE0E5] pb-4">
|
||||||
<Trans i18nKey={"chat"}>Чат</Trans>
|
<p className="text-sm font-semibold">
|
||||||
</p>
|
<Trans i18nKey={"chat"}>Чат демонстрации</Trans>
|
||||||
<Button
|
</p>
|
||||||
variant="tertiary"
|
<Button
|
||||||
icon={<CloseIcon />}
|
variant="tertiary"
|
||||||
onlyIcon
|
icon={<CloseIcon />}
|
||||||
onClick={onClose}
|
onlyIcon
|
||||||
/>
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={messagesRef}
|
ref={messagesRef}
|
||||||
className="flex-1 overflow-y-scroll flex flex-col gap-1 p-3 border-b border-[#DAE0E5]"
|
className="flex-1 overflow-y-scroll flex flex-col gap-1 px-4 pb-2 pt-0 border-b border-[#DAE0E5]"
|
||||||
>
|
>
|
||||||
{messages.map((message, index) => (
|
{messages.map((message, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -85,12 +90,15 @@ function Chat2({ onClose }: Props) {
|
|||||||
<div className="p-3 pl-4">
|
<div className="p-3 pl-4">
|
||||||
<form onSubmit={sendMessage} className="flex items-center gap-3">
|
<form onSubmit={sendMessage} className="flex items-center gap-3">
|
||||||
<input
|
<input
|
||||||
|
autoFocus={isMobile ? false : true}
|
||||||
ref={messageTextRef}
|
ref={messageTextRef}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t("writeAMessage")}
|
placeholder={t("writeAMessage")}
|
||||||
className="w-full text-sm bg-transparent outline-none"
|
className="w-full text-sm bg-transparent outline-none"
|
||||||
value={messageText}
|
value={messageText}
|
||||||
onChange={(e) => setMessageText(e.target.value)}
|
onChange={(e) =>
|
||||||
|
setMessageText(e.target.value.replace(/\s+/g, " "))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<button type="submit" className="text-[#49A1F5] outline-none">
|
<button type="submit" className="text-[#49A1F5] outline-none">
|
||||||
<SendChatIcon />
|
<SendChatIcon />
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import CloseIcon from "./icons/CloseIcon";
|
||||||
|
import HandOffIcon from "./icons/HandOffIcon";
|
||||||
|
// import MicroOnIcon from "./icons/MicroOnIcon";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
handleToggleControl: () => void;
|
||||||
|
handleKick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Dropdown({ handleToggleControl, handleKick }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="absolute py-2 mt-4 space-y-1 bg-white rounded-lg shadow w-60">
|
||||||
|
{/* <button className="flex items-center gap-2 px-4 py-1 text-sm">
|
||||||
|
<span className="text-[#77828C]">
|
||||||
|
<MicroOnIcon />
|
||||||
|
</span>
|
||||||
|
<span>Отключить микрофон</span>
|
||||||
|
</button> */}
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 px-4 py-1 text-sm"
|
||||||
|
onClick={handleToggleControl}
|
||||||
|
>
|
||||||
|
<span className="text-[#77828C]">
|
||||||
|
<HandOffIcon />
|
||||||
|
</span>
|
||||||
|
<span>Передать управление</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 px-4 py-1 text-sm"
|
||||||
|
onClick={handleKick}
|
||||||
|
>
|
||||||
|
<span className="text-[#EB5757]">
|
||||||
|
<CloseIcon />
|
||||||
|
</span>
|
||||||
|
<span>Исключить</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dropdown;
|
||||||
+69
-65
@@ -1,82 +1,86 @@
|
|||||||
import { useRef, useState } from "react";
|
import { isMobile } from "react-device-detect";
|
||||||
import CloseIcon from "./icons/CloseIcon";
|
import DesktopIcon from "./icons/DesktopIcon";
|
||||||
import HandOnIcon from "./icons/HandOnIcon";
|
import HandOnIcon from "./icons/HandOnIcon";
|
||||||
import Button from "./ui/Button";
|
import MobileIcon from "./icons/MobileIcon";
|
||||||
import MoreIcon from "./icons/MoreIcon";
|
import MoreIcon from "./icons/MoreIcon";
|
||||||
import { useOnClickOutside } from "usehooks-ts";
|
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
|
import Button from "./ui/Button";
|
||||||
import IUser from "../types/IUser";
|
import IUser from "../types/IUser";
|
||||||
|
import CloseIcon from "./icons/CloseIcon";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useClickAway } from "@uidotdev/usehooks";
|
||||||
|
|
||||||
interface UserProps {
|
interface Props {
|
||||||
|
me: IUser;
|
||||||
user: IUser;
|
user: IUser;
|
||||||
onTransferControl: (userId: string) => void;
|
handleTransferControl: () => void;
|
||||||
onKickUser: (userId: string) => void;
|
handleKick: () => void;
|
||||||
className?: string;
|
|
||||||
tooltip?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function User({
|
function User({ me, user, handleTransferControl, handleKick }: Props) {
|
||||||
user,
|
const [showMore, setShowMore] = useState(false);
|
||||||
onTransferControl,
|
|
||||||
onKickUser,
|
|
||||||
className,
|
|
||||||
tooltip = false,
|
|
||||||
}: UserProps) {
|
|
||||||
const [isShowMore, setIsShowMore] = useState(false);
|
|
||||||
const moreRef = useRef(null);
|
|
||||||
|
|
||||||
function handleClickOutside() {
|
const ref = useClickAway<HTMLDivElement>(() => {
|
||||||
setIsShowMore(false);
|
setShowMore(false);
|
||||||
}
|
});
|
||||||
|
|
||||||
useOnClickOutside(moreRef, handleClickOutside);
|
|
||||||
|
|
||||||
function handleClickTransferControl() {
|
|
||||||
onTransferControl(user.id);
|
|
||||||
setIsShowMore(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleClickKickUser() {
|
|
||||||
onKickUser(user.id);
|
|
||||||
setIsShowMore(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={moreRef} className="relative">
|
<div key={user.id} className="flex items-center gap-2">
|
||||||
<Button
|
<div className="relative w-6 h-6 bg-[#E6ECF2] rounded-full flex items-center justify-center">
|
||||||
variant="tertiary"
|
<p className="text-xs font-semibold">{user.name[0]?.toUpperCase()}</p>
|
||||||
icon={<MoreIcon />}
|
{user?.isControlAllowed && (
|
||||||
onlyIcon
|
<div className="absolute bottom-0 right-0 bg-[#49A1F5] w-2 h-2 rounded-full border border-white"></div>
|
||||||
onClick={() => setIsShowMore((prev) => !prev)}
|
)}
|
||||||
className="group relative"
|
</div>
|
||||||
>
|
<p className="text-xs">{user.name}</p>
|
||||||
{tooltip && <Tooltip text="Действия" />}
|
<div className="text-[#CCCCCC]">
|
||||||
</Button>
|
{isMobile ? <MobileIcon /> : <DesktopIcon />}
|
||||||
|
</div>
|
||||||
|
|
||||||
{isShowMore && (
|
{me.isAdmin && (
|
||||||
<div
|
<div ref={ref} className="relative z-10">
|
||||||
className={`absolute top-[calc(100%+18px)] bg-white rounded-lg py-2 z-10 shadow ${className}`}
|
<Button
|
||||||
>
|
variant="tertiary"
|
||||||
<button
|
icon={<MoreIcon />}
|
||||||
className="flex items-center gap-2 px-4 py-1 hover:bg-[#E6ECF2] transition-colors w-full"
|
onlyIcon
|
||||||
onClick={handleClickTransferControl}
|
onClick={() => setShowMore(true)}
|
||||||
>
|
/>
|
||||||
|
<Tooltip text={"Действия"} />
|
||||||
|
|
||||||
|
{showMore && (
|
||||||
|
<div className="absolute py-2 mt-4 space-y-1 bg-white rounded-lg shadow w-60">
|
||||||
|
{/* <button className="flex items-center gap-2 px-4 py-1 text-sm hover:bg-[#E6ECF2] transition-colors">
|
||||||
<span className="text-[#77828C]">
|
<span className="text-[#77828C]">
|
||||||
<HandOnIcon />
|
<MicroOnIcon />
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm whitespace-nowrap">
|
<span>Отключить микрофон</span>
|
||||||
Передать управление
|
</button> */}
|
||||||
</span>
|
<button
|
||||||
</button>
|
className="flex items-center w-full gap-2 px-4 py-1 text-sm hover:bg-[#E6ECF2] transition-colors"
|
||||||
<button
|
onClick={() => {
|
||||||
className="flex items-center gap-2 px-4 py-1 hover:bg-[#E6ECF2] transition-colors w-full"
|
handleTransferControl();
|
||||||
onClick={handleClickKickUser}
|
setShowMore(false);
|
||||||
>
|
}}
|
||||||
<span className="text-[#EB5757]">
|
>
|
||||||
<CloseIcon />
|
<span className="text-[#77828C]">
|
||||||
</span>
|
<HandOnIcon />
|
||||||
<span className="text-sm">Исключить</span>
|
</span>
|
||||||
</button>
|
<span>Передать управление</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="flex items-center w-full gap-2 px-4 py-1 text-sm hover:bg-[#E6ECF2] transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
handleKick();
|
||||||
|
setShowMore(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="text-[#EB5757]">
|
||||||
|
<CloseIcon />
|
||||||
|
</span>
|
||||||
|
<span>Исключить</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-3
@@ -7,8 +7,7 @@ import App from "./App";
|
|||||||
// import ErrorBoundary from "./ErrorBoundary";
|
// import ErrorBoundary from "./ErrorBoundary";
|
||||||
import HistoryPage from "./HistoryPage";
|
import HistoryPage from "./HistoryPage";
|
||||||
import ScheduledPage from "./ScheduledPage";
|
import ScheduledPage from "./ScheduledPage";
|
||||||
// import StreamPage2 from "./pages/StreamPage2";
|
import StreamPage from "./pages/StreamPage";
|
||||||
import StreamPage3 from "./pages/StreamPage3";
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -18,7 +17,7 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/stream/:id",
|
path: "/stream/:id",
|
||||||
element: <StreamPage3 />,
|
element: <StreamPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/history",
|
path: "/history",
|
||||||
|
|||||||
@@ -43,13 +43,14 @@ import DesktopIcon from "../components/icons/DesktopIcon";
|
|||||||
import MobileIcon from "../components/icons/MobileIcon";
|
import MobileIcon from "../components/icons/MobileIcon";
|
||||||
import ChatIcon from "../components/icons/ChatIcon";
|
import ChatIcon from "../components/icons/ChatIcon";
|
||||||
import useChatStore from "../stores/useChatStore";
|
import useChatStore from "../stores/useChatStore";
|
||||||
|
import User from "../components/User";
|
||||||
// import ChatIcon from "../components/icons/ChatIcon";
|
// import ChatIcon from "../components/icons/ChatIcon";
|
||||||
|
|
||||||
// import MoreIcon from "../components/icons/MoreIcon";
|
// import MoreIcon from "../components/icons/MoreIcon";
|
||||||
|
|
||||||
const userId = uuidv4();
|
const userId = uuidv4();
|
||||||
|
|
||||||
function StreamPage3() {
|
function StreamPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [WSUrl, setWSUrl] = useState<string>("");
|
const [WSUrl, setWSUrl] = useState<string>("");
|
||||||
@@ -218,6 +219,17 @@ function StreamPage3() {
|
|||||||
toast.info(`Вы получили разрешение на управление`);
|
toast.info(`Вы получили разрешение на управление`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("kick", (userId) => {
|
||||||
|
if (useStreamUserStore.getState().me?.id !== userId) return;
|
||||||
|
|
||||||
|
window.close();
|
||||||
|
socket.disconnect();
|
||||||
|
setPeerInstance(undefined);
|
||||||
|
setWSUrl("");
|
||||||
|
setUsers([]);
|
||||||
|
setRemoteStreams([]);
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("message", ({ userId, text }) => {
|
socket.on("message", ({ userId, text }) => {
|
||||||
setMessages([...useChatStore.getState().messages, { userId, text }]);
|
setMessages([...useChatStore.getState().messages, { userId, text }]);
|
||||||
});
|
});
|
||||||
@@ -289,10 +301,13 @@ function StreamPage3() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function requestControl(userId: string) {
|
function requestControl(userId: string) {
|
||||||
console.log("requestControl func", userId);
|
|
||||||
socket?.emit("request-control", userId);
|
socket?.emit("request-control", userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function kick(userId: string) {
|
||||||
|
socket?.emit("kick", userId);
|
||||||
|
}
|
||||||
|
|
||||||
async function getActiveSession() {
|
async function getActiveSession() {
|
||||||
const activeSession: any = await api
|
const activeSession: any = await api
|
||||||
.get(`activeSessions/${params.id}`)
|
.get(`activeSessions/${params.id}`)
|
||||||
@@ -443,50 +458,22 @@ function StreamPage3() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-4 w-px bg-[#DAE0E5] lg:block hidden"></div>
|
<div className="h-4 w-px bg-[#DAE0E5] lg:block hidden"></div>
|
||||||
<div className="hidden gap-6 lg:flex">
|
<div className="hidden gap-6 lg:flex">
|
||||||
{users.map((user) => {
|
{me &&
|
||||||
if (user.id !== userId) {
|
users.map((user) => {
|
||||||
return (
|
if (user.id !== userId) {
|
||||||
<div key={user.id} className="flex items-center gap-2">
|
return (
|
||||||
<div className="relative w-6 h-6 bg-[#E6ECF2] rounded-full flex items-center justify-center">
|
<User
|
||||||
<p className="text-xs font-semibold">
|
me={me}
|
||||||
{name[0]?.toUpperCase()}
|
user={user}
|
||||||
</p>
|
handleTransferControl={() => transferControl(user.id)}
|
||||||
{user?.isControlAllowed && (
|
handleKick={() => kick(user.id)}
|
||||||
<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="text-[#CCCCCC]">
|
|
||||||
{isMobile ? <MobileIcon /> : <DesktopIcon />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{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>
|
|
||||||
{/* </div> */}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 max-lg:flex-col lg:ml-auto">
|
<div className="flex items-center gap-2 lg:gap-4 max-lg:flex-col lg:ml-auto">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -496,30 +483,33 @@ function StreamPage3() {
|
|||||||
/>
|
/>
|
||||||
<Tooltip text={isShowChat ? "Скрыть чат" : "Показать чат"} />
|
<Tooltip text={isShowChat ? "Скрыть чат" : "Показать чат"} />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative group">
|
<div className="w-px h-4 bg-[#DAE0E5] max-lg:hidden"></div>
|
||||||
<Button
|
<div className="flex gap-2 max-lg:flex-col lg:ml-auto">
|
||||||
variant="secondary"
|
|
||||||
icon={<ShareIcon />}
|
|
||||||
onlyIcon
|
|
||||||
onClick={() => setModal(<InviteModal />)}
|
|
||||||
/>
|
|
||||||
<Tooltip text={"Поделиться"} />
|
|
||||||
</div>
|
|
||||||
{!isIOS && (
|
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={isFullscreen ? <WindowIcon /> : <FullscreenIcon />}
|
icon={<ShareIcon />}
|
||||||
onlyIcon
|
onlyIcon
|
||||||
onClick={toggleFullscreen}
|
onClick={() => setModal(<InviteModal />)}
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
text={
|
|
||||||
isFullscreen ? "Оконный режим" : "Полноэкранный режим"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<Tooltip text={"Поделиться"} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
{!isIOS && (
|
||||||
|
<div className="relative group">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
icon={isFullscreen ? <WindowIcon /> : <FullscreenIcon />}
|
||||||
|
onlyIcon
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
text={
|
||||||
|
isFullscreen ? "Оконный режим" : "Полноэкранный режим"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex flex-1">
|
<div className="relative flex flex-1">
|
||||||
@@ -607,4 +597,4 @@ function StreamPage3() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default StreamPage3;
|
export default StreamPage;
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
.entering {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entered {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exiting {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exited {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--toastify-toast-offset: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Toastify__toast-body {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 4px 0 0;
|
|
||||||
align-items: normal;
|
|
||||||
font-family: "Inter", sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #111c26;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Toastify__toast-icon {
|
|
||||||
width: auto;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user