Add UserCameraAvatar image and enhance SessionUsersPanel with session management features
- Introduced UserCameraAvatar.png for user camera display. - Updated SessionUsersPanel to accept a session prop, allowing for identification of local user as session organizer. - Implemented logic to determine if participants are organizers based on session data. - Adjusted admin control logic in UserCamera and UserCameraControls for better role management. - Improved logging and conditional rendering in ParticipantItem and UserCamera components for enhanced debugging and user experience.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 670 KiB |
@@ -4,6 +4,9 @@ import DraggableContainer from "./DraggableContainer";
|
||||
import { useWebRTC } from "../hooks/useWebRTC";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { Session } from "../types/Session";
|
||||
import { getGuestId } from "../lib/guestId";
|
||||
import { useMe } from "../hooks/useAuth";
|
||||
|
||||
const LOCAL_CAMERAS_COUNT = 1;
|
||||
|
||||
@@ -11,12 +14,14 @@ interface SessionUsersPanelProps {
|
||||
roomId: string;
|
||||
autoJoin?: boolean;
|
||||
mode?: "full" | "mini";
|
||||
session?: Session;
|
||||
}
|
||||
|
||||
function SessionUsersPanel({
|
||||
roomId,
|
||||
autoJoin = false,
|
||||
mode = "full",
|
||||
session,
|
||||
}: SessionUsersPanelProps) {
|
||||
const {
|
||||
localStream,
|
||||
@@ -29,6 +34,22 @@ function SessionUsersPanel({
|
||||
} = useWebRTC(roomId, autoJoin);
|
||||
|
||||
const hasLocalStream = localStream !== null;
|
||||
const { data: user } = useMe();
|
||||
|
||||
// Определяем, является ли локальный пользователь организатором сессии
|
||||
const isLocalUserOrganizer = session
|
||||
? !!(session.userId && user?.id === session.userId) ||
|
||||
!!(session.guestId && getGuestId() === session.guestId)
|
||||
: false;
|
||||
|
||||
// Функция для определения, является ли конкретный участник организатором
|
||||
const isParticipantOrganizer = (participantId: string) => {
|
||||
if (!session) return false;
|
||||
return (
|
||||
(session.userId && participantId === session.userId) ||
|
||||
(session.guestId && participantId === session.guestId)
|
||||
);
|
||||
};
|
||||
|
||||
// Логируем изменения hasLocalStream для отладки
|
||||
useEffect(() => {
|
||||
@@ -180,8 +201,8 @@ function SessionUsersPanel({
|
||||
name="Вы"
|
||||
isMuted={isLocalAudioMuted}
|
||||
isVideoOff={isLocalVideoMuted}
|
||||
isControlDisabled={false}
|
||||
isAdmin={true}
|
||||
hasControl={false}
|
||||
isAdmin={isLocalUserOrganizer}
|
||||
isLocal={true}
|
||||
mediaStream={localStream}
|
||||
onMute={toggleAudio}
|
||||
@@ -214,8 +235,8 @@ function SessionUsersPanel({
|
||||
isMuted={participant.isMuted || false}
|
||||
isVideoOff={participant.isVideoOff || false}
|
||||
isSpeaking={participant.isSpeaking}
|
||||
isControlDisabled={true}
|
||||
isAdmin={false}
|
||||
hasControl={false}
|
||||
isAdmin={isParticipantOrganizer(participant.id) || undefined}
|
||||
mediaStream={participant.stream}
|
||||
hasLocalMediaPermission={hasLocalStream}
|
||||
onMute={() => console.log(`Mute user ${participant.id}`)}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PopupWrapper from "../PopupWrapper";
|
||||
import ActionsPopover from "../ui/ActionsPopover";
|
||||
import MicrophoneFilledIcon from "../icons/MicrophoneFilledIcon";
|
||||
import VideoOffFilledIcon from "../icons/VideoOffFilledIcon";
|
||||
import HandRaisedFilledIcon from "../icons/HandRaisedFilledIcon";
|
||||
import XMarkFilledIcon from "../icons/XMarkFilledIcon";
|
||||
@@ -160,19 +159,32 @@ function ParticipantItem({
|
||||
|
||||
// Логируем каждый рендер
|
||||
console.log(
|
||||
`[ParticipantItem RENDER] ${isLocal ? "Local" : "Remote"} ${
|
||||
participant.id.slice(0, 8)
|
||||
} - isMuted=${isMuted}, isVideoOff=${isVideoOff}, hasStream=${!!participant.stream}`
|
||||
`[ParticipantItem RENDER] ${
|
||||
isLocal ? "Local" : "Remote"
|
||||
} ${participant.id.slice(
|
||||
0,
|
||||
8
|
||||
)} - isMuted=${isMuted}, isVideoOff=${isVideoOff}, hasStream=${!!participant.stream}`
|
||||
);
|
||||
|
||||
// Логируем состояние участника для отладки
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
`[ParticipantItem] ${isLocal ? "Local" : "Remote"} ${
|
||||
participant.id.slice(0, 8)
|
||||
} - isMuted: ${isMuted}, isVideoOff: ${isVideoOff}, participant.isMuted: ${participant.isMuted}, participant.isVideoOff: ${participant.isVideoOff}`
|
||||
`[ParticipantItem] ${isLocal ? "Local" : "Remote"} ${participant.id.slice(
|
||||
0,
|
||||
8
|
||||
)} - isMuted: ${isMuted}, isVideoOff: ${isVideoOff}, participant.isMuted: ${
|
||||
participant.isMuted
|
||||
}, participant.isVideoOff: ${participant.isVideoOff}`
|
||||
);
|
||||
}, [isMuted, isVideoOff, isLocal, participant.id, participant.isMuted, participant.isVideoOff]);
|
||||
}, [
|
||||
isMuted,
|
||||
isVideoOff,
|
||||
isLocal,
|
||||
participant.id,
|
||||
participant.isMuted,
|
||||
participant.isVideoOff,
|
||||
]);
|
||||
|
||||
// Определяем, является ли этот конкретный участник организатором сессии
|
||||
const isThisParticipantOrganizer =
|
||||
@@ -220,7 +232,7 @@ function ParticipantItem({
|
||||
<ActionsPopover
|
||||
options={[
|
||||
{
|
||||
icon: <MicrophoneFilledIcon />,
|
||||
icon: <MicrophoneOffFilledIcon />,
|
||||
label: "Выключить микрофон",
|
||||
onClick: () => {
|
||||
console.log("Mute participant:", participant.id);
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import HandRaisedOffFilledIcon from "../icons/HandRaisedOffFilledIcon";
|
||||
import HandRaisedFilledIcon from "../icons/HandRaisedFilledIcon";
|
||||
import MicrophoneFilledIcon from "../icons/MicrophoneFilledIcon";
|
||||
import VideoOffFilledIcon from "../icons/VideoOffFilledIcon";
|
||||
import MicrophoneOffIcon from "../icons/MicrophoneOffIcon";
|
||||
import VideoFilledIcon from "../icons/VideoFilledIcon";
|
||||
import ControlButton from "./ControlButton";
|
||||
import Admin from "../indicators/Admin";
|
||||
import clsx from "clsx";
|
||||
import VolumeIcon from "../icons/VolumeIcon";
|
||||
@@ -16,17 +13,13 @@ import MicrophoneOffFilledIcon from "../icons/MicrophoneOffFilledIcon";
|
||||
interface UserCameraControlsProps {
|
||||
isMuted: boolean;
|
||||
isVideoOff: boolean;
|
||||
isControlDisabled: boolean;
|
||||
isAdmin: boolean;
|
||||
onMute: () => void;
|
||||
onVideoOff: () => void;
|
||||
onCanControl: () => void;
|
||||
hasControl: boolean;
|
||||
}
|
||||
|
||||
interface UserCameraProps {
|
||||
isMuted: boolean;
|
||||
isVideoOff: boolean;
|
||||
isControlDisabled: boolean;
|
||||
hasControl?: boolean;
|
||||
onMute: () => void;
|
||||
onVideoOff: () => void;
|
||||
onCanControl: () => void;
|
||||
@@ -44,10 +37,10 @@ interface UserCameraProps {
|
||||
export default function UserCamera({
|
||||
isMuted,
|
||||
isVideoOff,
|
||||
isControlDisabled,
|
||||
onMute,
|
||||
onVideoOff,
|
||||
onCanControl,
|
||||
hasControl = false,
|
||||
// onMute,
|
||||
// onVideoOff,
|
||||
// onCanControl,
|
||||
isAdmin = false,
|
||||
name = "Гость",
|
||||
mediaStream = null,
|
||||
@@ -299,11 +292,11 @@ export default function UserCamera({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"select-none group relative pointer-events-auto",
|
||||
mode === "mini"
|
||||
? "aspect-square h-fit group relative flex-shrink-0 pointer-events-auto 2xl:hover:w-[10.833vw] 2xl:w-[6.944vw] sm:w-[15.625vw] w-[27.778vw]"
|
||||
: "aspect-video group relative pointer-events-auto max-w-full 2xl:h-full object-cover",
|
||||
? "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",
|
||||
isLocal && "order-last",
|
||||
// isVideoOff ? "bg-green-500" : "bg-yellow-500/10",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
@@ -321,39 +314,12 @@ export default function UserCamera({
|
||||
}}
|
||||
onClick={handleVideoClick}
|
||||
>
|
||||
{isAdmin && mode === "mini" && (
|
||||
<Admin className="absolute top-0 right-0 z-10" />
|
||||
)}
|
||||
|
||||
{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]">
|
||||
{name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Заглушка когда нет видео */}
|
||||
{(!mediaStream || isVideoOff) && (
|
||||
// <div className="flex absolute inset-0 justify-center items-center bg-gradient-to-br from-gray-700 to-gray-900">
|
||||
// <div className="flex flex-col gap-2 items-center text-white/60">
|
||||
// <div className="2xl:size-[2.778vw] size-10">
|
||||
// <VideoOffFilledIcon />
|
||||
// </div>
|
||||
// <span className="text-xs">Нет видео</span>
|
||||
// </div>
|
||||
// </div>
|
||||
<div className="2xl:rounded-[1.667vw] rounded-2xl absolute inset-0 bg-white/15 flex items-start justify-center">
|
||||
<div className="rounded-full 2xl:w-1/3 aspect-square bg-[#7B60F3] mt-12 flex items-center justify-center text-6xl font-medium text-white">
|
||||
ВД
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<video
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
"object-cover size-full 2xl:rounded-[1.667vw] rounded-2xl",
|
||||
isLocal && "scale-x-[-1]",
|
||||
(!mediaStream || isVideoOff) && "hidden"
|
||||
isLocal && "scale-x-[-1]"
|
||||
// (!mediaStream || isVideoOff) && ""
|
||||
)}
|
||||
autoPlay
|
||||
muted={isLocal ? true : isAudioMuted}
|
||||
@@ -418,6 +384,20 @@ export default function UserCamera({
|
||||
handleVideoClick();
|
||||
}}
|
||||
/>
|
||||
{isAdmin && mode === "mini" && (
|
||||
<Admin className="absolute top-0 right-0 z-10" />
|
||||
)}
|
||||
|
||||
{/* Заглушка когда нет видео */}
|
||||
{(!mediaStream || isVideoOff) && (
|
||||
<div className="select-none pointer-events-none absolute inset-0 bg-[url(/img/UserCameraAvatar.png)] bg-cover bg-center 2xl:rounded-[1.667vw] rounded-2xl" />
|
||||
)}
|
||||
|
||||
{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]">
|
||||
{name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопка управления звуком для удаленных участников */}
|
||||
{!isLocal && mediaStream && !isVideoOff && (
|
||||
@@ -440,27 +420,18 @@ 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">
|
||||
<p className="font-medium text-white button-m">{name}</p>
|
||||
{isMuted && (
|
||||
<div className="2xl:size-[1.111vw] size-4 text-white/50">
|
||||
<MicrophoneOffFilledIcon />
|
||||
</div>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Admin className="2xl:size-[1.111vw] size-4 absolute 2xl:-top-[0.278vw] 2xl:-right-[0.278vw] -right-1 -top-1" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Элементы управления только для удаленных участников */}
|
||||
{/* Индикаторы состояния только для удаленных участников */}
|
||||
{!isLocal && mode === "mini" && (
|
||||
<UserCameraControls
|
||||
isMuted={isMuted}
|
||||
isVideoOff={isVideoOff}
|
||||
isControlDisabled={isControlDisabled}
|
||||
isAdmin={isAdmin || false}
|
||||
onMute={onMute}
|
||||
onVideoOff={onVideoOff}
|
||||
onCanControl={onCanControl}
|
||||
hasControl={hasControl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -470,58 +441,33 @@ export default function UserCamera({
|
||||
function UserCameraControls({
|
||||
isMuted,
|
||||
isVideoOff,
|
||||
isControlDisabled,
|
||||
isAdmin,
|
||||
onMute,
|
||||
onVideoOff,
|
||||
onCanControl,
|
||||
hasControl,
|
||||
}: UserCameraControlsProps) {
|
||||
return (
|
||||
<div className="absolute transition-[bottom] duration-300 2xl:bottom-[0.278vw] 2xl:group-hover:bottom-[0.556vw] group-hover:bottom-2 bottom-1 left-1/2 -translate-x-1/2">
|
||||
{/* Индикатор muted - показывается всегда */}
|
||||
<div
|
||||
className={clsx(
|
||||
"2xl:size-[1.667vw] size-6 bg-[#14141426] backdrop-blur-[4px] transition-opacity duration-300 rounded-full flex items-center justify-center z-10a absolute left-1/2 -translate-x-1/2 2xl:bottom-0 [0.278vw] group-hover:opacity-0",
|
||||
isMuted ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
>
|
||||
<div className="size-[0.972vw] text-white">
|
||||
<MicrophoneOffIcon />
|
||||
<div
|
||||
className="select-none absolute transition-opacity 2xl:bottom-[0.278vw] bottom-1 left-1/2 -translate-x-1/2 flex 2xl:gap-[0.278vw] gap-1 2xl:mb-[0.278vw] mb-1 group-hover:opacity-100 opacity-0"
|
||||
// onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Индикатор микрофона */}
|
||||
<div className="2xl:size-[1.667vw] size-6 bg-[#14141426] backdrop-blur-[4px] rounded-full flex items-center justify-center">
|
||||
<div className="2xl:size-[0.972vw] size-3.5 text-white">
|
||||
{isMuted ? <MicrophoneOffFilledIcon /> : <MicrophoneFilledIcon />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Кнопки управления - только для администраторов */}
|
||||
{isAdmin && (
|
||||
<div
|
||||
className="flex gap-[0.278vw] mb-[0.278vw] group-hover:opacity-100 opacity-0 transition-opacity duration-300"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ControlButton
|
||||
icon={
|
||||
isMuted ? <MicrophoneOffFilledIcon /> : <MicrophoneFilledIcon />
|
||||
}
|
||||
size={"small"}
|
||||
disabled={false}
|
||||
onClick={onMute}
|
||||
/>
|
||||
<ControlButton
|
||||
icon={isVideoOff ? <VideoOffFilledIcon /> : <VideoFilledIcon />}
|
||||
size={"small"}
|
||||
disabled={false}
|
||||
onClick={onVideoOff}
|
||||
/>
|
||||
<ControlButton
|
||||
icon={
|
||||
isControlDisabled ? (
|
||||
<HandRaisedOffFilledIcon />
|
||||
) : (
|
||||
<HandRaisedFilledIcon />
|
||||
)
|
||||
}
|
||||
size={"small"}
|
||||
disabled={false}
|
||||
onClick={onCanControl}
|
||||
/>
|
||||
{/* Индикатор видео */}
|
||||
<div className="2xl:size-[1.667vw] size-6 bg-[#14141426] backdrop-blur-[4px] rounded-full flex items-center justify-center">
|
||||
<div className="2xl:size-[0.972vw] size-3.5 text-white">
|
||||
{isVideoOff ? <VideoOffFilledIcon /> : <VideoFilledIcon />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Индикатор управления - показывается только если hasControl = true */}
|
||||
{hasControl && (
|
||||
<div className="2xl:size-[1.667vw] size-6 bg-[#14141426] backdrop-blur-[4px] rounded-full flex items-center justify-center">
|
||||
<div className="2xl:size-[0.972vw] size-3.5 text-white">
|
||||
<HandRaisedFilledIcon />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -298,7 +298,12 @@ function SessionPage() {
|
||||
</ActionsSidebarWrapper>
|
||||
|
||||
{/* WebRTC видеочат - работает всегда, пока пользователь на странице */}
|
||||
<SessionUsersPanel roomId={session.id} autoJoin={true} mode={mode} />
|
||||
<SessionUsersPanel
|
||||
roomId={session.id}
|
||||
autoJoin={true}
|
||||
mode={mode}
|
||||
session={session}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user