import { Hand, LogOut, Maximize, MessageSquare, Mic, MicOff, Minimize, PictureInPicture2, ScreenShare, ScreenShareOff, Settings, Video, VideoOff, } from 'lucide-react' import { Track } from 'livekit-client' import { DisconnectButton, useLocalParticipant, useTrackToggle } from '@livekit/components-react' import { useToast } from '@/components/ui/ToastProvider' import { useIsCompactViewport } from '@/hooks/useIsCompactViewport' import type { HandQueueEntry } from '@/hooks/useChat' import { StageViewMenu, type StageViewProps } from '@/components/room/StageViewOptions' import { HandQueueMenu } from '@/components/room/HandQueueMenu' import { SCREEN_SHARE_CAPTURE_OPTIONS } from '@/lib/screenShareOptions' interface RoomToolbarProps extends StageViewProps { /** Показывать ли кнопку чата — `JoinOut.chat_enabled` И чат не помечен недоступным (close-код 4404). */ chatVisible: boolean chatOpen: boolean /** Число непрочитанных сообщений, накопленных со времени последнего открытия панели. */ chatUnreadCount: number onToggleChat: () => void /** Открыть диалог настроек устройств. */ onToggleSettings: () => void /** `document.fullscreenEnabled` — false скрывает кнопку. */ fullscreenSupported: boolean fullscreenActive: boolean onToggleFullscreen: () => void /** Доступен ли мини-плеер хоть в каком-то виде (Document PiP либо video-фолбэк) — false в Firefox скрывает кнопку. */ pipSupported: boolean pipActive: boolean onTogglePiP: () => void /** `JoinOut.hand_queue_enabled` — при `false` кнопка «Рука» и очередь не рендерятся вовсе. */ handQueueEnabled: boolean /** * Очередь поднятых рук целиком (задача B1, `useChat().handQueue`) — сама * решает, поднята ли СВОЯ рука (сравнивая с `localParticipant.identity` * через `useLocalParticipant`), и показывает бейдж общего счётчика. */ handQueue: HandQueueEntry[] onRaiseHand: () => void onLowerHand: () => void /** Опустить ЧУЖУЮ руку по identity — только организатору (панель очереди, `HandQueueMenu`). */ onLowerHandById: (identity: string) => void /** Тулбар в оверлее полноэкранного режима — см. докстринг `RoomTopbar.overlayVisible`, тот же механизм. */ overlayVisible?: boolean /** * Пользователь нажал «Выйти» — вызывается ПЕРЕД тем, как `DisconnectButton` * отключит комнату (обработчики в `mergeProps` вызываются цепочкой). Нужен * `RoomPage`, чтобы отличить намеренный выход от разрыва: причина * `CLIENT_INITIATED` приходит и от кнопки, и от livekit-client при заморозке * вкладки — см. докстринг `handleDisconnected`. */ onLeave?: () => void } /** * Нижний тулбар комнаты: микрофон/камера/демонстрация экрана/рука/очередь * рук/вид сцены/настройки устройств/полноэкранный режим/мини-плеер/чат/выход — * собственные кнопки на хуках LiveKit (useTrackToggle/DisconnectButton) и * панели чата, стилизованные по design/mockups/room.html. * * Кнопка «Вид» (режимы показа и скрытие остальных) рендерится ТОЛЬКО на * широком экране — условным рендерингом, а не скрытием через CSS: демонстрация * и мини-плеер скрыты на узком экране через CSS (см. `styles/room.css`), а те * же настройки показа сцены доступны секцией «Вид» в шторке настроек * (`DeviceSettingsDialog`). «Рука» — сознательное исключение из этой * экономии: поднять руку посреди разговора — действие со временем жизни в * секунды, прятать его в шторку настроек означало бы делать его практически * недоступным с телефона. «Очередь» видна ЛЮБОМУ участнику (сессия * «28-hand-queue-for-all»: раньше только организатору), по той же причине * оставлена в тулбаре, а не в шторке: любому участнику с телефона тоже нужно * видеть очередь сразу. Обе кнопки целиком гасятся `handQueueEnabled` * (`JoinOut.hand_queue_enabled`, отключаемый модуль в админке). * Полноэкранный режим на мобильном ОСТАЁТСЯ в тулбаре (не спрятан в шторку, * как демонстрация/мини-плеер) — по решению оператора вход в него должен * быть по аналогии с десктопом; если из-за этого кнопки не помещаются в один * ряд, `mobileButtonCount`/`.tb-wrap-grid` ниже раскладывают их равномерной * сеткой, а не как получится через `flex-wrap`. */ export function RoomToolbar({ chatVisible, chatOpen, chatUnreadCount, onToggleChat, onToggleSettings, fullscreenSupported, fullscreenActive, onToggleFullscreen, pipSupported, pipActive, onTogglePiP, handQueueEnabled, handQueue, onRaiseHand, onLowerHand, onLowerHandById, layoutMode, onLayoutModeChange, hideOthers, onHideOthersChange, overlayVisible, onLeave, }: RoomToolbarProps) { const toast = useToast() const isCompact = useIsCompactViewport() const { localParticipant } = useLocalParticipant() const handRaised = handQueue.some((entry) => entry.identity === localParticipant.identity) // Сколько кнопок реально видно на мобильном (демонстрация/мини-окно там // скрыты через CSS всегда, «Вид» не рендерится вовсе — см. докстринг) — // считаем в JS, а не через CSS-селекторы вида `:nth-child`: у «Очереди» // в DOM есть своя обёртка `.tb-menu-wrap`, из-за которой позиция остальных // кнопок «плывёт», и подсчёт по структуре DOM был бы хрупким. От порога // зависит `.tb-wrap-grid` в room.css — ниже он переключает перенос на // 2 строки с «как поместится» (flex-wrap) на равномерную сетку 4 колонки // (7 → 4+3, 8 → 4+4). «Рука»/«Очередь» — обе теперь видны любому участнику // (сессия «28-hand-queue-for-all»), их наличие зависит только от // `handQueueEnabled`, не от роли. const mobileButtonCount = 2 /* микрофон, камера */ + (handQueueEnabled ? 2 : 0) /* рука, очередь */ + 1 /* настройки */ + (fullscreenSupported ? 1 : 0) + (chatVisible ? 1 : 0) + 1 /* выйти */ const mic = useTrackToggle({ source: Track.Source.Microphone }) const camera = useTrackToggle({ source: Track.Source.Camera }) const screenShare = useTrackToggle({ source: Track.Source.ScreenShare, captureOptions: SCREEN_SHARE_CAPTURE_OPTIONS, // Отмена браузерного диалога выбора экрана/окна (пользователь нажал // «Отмена») приходит сюда как NotAllowedError — молча игнорируем, это не // ошибка приложения. Прочие сбои (например, `NotReadableError` при // занятом источнике) показываем тостом. onDeviceError: (error) => { if (error.name === 'NotAllowedError') return toast.show('Не удалось начать демонстрацию экрана', 'error') }, }) return ( ) }