From d7ae462ed5f8b2dd8577bd349a9ab44e37de8ad6 Mon Sep 17 00:00:00 2001 From: Max Ronzhin Date: Sun, 2 Aug 2026 04:58:56 +0300 Subject: [PATCH] =?UTF-8?q?fix(room):=20=D0=BE=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D1=8C=20=D0=BF=D0=BE=D0=B4=D0=BD=D1=8F=D1=82=D1=8B=D1=85?= =?UTF-8?q?=20=D1=80=D1=83=D0=BA=20=E2=80=94=20=D0=BA=D0=BE=D0=BC=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=BE=D0=BF=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20?= =?UTF-8?q?=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D0=B8=20=D0=B2=D0=BE=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=81=D1=8C=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HandQueuePanel рендерился как боковая панель на весь экран на мобильном (.chat-panel, унаследовано от чата) — для короткого списка поднятых рук это избыточно, история переписки тут ни при чём. Переделано в HandQueueMenu — тот же самодостаточный поповер над кнопкой, что и «Вид» (StageViewMenu): своё состояние открытия, закрытие по клику вне/Escape, .tb-menu вместо боковой панели. Кнопка «Очередь» больше не получает open-состояние пропсами сверху — сама решает, открыта ли, и сама проверяет useIsOrganizer(). Список внутри растёт вместе с очередью (компактно при 1–2 поднятых руках), но не бесконечно — после ~10 строк упирается в max-height и скроллится дальше. --- .../src/components/room/HandQueueMenu.tsx | 99 +++++++++++++++++++ .../src/components/room/HandQueuePanel.tsx | 60 ----------- frontend/src/components/room/RoomToolbar.tsx | 29 +----- frontend/src/pages/RoomPage.tsx | 16 +-- frontend/src/styles/room.css | 14 ++- 5 files changed, 115 insertions(+), 103 deletions(-) create mode 100644 frontend/src/components/room/HandQueueMenu.tsx delete mode 100644 frontend/src/components/room/HandQueuePanel.tsx diff --git a/frontend/src/components/room/HandQueueMenu.tsx b/frontend/src/components/room/HandQueueMenu.tsx new file mode 100644 index 0000000..f0da65d --- /dev/null +++ b/frontend/src/components/room/HandQueueMenu.tsx @@ -0,0 +1,99 @@ +import { useEffect, useRef, useState } from 'react' +import { Hand, ListOrdered } from 'lucide-react' +import { useIsOrganizer } from '@/hooks/useIsOrganizer' +import type { HandQueueEntry } from '@/hooks/useChat' + +interface HandQueueMenuProps { + queue: HandQueueEntry[] + onLower: (identity: string) => void +} + +/** + * Кнопка «Очередь» в тулбаре с поповером над ней — видна только организатору + * (задача B1). Тот же самодостаточный паттерн, что и `StageViewMenu` («Вид»): + * собственное состояние открытия, закрытие по клику вне/Escape, поповер + * `.tb-menu` над кнопкой — а не боковая панель на весь экран (как чат): + * очередь рук — короткий список, а не история переписки, разворачивать её + * во весь экран незачем и на мобильном. + * + * Размер поповера подстраивается под число записей — `.hand-queue-list` + * растёт вместе со списком и не даёт пустого места при 1–2 поднятых руках, + * но не бесконечно: после ~10 строк список упирается в `max-height` и дальше + * скроллится (см. room.css) — иначе организатор на энергичной встрече + * получил бы поповер выше экрана. + */ +export function HandQueueMenu({ queue, onLower }: HandQueueMenuProps) { + const isOrganizer = useIsOrganizer() + const [open, setOpen] = useState(false) + const wrapRef = useRef(null) + + useEffect(() => { + if (!open) return + + function handlePointerDown(event: MouseEvent) { + if (wrapRef.current && !wrapRef.current.contains(event.target as Node)) { + setOpen(false) + } + } + function handleKeydown(event: KeyboardEvent) { + if (event.key === 'Escape') setOpen(false) + } + + document.addEventListener('mousedown', handlePointerDown) + document.addEventListener('keydown', handleKeydown) + return () => { + document.removeEventListener('mousedown', handlePointerDown) + document.removeEventListener('keydown', handleKeydown) + } + }, [open]) + + if (!isOrganizer) return null + + return ( +
+ + {open && ( +
+ {queue.length === 0 ? ( +

Пока никто не поднял руку

+ ) : ( +
    + {queue.map((entry, index) => ( +
  1. + {index + 1} + + + +
  2. + ))} +
+ )} +
+ )} +
+ ) +} diff --git a/frontend/src/components/room/HandQueuePanel.tsx b/frontend/src/components/room/HandQueuePanel.tsx deleted file mode 100644 index 1fdba37..0000000 --- a/frontend/src/components/room/HandQueuePanel.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Hand, X } from 'lucide-react' -import { useIsOrganizer } from '@/hooks/useIsOrganizer' -import type { HandQueueEntry } from '@/hooks/useChat' - -interface HandQueuePanelProps { - queue: HandQueueEntry[] - onLower: (identity: string) => void - onClose: () => void -} - -/** - * Панель очереди поднятых рук — видна только организатору (задача B1). - * Визуально — тот же боковой контейнер, что и `ChatPanel` (`.chat-panel`, - * включая мобильное поведение «во весь экран» на ≤900px), содержимое своё: - * упорядоченный список с позицией и кнопкой «Опустить» на каждой строке — - * организатору разрешено опускать чужую руку (решение оператора, задача B1). - * - * `RoomPage` гейтит рендер по `handQueueOpen` (как и `ChatPanel` по - * `chatOpen`) — свой `useIsOrganizer()` здесь ДОПОЛНИТЕЛЬНАЯ, а не - * единственная защита: `RoomToolbar` уже не показывает кнопку открытия - * не-организатору, это подстраховка на случай прямого рендера. - */ -export function HandQueuePanel({ queue, onLower, onClose }: HandQueuePanelProps) { - const isOrganizer = useIsOrganizer() - if (!isOrganizer) return null - - return ( - - ) -} diff --git a/frontend/src/components/room/RoomToolbar.tsx b/frontend/src/components/room/RoomToolbar.tsx index a6bba41..ad58bed 100644 --- a/frontend/src/components/room/RoomToolbar.tsx +++ b/frontend/src/components/room/RoomToolbar.tsx @@ -1,6 +1,5 @@ import { Hand, - ListOrdered, LogOut, Maximize, MessageSquare, @@ -18,9 +17,9 @@ import { Track, type ScreenShareCaptureOptions } from 'livekit-client' import { DisconnectButton, useLocalParticipant, useTrackToggle } from '@livekit/components-react' import { useToast } from '@/components/ui/ToastProvider' import { useIsCompactViewport } from '@/hooks/useIsCompactViewport' -import { useIsOrganizer } from '@/hooks/useIsOrganizer' import type { HandQueueEntry } from '@/hooks/useChat' import { StageViewMenu, type StageViewProps } from '@/components/room/StageViewOptions' +import { HandQueueMenu } from '@/components/room/HandQueueMenu' /** * Опции захвата демонстрации экрана: `audio: true` — звук @@ -67,8 +66,8 @@ interface RoomToolbarProps extends StageViewProps { handQueue: HandQueueEntry[] onRaiseHand: () => void onLowerHand: () => void - handQueueOpen: boolean - onToggleHandQueue: () => void + /** Опустить ЧУЖУЮ руку по identity — только организатору (панель очереди, `HandQueueMenu`). */ + onLowerHandById: (identity: string) => void } /** @@ -104,8 +103,7 @@ export function RoomToolbar({ handQueue, onRaiseHand, onLowerHand, - handQueueOpen, - onToggleHandQueue, + onLowerHandById, layoutMode, onLayoutModeChange, hideOthers, @@ -113,7 +111,6 @@ export function RoomToolbar({ }: RoomToolbarProps) { const toast = useToast() const isCompact = useIsCompactViewport() - const isOrganizer = useIsOrganizer() const { localParticipant } = useLocalParticipant() const handRaised = handQueue.some((entry) => entry.identity === localParticipant.identity) const mic = useTrackToggle({ source: Track.Source.Microphone }) @@ -197,23 +194,7 @@ export function RoomToolbar({ Рука - {isOrganizer && ( - - )} + {!isCompact && ( setChatOpen(false)} /> )} - {handQueueOpen && ( - chat.lowerHand(identity)} - onClose={() => setHandQueueOpen(false)} - /> - )} chat.lowerHand()} - handQueueOpen={handQueueOpen} - onToggleHandQueue={() => setHandQueueOpen((open) => !open)} + onLowerHandById={(identity) => chat.lowerHand(identity)} layoutMode={layoutMode} onLayoutModeChange={handleLayoutModeChange} hideOthers={hideOthers} diff --git a/frontend/src/styles/room.css b/frontend/src/styles/room.css index 9be629b..5ba642a 100644 --- a/frontend/src/styles/room.css +++ b/frontend/src/styles/room.css @@ -593,16 +593,22 @@ video[data-lk-source='screen_share'] { object-fit: contain; background: #000; } } } -/* ---------- Панель очереди поднятых рук (`HandQueuePanel`, задача B1) ---------- - * Контейнер — `.chat-panel` (та же геометрия и мобильное поведение), список - * — свой. */ +/* ---------- Поповер очереди поднятых рук (`HandQueueMenu`, задача B1) ---------- + * Контейнер — `.tb-menu` (тот же поповер над кнопкой, что у «Вида»), не + * `.chat-panel`: очередь — короткий список, а не история переписки, + * разворачивать её на весь экран/боковой панелью незачем даже на мобильном. */ +.hand-queue-menu { width: 300px; padding: var(--space-3); } .hand-queue-list { list-style: none; margin: 0; - padding: var(--space-3); + padding: 0; display: flex; flex-direction: column; gap: var(--space-2); + /* Высота растёт вместе со списком (при 1–2 записях поповер компактный), но + не безгранично — после ~10 строк упирается в потолок и скроллится + дальше, иначе на энергичной встрече поповер вылез бы выше экрана. */ + max-height: 460px; overflow-y: auto; } .hand-queue-item {