diff --git a/.gitignore b/.gitignore index 4f5dbf29..608168fe 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ db.sqlite3 # IDEs .idea/ .vscode/ +.cursor/ *.iml .devcontainer diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eb62c0a..ad5ac938 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to - ✨(frontend) add participant color gradient when camera is off #1490 - ✨(all) allow forcing SSO display name for authenticated users - ➕(frontend) install vite-plugin-static-copy for MediaPipe WASM assets +- ✨(frontend) share OneToOneFocusLayout between PiP and main room ### Changed diff --git a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx index 13d476fb..d7c7a645 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/VideoConference.tsx @@ -6,7 +6,7 @@ import { log, } from '@livekit/components-core' import { type Participant, RoomEvent, Track } from 'livekit-client' -import React, { useCallback, useRef, useState, useEffect } from 'react' +import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react' import { ConnectionStateToast, FocusLayoutContainer, @@ -40,6 +40,7 @@ import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce' import { ReactionPortals } from '@/features/reactions/components/ReactionPortals' import { CarouselLayout } from '@/features/layout/components/CarouselLayout' import { GridLayout } from '@/features/layout/components/GridLayout' +import { OneToOneFocusLayout } from '@/features/layout/components/OneToOneFocusLayout' import { RoomContentArea } from '@/features/layout/components/RoomContentArea' import { usePictureInPicture } from '@/features/pip/hooks/usePictureInPicture' import { PipRoomPlaceholder } from '@/features/pip/components/PipRoomPlaceholder' @@ -123,6 +124,26 @@ export function VideoConference({ ...props }: VideoConferenceProps) { (track) => !isEqualTrackRef(track, focusTrack) ) + const cameraTracks = useMemo( + () => tracks.filter((t) => t.source === Track.Source.Camera), + [tracks] + ) + + const isOneToOne = !focusTrack && cameraTracks.length <= 2 + + const oneToOneMainTrack = useMemo(() => { + if (!isOneToOne) return undefined + const remote = cameraTracks.find((t) => !t.participant?.isLocal) + const local = cameraTracks.find((t) => t.participant?.isLocal) + return remote ?? local + }, [isOneToOne, cameraTracks]) + + const oneToOneThumbnailTrack = useMemo(() => { + if (!isOneToOne) return undefined + const local = cameraTracks.find((t) => t.participant?.isLocal) + return oneToOneMainTrack === local ? undefined : local + }, [isOneToOne, cameraTracks, oneToOneMainTrack]) + const { isOpen: isPictureInPictureOpen } = usePictureInPicture() // handle pin announcements @@ -258,7 +279,12 @@ export function VideoConference({ ...props }: VideoConferenceProps) { ) : ( <> - {!focusTrack ? ( + {isOneToOne ? ( + + ) : !focusTrack ? (