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:
2025-11-06 17:15:30 +05:00
parent b6eeaafe42
commit 28091d732a
16 changed files with 178 additions and 106 deletions
@@ -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
)}
>
+18 -9
View File
@@ -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 />,
+1 -1
View File
@@ -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 />
+1 -1
View File
@@ -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 />
+8 -6
View File
@@ -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) => (
+7 -3
View File
@@ -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>
);
+8 -9
View File
@@ -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}
+22 -22
View File
@@ -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,
};
};
+4 -1
View File
@@ -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
+75 -30
View File
@@ -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
+1 -1
View File
@@ -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 />
+15 -17
View File
@@ -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 видеочат - работает всегда, пока пользователь на странице */}
+6 -2
View File
@@ -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;