Enhance UI components and functionality across the application
- Updated ActionsSidebarWrapper to accept a ref for improved positioning. - Enhanced SessionUsersPanel with new props for participant management, including mute and disable video functionalities. - Added vertical positioning option to ActionsPopover for better alignment. - Modified QRCodePopup and SharePopup to include a second argument in setPopup for type differentiation. - Refactored ControlButton and Tooltip components for improved accessibility and styling. - Updated UserCamera to integrate ActionsPopover for participant controls, enhancing user interaction. - Improved PopoverWrapper to handle dynamic positioning based on parent element. - Adjusted UserDevicesControls for better layout consistency and responsiveness. - Enhanced popup management in the popupStore to track popup types.
This commit is contained in:
@@ -5,22 +5,25 @@ interface ActionsSidebarWrapperProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
show?: boolean;
|
||||
ref?: React.RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
function ActionsSidebarWrapper({
|
||||
children,
|
||||
className,
|
||||
ref,
|
||||
show = true,
|
||||
}: ActionsSidebarWrapperProps) {
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{show && (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={clsx(
|
||||
"flex 2xl:flex 2xl:gap-[0.556vw] 2xl:flex-col gap-2 max-2xl:p-2 max-2xl:rounded-[32px] absolute 2xl:top-1/2 2xl:-translate-y-1/2 2xl:right-[1.111vw] max-2xl:left-1/2 max-2xl:-translate-x-1/2 max-2xl:bottom-2 max-2xl:bg-[#00000026] max-2xl:backdrop-blur",
|
||||
"flex 2xl:gap-[0.556vw] 2xl:flex-col gap-2 max-2xl:p-2 max-2xl:rounded-[32px] absolute 2xl:top-1/2 2xl:-translate-y-1/2 2xl:right-[1.111vw] max-2xl:left-[calc(50%-148px)] max-2xl:bottom-2 max-2xl:bg-[#00000026] before:absolute before:backdrop-blur before:inset-0 before:z-0 before:rounded-[32px] z-10",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { Session } from "../types/Session";
|
||||
import { getGuestId } from "../lib/guestId";
|
||||
import { useMe } from "../hooks/useAuth";
|
||||
|
||||
// for test cameras grid
|
||||
const LOCAL_CAMERAS_COUNT = 1;
|
||||
|
||||
interface SessionUsersPanelProps {
|
||||
@@ -31,6 +32,9 @@ function SessionUsersPanel({
|
||||
toggleAudio,
|
||||
toggleVideo,
|
||||
updateSpeakingState,
|
||||
muteParticipant,
|
||||
disableParticipantVideo,
|
||||
currentUserId,
|
||||
} = useWebRTC(roomId, autoJoin);
|
||||
|
||||
const hasLocalStream = localStream !== null;
|
||||
@@ -189,7 +193,14 @@ function SessionUsersPanel({
|
||||
"z-[100] 2xl:gap-[0.556vw] gap-2",
|
||||
mode === "mini"
|
||||
? "flex"
|
||||
: `2xl:p-[2.778vw_5vw_5vw] p-[12px_12px_72px] w-full 2xl:h-dvh grid grid-cols-${gridColumns} relative 2xl:bg-black auto-rows-fr`
|
||||
: `2xl:p-[2.778vw_5vw_5vw] p-[12px_12px_72px] w-full 2xl:h-dvh min-h-fulla grid relative 2xl:bg-black auto-rows-fr`,
|
||||
gridColumns === 1
|
||||
? "grid-cols-1"
|
||||
: gridColumns === 2
|
||||
? "grid-cols-2"
|
||||
: gridColumns === 3
|
||||
? "grid-cols-3"
|
||||
: "grid-cols-4"
|
||||
)}
|
||||
>
|
||||
{mode === "full" && <div className="fixed bg-black inset-0 2xl:hidden" />}
|
||||
@@ -205,10 +216,9 @@ function SessionUsersPanel({
|
||||
isAdmin={isLocalUserOrganizer}
|
||||
isLocal={true}
|
||||
mediaStream={localStream}
|
||||
onMute={toggleAudio}
|
||||
onVideoOff={toggleVideo}
|
||||
onCanControl={() => console.log("Toggle control")}
|
||||
onSpeakingChange={handleSpeakingChange}
|
||||
isLocalUserOrganizer={isLocalUserOrganizer}
|
||||
participantId={currentUserId}
|
||||
className={clsx(
|
||||
mode === "full" &&
|
||||
(activeCamerasCount <= 2 ? "m-auto" : "min-w-full min-h-full")
|
||||
@@ -239,11 +249,10 @@ function SessionUsersPanel({
|
||||
isAdmin={isParticipantOrganizer(participant.id) || undefined}
|
||||
mediaStream={participant.stream}
|
||||
hasLocalMediaPermission={hasLocalStream}
|
||||
onMute={() => console.log(`Mute user ${participant.id}`)}
|
||||
onVideoOff={() => console.log(`Video off user ${participant.id}`)}
|
||||
onCanControl={() =>
|
||||
console.log(`Can control user ${participant.id}`)
|
||||
}
|
||||
isLocalUserOrganizer={isLocalUserOrganizer}
|
||||
participantId={participant.id}
|
||||
onMuteParticipant={muteParticipant}
|
||||
onDisableParticipantVideo={disableParticipantVideo}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -230,6 +230,7 @@ function ParticipantItem({
|
||||
{/* Действия только для удаленных участников и только для организатора */}
|
||||
{!isLocal && isOrganizer && (
|
||||
<ActionsPopover
|
||||
position="vertical"
|
||||
options={[
|
||||
{
|
||||
icon: <MicrophoneOffFilledIcon />,
|
||||
|
||||
@@ -30,7 +30,7 @@ function QRCodePopup({ link }: QRCodePopupProps) {
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => setPopup(<SharePopup link={link} />)}
|
||||
onClick={() => setPopup(<SharePopup link={link} />, "share")}
|
||||
>
|
||||
<div className="2xl:size-[1.111vw] size-4">
|
||||
<ChevronLeftIcon />
|
||||
|
||||
@@ -39,7 +39,7 @@ function SharePopup({ link }: { link: string }) {
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => setPopup(<QRCodePopup link={link} />)}
|
||||
onClick={() => setPopup(<QRCodePopup link={link} />, "share")}
|
||||
>
|
||||
<div className="2xl:size-[1.111vw] size-4">
|
||||
<QRIcon />
|
||||
|
||||
@@ -12,16 +12,18 @@ interface ActionsPopoverProps {
|
||||
icon?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}[];
|
||||
isOpened?: boolean;
|
||||
className?: string;
|
||||
position?: "side" | "center" | "vertical"; // side - сбоку от элемента, center - по центру экрана, vertical - сверху/снизу
|
||||
customParentRef?: React.RefObject<HTMLElement | null>;
|
||||
}
|
||||
|
||||
export default function ActionsPopover({
|
||||
options,
|
||||
isOpened = false,
|
||||
customParentRef,
|
||||
className,
|
||||
position,
|
||||
}: ActionsPopoverProps) {
|
||||
const [opened, setOpened] = useState(isOpened);
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -36,7 +38,7 @@ export default function ActionsPopover({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="max-sm:max-h-6">
|
||||
<div ref={ref} className="max-2xl:max-h-6">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
className={clsx(
|
||||
@@ -52,8 +54,8 @@ export default function ActionsPopover({
|
||||
|
||||
<PopoverWrapper
|
||||
isOpened={opened}
|
||||
parentElRef={buttonRef}
|
||||
position="vertical"
|
||||
parentRef={customParentRef || buttonRef}
|
||||
position={position}
|
||||
className={clsx("2xl:w-[17.222vw] max-2xl:w-[192px]", className)}
|
||||
>
|
||||
{options.map((option) => (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import clsx from "clsx";
|
||||
import Warning from "../indicators/Warning";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
||||
interface ControlButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
@@ -21,7 +22,7 @@ function ControlButton({
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
className={clsx(
|
||||
"backdrop-blur-[10px] rounded-full transition-colors cursor-pointer outline-none disabled:!cursor-default",
|
||||
"backdrop-blur-[10px]a relative rounded-full transition-colors cursor-pointer outline-none disabled:!cursor-default",
|
||||
size === "large"
|
||||
? "2xl:p-[0.833vw] p-3 bg-[#FFFFFF]/15 hover:bg-[#FFFFFF]/25"
|
||||
: "2xl:p-[0.417vw] p-[6px] bg-[#141414]/15 hover:bg-[#141414]/25",
|
||||
@@ -39,9 +40,12 @@ function ControlButton({
|
||||
{icon}
|
||||
</div>
|
||||
{disabled && size === "large" && (
|
||||
<div className="absolute 2xl:-top-[0.139vw] 2xl:-right-[0.139vw] -top-0.5 -right-0.5">
|
||||
<Tooltip
|
||||
label="Нет доступа"
|
||||
className="absolute 2xl:-top-[0.139vw] 2xl:-right-[0.139vw] -top-0.5 -right-0.5"
|
||||
>
|
||||
<Warning type="critical" className="text-white" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import FloatingActionButton from "./FloatingActionButton";
|
||||
import PopoverWrapper from "./PopoverWrapper";
|
||||
import MoreIcon from "../icons/MoreIcon";
|
||||
import { useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import Button from "./Button";
|
||||
import ChatFilledIcon from "../icons/ChatFilledIcon";
|
||||
import UsersFilledIcon from "../icons/UsersFilledIcon";
|
||||
@@ -19,13 +19,12 @@ import type { Session } from "../../types/Session";
|
||||
|
||||
interface ControlsPopoverProps {
|
||||
session?: Session;
|
||||
parentRef: React.RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
function ControlsPopover({ session }: ControlsPopoverProps) {
|
||||
function ControlsPopover({ session, parentRef }: ControlsPopoverProps) {
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const ref = useClickAway<HTMLDivElement>(() => setIsOpened(false));
|
||||
|
||||
const { setPopup } = usePopupStore();
|
||||
@@ -33,20 +32,21 @@ function ControlsPopover({ session }: ControlsPopoverProps) {
|
||||
|
||||
function handleClickOpenChatPopup() {
|
||||
setIsOpened(false);
|
||||
setPopup(<ChatPopup sessionId={session?.id} />);
|
||||
setPopup(<ChatPopup sessionId={session?.id} />, "chat");
|
||||
}
|
||||
|
||||
function handleClickOpenParticipantsPopup() {
|
||||
setIsOpened(false);
|
||||
if (session) {
|
||||
setPopup(<ParticipantsPopup session={session} />);
|
||||
setPopup(<ParticipantsPopup session={session} />, "participants");
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickOpenSharePopup() {
|
||||
setIsOpened(false);
|
||||
setPopup(
|
||||
<SharePopup link={`${window.location.origin}/sessions/${session?.id}`} />
|
||||
<SharePopup link={`${window.location.origin}/sessions/${session?.id}`} />,
|
||||
"share"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ function ControlsPopover({ session }: ControlsPopoverProps) {
|
||||
return (
|
||||
<div className="order-3 2xl:hidden z-[9999]" ref={ref}>
|
||||
<FloatingActionButton
|
||||
ref={buttonRef}
|
||||
className={clsx(isOpened && "!bg-[#7B60F3]")}
|
||||
onClick={() => setIsOpened(!isOpened)}
|
||||
>
|
||||
@@ -68,7 +67,7 @@ function ControlsPopover({ session }: ControlsPopoverProps) {
|
||||
</FloatingActionButton>
|
||||
<PopoverWrapper
|
||||
isOpened={isOpened}
|
||||
parentElRef={buttonRef}
|
||||
parentRef={parentRef}
|
||||
position="center"
|
||||
className="w-[248px]"
|
||||
>
|
||||
|
||||
@@ -5,12 +5,14 @@ interface FloatingActionButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: React.ReactNode;
|
||||
variant?: "default" | "critical";
|
||||
isActive?: boolean;
|
||||
ref?: React.RefObject<HTMLButtonElement | null>;
|
||||
}
|
||||
|
||||
function FloatingActionButton({
|
||||
children,
|
||||
variant = "default",
|
||||
isActive = false,
|
||||
className,
|
||||
onClick,
|
||||
disabled,
|
||||
@@ -25,8 +27,9 @@ function FloatingActionButton({
|
||||
className={clsx(
|
||||
"2xl:p-[0.833vw] p-3 rounded-full transition-all cursor-pointer disabled:!cursor-default outline-none backdrop-blur-[10px]",
|
||||
variant === "default" &&
|
||||
"2xl:border-[0.069vw] border border-[#FFFFFF]/15 bg-[#000000]/15 hover:bg-[#000000]/25 hover:border-[#FFFFFF]/25 enabled:active:bg-[#7B60F3] enabled:active:border-[#FFFFFF]/25",
|
||||
"2xl:ring-[0.069vw] ring-1 ring-[#FFFFFF]/15 bg-[#000000]/15 hover:bg-[#000000]/25 hover:ring-[#FFFFFF]/25 enabled:active:bg-[#7B60F3] enabled:active:ring-[#FFFFFF]/25",
|
||||
variant === "critical" && "bg-[#FF4517] hover:bg-[#FF4517]/85",
|
||||
isActive && variant === "default" && "!bg-[#7B60F3] !ring-[#FFFFFF]/25",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -4,7 +4,7 @@ import clsx from "clsx";
|
||||
|
||||
interface PopoverProps {
|
||||
isOpened: boolean;
|
||||
parentElRef: React.RefObject<HTMLButtonElement | HTMLDivElement | null>;
|
||||
parentRef: React.RefObject<HTMLElement | null>;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
position?: "side" | "center" | "vertical"; // side - сбоку от элемента, center - по центру экрана, vertical - сверху/снизу
|
||||
@@ -12,28 +12,27 @@ interface PopoverProps {
|
||||
|
||||
function PopoverWrapper({
|
||||
isOpened,
|
||||
parentElRef,
|
||||
parentRef,
|
||||
children,
|
||||
className,
|
||||
position = "side",
|
||||
}: PopoverProps) {
|
||||
const [openUpwards, setOpenUpwards] = useState(false);
|
||||
const [menuHeight, setMenuHeight] = useState(0);
|
||||
const [, forceUpdate] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isOpened &&
|
||||
parentElRef.current &&
|
||||
(position === "side" || position === "vertical")
|
||||
parentRef.current &&
|
||||
(position === "side" || position === "vertical" || position === "center")
|
||||
) {
|
||||
const buttonRect = parentElRef.current.getBoundingClientRect();
|
||||
console.log(buttonRect);
|
||||
const spaceBelow = window.innerHeight - buttonRect.bottom;
|
||||
const spaceAbove = buttonRect.top;
|
||||
const parentRect = parentRef.current.getBoundingClientRect();
|
||||
console.log(parentRect);
|
||||
const spaceBelow = window.innerHeight - parentRect.bottom;
|
||||
const spaceAbove = parentRect.top;
|
||||
setOpenUpwards(spaceBelow < menuHeight && spaceAbove > menuHeight);
|
||||
}
|
||||
}, [parentElRef, isOpened, menuHeight, position]);
|
||||
}, [parentRef, isOpened, menuHeight, position]);
|
||||
|
||||
// Обновляем позицию при изменении положения родительского элемента
|
||||
useEffect(() => {
|
||||
@@ -42,7 +41,6 @@ function PopoverWrapper({
|
||||
let animationFrameId: number;
|
||||
|
||||
const updatePosition = () => {
|
||||
forceUpdate((prev) => prev + 1);
|
||||
animationFrameId = requestAnimationFrame(updatePosition);
|
||||
};
|
||||
|
||||
@@ -56,15 +54,17 @@ function PopoverWrapper({
|
||||
}, [isOpened]);
|
||||
|
||||
const getPositionStyles = () => {
|
||||
if (!parentElRef.current) return {};
|
||||
if (!parentRef.current) return {};
|
||||
|
||||
const buttonRect = parentElRef.current.getBoundingClientRect();
|
||||
const parentRect = parentRef.current.getBoundingClientRect();
|
||||
|
||||
if (position === "center") {
|
||||
// Позиционирование по центру экрана
|
||||
// Позиционирование по центру относительно parentRef, сверху или снизу
|
||||
return {
|
||||
left: "50%",
|
||||
bottom: buttonRect.height + 16, // 16px отступ от кнопки
|
||||
left: parentRect.left + parentRect.width / 2,
|
||||
top: openUpwards
|
||||
? parentRect.top - menuHeight - 8
|
||||
: parentRect.bottom + 8,
|
||||
transform: "translateX(-50%)",
|
||||
};
|
||||
}
|
||||
@@ -72,19 +72,19 @@ function PopoverWrapper({
|
||||
if (position === "vertical") {
|
||||
// Позиционирование сверху или снизу от элемента, выравнивание по правому краю
|
||||
return {
|
||||
left: buttonRect.right,
|
||||
left: parentRect.right,
|
||||
top: openUpwards
|
||||
? buttonRect.top - menuHeight - 4
|
||||
: buttonRect.bottom + 4,
|
||||
? parentRect.top - menuHeight - 4
|
||||
: parentRect.bottom + 4,
|
||||
};
|
||||
}
|
||||
|
||||
// Позиционирование сбоку (существующая логика)
|
||||
return {
|
||||
left: buttonRect.left + buttonRect.width,
|
||||
left: parentRect.left + parentRect.width,
|
||||
top: openUpwards
|
||||
? buttonRect.top - menuHeight - 4
|
||||
: buttonRect.bottom + 4,
|
||||
? parentRect.top - menuHeight - 4
|
||||
: parentRect.bottom + 4,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import clsx from "clsx";
|
||||
import { motion } from "motion/react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
@@ -6,6 +7,7 @@ interface TooltipProps {
|
||||
children: React.ReactNode;
|
||||
position?: "top" | "bottom" | "left" | "right" | "cursor";
|
||||
showDelay?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface TooltipPosition {
|
||||
@@ -21,6 +23,7 @@ export default function Tooltip({
|
||||
children,
|
||||
position = "top",
|
||||
showDelay = 500,
|
||||
className,
|
||||
}: TooltipProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [tooltipPosition, setTooltipPosition] = useState<TooltipPosition>({});
|
||||
@@ -64,7 +67,7 @@ export default function Tooltip({
|
||||
}, [isVisible, position]);
|
||||
|
||||
return (
|
||||
<div ref={tooltipWrapperRef} className="cursor-pointer">
|
||||
<div ref={tooltipWrapperRef} className={clsx("cursor-pointer", className)}>
|
||||
{children}
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
|
||||
@@ -9,6 +9,8 @@ import VolumeIcon from "../icons/VolumeIcon";
|
||||
import VolumeOffIcon from "../icons/VolumeOffIcon";
|
||||
import { useVoiceActivity } from "../../hooks/useVoiceActivity";
|
||||
import MicrophoneOffFilledIcon from "../icons/MicrophoneOffFilledIcon";
|
||||
import ActionsPopover from "./ActionsPopover";
|
||||
import XMarkFilledIcon from "../icons/XMarkFilledIcon";
|
||||
|
||||
interface UserCameraControlsProps {
|
||||
isMuted: boolean;
|
||||
@@ -20,9 +22,6 @@ interface UserCameraProps {
|
||||
isMuted: boolean;
|
||||
isVideoOff: boolean;
|
||||
hasControl?: boolean;
|
||||
onMute: () => void;
|
||||
onVideoOff: () => void;
|
||||
onCanControl: () => void;
|
||||
isAdmin?: boolean;
|
||||
name?: string;
|
||||
mediaStream?: MediaStream | null;
|
||||
@@ -32,15 +31,16 @@ interface UserCameraProps {
|
||||
hasLocalMediaPermission?: boolean; // Есть ли у локального пользователя разрешение на медиа
|
||||
mode: "full" | "mini";
|
||||
className?: string;
|
||||
isLocalUserOrganizer?: boolean; // Является ли текущий пользователь организатором сессии
|
||||
participantId?: string; // ID участника для управления
|
||||
onMuteParticipant?: (participantId: string) => void;
|
||||
onDisableParticipantVideo?: (participantId: string) => void;
|
||||
}
|
||||
|
||||
export default function UserCamera({
|
||||
isMuted,
|
||||
isVideoOff,
|
||||
hasControl = false,
|
||||
// onMute,
|
||||
// onVideoOff,
|
||||
// onCanControl,
|
||||
isAdmin = false,
|
||||
name = "Гость",
|
||||
mediaStream = null,
|
||||
@@ -50,8 +50,14 @@ export default function UserCamera({
|
||||
hasLocalMediaPermission = false,
|
||||
mode = "full",
|
||||
className,
|
||||
isLocalUserOrganizer = false,
|
||||
participantId,
|
||||
onMuteParticipant,
|
||||
onDisableParticipantVideo,
|
||||
}: UserCameraProps) {
|
||||
const ref = useRef<HTMLVideoElement>(null);
|
||||
const actionsPopoverParentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Для удаленных участников: если у локального пользователя есть разрешение на медиа - unmute, иначе mute (для autoplay)
|
||||
const [isAudioMuted, setIsAudioMuted] = useState(!hasLocalMediaPermission);
|
||||
|
||||
@@ -105,17 +111,6 @@ export default function UserCamera({
|
||||
// isSpeaking уже учитывает threshold и debounce (1 сек)
|
||||
const ringOpacity = isSpeaking ? 1 : 0;
|
||||
|
||||
// Логируем для отладки (отключено для снижения шума)
|
||||
// useEffect(() => {
|
||||
// console.log(
|
||||
// `[${name}${
|
||||
// isLocal ? " (local)" : ""
|
||||
// }] isSpeaking: ${isSpeaking}, ringOpacity: ${ringOpacity.toFixed(
|
||||
// 2
|
||||
// )}, isMuted: ${isMuted}`
|
||||
// );
|
||||
// }, [isSpeaking, ringOpacity, name, isMuted, isLocal]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && mediaStream) {
|
||||
console.log(
|
||||
@@ -295,18 +290,11 @@ export default function UserCamera({
|
||||
"select-none group relative pointer-events-auto",
|
||||
mode === "mini"
|
||||
? "aspect-square h-fit flex-shrink-0 2xl:hover:w-[10.833vw] 2xl:w-[6.944vw] sm:w-[15.625vw] w-[27.778vw]"
|
||||
: "aspect-video max-w-full h-full",
|
||||
: "aspect-video max-2xl:min-w-full max-2xl:landscape:max-w-full h-full flex flex-col justify-end",
|
||||
isLocal && "order-last",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
// boxShadow: isSpeaking
|
||||
// ? `0 4px 40px 0 rgba(15,16,17,0.1), 0 2px 2px 0 rgba(0,0,0,0.06), 0 0 0 ${
|
||||
// window.innerWidth >= 1536 ? "0.139vw" : "2px"
|
||||
// } rgba(34, 197, 94, ${ringOpacity})`
|
||||
// : `0 4px 40px 0 rgba(15,16,17,0.1), 0 2px 2px 0 rgba(0,0,0,0.06), 0 0 0 ${
|
||||
// window.innerWidth >= 1536 ? "0.069vw" : "1px"
|
||||
// } rgba(255, 255, 255, 0.3)`,
|
||||
transition:
|
||||
mode === "mini"
|
||||
? "box-shadow 0.1s ease-out, width 0.3s, background-color 0.3s"
|
||||
@@ -317,9 +305,8 @@ export default function UserCamera({
|
||||
<video
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
"object-cover size-full 2xl:rounded-[1.667vw] rounded-2xl",
|
||||
"object-cover size-full 2xl:rounded-[1.667vw] rounded-2xl absolute",
|
||||
isLocal && "scale-x-[-1]"
|
||||
// (!mediaStream || isVideoOff) && ""
|
||||
)}
|
||||
autoPlay
|
||||
muted={isLocal ? true : isAudioMuted}
|
||||
@@ -394,7 +381,7 @@ export default function UserCamera({
|
||||
)}
|
||||
|
||||
{mode === "mini" && (
|
||||
<div className="absolute whitespace-nowrap transition-opacity duration-300 group-hover:opacity-100 opacity-0 text-white button-s top-[0.556vw] left-1/2 translate-x-[-50%] px-[0.556vw] py-[0.278vw] rounded-full bg-[#14141440] backdrop-blur-[4px]">
|
||||
<div className="absolute whitespace-nowrap transition-opacity duration-300 group-hover:opacity-100 opacity-0 text-white button-s 2xl:top-[0.556vw] top-2 left-1/2 -translate-x-1/2 2xl:px-[0.556vw] 2xl:py-[0.278vw] px-2 py-1 rounded-full bg-[#14141440] backdrop-blur-[4px]">
|
||||
{name}
|
||||
</div>
|
||||
)}
|
||||
@@ -418,10 +405,68 @@ export default function UserCamera({
|
||||
)}
|
||||
|
||||
{mode === "full" && (
|
||||
<div className="2xl:px-[1.111vw] 2xl:py-[0.556vw] px-4 py-2 bg-[#141414]/25 backdrop-blur-[10px] 2xl:rounded-[1.111vw] rounded-2xl absolute 2xl:bottom-[1.111vw] bottom-4 left-1/2 -translate-x-1/2 z-10 flex 2xl:gap-[0.556vw] gap-2 items-center">
|
||||
<div
|
||||
ref={actionsPopoverParentRef}
|
||||
className="after:z-[-1] after:absolute after:inset-0 after:backdrop-blur-[10px] after:2xl:rounded-[1.111vw] after:rounded-2xl 2xl:px-[1.111vw] 2xl:py-[0.556vw] px-4 py-2 bg-[#141414]/25 2xl:rounded-[1.111vw] rounded-2xl absoluten relative mx-auto 2xl:bottom-[1.111vw] bottom-4 z-[100]a flex 2xl:gap-[0.556vw] gap-2 items-center"
|
||||
>
|
||||
<p className="font-medium text-white button-m">{name}</p>
|
||||
{isMuted && (
|
||||
<div className="2xl:size-[1.111vw] size-4 text-[#FFFFFF]/50">
|
||||
<MicrophoneOffFilledIcon />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ActionsPopover для удаленных участников, доступен только организатору */}
|
||||
{!isLocal && isLocalUserOrganizer && participantId && (
|
||||
// <div onClick={(e) => e.stopPropagation()}>
|
||||
<ActionsPopover
|
||||
position="center"
|
||||
customParentRef={actionsPopoverParentRef}
|
||||
options={[
|
||||
{
|
||||
icon: <MicrophoneOffFilledIcon />,
|
||||
label: "Выключить микрофон",
|
||||
onClick: () => {
|
||||
if (onMuteParticipant && participantId) {
|
||||
console.log("Mute participant:", participantId);
|
||||
onMuteParticipant(participantId);
|
||||
}
|
||||
},
|
||||
disabled: isMuted || !mediaStream,
|
||||
},
|
||||
{
|
||||
icon: <VideoOffFilledIcon />,
|
||||
label: "Выключить камеру",
|
||||
onClick: () => {
|
||||
if (onDisableParticipantVideo && participantId) {
|
||||
console.log("Turn off video:", participantId);
|
||||
onDisableParticipantVideo(participantId);
|
||||
}
|
||||
},
|
||||
disabled: isVideoOff || !mediaStream,
|
||||
},
|
||||
{
|
||||
icon: <HandRaisedFilledIcon />,
|
||||
label: "Передать управление",
|
||||
onClick: () => {
|
||||
console.log("Grant control:", participantId);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <XMarkFilledIcon />,
|
||||
label: "Удалить со встречи",
|
||||
onClick: () => {
|
||||
console.log("Remove participant:", participantId);
|
||||
},
|
||||
},
|
||||
]}
|
||||
className="max-2xl:w-[192px]"
|
||||
/>
|
||||
// </div>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<Admin className="2xl:size-[1.111vw] size-4 absolute 2xl:-top-[0.278vw] 2xl:-right-[0.278vw] -right-1 -top-1" />
|
||||
<Admin className="2xl:size-[1.111vw] size-4 absolute 2xl:-top-[0.278vw] 2xl:-right-[0.278vw] -right-1 -top-1 ring-[#323232]" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -35,9 +35,10 @@ export default function UserDevicesControls({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"before:absolute before:inset-0 before:rounded-[1.667vw] before:backdrop-blur-[10px] bg-[#00000040] gap-[0.278vw] rounded-[1.667vw] pointer-events-auto p-[0.556vw]",
|
||||
mode === "mini"
|
||||
? "hidden order-last 2xl:grid grid-cols-2 gap-[0.278vw] aspect-square p-[0.556vw] flex-wrap justify-between items-center size-[6.944vw] rounded-[1.667vw] bg-[#00000040] shadow-[0_4px_40px_0_#0F10111A] backdrop-blur-[10px] pointer-events-auto"
|
||||
: "2xl:flex hidden items-center gap-[0.278vw] p-[0.556vw] rounded-[1.667vw] ring-[0.104vw] ring-[#FFFFFF]/15 absolute bottom-[0.556vw] left-1/2 -translate-x-1/2 bg-[#00000040] backdrop-blur-[10px]"
|
||||
? "hidden order-last 2xl:grid grid-cols-2 aspect-square size-[6.944vw] shadow-[0_4px_40px_0_#0F10111A] relative"
|
||||
: "2xl:flex hidden ring-[0.104vw] ring-[#FFFFFF]/15 items-center absolute bottom-[0.556vw] left-[calc(50%-(3*0.278vw+2*0.556vw+4*2*0.833vw+4*1.111vw)/2)]"
|
||||
)}
|
||||
>
|
||||
<ControlButton
|
||||
|
||||
@@ -40,7 +40,7 @@ function HomePage() {
|
||||
</FloatingActionButton> */}
|
||||
<FloatingActionButton
|
||||
variant="default"
|
||||
onClick={() => setPopup(<ChatPopup />)}
|
||||
onClick={() => setPopup(<ChatPopup />, "chat")}
|
||||
>
|
||||
<div className="2xl:size-[1.111vw] size-4 text-white">
|
||||
<ChatFilledIcon />
|
||||
|
||||
@@ -13,7 +13,7 @@ import usePopupStore from "../store/popupStore";
|
||||
import ControlsPopover from "../components/ui/ControlsPopover";
|
||||
import ChatPopup from "../components/popups/ChatPopup";
|
||||
import SharePopup from "../components/popups/SharePopup";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "../lib/api";
|
||||
@@ -32,7 +32,7 @@ import QuitSessionModal from "../components/modals/QuitSessionModal";
|
||||
import useModalStore from "../store/modalStore";
|
||||
|
||||
function SessionPage() {
|
||||
const { setPopup } = usePopupStore();
|
||||
const { setPopup, popupType } = usePopupStore();
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
// Загружаем данные пользователя сразу при входе на страницу сессии
|
||||
@@ -88,17 +88,20 @@ function SessionPage() {
|
||||
const session = sessionData?.session;
|
||||
|
||||
function handleChatOpen() {
|
||||
setPopup(<ChatPopup sessionId={id} />);
|
||||
setPopup(<ChatPopup sessionId={id} />, "chat");
|
||||
}
|
||||
|
||||
function handleParticipantsOpen() {
|
||||
if (session) {
|
||||
setPopup(<ParticipantsPopup session={session} />);
|
||||
setPopup(<ParticipantsPopup session={session} />, "participants");
|
||||
}
|
||||
}
|
||||
|
||||
function handleShareOpen() {
|
||||
setPopup(<SharePopup link={`${window.location.origin}/sessions/${id}`} />);
|
||||
setPopup(
|
||||
<SharePopup link={`${window.location.origin}/sessions/${id}`} />,
|
||||
"share"
|
||||
);
|
||||
}
|
||||
|
||||
const [mode, setMode] = useState<"full" | "mini">("mini");
|
||||
@@ -111,16 +114,6 @@ function SessionPage() {
|
||||
setModal(<QuitSessionModal onQuitSession={() => navigate("/test")} />);
|
||||
}
|
||||
|
||||
// Не перенаправляем автоматически - пользователи могут продолжать общаться
|
||||
// useEffect(() => {
|
||||
// if (session?.status === "ended") {
|
||||
// const timer = setTimeout(() => {
|
||||
// navigate("/test");
|
||||
// }, 5000);
|
||||
// return () => clearTimeout(timer);
|
||||
// }
|
||||
// }, [session?.status, navigate]);
|
||||
|
||||
const {
|
||||
localStream,
|
||||
toggleAudio,
|
||||
@@ -130,6 +123,8 @@ function SessionPage() {
|
||||
participants,
|
||||
} = useWebRTC(session?.id, true);
|
||||
|
||||
const actionsSidebarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50">
|
||||
@@ -216,7 +211,7 @@ function SessionPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ActionsSidebarWrapper className="z-[150]">
|
||||
<ActionsSidebarWrapper ref={actionsSidebarRef} className="z-[150]">
|
||||
<FloatingActionButton onClick={toggleMode}>
|
||||
<div className="2xl:size-[1.111vw] size-4 text-white">
|
||||
{mode === "full" ? <FullscreenExitIcon /> : <FullscreenIcon />}
|
||||
@@ -225,6 +220,7 @@ function SessionPage() {
|
||||
<FloatingActionButton
|
||||
className="max-2xl:hidden"
|
||||
onClick={handleChatOpen}
|
||||
isActive={popupType === "chat"}
|
||||
>
|
||||
<div className="size-[1.111vw] text-white">
|
||||
<ChatFilledIcon />
|
||||
@@ -233,6 +229,7 @@ function SessionPage() {
|
||||
<FloatingActionButton
|
||||
className="relative max-2xl:hidden"
|
||||
onClick={handleParticipantsOpen}
|
||||
isActive={popupType === "participants"}
|
||||
>
|
||||
<div className="size-[1.111vw] text-white">
|
||||
<UsersFilledIcon />
|
||||
@@ -246,6 +243,7 @@ function SessionPage() {
|
||||
<FloatingActionButton
|
||||
className="max-2xl:hidden"
|
||||
onClick={handleShareOpen}
|
||||
isActive={popupType === "share"}
|
||||
>
|
||||
<div className="size-[1.111vw] text-white">
|
||||
<ShareFilledIcon />
|
||||
@@ -294,7 +292,7 @@ function SessionPage() {
|
||||
<ExitFilledIcon />
|
||||
</div>
|
||||
</FloatingActionButton>
|
||||
<ControlsPopover session={session} />
|
||||
<ControlsPopover session={session} parentRef={actionsSidebarRef} />
|
||||
</ActionsSidebarWrapper>
|
||||
|
||||
{/* WebRTC видеочат - работает всегда, пока пользователь на странице */}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { create } from "zustand";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type PopupType = "chat" | "participants" | "share" | null;
|
||||
|
||||
interface PopupState {
|
||||
popup: ReactNode | null;
|
||||
setPopup: (popup: ReactNode | null) => void;
|
||||
popupType: PopupType;
|
||||
setPopup: (popup: ReactNode | null, type?: PopupType) => void;
|
||||
}
|
||||
|
||||
const usePopupStore = create<PopupState>()((set) => ({
|
||||
popup: null,
|
||||
setPopup: (popup) => set({ popup }),
|
||||
popupType: null,
|
||||
setPopup: (popup, type = null) => set({ popup, popupType: type }),
|
||||
}));
|
||||
|
||||
export default usePopupStore;
|
||||
|
||||
Reference in New Issue
Block a user