From 5a96d1769ed0a51699397b8e9b40e7fe56bbc57b Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 27 Jul 2026 18:19:01 +0200 Subject: [PATCH] Only show layout switch when it has an effect on the layout --- src/state/CallViewModel/CallViewModel.test.ts | 13 ++- src/state/CallViewModel/CallViewModel.ts | 82 ++++++++++++------- 2 files changed, 62 insertions(+), 33 deletions(-) diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index 0f77a2db9..7933039f3 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -536,11 +536,15 @@ describe.each([ withTestScheduler(({ behavior, expectObservable }) => { // Starts as a one-on-one call, then Alice shares her screen, then Bob // joins, and finally Alice stops sharing her screen - const participantInputMarbles = " a--b"; - const aliceSharingInputMarbles = "ny-n"; + const participantInputMarbles = " a--b"; + const aliceSharingInputMarbles = " ny-n"; // Starts in one-on-one mobile layout, then goes to spotlight layout for // the screen sharing and group call cases - const expectedLayoutMarbles = " ab-c"; + const expectedLayoutMarbles = " ab-c"; + // Whether the layout switch is visible. It should be hidden while in + // one-on-one layout. + const expectedLayoutSwitchMarbles = "ny--"; + withCallViewModel( { remoteParticipants$: behavior(participantInputMarbles, { @@ -579,6 +583,9 @@ describe.each([ }, }, ); + expectObservable( + vm.layoutSwitchVm$.pipe(map((vm) => vm !== null)), + ).toBe(expectedLayoutSwitchMarbles, yesNo); }, ); }); diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 5a066453e..6fac5ee5a 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -1108,41 +1108,45 @@ export function createCallViewModel$( ), ); - const oneOnOneLayoutMedia$: Observable<{ + const oneOnOneLayoutMedia$: Behavior<{ local: LocalUserMediaViewModel; remote: UserMediaViewModel | RingingMediaViewModel; - } | null> = combineLatest([userMedia$, screenShares$]).pipe( - switchMap(([userMedia, screenShares]) => { - // One-on-one layout only supports 2 user media, no screen shares - if (userMedia.length <= 2 && screenShares.length === 0) { - const local = userMedia.find( - (vm): vm is WrappedUserMediaViewModel & LocalUserMediaViewModel => - vm.type === "user" && vm.local, - ); - - if (local !== undefined) { - const remote = userMedia.find( - (vm): vm is WrappedUserMediaViewModel & RemoteUserMediaViewModel => - vm.type === "user" && !vm.local, + } | null> = scope.behavior( + combineLatest([userMedia$, screenShares$]).pipe( + switchMap(([userMedia, screenShares]) => { + // One-on-one layout only supports 2 user media, no screen shares + if (userMedia.length <= 2 && screenShares.length === 0) { + const local = userMedia.find( + (vm): vm is WrappedUserMediaViewModel & LocalUserMediaViewModel => + vm.type === "user" && vm.local, ); - if (remote !== undefined) return of({ local, remote }); - - // If there's no other user media in the call (could still happen in - // this branch due to the duplicate tiles option), we could possibly - // show ringing media instead - if (userMedia.length === 1) - return ringingMedia$.pipe( - map( - (ringingMedia) => - ringingMedia && { local, remote: ringingMedia }, - ), + if (local !== undefined) { + const remote = userMedia.find( + ( + vm, + ): vm is WrappedUserMediaViewModel & RemoteUserMediaViewModel => + vm.type === "user" && !vm.local, ); - } - } - return of(null); - }), + if (remote !== undefined) return of({ local, remote }); + + // If there's no other user media in the call (could still happen in + // this branch due to the duplicate tiles option), we could possibly + // show ringing media instead + if (userMedia.length === 1) + return ringingMedia$.pipe( + map( + (ringingMedia) => + ringingMedia && { local, remote: ringingMedia }, + ), + ); + } + } + + return of(null); + }), + ), ); const oneOnOneDesktopLayoutMedia$: Observable = @@ -1358,6 +1362,22 @@ export function createCallViewModel$( layoutMedia$.pipe(map(({ edgeToEdge }) => edgeToEdge)), ); + // Only show the layout switch in cases where it has an effect on the layout + const showLayoutSwitch$ = windowMode$.pipe( + switchMap((windowMode) => { + switch (windowMode) { + case "normal": + return of(true); + case "flat": + return oneOnOneLayoutMedia$.pipe( + map((oneOnOne) => oneOnOne === null), + ); + default: + return of(false); + } + }), + ); + const screenTap$ = new Subject(); const controlsTap$ = new Subject(); const screenHover$ = new Subject(); @@ -1775,7 +1795,9 @@ export function createCallViewModel$( spotlightExpanded$: spotlightExpanded$, toggleSpotlightExpanded$: toggleSpotlightExpanded$, - layoutSwitchVm$: constant(layoutSwitchVm), + layoutSwitchVm$: scope.behavior( + showLayoutSwitch$.pipe(map((show) => (show ? layoutSwitchVm : null))), + ), layout$: layout$, localMatrixLivekitMember$, remoteMatrixLivekitMembers$: scope.behavior(