From 43cc1ae3e8c27b20968e423fa5a2616597932b40 Mon Sep 17 00:00:00 2001 From: Max Ronzhin Date: Thu, 30 Jul 2026 00:25:02 +0300 Subject: [PATCH] =?UTF-8?q?feat(room):=20=D0=B7=D0=B0=D0=BA=D1=80=D0=B5?= =?UTF-8?q?=D0=BF=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=83=D1=87=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D0=BD=D0=B8=D0=BA=D0=B0=20=D0=B4=D0=BE=D1=81=D1=82=D1=83?= =?UTF-8?q?=D0=BF=D0=BD=D0=BE=20=D0=B8=D0=B7=20=D0=BB=D1=8E=D0=B1=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BA=D0=B0=D0=B7=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Булавка закрепления раньше рендерилась только в «Стандарте» — в «Плитках» и «Живых плитках» её не было вовсе, хотя крупной плитки для показа закреплённого там и так нет. Теперь кнопка есть на любой плитке в любом режиме, а сам клик закрепления (не открепления) переключает вид на «Стандарт» — иначе закреплённого негде показать крупно. Открепление обратно на предыдущий вид не переключает — это разовое действие, а не устойчивый выбор пользователя, дёргать раскладку туда-обратно не нужно. Переключение при закреплении не сохраняется в localStorage (в отличие от явного выбора режима в тулбаре/настройках) по той же причине. Заодно закрывает задачу «закрепление на мобильном»: раньше кнопки не было в плиточных режимах даже там, а на тач-устройствах она и так всегда видна (`@media (hover: none)`), отдельной мобильной доработки не потребовалось. --- frontend/src/components/room/RoomStage.tsx | 30 +++++++++++++++++----- frontend/src/pages/RoomPage.tsx | 10 ++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) 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 && (