From 062e0a00fbdbe6f96ba9364b5189799c5df3876d Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 27 Jul 2026 15:58:01 +0200 Subject: [PATCH 1/4] Remove unused variables from CallViewModel tests --- src/state/CallViewModel/CallViewModel.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index 8c7ef4415..26cdf28ee 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -505,7 +505,7 @@ describe.each([ }); test("one-on-one portrait layout shows name tags in room with 3 members", () => { - withTestScheduler(({ behavior, schedule, expectObservable }) => { + withTestScheduler(({ expectObservable }) => { withCallViewModel( { remoteParticipants$: constant([aliceParticipant]), @@ -1066,7 +1066,7 @@ describe.each([ }); test("expanded spotlight layout hides PiP tile in one-on-one voice call", () => { - withTestScheduler(({ behavior, schedule, expectObservable }) => { + withTestScheduler(({ expectObservable }) => { withCallViewModel( { remoteParticipants$: constant([aliceParticipant]), From 0b21cff020a0253e953916890d58b4c6e39c44d7 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 14 Jul 2026 16:47:03 +0200 Subject: [PATCH 2/4] Rename one-on-one layouts to be orientation-agnostic --- ...e.css => OneOnOneDesktopLayout.module.css} | 0 ...peLayout.tsx => OneOnOneDesktopLayout.tsx} | 14 +++--- ...le.css => OneOnOneMobileLayout.module.css} | 0 ...aitLayout.tsx => OneOnOneMobileLayout.tsx} | 14 +++--- src/room/InCallView.tsx | 8 ++-- .../__snapshots__/InCallView.test.tsx.snap | 6 +-- src/state/CallViewModel/CallViewModel.test.ts | 44 +++++++++---------- src/state/CallViewModel/CallViewModel.ts | 36 +++++++-------- ...capeLayout.ts => OneOnOneDesktopLayout.ts} | 12 ++--- ...traitLayout.ts => OneOnOneMobileLayout.ts} | 12 ++--- src/state/layout-types.ts | 24 +++++----- 11 files changed, 85 insertions(+), 85 deletions(-) rename src/grid/{OneOnOneLandscapeLayout.module.css => OneOnOneDesktopLayout.module.css} (100%) rename src/grid/{OneOnOneLandscapeLayout.tsx => OneOnOneDesktopLayout.tsx} (81%) rename src/grid/{OneOnOnePortraitLayout.module.css => OneOnOneMobileLayout.module.css} (100%) rename src/grid/{OneOnOnePortraitLayout.tsx => OneOnOneMobileLayout.tsx} (78%) rename src/state/{OneOnOneLandscapeLayout.ts => OneOnOneDesktopLayout.ts} (75%) rename src/state/{OneOnOnePortraitLayout.ts => OneOnOneMobileLayout.ts} (79%) diff --git a/src/grid/OneOnOneLandscapeLayout.module.css b/src/grid/OneOnOneDesktopLayout.module.css similarity index 100% rename from src/grid/OneOnOneLandscapeLayout.module.css rename to src/grid/OneOnOneDesktopLayout.module.css diff --git a/src/grid/OneOnOneLandscapeLayout.tsx b/src/grid/OneOnOneDesktopLayout.tsx similarity index 81% rename from src/grid/OneOnOneLandscapeLayout.tsx rename to src/grid/OneOnOneDesktopLayout.tsx index 1e21d1121..ccf24977f 100644 --- a/src/grid/OneOnOneLandscapeLayout.tsx +++ b/src/grid/OneOnOneDesktopLayout.tsx @@ -10,28 +10,28 @@ import { type ReactNode, useCallback, useMemo } from "react"; import { useObservableEagerState } from "observable-hooks"; import classNames from "classnames"; -import { type OneOnOneLandscapeLayout as OneOnOneLandscapeLayoutModel } from "../state/layout-types.ts"; +import { type OneOnOneDesktopLayout as OneOnOneDesktopLayoutModel } from "../state/layout-types.ts"; import { type CallLayout, arrangeTiles } from "./CallLayout"; -import styles from "./OneOnOneLandscapeLayout.module.css"; +import styles from "./OneOnOneDesktopLayout.module.css"; import { type DragCallback, useUpdateLayout } from "./Grid"; import { useBehavior } from "../useBehavior"; /** - * An implementation of the "one-on-one" layout for landscape screens, in which + * An implementation of the "one-on-one" layout for desktop platforms, in which * the remote participant is shown at maximum size, overlaid by a small view of * the local participant. */ -export const makeOneOnOneLandscapeLayout: CallLayout< - OneOnOneLandscapeLayoutModel +export const makeOneOnOneDesktopLayout: CallLayout< + OneOnOneDesktopLayoutModel > = ({ minBounds$ }) => ({ foreground: "fixed", - fixed: function OneOnOneLandscapeLayoutFixed({ ref }): ReactNode { + fixed: function OneOnOneDesktopLayoutFixed({ ref }): ReactNode { useUpdateLayout(); return
; }, - scrolling: function OneOnOneLandscapeLayoutScrolling({ + scrolling: function OneOnOneDesktopLayoutScrolling({ ref, model, Slot, diff --git a/src/grid/OneOnOnePortraitLayout.module.css b/src/grid/OneOnOneMobileLayout.module.css similarity index 100% rename from src/grid/OneOnOnePortraitLayout.module.css rename to src/grid/OneOnOneMobileLayout.module.css diff --git a/src/grid/OneOnOnePortraitLayout.tsx b/src/grid/OneOnOneMobileLayout.tsx similarity index 78% rename from src/grid/OneOnOnePortraitLayout.tsx rename to src/grid/OneOnOneMobileLayout.tsx index 4f7c9f45d..628ade1c8 100644 --- a/src/grid/OneOnOnePortraitLayout.tsx +++ b/src/grid/OneOnOneMobileLayout.tsx @@ -9,23 +9,23 @@ Please see LICENSE in the repository root for full details. import { type ReactNode, useCallback } from "react"; import classNames from "classnames"; -import { type OneOnOnePortraitLayout as OneOnOnePortraitLayoutModel } from "../state/layout-types.ts"; +import { type OneOnOneMobileLayout as OneOnOneMobileLayoutModel } from "../state/layout-types.ts"; import { type CallLayout } from "./CallLayout"; -import styles from "./OneOnOnePortraitLayout.module.css"; +import styles from "./OneOnOneMobileLayout.module.css"; import { type DragCallback, useUpdateLayout } from "./Grid"; import { useBehavior } from "../useBehavior"; /** - * An implementation of the "one-on-one" layout for portrait screens, in which + * An implementation of the "one-on-one" layout for mobile platforms, in which * the remote participant is shown at maximum size, overlaid by a small view of * the local participant. */ -export const makeOneOnOnePortraitLayout: CallLayout< - OneOnOnePortraitLayoutModel +export const makeOneOnOneMobileLayout: CallLayout< + OneOnOneMobileLayoutModel > = () => ({ foreground: "scrolling", - fixed: function OneOnOnePortraitLayoutFixed({ ref, model, Slot }): ReactNode { + fixed: function OneOnOneMobileLayoutFixed({ ref, model, Slot }): ReactNode { useUpdateLayout(); return (
@@ -38,7 +38,7 @@ export const makeOneOnOnePortraitLayout: CallLayout< ); }, - scrolling: function OneOnOnePortraitLayoutScrolling({ + scrolling: function OneOnOneMobileLayoutScrolling({ ref, model, Slot, diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 58b378aec..322380eee 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -49,8 +49,8 @@ import { type EncryptionSystem } from "../e2ee/sharedKeyManagement"; import { E2eeType } from "../e2ee/e2eeType"; import { makeGridLayout } from "../grid/GridLayout"; import { type CallLayoutOutputs } from "../grid/CallLayout"; -import { makeOneOnOneLandscapeLayout } from "../grid/OneOnOneLandscapeLayout"; -import { makeOneOnOnePortraitLayout } from "../grid/OneOnOnePortraitLayout"; +import { makeOneOnOneDesktopLayout } from "../grid/OneOnOneDesktopLayout"; +import { makeOneOnOneMobileLayout } from "../grid/OneOnOneMobileLayout"; import { makeSpotlightExpandedLayout } from "../grid/SpotlightExpandedLayout"; import { makeSpotlightLandscapeLayout } from "../grid/SpotlightLandscapeLayout"; import { makeSpotlightPortraitLayout } from "../grid/SpotlightPortraitLayout"; @@ -483,8 +483,8 @@ export const InCallView: FC = ({ "spotlight-landscape": makeSpotlightLandscapeLayout(inputs), "spotlight-portrait": makeSpotlightPortraitLayout(inputs), "spotlight-expanded": makeSpotlightExpandedLayout(inputs), - "one-on-one-landscape": makeOneOnOneLandscapeLayout(inputs), - "one-on-one-portrait": makeOneOnOnePortraitLayout(inputs), + "one-on-one-desktop": makeOneOnOneDesktopLayout(inputs), + "one-on-one-mobile": makeOneOnOneMobileLayout(inputs), }; }, [gridBoundsObservable$]); diff --git a/src/room/__snapshots__/InCallView.test.tsx.snap b/src/room/__snapshots__/InCallView.test.tsx.snap index 6b6346c7c..5499623d1 100644 --- a/src/room/__snapshots__/InCallView.test.tsx.snap +++ b/src/room/__snapshots__/InCallView.test.tsx.snap @@ -87,14 +87,14 @@ exports[`InCallView > rendering > renders 1`] = ` class="_scrollingGrid_4e7ff8 _grid_b0d1cd" >
): Observable { pip: pip?.id, }), ); - case "one-on-one-landscape": + case "one-on-one-desktop": return combineLatest( [l.spotlight.media$, l.pip.media$], (spotlight, pip) => ({ @@ -214,7 +214,7 @@ function summarizeLayout$(l$: Observable): Observable { pip: pip.id, }), ); - case "one-on-one-portrait": + case "one-on-one-mobile": return combineLatest( [ l.spotlight.media$, @@ -437,7 +437,7 @@ describe.each([ expectedLayoutMarbles, { a: { - type: "one-on-one-landscape", + type: "one-on-one-desktop", pip: `${localId}:0`, spotlight: `${aliceId}:0`, }, @@ -453,7 +453,7 @@ describe.each([ }); }); - test("one-on-one portrait layout shows local tile when video is enabled", () => { + test("one-on-one mobile layout shows local tile when video is enabled", () => { withTestScheduler(({ behavior, schedule, expectObservable }) => { // Local participant enables their video, then disables it const videoInputMarbles = " ny--n"; @@ -480,19 +480,19 @@ describe.each([ expectedLayoutMarbles, { a: { - type: "one-on-one-portrait", + type: "one-on-one-mobile", spotlight: [`${aliceId}:0`], pip: undefined, pipSize: "lg", }, b: { - type: "one-on-one-portrait", + type: "one-on-one-mobile", spotlight: [`${aliceId}:0`], pip: `${localId}:0`, pipSize: "lg", }, c: { - type: "one-on-one-portrait", + type: "one-on-one-mobile", spotlight: [`${aliceId}:0`], pip: `${localId}:0`, pipSize: "sm", @@ -504,7 +504,7 @@ describe.each([ }); }); - test("one-on-one portrait layout shows name tags in room with 3 members", () => { + test("one-on-one mobile layout shows name tags in room with 3 members", () => { withTestScheduler(({ expectObservable }) => { withCallViewModel( { @@ -512,13 +512,13 @@ describe.each([ // Both Alice and Bob are with us in the room roomMembers: [local, alice, bob], rtcMembers$: constant([localRtcMember, aliceRtcMember]), - windowSize$: constant({ width: 380, height: 700 }), // Mobile phone in portrait + windowSize$: constant({ width: 380, height: 700 }), // Mobile phone }, (vm) => { - // Uses one-on-one portrait layout + // Uses one-on-one mobile layout expectObservable(summarizeLayout$(vm.layout$)).toBe("a", { a: { - type: "one-on-one-portrait", + type: "one-on-one-mobile", spotlight: [`${aliceId}:0`], pip: undefined, pipSize: "lg", @@ -706,14 +706,14 @@ describe.each([ { a: { // This is the expected one-on-one layout for a narrow window - type: "one-on-one-portrait", + type: "one-on-one-mobile", spotlight: [`${aliceId}:0`], pip: undefined, pipSize: "lg", }, b: { - // In a larger window, expect the normal one-on-one layout - type: "one-on-one-landscape", + // In a larger window, expect the one-on-one desktop layout + type: "one-on-one-desktop", pip: `${localId}:0`, spotlight: `${aliceId}:0`, }, @@ -1187,7 +1187,7 @@ describe.each([ grid: [`${localId}:0`], }, b: { - type: "one-on-one-landscape", + type: "one-on-one-desktop", pip: `${localId}:0`, spotlight: `${aliceId}:0`, }, @@ -1230,7 +1230,7 @@ describe.each([ grid: [`${localId}:0`], }, b: { - type: "one-on-one-landscape", + type: "one-on-one-desktop", pip: `${localId}:0`, spotlight: `${aliceId}:0`, }, @@ -1240,7 +1240,7 @@ describe.each([ grid: [`${localId}:0`, `${aliceId}:0`, `${daveId}:0`], }, d: { - type: "one-on-one-landscape", + type: "one-on-one-desktop", pip: `${localId}:0`, spotlight: `${daveId}:0`, }, @@ -1464,7 +1464,7 @@ describe.each([ // ringing the entire time (even once timed out) expectObservable(summarizeLayout$(vm.layout$)).toBe("a", { a: { - type: "one-on-one-landscape", + type: "one-on-one-desktop", spotlight: `${localId}:0`, pip: `ringing:${aliceUserId}`, }, @@ -1510,12 +1510,12 @@ describe.each([ // ringing the entire time expectObservable(summarizeLayout$(vm.layout$)).toBe("a 20ms b", { a: { - type: "one-on-one-landscape", + type: "one-on-one-desktop", spotlight: `${localId}:0`, pip: `ringing:${aliceUserId}`, }, b: { - type: "one-on-one-landscape", + type: "one-on-one-desktop", spotlight: `${aliceId}:0`, pip: `${localId}:0`, }, diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 4f533570f..d99319719 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -70,8 +70,8 @@ import { setPipEnabled$ } from "../../controls"; import { TileStore } from "../TileStore"; import { gridLikeLayout } from "../GridLikeLayout"; import { spotlightExpandedLayout } from "../SpotlightExpandedLayout"; -import { oneOnOneLandscapeLayout } from "../OneOnOneLandscapeLayout"; -import { oneOnOnePortraitLayout } from "../OneOnOnePortraitLayout"; +import { oneOnOneDesktopLayout } from "../OneOnOneDesktopLayout"; +import { oneOnOneMobileLayout } from "../OneOnOneMobileLayout"; import { pipLayout } from "../PipLayout"; import { type EncryptionSystem } from "../../e2ee/sharedKeyManagement"; import { @@ -93,8 +93,8 @@ import { type GridLayoutMedia, type Layout, type LayoutMedia, - type OneOnOneLandscapeLayoutMedia, - type OneOnOnePortraitLayoutMedia, + type OneOnOneDesktopLayoutMedia, + type OneOnOneMobileLayoutMedia, type SpotlightExpandedLayoutMedia, type SpotlightLandscapeLayoutMedia, type SpotlightPortraitLayoutMedia, @@ -1145,19 +1145,19 @@ export function createCallViewModel$( }), ); - const oneOnOneLandscapeLayoutMedia$: Observable = + const oneOnOneDesktopLayoutMedia$: Observable = oneOnOneLayoutMedia$.pipe( map((media) => { if (media === null) return null; return media.remote.type === "ringing" ? { - type: "one-on-one-landscape" as const, + type: "one-on-one-desktop" as const, edgeToEdge: false, spotlight: media.local, pip: media.remote, } : { - type: "one-on-one-landscape" as const, + type: "one-on-one-desktop" as const, edgeToEdge: false, spotlight: media.remote, pip: media.local, @@ -1165,13 +1165,13 @@ export function createCallViewModel$( }), ); - const oneOnOnePortraitLayoutMedia$: Observable = + const oneOnOneMobileLayoutMedia$: Observable = oneOnOneLayoutMedia$.pipe( switchMap((media) => { if (media === null) return of(null); return media.local.videoEnabled$.pipe( map((videoEnabled) => ({ - type: "one-on-one-portrait" as const, + type: "one-on-one-mobile" as const, edgeToEdge: true as const, spotlight: media.remote, pip: videoEnabled ? media.local : undefined, @@ -1227,7 +1227,7 @@ export function createCallViewModel$( switchMap((gridMode) => { switch (gridMode) { case "grid": - return oneOnOneLandscapeLayoutMedia$.pipe( + return oneOnOneDesktopLayoutMedia$.pipe( switchMap((oneOnOne) => oneOnOne === null ? gridLayoutMedia$ : of(oneOnOne), ), @@ -1244,7 +1244,7 @@ export function createCallViewModel$( }), ); case "narrow": - return oneOnOnePortraitLayoutMedia$.pipe( + return oneOnOneMobileLayoutMedia$.pipe( switchMap((oneOnOne) => oneOnOne === null ? combineLatest([grid$, spotlight$], (grid, spotlight) => @@ -1295,8 +1295,8 @@ export function createCallViewModel$( // indicators. And in one-on-one layout there's no question as to who is // speaking. case "spotlight-expanded": - case "one-on-one-landscape": - case "one-on-one-portrait": + case "one-on-one-desktop": + case "one-on-one-mobile": return false; default: return true; @@ -1308,7 +1308,7 @@ export function createCallViewModel$( const showNameTags$ = scope.behavior( layoutMedia$.pipe( switchMap((l) => - l.type === "pip" || l.type === "one-on-one-portrait" + l.type === "pip" || l.type === "one-on-one-mobile" ? matrixRoomMembers$.pipe( map( (members) => @@ -1513,16 +1513,16 @@ export function createCallViewModel$( prevTiles, ); break; - case "one-on-one-landscape": - [layout, newTiles] = oneOnOneLandscapeLayout( + case "one-on-one-desktop": + [layout, newTiles] = oneOnOneDesktopLayout( media, landscapePipAlignment$, prevTiles, ); pip = layout.pip; break; - case "one-on-one-portrait": - [layout, newTiles] = oneOnOnePortraitLayout( + case "one-on-one-mobile": + [layout, newTiles] = oneOnOneMobileLayout( media, portraitPipSize$, portraitPipAlignment$, diff --git a/src/state/OneOnOneLandscapeLayout.ts b/src/state/OneOnOneDesktopLayout.ts similarity index 75% rename from src/state/OneOnOneLandscapeLayout.ts rename to src/state/OneOnOneDesktopLayout.ts index 4198ff039..51fab4c87 100644 --- a/src/state/OneOnOneLandscapeLayout.ts +++ b/src/state/OneOnOneDesktopLayout.ts @@ -10,19 +10,19 @@ import { type BehaviorSubject } from "rxjs"; import { type Alignment, - type OneOnOneLandscapeLayout, - type OneOnOneLandscapeLayoutMedia, + type OneOnOneDesktopLayout, + type OneOnOneDesktopLayoutMedia, } from "./layout-types"; import { type TileStore } from "./TileStore"; /** - * Produces a one-on-one landscape layout with the given media. + * Produces a one-on-one desktop layout with the given media. */ -export function oneOnOneLandscapeLayout( - media: OneOnOneLandscapeLayoutMedia, +export function oneOnOneDesktopLayout( + media: OneOnOneDesktopLayoutMedia, pipAlignment$: BehaviorSubject, prevTiles: TileStore, -): [OneOnOneLandscapeLayout, TileStore] { +): [OneOnOneDesktopLayout, TileStore] { const update = prevTiles.from(2); update.registerGridTile(media.pip); update.registerGridTile(media.spotlight); diff --git a/src/state/OneOnOnePortraitLayout.ts b/src/state/OneOnOneMobileLayout.ts similarity index 79% rename from src/state/OneOnOnePortraitLayout.ts rename to src/state/OneOnOneMobileLayout.ts index 98314a039..d8395f0ca 100644 --- a/src/state/OneOnOnePortraitLayout.ts +++ b/src/state/OneOnOneMobileLayout.ts @@ -10,21 +10,21 @@ import { type BehaviorSubject } from "rxjs"; import { type Alignment, - type OneOnOnePortraitLayout, - type OneOnOnePortraitLayoutMedia, + type OneOnOneMobileLayout, + type OneOnOneMobileLayoutMedia, } from "./layout-types"; import { type TileStore } from "./TileStore"; import { type Behavior } from "./Behavior"; /** - * Produces a one-on-one portrait layout with the given media. + * Produces a one-on-one mobile layout with the given media. */ -export function oneOnOnePortraitLayout( - media: OneOnOnePortraitLayoutMedia, +export function oneOnOneMobileLayout( + media: OneOnOneMobileLayoutMedia, pipSize$: Behavior<"sm" | "lg">, pipAlignment$: BehaviorSubject, prevTiles: TileStore, -): [OneOnOnePortraitLayout, TileStore] { +): [OneOnOneMobileLayout, TileStore] { const update = prevTiles.from(media.pip === undefined ? 0 : 1); update.registerSpotlight([media.spotlight], true, "transparent"); if (media.pip !== undefined) update.registerGridTile(media.pip); diff --git a/src/state/layout-types.ts b/src/state/layout-types.ts index 2b0d459da..5c63a9c90 100644 --- a/src/state/layout-types.ts +++ b/src/state/layout-types.ts @@ -45,15 +45,15 @@ export interface SpotlightExpandedLayoutMedia { pip?: UserMediaViewModel; } -export interface OneOnOneLandscapeLayoutMedia { - type: "one-on-one-landscape"; +export interface OneOnOneDesktopLayoutMedia { + type: "one-on-one-desktop"; edgeToEdge: false; spotlight: UserMediaViewModel; pip: LocalUserMediaViewModel | RingingMediaViewModel; } -export interface OneOnOnePortraitLayoutMedia { - type: "one-on-one-portrait"; +export interface OneOnOneMobileLayoutMedia { + type: "one-on-one-mobile"; edgeToEdge: true; spotlight: UserMediaViewModel | RingingMediaViewModel; pip?: LocalUserMediaViewModel; @@ -70,8 +70,8 @@ export type LayoutMedia = | SpotlightLandscapeLayoutMedia | SpotlightPortraitLayoutMedia | SpotlightExpandedLayoutMedia - | OneOnOneLandscapeLayoutMedia - | OneOnOnePortraitLayoutMedia + | OneOnOneDesktopLayoutMedia + | OneOnOneMobileLayoutMedia | PipLayoutMedia; export interface Alignment { @@ -108,15 +108,15 @@ export interface SpotlightExpandedLayout { pipAlignment$: BehaviorSubject; } -export interface OneOnOneLandscapeLayout { - type: "one-on-one-landscape"; +export interface OneOnOneDesktopLayout { + type: "one-on-one-desktop"; spotlight: GridTileViewModel; pip: GridTileViewModel; pipAlignment$: BehaviorSubject; } -export interface OneOnOnePortraitLayout { - type: "one-on-one-portrait"; +export interface OneOnOneMobileLayout { + type: "one-on-one-mobile"; spotlight: SpotlightTileViewModel; pip?: GridTileViewModel; pipSize$: Behavior<"sm" | "lg">; @@ -137,6 +137,6 @@ export type Layout = | SpotlightLandscapeLayout | SpotlightPortraitLayout | SpotlightExpandedLayout - | OneOnOneLandscapeLayout - | OneOnOnePortraitLayout + | OneOnOneDesktopLayout + | OneOnOneMobileLayout | PipLayout; From aa6732da198a17f34dcbad63553255ffdc22d93a Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 14 Jul 2026 16:48:45 +0200 Subject: [PATCH 3/4] Use an actual one-on-one layout in landscape on mobile --- src/state/CallViewModel/CallViewModel.test.ts | 62 ++++++++++++++++++- src/state/CallViewModel/CallViewModel.ts | 28 +++++---- 2 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index 371b793b0..5affaa877 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -532,6 +532,58 @@ describe.each([ }); }); + test("landscape mobile layouts show screen shares and group call participants", () => { + 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"; + // Starts in one-on-one mobile layout, then goes to spotlight layout for + // the screen sharing and group call cases + const expectedLayoutMarbles = " ab-c"; + withCallViewModel( + { + remoteParticipants$: behavior(participantInputMarbles, { + a: [aliceParticipant], + b: [aliceParticipant, bobParticipant], + }), + roomMembers: [local, alice, bob], + rtcMembers$: behavior(participantInputMarbles, { + a: [localRtcMember, aliceRtcMember], + b: [localRtcMember, aliceRtcMember, bobRtcMember], + }), + sharingScreen: new Map([ + [aliceParticipant, behavior(aliceSharingInputMarbles, yesNo)], + ]), + windowSize$: constant({ width: 700, height: 380 }), // Mobile phone in landscape + }, + (vm) => { + expectObservable(summarizeLayout$(vm.layout$)).toBe( + expectedLayoutMarbles, + { + a: { + type: "one-on-one-mobile", + spotlight: [`${aliceId}:0`], + pip: undefined, + pipSize: "sm", + }, + b: { + type: "spotlight-expanded", + spotlight: [`${aliceId}:0:screen-share`], + pip: `${aliceId}:0`, + }, + c: { + type: "spotlight-expanded", + spotlight: [`${aliceId}:0`], + pip: undefined, + }, + }, + ); + }, + ); + }); + }); + test("participants stay in the same order unless to appear/disappear", () => { withTestScheduler(({ behavior, schedule, expectObservable }) => { const visibilityInputMarbles = "a"; @@ -1066,7 +1118,7 @@ describe.each([ }); test("expanded spotlight layout hides PiP tile in one-on-one voice call", () => { - withTestScheduler(({ expectObservable }) => { + withTestScheduler(({ schedule, expectObservable }) => { withCallViewModel( { remoteParticipants$: constant([aliceParticipant]), @@ -1076,9 +1128,15 @@ describe.each([ [localParticipant, constant(false)], [aliceParticipant, constant(false)], ]), - windowSize$: constant({ width: 700, height: 380 }), // Mobile phone in landscape }, (vm) => { + schedule("s", { + s: () => vm.setGridMode("spotlight"), + }); + schedule("a", { + a: () => vm.toggleSpotlightExpanded$.value!(), + }); + // Layout should show remote tile only expectObservable(summarizeLayout$(vm.layout$)).toBe("a", { a: { diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index d99319719..7b94e9ff2 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -1257,17 +1257,23 @@ export function createCallViewModel$( ), ); case "flat": - return gridMode$.pipe( - switchMap((gridMode) => { - switch (gridMode) { - case "grid": - // Yes, grid mode actually gets you a "spotlight" layout in - // this window mode. - return spotlightLandscapeLayoutMedia$(true); - case "spotlight": - return spotlightExpandedLayoutMedia$(true); - } - }), + return oneOnOneMobileLayoutMedia$.pipe( + switchMap((oneOnOne) => + oneOnOne === null + ? gridMode$.pipe( + switchMap((gridMode) => { + switch (gridMode) { + case "grid": + // Yes, grid mode actually gets you a "spotlight" layout in + // this window mode. + return spotlightLandscapeLayoutMedia$(true); + case "spotlight": + return spotlightExpandedLayoutMedia$(true); + } + }), + ) + : of(oneOnOne), + ), ); case "pip": return pipLayoutMedia$; From 5c35288162e69e477f83f2403bdffe5e2e4a7fee Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 14 Jul 2026 17:16:44 +0200 Subject: [PATCH 4/4] Turn the local video tile landscape in landscape mobile one-on-one calls --- src/grid/OneOnOneMobileLayout.module.css | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/grid/OneOnOneMobileLayout.module.css b/src/grid/OneOnOneMobileLayout.module.css index 999f504d5..e781726c9 100644 --- a/src/grid/OneOnOneMobileLayout.module.css +++ b/src/grid/OneOnOneMobileLayout.module.css @@ -19,14 +19,28 @@ Please see LICENSE in the repository root for full details. inset: var(--cpd-space-4x); } +/* Give the PiP a landscape aspect ratio */ .pip[data-size="sm"] { - inline-size: 88px; - block-size: 132px; + inline-size: 132px; + block-size: 88px; } .pip[data-size="lg"] { - inline-size: 140px; - block-size: 210px; + inline-size: 210px; + block-size: 140px; +} + +@media (max-width: 600px) { + /* Give the PiP a portrait aspect ratio */ + .pip[data-size="sm"] { + inline-size: 88px; + block-size: 132px; + } + + .pip[data-size="lg"] { + inline-size: 140px; + block-size: 210px; + } } .pip[data-block-alignment="start"] {