fix(room): мини-окно игнорировало закрепление, а демонстрация слетала от реплики
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled

Мини-плеер намеренно вёл себя иначе, чем основное окно: без удержания
демонстрации экрана (holdScreenShare), без приоритета говорящего с
включённой камерой, без антидребезга говорящего и с собственным чистым
useState для закрепления. На практике это читалось как поломка —
закрепление, сделанное в основном окне, в мини-окне не действовало, а
демонстрация экрана пропадала, стоило кому-то сказать слово.

Теперь pickStageFocus получает одинаковые правила в обоих вариантах
сцены. Единственное сознательное отличие — localKey («показать себя»
последним фолбэком), он остаётся только у мини-плеера: это защита от
дефекта 0.0.11, когда мини-окно открывалось на самом пользователе.

Закрепление переезжает между окнами тем же мостиком через RoomPage,
что и фокус (initialPinnedKey/onPinnedKeyChange). Отдельный общий
источник правды не нужен: экземпляр сцены в каждый момент ровно один —
пока открыт Document PiP, основное окно показывает заглушку.

Заодно в снятии закрепления «участник вышел из комнаты» добавлена
охрана tracksKnown. На первом рендере нового экземпляра сцены useTracks
отдаёт пустой массив, и пустой набор читался как «все вышли»: приехавшее
через initialPinnedKey закрепление обнулялось прямо при монтировании,
то есть мини-плеер терял его каждый раз.

Надпись на булавке — «Закрепить» вместо «Закрепить в основном окне»:
закрепление больше не ограничено основным окном.
This commit is contained in:
2026-08-03 18:21:43 +03:00
parent a9e24f6692
commit fa8270c156
4 changed files with 113 additions and 47 deletions

View File

@@ -208,6 +208,12 @@ export function RoomPage() {
// открывал мини-окно на самом пользователе. Подробнее — докстринг `RoomStage`.
const [stageFocusKey, setStageFocusKey] = useState<string | null>(null)
// Закрепление участника (булавка на плитке) — по той же причине и тем же
// мостиком, что и `stageFocusKey`: экземпляр `RoomStage` при открытии
// мини-плеера пересоздаётся, и до 0.0.25 закрепление, сделанное в основном
// окне, в мини-окно не попадало вовсе (там был свой чистый `useState`).
const [stagePinnedKey, setStagePinnedKey] = useState<string | null>(null)
// Сохранённый выбор устройств — читаем через собственный вызов
// usePersistentUserChoices (независимый от того, что использует
// DeviceSettingsDialog: там свой вызов хука со своим состоянием). ВАЖНО:
@@ -321,6 +327,8 @@ export function RoomPage() {
onHideOthers={() => setHideOthers(true)}
initialFocusKey={stageFocusKey}
onFocusKeyChange={setStageFocusKey}
initialPinnedKey={stagePinnedKey}
onPinnedKeyChange={setStagePinnedKey}
onPinFocus={handlePinFocus}
raisedHandIdentities={raisedHandIdentities}
conferenceId={joinState.conferenceId}
@@ -373,10 +381,17 @@ export function RoomPage() {
`variant="pip"` — мини-плеер
показывает только активное окно (одну плитку), без карусели/грида
основного окна. `initialFocusKey` — то, что было крупно в основном
окне: без него мини-окно открывалось на самом пользователе. */}
окне: без него мини-окно открывалось на самом пользователе;
`initialPinnedKey` — закрепление оттуда же. */}
{pip.pipWindow &&
createPortal(
<RoomStage variant="pip" initialFocusKey={stageFocusKey} onFocusKeyChange={setStageFocusKey} />,
<RoomStage
variant="pip"
initialFocusKey={stageFocusKey}
onFocusKeyChange={setStageFocusKey}
initialPinnedKey={stagePinnedKey}
onPinnedKeyChange={setStagePinnedKey}
/>,
pip.pipWindow.document.body,
)}
<ForcedMuteWatcher event={chat.lastForcedMute} />