diff --git a/frontend/src/components/room/RoomStage.tsx b/frontend/src/components/room/RoomStage.tsx index f6c5858..4f4939e 100644 --- a/frontend/src/components/room/RoomStage.tsx +++ b/frontend/src/components/room/RoomStage.tsx @@ -179,6 +179,7 @@ export function RoomStage({ onHideOthers, initialFocusKey = null, onFocusKeyChange, + onPinFocus, }: { variant?: 'full' | 'pip' /** Выбранный пользователем режим показа; игнорируется при `variant="pip"`. */ @@ -193,6 +194,14 @@ export function RoomStage({ initialFocusKey?: string | null /** Сообщать наружу текущий фокус, чтобы его пережил переезд сцены в мини-плеер и обратно. */ onFocusKeyChange?: (key: string | null) => void + /** + * Участника только что закрепили (не открепили) в режиме без крупной + * плитки — сцена сама переключиться не может (режим живёт в `RoomPage`), + * поэтому просит переключить на `standard`, где закреплённого есть куда + * показать крупно. Открепление НЕ вызывает колбэк — вид назад не + * переключаем (см. докстринг `RoomPage`, обоснование решения в коммите). + */ + onPinFocus?: () => void }) { const room = useRoomContext() const isCompact = useIsCompactViewport() @@ -337,9 +346,16 @@ export function RoomStage({ * Закрепить/открепить плитку: повторное нажатие на уже закреплённой снимает * закрепление. Ключ приходит из самой плитки (она знает свой трек из * контекста — в карусели/гриде плитки рендерятся шаблоном, без пропсов). + * + * Закрепление доступно из ЛЮБОГО режима показа (задача 3.1 → 3.3): в + * плиточных режимах крупной плитки нет, поэтому само закрепление (не + * открепление) просит `RoomPage` переключить вид на `standard` — + * закреплённого иначе негде показать крупно. */ function handleTogglePin(key: string) { - setPinnedKey((prev) => (prev === key ? null : key)) + const willPin = pinnedKey !== key + setPinnedKey(willPin ? key : null) + if (willPin) onPinFocus?.() } // Мини-плеер показывает ТОЛЬКО активное окно — без карусели/ @@ -372,23 +388,23 @@ export function RoomStage({ : [] : carouselTracks const showCarousel = !hideOthers && sideTracks.length > 0 - // Закрепление имеет смысл только там, где есть «крупная плитка» — - // в плиточных режимах фокуса нет, поэтому и булавки на плитках нет. - const pinProps = - effectiveMode === 'standard' ? { pinnedKey, onTogglePin: handleTogglePin } : {} + // Булавка закрепления есть на КАЖДОЙ плитке во всех режимах (задача 3.3): + // сама кнопка вызывает переключение на `standard`, где закреплённый и + // попадёт в фокус (см. `handleTogglePin`/`onPinFocus`). + const pinProps = { pinnedKey, onTogglePin: handleTogglePin } function renderMain(): ReactNode { if (effectiveMode === 'tiles') { return ( - + ) } if (effectiveMode === 'live-tiles') { return ( 0 ? liveCameraTracks : cameraTracks}> - + ) } diff --git a/frontend/src/pages/RoomPage.tsx b/frontend/src/pages/RoomPage.tsx index 18b330e..f807a71 100644 --- a/frontend/src/pages/RoomPage.tsx +++ b/frontend/src/pages/RoomPage.tsx @@ -169,6 +169,15 @@ export function RoomPage() { saveStageLayoutMode(mode) }, []) + // Закрепление участника из плиточного режима переключает вид на «Стандарт» + // (см. докстринг `RoomStage`, `onPinFocus`) — но это НЕ то же самое, что + // явный выбор пользователя: переключение не сохраняется в localStorage + // (в отличие от `handleLayoutModeChange`), иначе разовое закрепление в этой + // встрече поменяло бы устойчивое предпочтение режима для всех следующих. + const handlePinFocus = useCallback(() => { + setLayoutMode('standard') + }, []) + // Ключ трека, который сцена показывает крупно. Живёт ЗДЕСЬ, а не только // внутри `RoomStage`, потому что при открытии мини-плеера сцена // размонтируется в основном окне и монтируется заново в PiP-окне (портал @@ -267,6 +276,7 @@ export function RoomPage() { onHideOthers={() => setHideOthers(true)} initialFocusKey={stageFocusKey} onFocusKeyChange={setStageFocusKey} + onPinFocus={handlePinFocus} /> )} {chatVisible && chatOpen && (