diff --git a/client/.env b/client/.env index 0f22f67..5e374be 100644 --- a/client/.env +++ b/client/.env @@ -1,4 +1,4 @@ -VITE_API_URL=http://localhost:3000 -VITE_WEBRTC_URL=http://localhost:3001 -# VITE_API_URL=https://stream.graff.estate/api -# VITE_WEBRTC_URL=https://stream.graff.estate \ No newline at end of file +# VITE_API_URL=http://localhost:3000 +# VITE_WEBRTC_URL=http://localhost:3001 +VITE_API_URL=https://stream.graff.estate/api +VITE_WEBRTC_URL=https://stream.graff.estate \ No newline at end of file diff --git a/client/src/components/DraggableContainer.tsx b/client/src/components/DraggableContainer.tsx index 9ba2011..a6a00e5 100644 --- a/client/src/components/DraggableContainer.tsx +++ b/client/src/components/DraggableContainer.tsx @@ -152,6 +152,24 @@ export default function DraggableContainer({ initialPosition: { top: 0, left: 0 }, }); + // Функция для преобразования padding в пиксели + const parsePadding = (paddingValue: number | string): number => { + if (typeof paddingValue === "number") return paddingValue; + + const value = parseFloat(paddingValue); + if (paddingValue.endsWith("vw")) { + return (value / 100) * window.innerWidth; + } else if (paddingValue.endsWith("vh")) { + return (value / 100) * window.innerHeight; + } else if (paddingValue.endsWith("%")) { + // Для процентов берем среднее между шириной и высотой + return (value / 100) * Math.min(window.innerWidth, window.innerHeight); + } else { + // По умолчанию считаем что это пиксели + return value; + } + }; + // Функция для преобразования угла в позицию const getPositionFromCorner = (corner: Corner): Position => { switch (corner) { @@ -446,14 +464,75 @@ export default function DraggableContainer({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDragging]); + // Конвертируем позицию из transform-based в absolute только после стабилизации размеров + // Это нужно для корректной работы внутренних поповеров useEffect(() => { - if (!containerRef.current || enableSnapping) return; - const rect = containerRef.current.getBoundingClientRect(); - setPosition({ - top: rect.top, - left: rect.left, + if (!containerRef.current || enableSnapping || !position.transform) return; + + const container = containerRef.current; + let timeoutId: NodeJS.Timeout; + let lastHeight = 0; + + // Используем ResizeObserver для отслеживания изменений размера + const resizeObserver = new ResizeObserver((entries) => { + const entry = entries[0]; + const currentHeight = entry.contentRect.height; + + // Если высота изменилась, перезапускаем таймер + if (currentHeight !== lastHeight) { + lastHeight = currentHeight; + clearTimeout(timeoutId); + + timeoutId = setTimeout(() => { + if (!containerRef.current) return; + const rect = containerRef.current.getBoundingClientRect(); + let { top, left } = rect; + + // Проверяем, не выходит ли попап за границы экрана + const windowHeight = window.innerHeight; + const windowWidth = window.innerWidth; + + // Корректируем вертикальную позицию если нужно + if (centerVertical) { + if (rect.bottom > windowHeight) { + // Попап выходит за нижнюю границу - прижимаем к низу с отступом + top = windowHeight - rect.height - parsePadding(padding); + } else if (rect.top < 0) { + // Попап выходит за верхнюю границу - прижимаем к верху с отступом + top = parsePadding(padding); + } + } + + // Корректируем горизонтальную позицию если нужно + if (centerHorizontal) { + if (rect.right > windowWidth) { + left = windowWidth - rect.width - parsePadding(padding); + } else if (rect.left < 0) { + left = parsePadding(padding); + } + } + + setPosition({ + top, + left, + }); + }, 100); + } }); - }, [enableSnapping]); + + resizeObserver.observe(container); + + return () => { + resizeObserver.disconnect(); + clearTimeout(timeoutId); + }; + }, [ + enableSnapping, + position.transform, + centerVertical, + centerHorizontal, + padding, + ]); // Устанавливаем cursor стили на элемент-хэндл useEffect(() => { diff --git a/client/src/components/PopupWrapper.tsx b/client/src/components/PopupWrapper.tsx index d6a9f57..e69af85 100644 --- a/client/src/components/PopupWrapper.tsx +++ b/client/src/components/PopupWrapper.tsx @@ -20,7 +20,7 @@ function PopupWrapper({ return (
diff --git a/client/src/components/SessionUsersPanel.tsx b/client/src/components/SessionUsersPanel.tsx index 3c9138d..dc9f33e 100644 --- a/client/src/components/SessionUsersPanel.tsx +++ b/client/src/components/SessionUsersPanel.tsx @@ -208,7 +208,7 @@ function SessionUsersPanel({ isVideoOff={participant.isVideoOff || false} isSpeaking={participant.isSpeaking} isControlDisabled={true} - isAdmin={true} + isAdmin={false} mediaStream={participant.stream} hasLocalMediaPermission={hasLocalStream} onMute={() => console.log(`Mute user ${participant.id}`)} diff --git a/client/src/components/indicators/Admin.tsx b/client/src/components/indicators/Admin.tsx index d0bf3ed..aed51d3 100644 --- a/client/src/components/indicators/Admin.tsx +++ b/client/src/components/indicators/Admin.tsx @@ -5,11 +5,11 @@ export default function Admin({ className }: { className?: string }) { return (
-
+
diff --git a/client/src/components/popups/ChatPopup.tsx b/client/src/components/popups/ChatPopup.tsx index 9a018f1..6142979 100644 --- a/client/src/components/popups/ChatPopup.tsx +++ b/client/src/components/popups/ChatPopup.tsx @@ -13,7 +13,9 @@ interface ChatPopupProps { sessionId?: string; } -export default function ChatPopup({ sessionId: sessionIdProp }: ChatPopupProps = {}) { +export default function ChatPopup({ + sessionId: sessionIdProp, +}: ChatPopupProps = {}) { const headerRef = useRef(null); // Получаем sessionId из пропсов или из URL параметров const { id: sessionIdFromParams } = useParams<{ id: string }>(); @@ -32,7 +34,10 @@ export default function ChatPopup({ sessionId: sessionIdProp }: ChatPopupProps = // Обновляем userId в WebRTC сервисе при авторизации useEffect(() => { if (user?.id && updateUserId) { - console.log("[ChatPopup] User authenticated, updating WebRTC userId to:", user.id); + console.log( + "[ChatPopup] User authenticated, updating WebRTC userId to:", + user.id + ); updateUserId(user.id); } }, [user?.id, updateUserId]); @@ -42,10 +47,23 @@ export default function ChatPopup({ sessionId: sessionIdProp }: ChatPopupProps = const myIdentifier = user?.id || currentUserId; console.log("[ChatPopup] Rendering with sessionId:", sessionId); - console.log("[ChatPopup] My identifier:", myIdentifier, "isAuthenticated:", !!user, "user.id:", user?.id, "currentUserId:", currentUserId); + console.log( + "[ChatPopup] My identifier:", + myIdentifier, + "isAuthenticated:", + !!user, + "user.id:", + user?.id, + "currentUserId:", + currentUserId + ); // Загружаем историю через REST API - const { data: historyMessages = [], isLoading, error } = useChatHistory(sessionId); + const { + data: historyMessages = [], + isLoading, + error, + } = useChatHistory(sessionId); console.log("[ChatPopup] Chat history state:", { historyMessages: historyMessages.length, @@ -67,9 +85,9 @@ export default function ChatPopup({ sessionId: sessionIdProp }: ChatPopupProps = const newRealtimeMessages = sessionRealtimeMessages.filter( (m) => !historyIds.has(m.id) ); - + // Дедупликация на всякий случай - убираем дубликаты по ID - const allMessagesMap = new Map(); + const allMessagesMap = new Map(); [...historyMessages, ...newRealtimeMessages].forEach((msg) => { allMessagesMap.set(msg.id, msg); }); @@ -85,7 +103,10 @@ export default function ChatPopup({ sessionId: sessionIdProp }: ChatPopupProps = newRealtime: newRealtimeMessages.length, total: allMessages.length, historyIds: Array.from(historyIds), - realtimeIds: realtimeMessages.map(m => ({ id: m.id, sessionId: m.sessionId })), + realtimeIds: realtimeMessages.map((m) => ({ + id: m.id, + sessionId: m.sessionId, + })), }); function onMessageSend(message: string) { @@ -103,10 +124,10 @@ export default function ChatPopup({ sessionId: sessionIdProp }: ChatPopupProps = > -
+
({ - id: m.id, - senderId: m.senderId, - isFromMe: m.senderId === currentUserId, - content: m.content.substring(0, 20) - }))); + console.log( + "[MessageFeed] Messages:", + messages.map((m) => ({ + id: m.id, + senderId: m.senderId, + isFromMe: m.senderId === currentUserId, + content: m.content.substring(0, 20), + })) + ); // Скролл при первой загрузке сообщений useEffect(() => { @@ -313,13 +337,14 @@ function MessageInput({ e.preventDefault(); const textarea = textareaRef.current; if (!textarea) return; - + const start = textarea.selectionStart; const end = textarea.selectionEnd; - const newMessage = message.substring(0, start) + "\n" + message.substring(end); - + const newMessage = + message.substring(0, start) + "\n" + message.substring(end); + setMessage(newMessage); - + // Устанавливаем курсор после вставленного переноса строки setTimeout(() => { textarea.selectionStart = textarea.selectionEnd = start + 1; diff --git a/client/src/components/popups/ParticipantsPopup.tsx b/client/src/components/popups/ParticipantsPopup.tsx index 7c0558c..b17b4d2 100644 --- a/client/src/components/popups/ParticipantsPopup.tsx +++ b/client/src/components/popups/ParticipantsPopup.tsx @@ -128,7 +128,10 @@ function ParticipantItem({ : undefined; return ( -
+
diff --git a/client/src/components/ui/ActionsPopover.tsx b/client/src/components/ui/ActionsPopover.tsx index d9635f4..25a3169 100644 --- a/client/src/components/ui/ActionsPopover.tsx +++ b/client/src/components/ui/ActionsPopover.tsx @@ -54,7 +54,7 @@ export default function ActionsPopover({ isOpened={opened} parentElRef={buttonRef} position="vertical" - className={clsx("2xl:w-[17.222vw] w-[53.333vw]", className)} + className={clsx("2xl:w-[17.222vw] max-2xl:w-[192px]", className)} > {options.map((option) => (