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:
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user