This commit is contained in:
2024-12-22 15:11:05 +05:00
parent 0b5fa1df30
commit 91343bd2f6
8 changed files with 279 additions and 36 deletions
+2 -2
View File
@@ -4,5 +4,5 @@ VITE_COORD_URL=https://coord.graff.tech
VITE_CRM_API_URL=https://crm.stream.graff.tech/api
# VITE_API_URL=http://localhost:5002
VITE_API_URL=https://stream.graff.tech/api
# VITE_SOCKET_URL=http://localhost:5003
VITE_SOCKET_URL=https://stream.graff.tech
VITE_SOCKET_URL=http://localhost:5003
# VITE_SOCKET_URL=https://stream.graff.tech
+10 -2
View File
@@ -9,6 +9,7 @@ import IUser from "../types/IUser";
import CloseIcon from "./icons/CloseIcon";
import { useState } from "react";
import { useClickAway } from "@uidotdev/usehooks";
import SpinnerIcon from "./icons/SpinnerIcon";
interface Props {
me: IUser;
@@ -25,7 +26,12 @@ function User({ me, user, handleTransferControl, handleKick }: Props) {
});
return (
<div key={user.id} className="flex items-center gap-2">
<div key={user.id} className="flex items-center justify-between gap-2">
<div
className={`flex items-center gap-2 ${
!user.isVideoInitialized ? "opacity-50" : ""
}`}
>
<div className="relative w-6 h-6 bg-[#E6ECF2] rounded-full flex items-center justify-center">
<p className="text-xs font-semibold">{user.name[0]?.toUpperCase()}</p>
{user?.isControlAllowed && (
@@ -36,6 +42,8 @@ function User({ me, user, handleTransferControl, handleKick }: Props) {
<div className="text-[#CCCCCC]">
{isMobile ? <MobileIcon /> : <DesktopIcon />}
</div>
{!user.isVideoInitialized && <SpinnerIcon />}
</div>
{me.isAdmin && (
<div ref={ref} className="relative">
@@ -48,7 +56,7 @@ function User({ me, user, handleTransferControl, handleKick }: Props) {
<Tooltip text={"Действия"} />
{showMore && (
<div className="absolute py-2 mt-4 space-y-1 bg-white rounded-lg shadow w-60">
<div className="absolute py-2 mt-4 space-y-1 -translate-x-[calc(100%-32px)] bg-white rounded-lg shadow w-60 z-10">
{/* <button className="flex items-center gap-2 px-4 py-1 text-sm hover:bg-[#E6ECF2] transition-colors">
<span className="text-[#77828C]">
<MicroOnIcon />
+71
View File
@@ -0,0 +1,71 @@
import { Trans } from "react-i18next";
// import useSocketStore from "../stores/useSocketStore";
import CloseIcon from "./icons/CloseIcon";
import Button from "./ui/Button";
import useStreamUserStore from "../stores/useStreamUserStore";
import User from "./User";
interface Props {
onClose: () => void;
transferControl: (userId: string) => void;
kick: (userId: string) => void;
}
function Users({ onClose, transferControl, kick }: Props) {
const { me, users } = useStreamUserStore();
return (
<div
className={`fixed top-0 right-0 h-dvh flex flex-col w-[296px] bg-white border-t border-[#DAE0E5]`}
>
<div className="p-4 pb-2">
<div className="flex items-center justify-between border-b border-[#DAE0E5] pb-4">
<p className="text-sm font-semibold">
<Trans i18nKey={"chat"}>Участники</Trans>
</p>
<Button
variant="tertiary"
icon={<CloseIcon />}
onlyIcon
onClick={onClose}
/>
</div>
</div>
<div className="flex-1 overflow-y-scroll overflow-x-hidden flex flex-col gap-1 px-4 pb-2 pt-0 border-b border-[#DAE0E5]">
{me &&
users.map((user) => {
if (user.id !== me.id) {
return (
<User
me={me}
user={user}
handleTransferControl={() => transferControl(user.id)}
handleKick={() => kick(user.id)}
/>
);
}
})}
</div>
{/* <div className="p-3 pl-4">
<form onSubmit={sendMessage} className="flex items-center gap-3">
<input
autoFocus={isMobile ? false : true}
ref={messageTextRef}
type="text"
placeholder={t("writeAMessage")}
className="w-full text-sm bg-transparent outline-none"
value={messageText}
onChange={(e) =>
setMessageText(e.target.value.replace(/\s+/g, " "))
}
/>
<button type="submit" className="text-[#49A1F5] outline-none">
<SendChatIcon />
</button>
</form>
</div> */}
</div>
);
}
export default Users;
+21 -6
View File
@@ -5,6 +5,7 @@ import useIsAudioActive from "use-is-audio-active";
import IUser from "../types/IUser";
import SoundOffIcon from "./icons/SoundOffIcon";
import SoundOnIcon from "./icons/SoundOnIcon";
import ChevronDownIcon from "./icons/ChevronDownIcon";
interface Props {
mediaStream: MediaStream | null;
@@ -17,6 +18,7 @@ function Video({ mediaStream, muted, user }: Props) {
const isSpeaking = useIsAudioActive({ source: mediaStream });
const [_muted, setMuted] = useState(muted);
const [isLoading, setIsLoading] = useState(true);
const [minimized, setMinimized] = useState(user?.isAdmin ? false : true);
function toggleSound() {
if (!remoteVideoRef.current) return;
@@ -44,21 +46,34 @@ function Video({ mediaStream, muted, user }: Props) {
}, [remoteVideoRef.current]);
return (
<div className="relative">
<div
className={`relative border-2 rounded-lg ${
minimized ? "h-8 rounded-lg overflow-hidden" : ""
} ${!_muted && user?.micEnabled && isSpeaking ? "border-green-500" : "border-transparent"}`}
>
<video
ref={remoteVideoRef}
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"
}`}
className={`aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg object-cover bg-gray-500`}
playsInline
autoPlay
muted={_muted}
></video>
<div className="absolute bottom-0 flex items-center justify-between w-full p-2">
<p className="text-sm text-white">{user?.name}</p>
<div
className={`absolute -bottom-1.5 flex items-center justify-between w-full gap-2 p-2 ${
minimized ? "bg-black" : ""
}`}
>
<div className="flex items-center gap-2">
<button className="text-white" onClick={toggleSound}>
{_muted ? <SoundOffIcon /> : <SoundOnIcon />}
</button>
<p className="text-xs text-white truncate lg:text-sm">{user?.name}</p>
</div>
<button onClick={() => setMinimized((prev) => !prev)}>
<ChevronDownIcon
className={`w-5 h-5 ${!minimized ? "rotate-180" : ""}`}
/>
</button>
</div>
{isLoading && (
<div className="absolute top-0 left-0 flex items-center justify-center w-full h-full">
+66
View File
@@ -0,0 +1,66 @@
function SpinnerIcon() {
return (
<svg
width={24}
height={24}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="w-4 h-4"
>
<g>
<rect x={11} y={1} width={2} height={5} opacity={0.14} />
<rect
x={11}
y={1}
width={2}
height={5}
transform="rotate(30 12 12)"
opacity={0.29}
/>
<rect
x={11}
y={1}
width={2}
height={5}
transform="rotate(60 12 12)"
opacity={0.43}
/>
<rect
x={11}
y={1}
width={2}
height={5}
transform="rotate(90 12 12)"
opacity={0.57}
/>
<rect
x={11}
y={1}
width={2}
height={5}
transform="rotate(120 12 12)"
opacity={0.71}
/>
<rect
x={11}
y={1}
width={2}
height={5}
transform="rotate(150 12 12)"
opacity={0.86}
/>
<rect x={11} y={1} width={2} height={5} transform="rotate(180 12 12)" />
<animateTransform
attributeName="transform"
type="rotate"
calcMode="discrete"
dur="0.75s"
values="0 12 12;30 12 12;60 12 12;90 12 12;120 12 12;150 12 12;180 12 12;210 12 12;240 12 12;270 12 12;300 12 12;330 12 12;360 12 12"
repeatCount="indefinite"
/>
</g>
</svg>
);
}
export default SpinnerIcon;
+31
View File
@@ -0,0 +1,31 @@
interface Props {
className?: string;
}
function UsersIcon({ className = "" }: Props) {
return (
<svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M11.5504 8.63457C11.5504 10.6419 13.0948 12.7428 14.9999 12.7428C16.9051 12.7428 18.4495 10.6419 18.4495 8.63457C18.4495 6.62725 16.9051 5 14.9999 5C13.0948 5 11.5504 6.62725 11.5504 8.63457ZM11.5504 8.63457C11.5504 7.28973 12.2436 6.11548 13.2741 5.48689M11.5504 8.63457C11.5504 9.11955 11.6405 9.61 11.8041 10.075M15 14.9671C16.7818 14.9671 18.2309 13.0848 19.4873 14.416C19.9916 14.9503 20.1244 15.3012 20.4474 16.0591C20.6934 16.6363 20.9351 17.364 20.9944 18.0927C21.0379 18.6262 20.8272 19.1412 20.3999 19.4209C19.4866 20.0186 18.4171 19.9999 15 19.9999C11.5829 19.9999 10.5134 20.0186 9.6001 19.4209C9.17276 19.1412 8.96214 18.6262 9.0056 18.0927C9.06495 17.364 9.30658 16.6363 9.55259 16.0591M15 14.9671C13.2182 14.9671 11.7691 13.0848 10.5127 14.416M15 14.9671C14.2982 14.9671 13.6481 14.6751 13.041 14.4025C12.1066 13.9829 11.2743 13.6091 10.5127 14.416M15 14.9671C15.2552 14.9671 15.5035 14.9285 15.7455 14.8662M10.5127 14.416C10.0084 14.9503 9.87561 15.3012 9.55259 16.0591M10.5127 14.416C10.0364 14.9206 9.8915 15.2617 9.60474 15.9365L9.55259 16.0591M9.55259 16.0591C9.31704 16.6118 9.08551 17.3024 9.01414 17.9997M14.7595 12.7316C14.1418 12.6741 13.5688 12.3981 13.083 11.9823"
stroke="currentColor"
strokeWidth={1.5}
strokeLinejoin="round"
/>
<path
d="M3.91278 7.66535C3.91278 9.13738 5.07109 10.6781 6.49994 10.6781C7.9288 10.6781 9.08712 9.13738 9.08712 7.66535C9.08712 6.19332 7.9288 5 6.49994 5C5.07109 5 3.91278 6.19332 3.91278 7.66535ZM3.91278 7.66535C3.91278 6.67914 4.43269 5.81802 5.20556 5.35705M3.91278 7.66535C3.91278 8.021 3.98039 8.38066 4.1031 8.72164M10.5856 14.11C10.3433 13.5542 10.2437 13.2969 9.86547 12.9051C8.92317 11.9288 7.83636 13.3092 6.5 13.3092M6.5 13.3092C5.16364 13.3092 4.07683 11.9288 3.13453 12.9051M6.5 13.3092C5.97368 13.3092 5.48607 13.0951 5.03075 12.8952C4.32998 12.5874 3.70571 12.3133 3.13453 12.9051M6.5 13.3092C6.69139 13.3092 6.87765 13.2809 7.05911 13.2353M3.13453 12.9051C2.75631 13.2969 2.65671 13.5542 2.41445 14.11M3.13453 12.9051C2.77732 13.2751 2.66862 13.5252 2.45355 14.0201L2.41445 14.11M2.41445 14.11C2.22993 14.5333 2.04871 15.067 2.0042 15.6013C1.9716 15.9925 2.12957 16.3702 2.45008 16.5753C3.13502 17.0137 3.9372 17 6.5 17C7.68428 17 8.4926 17.0029 9.08712 16.9628M2.41445 14.11C2.23778 14.5153 2.06413 15.0218 2.01061 15.5331M6.31964 10.6699C5.85634 10.6277 5.42659 10.4253 5.06226 10.1204"
stroke="currentColor"
strokeWidth={1.5}
strokeLinejoin="round"
/>
</svg>
);
}
export default UsersIcon;
+65 -15
View File
@@ -43,7 +43,8 @@ import DesktopIcon from "../components/icons/DesktopIcon";
import MobileIcon from "../components/icons/MobileIcon";
import ChatIcon from "../components/icons/ChatIcon";
import useChatStore from "../stores/useChatStore";
import User from "../components/User";
import UsersIcon from "../components/icons/UsersIcon";
import Users from "../components/Users";
// import ChatIcon from "../components/icons/ChatIcon";
// import MoreIcon from "../components/icons/MoreIcon";
@@ -83,6 +84,7 @@ function StreamPage() {
const [isVideoInitialized, setIsVideoInitialized] = useState<boolean>(false);
const [step, setStep] = useState<number>(1);
const [isShowChat, setIsShowChat] = useState<boolean>(false);
const [isShowUsers, setIsShowUsers] = useState<boolean>(false);
const { isPortrait } = useMobileOrientation();
const { setMessages } = useChatStore();
@@ -184,6 +186,7 @@ function StreamPage() {
name,
peerId,
superAdmin,
isVideoInitialized,
},
},
});
@@ -245,6 +248,7 @@ function StreamPage() {
if (!permission) return;
setIsMicEnabled(track.enabled);
socket?.emit("mic-enabled", track.enabled);
});
}
@@ -308,6 +312,10 @@ function StreamPage() {
socket?.emit("kick", userId);
}
function videoInitialized() {
socket?.emit("video-initialized", userId);
}
async function getActiveSession() {
const activeSession: any = await api
.get(`activeSessions/${params.id}`)
@@ -350,6 +358,7 @@ function StreamPage() {
if (!isVideoInitialized) {
setModal(<LoadingModal />);
} else {
videoInitialized();
setModal(null);
}
}, [step, isVideoInitialized]);
@@ -399,7 +408,7 @@ function StreamPage() {
className="hidden pointer-events-none lg:block"
/>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-4 max-lg:flex-col">
<div className="items-center hidden gap-2 lg:flex">
<div className="relative w-6 h-6 bg-[#E6ECF2] rounded-full flex items-center justify-center">
<p className="text-xs font-semibold">
@@ -485,8 +494,33 @@ function StreamPage() {
)}
</div>
<div className="h-4 w-px bg-[#DAE0E5] lg:block hidden"></div>
<div className="hidden gap-6 lg:flex">
{me &&
<div className="flex gap-6">
<div className="relative group">
<Button
variant="secondary"
icon={<UsersIcon />}
onlyIcon
onClick={() => (
setIsShowUsers((prev) => !prev), setIsShowChat(false)
)}
/>
<Tooltip
text={
isShowChat
? "Скрыть участников"
: "Показать участников"
}
/>
{users.filter((user) => user.id !== userId).length >
0 && (
<div className="absolute flex items-center justify-center w-4 h-4 bg-[#49A1F5] rounded-full -top-1 -right-1">
<p className="text-[10px] text-white">
{users.filter((user) => user.id !== userId).length}
</p>
</div>
)}
</div>
{/* {me &&
users.map((user) => {
if (user.id !== userId) {
return (
@@ -500,7 +534,7 @@ function StreamPage() {
/>
);
}
})}
})} */}
</div>
</div>
<div className="flex items-center gap-2 lg:gap-4 max-lg:flex-col lg:ml-auto">
@@ -509,7 +543,9 @@ function StreamPage() {
variant="secondary"
icon={<ChatIcon />}
onlyIcon
onClick={() => setIsShowChat((prev) => !prev)}
onClick={() => (
setIsShowChat((prev) => !prev), setIsShowUsers(false)
)}
/>
<Tooltip
text={isShowChat ? "Скрыть чат" : "Показать чат"}
@@ -576,26 +612,33 @@ function StreamPage() {
)}
<Draggable
disabled={isMobile}
defaultClassName={`cursor-grab transition-opacity ${
me ? "opacity-100" : "opacity-0"
}`}
isMobile ? "overflow-y-auto h-dvh" : ""
} ${me ? "opacity-100" : "opacity-0"}`}
defaultClassNameDragging="cursor-grabbing"
>
<div className="absolute space-y-2 top-2 lg:left-2 max-lg:right-2">
<div className={`relative ${!permission ? "hidden" : ""}`}>
<div
className={`relative border-2 rounded-lg ${
!permission || !isCameraEnabled ? "hidden" : ""
} ${
isMicEnabled && isSpeaking
? "border-green-500"
: "border-transparent"
}`}
>
<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"
}`}
className={`aspect-video lg:w-[216px] w-[160px] lg:h-[162px] h-[120px] rounded-lg object-cover bg-gray-500 -scale-x-100`}
playsInline
autoPlay
muted
></video>
<div className="absolute bottom-0 p-2">
<p className="text-sm text-white">{name}</p>
<p className="text-xs text-white truncate lg:text-sm">
{name}
</p>
</div>
</div>
{remoteStreams.map(({ peerId, mediaStream }) => (
@@ -611,6 +654,13 @@ function StreamPage() {
</div>
{isShowChat && <Chat2 onClose={() => setIsShowChat(false)} />}
{isShowUsers && (
<Users
onClose={() => setIsShowUsers(false)}
transferControl={(userId) => transferControl(userId)}
kick={(userId) => kick(userId)}
/>
)}
{isPortrait && (
<div className="absolute top-0 left-0 flex flex-col items-center justify-center w-full h-full gap-2 bg-white">
+2
View File
@@ -6,6 +6,8 @@ interface IUser {
isControlAllowed: boolean;
isMicAllowed: boolean;
peerId: string;
micEnabled: boolean;
isVideoInitialized: boolean;
}
export default IUser;