mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-27 18:56:58 +00:00
✨(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:
committed by
aleb_the_flash
parent
faa86b8293
commit
4456137948
@@ -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 { useIsMobile } from '@/utils/useIsMobile'
|
||||||
import { navigateTo } from '@/navigation/navigateTo'
|
import { navigateTo } from '@/navigation/navigateTo'
|
||||||
import { connectionObserverStore } from '@/stores/connectionObserver'
|
import { connectionObserverStore } from '@/stores/connectionObserver'
|
||||||
import { PictureInPicturePortal } from '@/features/pip/components/PictureInPicturePortal'
|
import { PictureInPictureConference } from '@/features/pip/components/PictureInPictureConference'
|
||||||
import { Spinner } from '@/primitives/Spinner'
|
|
||||||
|
|
||||||
export const Conference = ({
|
export const Conference = ({
|
||||||
roomId,
|
roomId,
|
||||||
@@ -293,9 +292,7 @@ export const Conference = ({
|
|||||||
{...mediaDeviceError}
|
{...mediaDeviceError}
|
||||||
onClose={() => setMediaDeviceError({ error: null, kind: null })}
|
onClose={() => setMediaDeviceError({ error: null, kind: null })}
|
||||||
/>
|
/>
|
||||||
<PictureInPicturePortal>
|
<PictureInPictureConference />
|
||||||
<Spinner />
|
|
||||||
</PictureInPicturePortal>
|
|
||||||
</LiveKitRoom>
|
</LiveKitRoom>
|
||||||
</Screen>
|
</Screen>
|
||||||
</QueryAware>
|
</QueryAware>
|
||||||
|
|||||||
@@ -236,7 +236,8 @@
|
|||||||
"title": "Ihr Videoanruf befindet sich in einem anderen Fenster.",
|
"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.",
|
"description": "Im Bild-im-Bild-Modus bleiben Sie mit dem Anruf verbunden, während Sie andere Aufgaben erledigen.",
|
||||||
"bringBack": "Anruf hierher zurückholen"
|
"bringBack": "Anruf hierher zurückholen"
|
||||||
}
|
},
|
||||||
|
"controlBar": "Besprechungssteuerung"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"buttonLabel": "Weitere Optionen",
|
"buttonLabel": "Weitere Optionen",
|
||||||
|
|||||||
@@ -236,7 +236,8 @@
|
|||||||
"title": "Your video call is in another window.",
|
"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.",
|
"description": "Picture-in-Picture mode allows you to stay connected to the call while performing other tasks.",
|
||||||
"bringBack": "Bring the call back here"
|
"bringBack": "Bring the call back here"
|
||||||
}
|
},
|
||||||
|
"controlBar": "Meeting controls"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"buttonLabel": "More Options",
|
"buttonLabel": "More Options",
|
||||||
|
|||||||
@@ -236,7 +236,8 @@
|
|||||||
"title": "Votre appel vidéo est dans une autre fenêtre.",
|
"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.",
|
"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"
|
"bringBack": "Ramener l'appel ici"
|
||||||
}
|
},
|
||||||
|
"controlBar": "Commandes de la réunion"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"buttonLabel": "Plus d'options",
|
"buttonLabel": "Plus d'options",
|
||||||
|
|||||||
@@ -236,7 +236,8 @@
|
|||||||
"title": "Uw videogesprek bevindt zich in een ander venster.",
|
"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.",
|
"description": "Met de beeld-in-beeld-modus kunt u verbonden blijven met het gesprek terwijl u andere taken uitvoert.",
|
||||||
"bringBack": "Gesprek hier terughalen"
|
"bringBack": "Gesprek hier terughalen"
|
||||||
}
|
},
|
||||||
|
"controlBar": "Vergaderbesturing"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"buttonLabel": "Meer opties",
|
"buttonLabel": "Meer opties",
|
||||||
|
|||||||
Reference in New Issue
Block a user