diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx index 37e87854..d8bb285b 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Options/OptionsMenuItems.tsx @@ -7,6 +7,7 @@ import { EffectsMenuItem } from './EffectsMenuItem' import { SupportMenuItem } from './SupportMenuItem' import { TranscriptMenuItem } from './TranscriptMenuItem' import { ScreenRecordingMenuItem } from './ScreenRecordingMenuItem' +import { PictureInPictureMenuItem } from './PictureInPictureMenuItem' // @todo try refactoring it to use MenuList component export const OptionsMenuItems = () => { @@ -21,6 +22,7 @@ export const OptionsMenuItems = () => { + diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Options/PictureInPictureMenuItem.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Options/PictureInPictureMenuItem.tsx new file mode 100644 index 00000000..09d4777b --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/components/controls/Options/PictureInPictureMenuItem.tsx @@ -0,0 +1,24 @@ +import { MenuItem } from 'react-aria-components' +import { useTranslation } from 'react-i18next' +import { RiPictureInPicture2Line } from '@remixicon/react' +import { menuRecipe } from '@/primitives/menuRecipe' +import { useRoomPiP } from '../../../hooks/useRoomPiP' + +export const PictureInPictureMenuItem = () => { + const { t } = useTranslation('rooms', { keyPrefix: 'options.items' }) + const { isSupported, isOpen, toggle } = useRoomPiP() + + // Hide the entry when the browser doesn't support Document PiP. + if (!isSupported) return null + + return ( + + + {isOpen ? t('pictureInPicture.exit') : t('pictureInPicture.enter')} + + ) +} + diff --git a/src/frontend/src/features/rooms/livekit/hooks/RoomPiPProvider.tsx b/src/frontend/src/features/rooms/livekit/hooks/RoomPiPProvider.tsx new file mode 100644 index 00000000..3726daf7 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/hooks/RoomPiPProvider.tsx @@ -0,0 +1,39 @@ +import { useCallback, useMemo, useState } from 'react' +import { RoomPiPContext } from './roomPiPContext' + +export const RoomPiPProvider = ({ + children, +}: { + children: React.ReactNode +}) => { + const [isOpen, setIsOpen] = useState(false) + + // Feature detection for Document Picture-in-Picture. + const isSupported = + typeof window !== 'undefined' && 'documentPictureInPicture' in window + + const open = useCallback(() => setIsOpen(true), []) + const close = useCallback(() => setIsOpen(false), []) + const toggle = useCallback( + () => setIsOpen((current) => !current), + [] + ) + + const value = useMemo( + () => ({ + isSupported, + isOpen, + open, + close, + toggle, + }), + [close, isOpen, isSupported, open, toggle] + ) + + return ( + + {children} + + ) +} + diff --git a/src/frontend/src/features/rooms/livekit/hooks/roomPiPContext.ts b/src/frontend/src/features/rooms/livekit/hooks/roomPiPContext.ts new file mode 100644 index 00000000..075289b2 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/hooks/roomPiPContext.ts @@ -0,0 +1,12 @@ +import { createContext } from 'react' + +export type RoomPiPContextValue = { + isSupported: boolean + isOpen: boolean + open: () => void + close: () => void + toggle: () => void +} + +export const RoomPiPContext = createContext(null) + diff --git a/src/frontend/src/features/rooms/livekit/hooks/useRoomPiP.tsx b/src/frontend/src/features/rooms/livekit/hooks/useRoomPiP.tsx new file mode 100644 index 00000000..d1bc4bc2 --- /dev/null +++ b/src/frontend/src/features/rooms/livekit/hooks/useRoomPiP.tsx @@ -0,0 +1,11 @@ +import { useContext } from 'react' +import { RoomPiPContext } from './roomPiPContext' + +export const useRoomPiP = () => { + const context = useContext(RoomPiPContext) + if (!context) { + throw new Error('useRoomPiP must be used within a RoomPiPProvider') + } + return context +} + diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index c81dfc68..466d36e2 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -34,6 +34,8 @@ import { SettingsDialogExtendedKey } from '@/features/settings/type' import { useVideoResolutionSubscription } from '../hooks/useVideoResolutionSubscription' import { SettingsDialogProvider } from '@/features/settings/components/SettingsDialogProvider' import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal' +import { RoomPiP } from '../components/RoomPiP' +import { RoomPiPProvider } from '../hooks/RoomPiPProvider' import { getParticipantName } from '@/features/rooms/utils/getParticipantName' import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce' import { ReactionPortals } from '@/features/reactions/components/ReactionPortals' @@ -238,10 +240,11 @@ export function VideoConference({ ...props }: VideoConferenceProps) { }} > {isWeb() && ( - + + setIsShareErrorVisible(false)} @@ -289,7 +292,9 @@ export function VideoConference({ ...props }: VideoConferenceProps) { }} /> - + + + )}