(frontend) add a basic control bar to the PiP window

The PiP control bar shares state with the main window's control
bar — most importantly, the reaction toolbar and the mic/camera
toggles, which are fully stateful.

Sharing state works naturally here because the PiP content is
rendered through a portal into the same React tree, so the
controls in both windows read from and write to the same stores.

Co-authored-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
lebaudantoine
2026-05-20 16:52:29 +02:00
committed by aleb_the_flash
parent faa86b8293
commit 4456137948
9 changed files with 170 additions and 9 deletions
@@ -0,0 +1,8 @@
import { PictureInPicturePortal } from '@/features/pip/components/PictureInPicturePortal'
import { PipView } from '@/features/pip/components/PipView'
export const PictureInPictureConference = () => (
<PictureInPicturePortal>
<PipView />
</PictureInPicturePortal>
)
@@ -0,0 +1,64 @@
import { styled } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
import { AudioDevicesControl } from '@/features/rooms/livekit/components/controls/Device/AudioDevicesControl'
import { VideoDeviceControl } from '@/features/rooms/livekit/components/controls/Device/VideoDeviceControl'
import { ScreenShareToggle } from '@/features/rooms/livekit/components/controls/ScreenShareToggle'
import { LeaveButton } from '@/features/rooms/livekit/components/controls/LeaveButton'
import { HandToggle } from '@/features/rooms/livekit/components/controls/HandToggle'
import { StartMediaButton } from '@/features/rooms/livekit/components/controls/StartMediaButton'
import { ReactionsToggle } from '@/features/reactions/components/ReactionsToggle'
export const PipControlBar = ({
showScreenShare,
}: {
showScreenShare: boolean
}) => {
const { t } = useTranslation('rooms', {
keyPrefix: 'pictureInPicture',
})
return (
<PipControls
id="pip-control-bar"
role="toolbar"
aria-label={t('controlBar')}
>
<PipControlsCenter>
<AudioDevicesControl hideMenu />
<VideoDeviceControl hideMenu />
<ReactionsToggle />
{showScreenShare && <ScreenShareToggle />}
<HandToggle />
<StartMediaButton />
<LeaveButton />
</PipControlsCenter>
</PipControls>
)
}
const PipControls = styled('div', {
base: {
flex: '0 0 auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '0.5rem',
padding: '1.125rem',
width: '100%',
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
},
})
const PipControlsCenter = styled('div', {
base: {
display: 'flex',
flexWrap: 'nowrap',
justifyContent: 'center',
alignItems: 'center',
gap: '0.4rem',
flex: '1 1 auto',
},
})
@@ -0,0 +1,46 @@
import { useSnapshot } from 'valtio'
import { reactionsStore } from '@/stores/reactions'
import { useMemo, useRef } from 'react'
import { FloatingReaction } from '@/features/reactions/components/ReactionPortals'
import { Reaction } from '@/features/reactions/types'
import { css } from '@/styled-system/css'
export const PipFloatingReactions = () => {
const { reactions } = useSnapshot(reactionsStore)
return (
<>
{reactions.map((reaction) => (
<PipFloatingReaction key={reaction.id} reaction={reaction} />
))}
</>
)
}
const PipFloatingReaction = ({ reaction }: { reaction: Reaction }) => {
const containerRef = useRef<HTMLDivElement>(null)
const speed = useMemo(() => Math.random() * 1.5 + 0.5, [])
const scale = useMemo(() => Math.max(Math.random() + 0.5, 1), [])
return (
<div
ref={containerRef}
className={css({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
overflow: 'hidden',
})}
>
<FloatingReaction
emoji={reaction.emoji}
speed={speed}
scale={scale}
name={reaction.participantName}
isLocal={reaction.isLocal}
/>
</div>
)
}
@@ -0,0 +1,42 @@
import { PipControlBar } from './PipControlBar'
import { PipFloatingReactions } from './PipFloatingReactions'
import { ReactionsToolbar } from '@/features/reactions/components/toolbar/ReactionsToolbar'
import { styled } from '@/styled-system/jsx'
const Container = styled('div', {
base: {
position: 'relative',
width: '100%',
height: '100%',
display: 'grid',
gridTemplateRows: 'minmax(0, 1fr) auto auto',
backgroundColor: 'primaryDark.50',
// Disable LiveKit's own border-radius on tiles so our containers
// (GridCell, Thumbnail, StageFrame) own the clipping exclusively.
'--lk-border-radius': '4px',
'& .lk-participant-tile': {
height: '100%',
},
'& .lk-participant-media': {
height: '100%',
},
'& .lk-participant-media-video': {
height: '100%',
objectFit: 'cover',
},
'& .lk-grid-layout': {
height: '100%',
width: '100%',
},
},
})
export const PipView = () => {
return (
<Container>
<ReactionsToolbar adjustedCentering={false} />
<PipControlBar showScreenShare={false} />
<PipFloatingReactions />
</Container>
)
}
@@ -32,8 +32,7 @@ import { isFireFox } from '@/utils/livekit'
import { useIsMobile } from '@/utils/useIsMobile'
import { navigateTo } from '@/navigation/navigateTo'
import { connectionObserverStore } from '@/stores/connectionObserver'
import { PictureInPicturePortal } from '@/features/pip/components/PictureInPicturePortal'
import { Spinner } from '@/primitives/Spinner'
import { PictureInPictureConference } from '@/features/pip/components/PictureInPictureConference'
export const Conference = ({
roomId,
@@ -293,9 +292,7 @@ export const Conference = ({
{...mediaDeviceError}
onClose={() => setMediaDeviceError({ error: null, kind: null })}
/>
<PictureInPicturePortal>
<Spinner />
</PictureInPicturePortal>
<PictureInPictureConference />
</LiveKitRoom>
</Screen>
</QueryAware>
+2 -1
View File
@@ -236,7 +236,8 @@
"title": "Ihr Videoanruf befindet sich in einem anderen Fenster.",
"description": "Im Bild-im-Bild-Modus bleiben Sie mit dem Anruf verbunden, während Sie andere Aufgaben erledigen.",
"bringBack": "Anruf hierher zurückholen"
}
},
"controlBar": "Besprechungssteuerung"
},
"options": {
"buttonLabel": "Weitere Optionen",
+2 -1
View File
@@ -236,7 +236,8 @@
"title": "Your video call is in another window.",
"description": "Picture-in-Picture mode allows you to stay connected to the call while performing other tasks.",
"bringBack": "Bring the call back here"
}
},
"controlBar": "Meeting controls"
},
"options": {
"buttonLabel": "More Options",
+2 -1
View File
@@ -236,7 +236,8 @@
"title": "Votre appel vidéo est dans une autre fenêtre.",
"description": "Le mode image dans l'image vous permet de rester connecté à l'appel tout en effectuant d'autres tâches.",
"bringBack": "Ramener l'appel ici"
}
},
"controlBar": "Commandes de la réunion"
},
"options": {
"buttonLabel": "Plus d'options",
+2 -1
View File
@@ -236,7 +236,8 @@
"title": "Uw videogesprek bevindt zich in een ander venster.",
"description": "Met de beeld-in-beeld-modus kunt u verbonden blijven met het gesprek terwijl u andere taken uitvoert.",
"bringBack": "Gesprek hier terughalen"
}
},
"controlBar": "Vergaderbesturing"
},
"options": {
"buttonLabel": "Meer opties",