room-toolbar с 8 кнопками (520px) не помещался в мобильную ширину (375px) — крайние кнопки обрезались без возможности до них доскроллить. На мобильном убираем демонстрацию экрана/полноэкранный режим/мини-окно (редко нужны с телефона) и уплотняем оставшиеся 5 кнопок. В топбаре убираем инвайт-чип (номер + копирование ссылки) — сверху остаются только название конференции и статус.
216 lines
8.5 KiB
TypeScript
216 lines
8.5 KiB
TypeScript
import {
|
||
LogOut,
|
||
Maximize,
|
||
MessageSquare,
|
||
Mic,
|
||
MicOff,
|
||
Minimize,
|
||
PictureInPicture2,
|
||
ScreenShare,
|
||
ScreenShareOff,
|
||
Settings,
|
||
Video,
|
||
VideoOff,
|
||
} from 'lucide-react'
|
||
import { Track, type ScreenShareCaptureOptions } from 'livekit-client'
|
||
import { DisconnectButton, useTrackToggle } from '@livekit/components-react'
|
||
import { useToast } from '@/components/ui/ToastProvider'
|
||
|
||
/**
|
||
* Опции захвата демонстрации экрана: `audio: true` — звук
|
||
* вкладки/экрана там, где браузер его отдаёт (Chrome/Edge — вкладка почти
|
||
* всегда, целый экран — только Windows); `selfBrowserSurface: 'exclude'`
|
||
* — не предлагать в списке
|
||
* источников собственную вкладку (зеркальный туннель самой конференции);
|
||
* `surfaceSwitching: 'include'` — разрешить переключать источник прямо во
|
||
* время демонстрации, не останавливая её; `systemAudio: 'include'` — не
|
||
* запрещать захват системного звука при выборе «весь экран». Вынесено в
|
||
* модульную константу — `useTrackToggle` держит `JSON.stringify(captureOptions)`
|
||
* в зависимостях внутреннего `useMemo`, инлайновый литерал был бы безвреден,
|
||
* но константа явнее фиксирует неизменность опций.
|
||
*/
|
||
const SCREEN_SHARE_CAPTURE_OPTIONS: ScreenShareCaptureOptions = {
|
||
audio: true,
|
||
selfBrowserSurface: 'exclude',
|
||
surfaceSwitching: 'include',
|
||
systemAudio: 'include',
|
||
}
|
||
|
||
interface RoomToolbarProps {
|
||
/** Показывать ли кнопку чата — `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
|
||
}
|
||
|
||
/**
|
||
* Нижний тулбар комнаты: микрофон/камера/демонстрация экрана/
|
||
* настройки устройств/полноэкранный режим/мини-плеер/чат/выход — собственные
|
||
* кнопки на хуках LiveKit (useTrackToggle/DisconnectButton) и панели чата,
|
||
* стилизованные по design/mockups/room.html.
|
||
*/
|
||
export function RoomToolbar({
|
||
chatVisible,
|
||
chatOpen,
|
||
chatUnreadCount,
|
||
onToggleChat,
|
||
onToggleSettings,
|
||
fullscreenSupported,
|
||
fullscreenActive,
|
||
onToggleFullscreen,
|
||
pipSupported,
|
||
pipActive,
|
||
onTogglePiP,
|
||
}: RoomToolbarProps) {
|
||
const toast = useToast()
|
||
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 (
|
||
<footer className="room-toolbar">
|
||
<button
|
||
type="button"
|
||
{...mic.buttonProps}
|
||
className={`tb-btn${mic.enabled ? '' : ' is-off'}`}
|
||
aria-label={mic.enabled ? 'Выключить микрофон' : 'Включить микрофон'}
|
||
>
|
||
<span className="icon-shell">
|
||
{mic.enabled ? (
|
||
<Mic className="lucide" aria-hidden="true" />
|
||
) : (
|
||
<MicOff className="lucide" aria-hidden="true" />
|
||
)}
|
||
</span>
|
||
<span className="label">Микрофон</span>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
{...camera.buttonProps}
|
||
className={`tb-btn${camera.enabled ? '' : ' is-off'}`}
|
||
aria-label={camera.enabled ? 'Выключить камеру' : 'Включить камеру'}
|
||
>
|
||
<span className="icon-shell">
|
||
{camera.enabled ? (
|
||
<Video className="lucide" aria-hidden="true" />
|
||
) : (
|
||
<VideoOff className="lucide" aria-hidden="true" />
|
||
)}
|
||
</span>
|
||
<span className="label">Камера</span>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
{...screenShare.buttonProps}
|
||
className={`tb-btn tb-btn--screenshare${screenShare.enabled ? ' is-sharing' : ''}`}
|
||
aria-label={screenShare.enabled ? 'Остановить демонстрацию экрана' : 'Демонстрировать экран'}
|
||
>
|
||
<span className="icon-shell">
|
||
{screenShare.enabled ? (
|
||
<ScreenShareOff className="lucide" aria-hidden="true" />
|
||
) : (
|
||
<ScreenShare className="lucide" aria-hidden="true" />
|
||
)}
|
||
</span>
|
||
<span className="label">Демонстрация</span>
|
||
</button>
|
||
|
||
<button
|
||
type="button"
|
||
className="tb-btn"
|
||
aria-label="Настройки устройств"
|
||
onClick={onToggleSettings}
|
||
>
|
||
<span className="icon-shell">
|
||
<Settings className="lucide" aria-hidden="true" />
|
||
</span>
|
||
<span className="label">Устройства</span>
|
||
</button>
|
||
|
||
{fullscreenSupported && (
|
||
<button
|
||
type="button"
|
||
className={`tb-btn tb-btn--fullscreen${fullscreenActive ? ' is-panel-open' : ''}`}
|
||
aria-label={fullscreenActive ? 'Выйти из полноэкранного режима' : 'Развернуть на весь экран'}
|
||
aria-pressed={fullscreenActive}
|
||
onClick={onToggleFullscreen}
|
||
>
|
||
<span className="icon-shell">
|
||
{fullscreenActive ? (
|
||
<Minimize className="lucide" aria-hidden="true" />
|
||
) : (
|
||
<Maximize className="lucide" aria-hidden="true" />
|
||
)}
|
||
</span>
|
||
<span className="label">Экран</span>
|
||
</button>
|
||
)}
|
||
|
||
{pipSupported && (
|
||
<button
|
||
type="button"
|
||
className={`tb-btn tb-btn--pip${pipActive ? ' is-panel-open' : ''}`}
|
||
aria-label={pipActive ? 'Свернуть мини-окно' : 'Открыть мини-окно'}
|
||
aria-pressed={pipActive}
|
||
onClick={onTogglePiP}
|
||
>
|
||
<span className="icon-shell">
|
||
<PictureInPicture2 className="lucide" aria-hidden="true" />
|
||
</span>
|
||
<span className="label">Мини-окно</span>
|
||
</button>
|
||
)}
|
||
|
||
{chatVisible && (
|
||
<button
|
||
type="button"
|
||
className={`tb-btn${chatOpen ? ' is-panel-open' : ''}`}
|
||
aria-label={chatOpen ? 'Свернуть чат' : 'Открыть чат'}
|
||
aria-pressed={chatOpen}
|
||
onClick={onToggleChat}
|
||
>
|
||
<span className="icon-shell">
|
||
<MessageSquare className="lucide" aria-hidden="true" />
|
||
{chatUnreadCount > 0 && (
|
||
<span className="badge-count">{chatUnreadCount > 9 ? '9+' : chatUnreadCount}</span>
|
||
)}
|
||
</span>
|
||
<span className="label">Чат</span>
|
||
</button>
|
||
)}
|
||
|
||
<DisconnectButton className="tb-btn danger" aria-label="Выйти из конференции">
|
||
<span className="icon-shell">
|
||
<LogOut className="lucide" aria-hidden="true" />
|
||
</span>
|
||
<span className="label">Выйти</span>
|
||
</DisconnectButton>
|
||
</footer>
|
||
)
|
||
}
|