Refactor SessionUsersPanel to improve drag-and-drop functionality with enhanced state management and animations. Update UserCamera and UserDevicesControls for consistent styling and layout adjustments. Modify NewSessionPage to import updated SessionUsersPanel component.

This commit is contained in:
2025-10-21 17:14:50 +05:00
parent e8e781ae99
commit 0ce81e2e4f
6 changed files with 467 additions and 109 deletions
+105 -100
View File
@@ -1,9 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useState, useRef, useEffect } from "react";
import { useState, useRef, useEffect, useCallback } from "react";
import UserCamera from "./ui/UserCamera";
import UserDevicesControls from "./ui/UserDevicesControls";
import clsx from "clsx";
const DRAG_THRESHOLD = 15;
const OFFSET = 0.01111; // 1.111vw
const TRANSITION = "all 0.5s cubic-bezier(.63,.08,.37,.89)";
export default function SessionUsersPanel() {
const users = [
{
@@ -33,148 +36,150 @@ export default function SessionUsersPanel() {
},
];
function handleMute(id: number) {
console.log(`Mute user ${id}`);
}
function handleVideoOff(id: number) {
console.log(`Video off user ${id}`);
}
function handleCanControl(id: number) {
console.log(`Can control user ${id}`);
}
const [isTop, setIsTop] = useState(false);
const [isLeft, setIsLeft] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 });
const [corner, setCorner] = useState({ top: false, left: false });
const [dragState, setDragState] = useState<"idle" | "dragging" | "snapping" | "released">("idle");
const [position, setPosition] = useState({ x: 0, y: 0 });
const containerRef = useRef<HTMLDivElement>(null);
const dragOffset = useRef({ x: 0, y: 0 });
const dragStartPos = useRef({ x: 0, y: 0 });
const isDragStarted = useRef(false);
const DRAG_THRESHOLD = 15;
const dragDataRef = useRef({ offsetX: 0, offsetY: 0, startX: 0, startY: 0, hasStarted: false });
const handleMove = (e: MouseEvent | TouchEvent) => {
const getPointerPos = (e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) => ({
x: "touches" in e ? e.touches[0].clientX : e.clientX,
y: "touches" in e ? e.touches[0].clientY : e.clientY,
});
const handleMove = useCallback((e: MouseEvent | TouchEvent) => {
if (!containerRef.current) return;
if (!isDragStarted.current) {
const distance = Math.hypot(
("clientX" in e ? e.clientX : e.touches[0].clientX) -
dragStartPos.current.x,
("clientY" in e ? e.clientY : e.touches[0].clientY) -
dragStartPos.current.y
);
const pos = getPointerPos(e);
const { startX, startY, offsetX, offsetY, hasStarted } = dragDataRef.current;
if (distance >= DRAG_THRESHOLD) {
isDragStarted.current = true;
setIsDragging(true);
} else {
return;
}
if (!hasStarted) {
const distance = Math.hypot(pos.x - startX, pos.y - startY);
if (distance < DRAG_THRESHOLD) return;
dragDataRef.current.hasStarted = true;
setDragState("dragging");
}
if (isDragStarted.current) {
setDragPosition({
x:
("clientX" in e ? e.clientX : e.touches[0].clientX) -
dragOffset.current.x,
y:
("clientY" in e ? e.clientY : e.touches[0].clientY) -
dragOffset.current.y,
setPosition({ x: pos.x - offsetX, y: pos.y - offsetY });
}, []);
const handleEnd = useCallback(() => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const shouldBeTop = centerY < window.innerHeight / 2;
const shouldBeLeft = centerX < window.innerWidth / 2;
if (dragDataRef.current.hasStarted) {
// Фиксируем текущую позицию без transition
setPosition({ x: rect.left, y: rect.top });
setDragState("released");
// Запускаем анимацию к углу
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setDragState("snapping");
setCorner({ top: shouldBeTop, left: shouldBeLeft });
setTimeout(() => setDragState("idle"), 500);
});
});
} else {
setDragState("idle");
}
};
const handleMouseUp = () => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const center = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
const shouldBeTop = center.y < window.innerHeight / 2;
const shouldBeLeft = center.x < window.innerWidth / 2;
setIsDragging(!isDragStarted.current);
setIsTop(shouldBeTop);
setIsLeft(shouldBeLeft);
isDragStarted.current = false;
dragDataRef.current.hasStarted = false;
window.removeEventListener("mousemove", handleMove);
window.removeEventListener("mouseup", handleMouseUp);
};
window.removeEventListener("touchmove", handleMove);
window.removeEventListener("mouseup", handleEnd);
window.removeEventListener("touchend", handleEnd);
}, [handleMove]);
const handleMouseDown = (
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
) => {
const handleStart = (e: React.MouseEvent | React.TouchEvent) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const r_pos = { x: rect.left, y: rect.top };
const c_pos = {
x: "clientX" in e ? e.clientX : e.touches[0].clientX,
y: "clientY" in e ? e.clientY : e.touches[0].clientY,
const pos = getPointerPos(e);
dragDataRef.current = {
startX: pos.x,
startY: pos.y,
offsetX: pos.x - rect.left,
offsetY: pos.y - rect.top,
hasStarted: false,
};
dragStartPos.current = c_pos;
dragOffset.current = { x: c_pos.x - r_pos.x, y: c_pos.y - r_pos.y };
setDragPosition({ x: r_pos.x, y: r_pos.y });
setPosition({ x: rect.left, y: rect.top });
isDragStarted.current = false;
window.addEventListener("mousemove", handleMove);
window.addEventListener("touchmove", handleMove);
window.addEventListener("mouseup", handleMouseUp);
window.addEventListener("touchend", handleMouseUp);
window.addEventListener("mouseup", handleEnd);
window.addEventListener("touchend", handleEnd);
};
useEffect(() => {
return () => {
window.removeEventListener("mousemove", handleMove);
window.removeEventListener("mouseup", handleMouseUp);
window.removeEventListener("touchmove", handleMove);
window.removeEventListener("touchend", handleMouseUp);
window.removeEventListener("mouseup", handleEnd);
window.removeEventListener("touchend", handleEnd);
};
}, []);
}, [handleMove, handleEnd]);
const getStyle = (): React.CSSProperties => {
if (isDragStarted.current && isDragging) {
return {
left: `${dragPosition.x}px`,
top: `${dragPosition.y}px`,
transition: "none",
};
}
const offset = window.innerWidth * OFFSET;
// Вычисляем финальные координаты угла
const getCornerPosition = () => {
if (!containerRef.current) return { x: offset, y: offset };
const rect = containerRef.current.getBoundingClientRect();
return {
left: isLeft ? "1.111vw" : "calc(100vw - 1.111vw)",
top: isTop ? "1.111vw" : "calc(100vh - 1.111vw)",
transform: `translate(${isLeft ? "0" : "-100%"}, ${
isTop ? "0" : "-100%"
})`,
transition: "all 0.5s cubic-bezier(.63,.08,.37,.89)",
x: corner.left ? offset : window.innerWidth - offset - rect.width,
y: corner.top ? offset : window.innerHeight - offset - rect.height,
};
};
let style: React.CSSProperties;
if (dragState === "dragging" || dragState === "released") {
// Во время перетаскивания или сразу после отпускания
style = {
left: position.x,
top: position.y,
transition: "none"
};
} else {
// Анимация к углу или покой в углу
const cornerPos = getCornerPosition();
style = {
left: cornerPos.x,
top: cornerPos.y,
transition: dragState === "snapping" ? TRANSITION : "none",
};
}
return (
<div
ref={containerRef}
onMouseDown={handleMouseDown}
onTouchStart={handleMouseDown}
className="flex gap-4 active:cursor-grabbing cursor-grab absolute"
style={getStyle()}
onMouseDown={handleStart}
onTouchStart={handleStart}
className="flex absolute gap-4 active:cursor-grabbing cursor-grab"
style={style}
>
<div
className={clsx(
"flex gap-4 w-max",
isLeft ? "flex-row-reverse" : "flex-row",
isTop ? "items-start" : "items-end"
corner.left ? "flex-row-reverse" : "flex-row",
corner.top ? "items-start" : "items-end"
)}
>
{users.map((user) => (
<UserCamera
key={user.id}
onMute={() => handleMute(user.id)}
onVideoOff={() => handleVideoOff(user.id)}
onCanControl={() => handleCanControl(user.id)}
onMute={() => console.log(`Mute user ${user.id}`)}
onVideoOff={() => console.log(`Video off user ${user.id}`)}
onCanControl={() => console.log(`Can control user ${user.id}`)}
{...user}
/>
))}