feat(room): кнопка микрофона в мини-окне (PiP)
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled

Document PiP не показывает тулбар комнаты — во время звонка в мини-окне
нельзя было замьютить себя, не разворачивая основное окно. Кнопка лежит
поверх единственной плитки мини-плеера и использует тот же useTrackToggle,
что и тулбар: React-контекст LiveKitRoom не разрывается порталом в
pipWindow.document.body, поэтому состояние читается и меняется одинаково
что в основном окне, что в мини-плеере.
This commit is contained in:
2026-07-30 00:25:19 +03:00
parent 43cc1ae3e8
commit 5d39e0f076
2 changed files with 50 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useState, type ReactNode } from 'react'
import { EyeOff, Users } from 'lucide-react'
import { EyeOff, Mic, MicOff, Users } from 'lucide-react'
import { Track, type Participant } from 'livekit-client'
import {
CarouselLayout,
@@ -8,6 +8,7 @@ import {
isTrackReference,
useRoomContext,
useSpeakingParticipants,
useTrackToggle,
useTracks,
type TrackReferenceOrPlaceholder,
} from '@livekit/components-react'
@@ -93,6 +94,29 @@ function hasLiveVideo(track: TrackReferenceOrPlaceholder): boolean {
return isTrackReference(track) && !track.publication.isMuted
}
/**
* Кнопка микрофона поверх единственной плитки мини-плеера (задача A5): в
* Document PiP своего тулбара нет вовсе, а звонок продолжается — без неё
* замьютить себя можно, только развернув основное окно. `useTrackToggle` —
* тот же хук, что и в `RoomToolbar`, читает/меняет состояние ЧЕРЕЗ
* `RoomContext`, а не через DOM, поэтому работает одинаково что в основном
* окне, что в портале, рендерящем эту сцену внутрь `pipWindow.document.body`
* (React-контекст порталом не разрывается, в отличие от DOM-дерева).
*/
function PipMicToggle() {
const mic = useTrackToggle({ source: Track.Source.Microphone })
return (
<button
type="button"
{...mic.buttonProps}
className={`room-pip-mic-toggle${mic.enabled ? '' : ' is-off'}`}
aria-label={mic.enabled ? 'Выключить микрофон' : 'Включить микрофон'}
>
{mic.enabled ? <Mic className="lucide" aria-hidden="true" /> : <MicOff className="lucide" aria-hidden="true" />}
</button>
)
}
/**
* Основная сцена конференции: превью остальных участников + крупная плитка
* активного спикера (FocusLayoutContainer + CarouselLayout при нескольких
@@ -366,6 +390,7 @@ export function RoomStage({
return (
<section className="stage room-single-tile">
{focusTrack && <RoomParticipantTile trackRef={focusTrack} onStopSharing={handleStopSharing} />}
<PipMicToggle />
<RoomAudioRenderer />
</section>
)