Files
vidconf/frontend/src/pages/LobbyPage.tsx
Max Ronzhin 173d384f06
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
feat(admin): рычаги нагрузки медиа — потолок качества публикации и лимит плиток
instance_settings.media_limits (publish_quality_cap: off/720p/360p/180p,
stage_max_tiles: 4/9/16/25) — новая вкладка «Нагрузка» в админке, дефолты
(off/25) сохраняют текущее поведение существующих инсталляций.

Настройка отдаётся не только GET /admin/settings, но и в join-ответе
(JoinOut) — участнику нужно иметь её на руках ДО публикации трека, а
/admin/settings доступен только администратору.

Потолок качества применяется через RoomOptions.publishDefaults
(videoEncoding + videoSimulcastLayers на пресетах VideoPresets LiveKit) —
режет битрейт верхнего слоя симулкаста, реальное разрешение WebRTC
подстраивает сам. Лимит плиток — фильтрация STAGE_GRID_LAYOUTS по
columns*rows в StageGrid, лишние участники уходят на страницу пагинации
вместо подписки.

Значение приезжает в joinState вместе с токеном ДО первого рендера
LiveKitRoom (RoomPage не рендерит его, пока joinState не заполнен целиком),
поэтому смена настройки не переподключает уже вошедшего участника —
roomOptions пересчитывается по стабильной ссылке на joinState, которая
после подключения не меняется.

Проверено вживую на локальном стенде (docker compose --profile media):
сохранение/персист настроек, join отдаёт актуальные значения, уже
подключённый участник не разрывается при смене настройки в другом окне.
2026-08-02 19:53:23 +03:00

123 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Calendar, Link2, User, Video } from 'lucide-react'
import { ShellTopbar } from '@/components/layout/ShellTopbar'
import { AppFooter } from '@/components/layout/AppFooter'
import { LogoMedallion } from '@/components/ui/LogoMark'
import { createConference } from '@/api/conferences'
import { useToast } from '@/components/ui/ToastProvider'
import '@/styles/lobby.css'
/**
* Лобби — «хаб действий» (см. design/mockups/lobby.html).
*
* Предустановленных переговорных комнат больше нет: конференции создаются
* динамически. Мозаика из 4 плиток — единственный вход в остальные сценарии:
* мгновенное создание, календарь плановых конференций, вход по ссылке/номеру,
* «Мои конференции».
*
* Отклонение от макета: топбар здесь — переиспользуемый `ShellTopbar` (прямые
* ссылки Лобби/Календарь/Админка + пилюля пользователя), а не бургер-меню из
* lobby.html. `ShellTopbar` — общий компонент светлой оболочки (используется
* также в календаре/админке), поэтому бургер-меню из макета здесь
* сознательно не воспроизводится.
*/
export function LobbyPage() {
const navigate = useNavigate()
const toast = useToast()
const [creating, setCreating] = useState(false)
async function handleCreate() {
if (creating) return
setCreating(true)
try {
const conference = await createConference({})
if (!conference.join) {
toast.show('Не удалось создать конференцию. Попробуйте ещё раз', 'error')
return
}
navigate(`/room/${conference.slug}`, {
state: {
livekitUrl: conference.join.livekit_url,
token: conference.join.token,
title: conference.title,
conferenceId: conference.join.conference_id,
chatEnabled: conference.join.chat_enabled,
publishQualityCap: conference.join.publish_quality_cap,
stageMaxTiles: conference.join.stage_max_tiles,
number: conference.number,
slug: conference.slug,
},
})
} catch {
toast.show('Не удалось создать конференцию. Попробуйте ещё раз', 'error')
} finally {
setCreating(false)
}
}
return (
<div className="lobby-shell">
<ShellTopbar />
<main className="hub-main">
<div className="hub-wrap">
<div className="hub-intro">
<p className="eyebrow">[ ДОБРО ПОЖАЛОВАТЬ В VIDCONF ]</p>
<h1>Что делаем сегодня?</h1>
</div>
<div className="hub">
<button
type="button"
className="action action--create"
onClick={handleCreate}
disabled={creating}
aria-busy={creating}
>
<span className="action-icon" aria-hidden="true">
<Video className="icon" aria-hidden="true" />
</span>
<h2 className="action-title">Создать</h2>
<p className="action-object">конференцию</p>
<p className="action-note">{creating ? 'Создаём…' : 'начнётся сразу — вы первый участник'}</p>
</button>
<button type="button" className="action action--calendar" onClick={() => navigate('/calendar')}>
<span className="action-icon" aria-hidden="true">
<Calendar className="icon" aria-hidden="true" />
</span>
<h2 className="action-title">Календарь</h2>
<p className="action-object">конференций</p>
<p className="action-note">запланировать · закрепить · повторять</p>
</button>
<button type="button" className="action action--join" onClick={() => navigate('/join')}>
<span className="action-icon" aria-hidden="true">
<Link2 className="icon" aria-hidden="true" />
</span>
<h2 className="action-title">Подключиться</h2>
<p className="action-object">к конференции</p>
<p className="action-note">по ссылке или номеру</p>
</button>
<button type="button" className="action action--mine" onClick={() => navigate('/my-conferences')}>
<span className="action-icon" aria-hidden="true">
<User className="icon" aria-hidden="true" />
</span>
<h2 className="action-title">Мои</h2>
<p className="action-object">конференции</p>
<p className="action-note">закреплённые и предстоящие</p>
</button>
<div className="hub-logo" aria-hidden="true">
<LogoMedallion className="logo-mark" />
<span className="logo-word">VidConf</span>
</div>
</div>
</div>
</main>
<AppFooter />
</div>
)
}