Merge pull request #4128 from element-hq/landscape-default-grid

Use a proper one-on-one layout for landscape mobile calls
This commit is contained in:
Robin
2026-07-28 14:08:48 +02:00
committed by GitHub
11 changed files with 181 additions and 103 deletions

View File

@@ -10,28 +10,28 @@ import { type ReactNode, useCallback, useMemo } from "react";
import { useObservableEagerState } from "observable-hooks"; import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames"; 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 { type CallLayout, arrangeTiles } from "./CallLayout";
import styles from "./OneOnOneLandscapeLayout.module.css"; import styles from "./OneOnOneDesktopLayout.module.css";
import { type DragCallback, useUpdateLayout } from "./Grid"; import { type DragCallback, useUpdateLayout } from "./Grid";
import { useBehavior } from "../useBehavior"; 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 remote participant is shown at maximum size, overlaid by a small view of
* the local participant. * the local participant.
*/ */
export const makeOneOnOneLandscapeLayout: CallLayout< export const makeOneOnOneDesktopLayout: CallLayout<
OneOnOneLandscapeLayoutModel OneOnOneDesktopLayoutModel
> = ({ minBounds$ }) => ({ > = ({ minBounds$ }) => ({
foreground: "fixed", foreground: "fixed",
fixed: function OneOnOneLandscapeLayoutFixed({ ref }): ReactNode { fixed: function OneOnOneDesktopLayoutFixed({ ref }): ReactNode {
useUpdateLayout(); useUpdateLayout();
return <div ref={ref} />; return <div ref={ref} />;
}, },
scrolling: function OneOnOneLandscapeLayoutScrolling({ scrolling: function OneOnOneDesktopLayoutScrolling({
ref, ref,
model, model,
Slot, Slot,

View File

@@ -19,14 +19,28 @@ Please see LICENSE in the repository root for full details.
inset: var(--cpd-space-4x); inset: var(--cpd-space-4x);
} }
/* Give the PiP a landscape aspect ratio */
.pip[data-size="sm"] { .pip[data-size="sm"] {
inline-size: 88px; inline-size: 132px;
block-size: 132px; block-size: 88px;
} }
.pip[data-size="lg"] { .pip[data-size="lg"] {
inline-size: 140px; inline-size: 210px;
block-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"] { .pip[data-block-alignment="start"] {

View File

@@ -9,23 +9,23 @@ Please see LICENSE in the repository root for full details.
import { type ReactNode, useCallback } from "react"; import { type ReactNode, useCallback } from "react";
import classNames from "classnames"; 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 { type CallLayout } from "./CallLayout";
import styles from "./OneOnOnePortraitLayout.module.css"; import styles from "./OneOnOneMobileLayout.module.css";
import { type DragCallback, useUpdateLayout } from "./Grid"; import { type DragCallback, useUpdateLayout } from "./Grid";
import { useBehavior } from "../useBehavior"; 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 remote participant is shown at maximum size, overlaid by a small view of
* the local participant. * the local participant.
*/ */
export const makeOneOnOnePortraitLayout: CallLayout< export const makeOneOnOneMobileLayout: CallLayout<
OneOnOnePortraitLayoutModel OneOnOneMobileLayoutModel
> = () => ({ > = () => ({
foreground: "scrolling", foreground: "scrolling",
fixed: function OneOnOnePortraitLayoutFixed({ ref, model, Slot }): ReactNode { fixed: function OneOnOneMobileLayoutFixed({ ref, model, Slot }): ReactNode {
useUpdateLayout(); useUpdateLayout();
return ( return (
<div ref={ref} className={styles.layer}> <div ref={ref} className={styles.layer}>
@@ -38,7 +38,7 @@ export const makeOneOnOnePortraitLayout: CallLayout<
); );
}, },
scrolling: function OneOnOnePortraitLayoutScrolling({ scrolling: function OneOnOneMobileLayoutScrolling({
ref, ref,
model, model,
Slot, Slot,

View File

@@ -49,8 +49,8 @@ import { type EncryptionSystem } from "../e2ee/sharedKeyManagement";
import { E2eeType } from "../e2ee/e2eeType"; import { E2eeType } from "../e2ee/e2eeType";
import { makeGridLayout } from "../grid/GridLayout"; import { makeGridLayout } from "../grid/GridLayout";
import { type CallLayoutOutputs } from "../grid/CallLayout"; import { type CallLayoutOutputs } from "../grid/CallLayout";
import { makeOneOnOneLandscapeLayout } from "../grid/OneOnOneLandscapeLayout"; import { makeOneOnOneDesktopLayout } from "../grid/OneOnOneDesktopLayout";
import { makeOneOnOnePortraitLayout } from "../grid/OneOnOnePortraitLayout"; import { makeOneOnOneMobileLayout } from "../grid/OneOnOneMobileLayout";
import { makeSpotlightExpandedLayout } from "../grid/SpotlightExpandedLayout"; import { makeSpotlightExpandedLayout } from "../grid/SpotlightExpandedLayout";
import { makeSpotlightLandscapeLayout } from "../grid/SpotlightLandscapeLayout"; import { makeSpotlightLandscapeLayout } from "../grid/SpotlightLandscapeLayout";
import { makeSpotlightPortraitLayout } from "../grid/SpotlightPortraitLayout"; import { makeSpotlightPortraitLayout } from "../grid/SpotlightPortraitLayout";
@@ -483,8 +483,8 @@ export const InCallView: FC<InCallViewProps> = ({
"spotlight-landscape": makeSpotlightLandscapeLayout(inputs), "spotlight-landscape": makeSpotlightLandscapeLayout(inputs),
"spotlight-portrait": makeSpotlightPortraitLayout(inputs), "spotlight-portrait": makeSpotlightPortraitLayout(inputs),
"spotlight-expanded": makeSpotlightExpandedLayout(inputs), "spotlight-expanded": makeSpotlightExpandedLayout(inputs),
"one-on-one-landscape": makeOneOnOneLandscapeLayout(inputs), "one-on-one-desktop": makeOneOnOneDesktopLayout(inputs),
"one-on-one-portrait": makeOneOnOnePortraitLayout(inputs), "one-on-one-mobile": makeOneOnOneMobileLayout(inputs),
}; };
}, [gridBoundsObservable$]); }, [gridBoundsObservable$]);

View File

@@ -87,14 +87,14 @@ exports[`InCallView > rendering > renders 1`] = `
class="_scrollingGrid_4e7ff8 _grid_b0d1cd" class="_scrollingGrid_4e7ff8 _grid_b0d1cd"
> >
<div <div
class="_layer_b00c5f" class="_layer_185815"
> >
<div <div
class="_container_b00c5f _slot_b0d1cd" class="_container_185815 _slot_b0d1cd"
data-id="1" data-id="1"
> >
<div <div
class="_slot_b00c5f _local_b00c5f _slot_b0d1cd" class="_slot_185815 _local_185815 _slot_b0d1cd"
data-block-alignment="start" data-block-alignment="start"
data-id="0" data-id="0"
data-inline-alignment="end" data-inline-alignment="end"

View File

@@ -144,13 +144,13 @@ export interface SpotlightExpandedLayoutSummary {
} }
export interface OneOnOneLandscapeLayoutSummary { export interface OneOnOneLandscapeLayoutSummary {
type: "one-on-one-landscape"; type: "one-on-one-desktop";
spotlight: string; spotlight: string;
pip: string; pip: string;
} }
export interface OneOnOnePortraitLayoutSummary { export interface OneOnOnePortraitLayoutSummary {
type: "one-on-one-portrait"; type: "one-on-one-mobile";
spotlight: string[]; spotlight: string[];
pip?: string; pip?: string;
pipSize: "sm" | "lg"; pipSize: "sm" | "lg";
@@ -205,7 +205,7 @@ function summarizeLayout$(l$: Observable<Layout>): Observable<LayoutSummary> {
pip: pip?.id, pip: pip?.id,
}), }),
); );
case "one-on-one-landscape": case "one-on-one-desktop":
return combineLatest( return combineLatest(
[l.spotlight.media$, l.pip.media$], [l.spotlight.media$, l.pip.media$],
(spotlight, pip) => ({ (spotlight, pip) => ({
@@ -214,7 +214,7 @@ function summarizeLayout$(l$: Observable<Layout>): Observable<LayoutSummary> {
pip: pip.id, pip: pip.id,
}), }),
); );
case "one-on-one-portrait": case "one-on-one-mobile":
return combineLatest( return combineLatest(
[ [
l.spotlight.media$, l.spotlight.media$,
@@ -437,7 +437,7 @@ describe.each([
expectedLayoutMarbles, expectedLayoutMarbles,
{ {
a: { a: {
type: "one-on-one-landscape", type: "one-on-one-desktop",
pip: `${localId}:0`, pip: `${localId}:0`,
spotlight: `${aliceId}: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 }) => { withTestScheduler(({ behavior, schedule, expectObservable }) => {
// Local participant enables their video, then disables it // Local participant enables their video, then disables it
const videoInputMarbles = " ny--n"; const videoInputMarbles = " ny--n";
@@ -480,19 +480,19 @@ describe.each([
expectedLayoutMarbles, expectedLayoutMarbles,
{ {
a: { a: {
type: "one-on-one-portrait", type: "one-on-one-mobile",
spotlight: [`${aliceId}:0`], spotlight: [`${aliceId}:0`],
pip: undefined, pip: undefined,
pipSize: "lg", pipSize: "lg",
}, },
b: { b: {
type: "one-on-one-portrait", type: "one-on-one-mobile",
spotlight: [`${aliceId}:0`], spotlight: [`${aliceId}:0`],
pip: `${localId}:0`, pip: `${localId}:0`,
pipSize: "lg", pipSize: "lg",
}, },
c: { c: {
type: "one-on-one-portrait", type: "one-on-one-mobile",
spotlight: [`${aliceId}:0`], spotlight: [`${aliceId}:0`],
pip: `${localId}:0`, pip: `${localId}:0`,
pipSize: "sm", pipSize: "sm",
@@ -504,21 +504,21 @@ 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(({ behavior, schedule, expectObservable }) => { withTestScheduler(({ expectObservable }) => {
withCallViewModel( withCallViewModel(
{ {
remoteParticipants$: constant([aliceParticipant]), remoteParticipants$: constant([aliceParticipant]),
// Both Alice and Bob are with us in the room // Both Alice and Bob are with us in the room
roomMembers: [local, alice, bob], roomMembers: [local, alice, bob],
rtcMembers$: constant([localRtcMember, aliceRtcMember]), rtcMembers$: constant([localRtcMember, aliceRtcMember]),
windowSize$: constant({ width: 380, height: 700 }), // Mobile phone in portrait windowSize$: constant({ width: 380, height: 700 }), // Mobile phone
}, },
(vm) => { (vm) => {
// Uses one-on-one portrait layout // Uses one-on-one mobile layout
expectObservable(summarizeLayout$(vm.layout$)).toBe("a", { expectObservable(summarizeLayout$(vm.layout$)).toBe("a", {
a: { a: {
type: "one-on-one-portrait", type: "one-on-one-mobile",
spotlight: [`${aliceId}:0`], spotlight: [`${aliceId}:0`],
pip: undefined, pip: undefined,
pipSize: "lg", pipSize: "lg",
@@ -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", () => { test("participants stay in the same order unless to appear/disappear", () => {
withTestScheduler(({ behavior, schedule, expectObservable }) => { withTestScheduler(({ behavior, schedule, expectObservable }) => {
const visibilityInputMarbles = "a"; const visibilityInputMarbles = "a";
@@ -706,14 +758,14 @@ describe.each([
{ {
a: { a: {
// This is the expected one-on-one layout for a narrow window // 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`], spotlight: [`${aliceId}:0`],
pip: undefined, pip: undefined,
pipSize: "lg", pipSize: "lg",
}, },
b: { b: {
// In a larger window, expect the normal one-on-one layout // In a larger window, expect the one-on-one desktop layout
type: "one-on-one-landscape", type: "one-on-one-desktop",
pip: `${localId}:0`, pip: `${localId}:0`,
spotlight: `${aliceId}:0`, spotlight: `${aliceId}:0`,
}, },
@@ -1066,7 +1118,7 @@ describe.each([
}); });
test("expanded spotlight layout hides PiP tile in one-on-one voice call", () => { test("expanded spotlight layout hides PiP tile in one-on-one voice call", () => {
withTestScheduler(({ behavior, schedule, expectObservable }) => { withTestScheduler(({ schedule, expectObservable }) => {
withCallViewModel( withCallViewModel(
{ {
remoteParticipants$: constant([aliceParticipant]), remoteParticipants$: constant([aliceParticipant]),
@@ -1076,9 +1128,15 @@ describe.each([
[localParticipant, constant(false)], [localParticipant, constant(false)],
[aliceParticipant, constant(false)], [aliceParticipant, constant(false)],
]), ]),
windowSize$: constant({ width: 700, height: 380 }), // Mobile phone in landscape
}, },
(vm) => { (vm) => {
schedule("s", {
s: () => vm.setGridMode("spotlight"),
});
schedule("a", {
a: () => vm.toggleSpotlightExpanded$.value!(),
});
// Layout should show remote tile only // Layout should show remote tile only
expectObservable(summarizeLayout$(vm.layout$)).toBe("a", { expectObservable(summarizeLayout$(vm.layout$)).toBe("a", {
a: { a: {
@@ -1187,7 +1245,7 @@ describe.each([
grid: [`${localId}:0`], grid: [`${localId}:0`],
}, },
b: { b: {
type: "one-on-one-landscape", type: "one-on-one-desktop",
pip: `${localId}:0`, pip: `${localId}:0`,
spotlight: `${aliceId}:0`, spotlight: `${aliceId}:0`,
}, },
@@ -1230,7 +1288,7 @@ describe.each([
grid: [`${localId}:0`], grid: [`${localId}:0`],
}, },
b: { b: {
type: "one-on-one-landscape", type: "one-on-one-desktop",
pip: `${localId}:0`, pip: `${localId}:0`,
spotlight: `${aliceId}:0`, spotlight: `${aliceId}:0`,
}, },
@@ -1240,7 +1298,7 @@ describe.each([
grid: [`${localId}:0`, `${aliceId}:0`, `${daveId}:0`], grid: [`${localId}:0`, `${aliceId}:0`, `${daveId}:0`],
}, },
d: { d: {
type: "one-on-one-landscape", type: "one-on-one-desktop",
pip: `${localId}:0`, pip: `${localId}:0`,
spotlight: `${daveId}:0`, spotlight: `${daveId}:0`,
}, },
@@ -1464,7 +1522,7 @@ describe.each([
// ringing the entire time (even once timed out) // ringing the entire time (even once timed out)
expectObservable(summarizeLayout$(vm.layout$)).toBe("a", { expectObservable(summarizeLayout$(vm.layout$)).toBe("a", {
a: { a: {
type: "one-on-one-landscape", type: "one-on-one-desktop",
spotlight: `${localId}:0`, spotlight: `${localId}:0`,
pip: `ringing:${aliceUserId}`, pip: `ringing:${aliceUserId}`,
}, },
@@ -1510,12 +1568,12 @@ describe.each([
// ringing the entire time // ringing the entire time
expectObservable(summarizeLayout$(vm.layout$)).toBe("a 20ms b", { expectObservable(summarizeLayout$(vm.layout$)).toBe("a 20ms b", {
a: { a: {
type: "one-on-one-landscape", type: "one-on-one-desktop",
spotlight: `${localId}:0`, spotlight: `${localId}:0`,
pip: `ringing:${aliceUserId}`, pip: `ringing:${aliceUserId}`,
}, },
b: { b: {
type: "one-on-one-landscape", type: "one-on-one-desktop",
spotlight: `${aliceId}:0`, spotlight: `${aliceId}:0`,
pip: `${localId}:0`, pip: `${localId}:0`,
}, },

View File

@@ -70,8 +70,8 @@ import { setPipEnabled$ } from "../../controls";
import { TileStore } from "../TileStore"; import { TileStore } from "../TileStore";
import { gridLikeLayout } from "../GridLikeLayout"; import { gridLikeLayout } from "../GridLikeLayout";
import { spotlightExpandedLayout } from "../SpotlightExpandedLayout"; import { spotlightExpandedLayout } from "../SpotlightExpandedLayout";
import { oneOnOneLandscapeLayout } from "../OneOnOneLandscapeLayout"; import { oneOnOneDesktopLayout } from "../OneOnOneDesktopLayout";
import { oneOnOnePortraitLayout } from "../OneOnOnePortraitLayout"; import { oneOnOneMobileLayout } from "../OneOnOneMobileLayout";
import { pipLayout } from "../PipLayout"; import { pipLayout } from "../PipLayout";
import { type EncryptionSystem } from "../../e2ee/sharedKeyManagement"; import { type EncryptionSystem } from "../../e2ee/sharedKeyManagement";
import { import {
@@ -93,8 +93,8 @@ import {
type GridLayoutMedia, type GridLayoutMedia,
type Layout, type Layout,
type LayoutMedia, type LayoutMedia,
type OneOnOneLandscapeLayoutMedia, type OneOnOneDesktopLayoutMedia,
type OneOnOnePortraitLayoutMedia, type OneOnOneMobileLayoutMedia,
type SpotlightExpandedLayoutMedia, type SpotlightExpandedLayoutMedia,
type SpotlightLandscapeLayoutMedia, type SpotlightLandscapeLayoutMedia,
type SpotlightPortraitLayoutMedia, type SpotlightPortraitLayoutMedia,
@@ -1145,19 +1145,19 @@ export function createCallViewModel$(
}), }),
); );
const oneOnOneLandscapeLayoutMedia$: Observable<OneOnOneLandscapeLayoutMedia | null> = const oneOnOneDesktopLayoutMedia$: Observable<OneOnOneDesktopLayoutMedia | null> =
oneOnOneLayoutMedia$.pipe( oneOnOneLayoutMedia$.pipe(
map((media) => { map((media) => {
if (media === null) return null; if (media === null) return null;
return media.remote.type === "ringing" return media.remote.type === "ringing"
? { ? {
type: "one-on-one-landscape" as const, type: "one-on-one-desktop" as const,
edgeToEdge: false, edgeToEdge: false,
spotlight: media.local, spotlight: media.local,
pip: media.remote, pip: media.remote,
} }
: { : {
type: "one-on-one-landscape" as const, type: "one-on-one-desktop" as const,
edgeToEdge: false, edgeToEdge: false,
spotlight: media.remote, spotlight: media.remote,
pip: media.local, pip: media.local,
@@ -1165,13 +1165,13 @@ export function createCallViewModel$(
}), }),
); );
const oneOnOnePortraitLayoutMedia$: Observable<OneOnOnePortraitLayoutMedia | null> = const oneOnOneMobileLayoutMedia$: Observable<OneOnOneMobileLayoutMedia | null> =
oneOnOneLayoutMedia$.pipe( oneOnOneLayoutMedia$.pipe(
switchMap((media) => { switchMap((media) => {
if (media === null) return of(null); if (media === null) return of(null);
return media.local.videoEnabled$.pipe( return media.local.videoEnabled$.pipe(
map((videoEnabled) => ({ map((videoEnabled) => ({
type: "one-on-one-portrait" as const, type: "one-on-one-mobile" as const,
edgeToEdge: true as const, edgeToEdge: true as const,
spotlight: media.remote, spotlight: media.remote,
pip: videoEnabled ? media.local : undefined, pip: videoEnabled ? media.local : undefined,
@@ -1227,7 +1227,7 @@ export function createCallViewModel$(
switchMap((gridMode) => { switchMap((gridMode) => {
switch (gridMode) { switch (gridMode) {
case "grid": case "grid":
return oneOnOneLandscapeLayoutMedia$.pipe( return oneOnOneDesktopLayoutMedia$.pipe(
switchMap((oneOnOne) => switchMap((oneOnOne) =>
oneOnOne === null ? gridLayoutMedia$ : of(oneOnOne), oneOnOne === null ? gridLayoutMedia$ : of(oneOnOne),
), ),
@@ -1244,7 +1244,7 @@ export function createCallViewModel$(
}), }),
); );
case "narrow": case "narrow":
return oneOnOnePortraitLayoutMedia$.pipe( return oneOnOneMobileLayoutMedia$.pipe(
switchMap((oneOnOne) => switchMap((oneOnOne) =>
oneOnOne === null oneOnOne === null
? combineLatest([grid$, spotlight$], (grid, spotlight) => ? combineLatest([grid$, spotlight$], (grid, spotlight) =>
@@ -1257,17 +1257,23 @@ export function createCallViewModel$(
), ),
); );
case "flat": case "flat":
return gridMode$.pipe( return oneOnOneMobileLayoutMedia$.pipe(
switchMap((gridMode) => { switchMap((oneOnOne) =>
switch (gridMode) { oneOnOne === null
case "grid": ? gridMode$.pipe(
// Yes, grid mode actually gets you a "spotlight" layout in switchMap((gridMode) => {
// this window mode. switch (gridMode) {
return spotlightLandscapeLayoutMedia$(true); case "grid":
case "spotlight": // Yes, grid mode actually gets you a "spotlight" layout in
return spotlightExpandedLayoutMedia$(true); // this window mode.
} return spotlightLandscapeLayoutMedia$(true);
}), case "spotlight":
return spotlightExpandedLayoutMedia$(true);
}
}),
)
: of(oneOnOne),
),
); );
case "pip": case "pip":
return pipLayoutMedia$; return pipLayoutMedia$;
@@ -1295,8 +1301,8 @@ export function createCallViewModel$(
// indicators. And in one-on-one layout there's no question as to who is // indicators. And in one-on-one layout there's no question as to who is
// speaking. // speaking.
case "spotlight-expanded": case "spotlight-expanded":
case "one-on-one-landscape": case "one-on-one-desktop":
case "one-on-one-portrait": case "one-on-one-mobile":
return false; return false;
default: default:
return true; return true;
@@ -1308,7 +1314,7 @@ export function createCallViewModel$(
const showNameTags$ = scope.behavior<boolean>( const showNameTags$ = scope.behavior<boolean>(
layoutMedia$.pipe( layoutMedia$.pipe(
switchMap((l) => switchMap((l) =>
l.type === "pip" || l.type === "one-on-one-portrait" l.type === "pip" || l.type === "one-on-one-mobile"
? matrixRoomMembers$.pipe( ? matrixRoomMembers$.pipe(
map( map(
(members) => (members) =>
@@ -1513,16 +1519,16 @@ export function createCallViewModel$(
prevTiles, prevTiles,
); );
break; break;
case "one-on-one-landscape": case "one-on-one-desktop":
[layout, newTiles] = oneOnOneLandscapeLayout( [layout, newTiles] = oneOnOneDesktopLayout(
media, media,
landscapePipAlignment$, landscapePipAlignment$,
prevTiles, prevTiles,
); );
pip = layout.pip; pip = layout.pip;
break; break;
case "one-on-one-portrait": case "one-on-one-mobile":
[layout, newTiles] = oneOnOnePortraitLayout( [layout, newTiles] = oneOnOneMobileLayout(
media, media,
portraitPipSize$, portraitPipSize$,
portraitPipAlignment$, portraitPipAlignment$,

View File

@@ -10,19 +10,19 @@ import { type BehaviorSubject } from "rxjs";
import { import {
type Alignment, type Alignment,
type OneOnOneLandscapeLayout, type OneOnOneDesktopLayout,
type OneOnOneLandscapeLayoutMedia, type OneOnOneDesktopLayoutMedia,
} from "./layout-types"; } from "./layout-types";
import { type TileStore } from "./TileStore"; 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( export function oneOnOneDesktopLayout(
media: OneOnOneLandscapeLayoutMedia, media: OneOnOneDesktopLayoutMedia,
pipAlignment$: BehaviorSubject<Alignment>, pipAlignment$: BehaviorSubject<Alignment>,
prevTiles: TileStore, prevTiles: TileStore,
): [OneOnOneLandscapeLayout, TileStore] { ): [OneOnOneDesktopLayout, TileStore] {
const update = prevTiles.from(2); const update = prevTiles.from(2);
update.registerGridTile(media.pip); update.registerGridTile(media.pip);
update.registerGridTile(media.spotlight); update.registerGridTile(media.spotlight);

View File

@@ -10,21 +10,21 @@ import { type BehaviorSubject } from "rxjs";
import { import {
type Alignment, type Alignment,
type OneOnOnePortraitLayout, type OneOnOneMobileLayout,
type OneOnOnePortraitLayoutMedia, type OneOnOneMobileLayoutMedia,
} from "./layout-types"; } from "./layout-types";
import { type TileStore } from "./TileStore"; import { type TileStore } from "./TileStore";
import { type Behavior } from "./Behavior"; 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( export function oneOnOneMobileLayout(
media: OneOnOnePortraitLayoutMedia, media: OneOnOneMobileLayoutMedia,
pipSize$: Behavior<"sm" | "lg">, pipSize$: Behavior<"sm" | "lg">,
pipAlignment$: BehaviorSubject<Alignment>, pipAlignment$: BehaviorSubject<Alignment>,
prevTiles: TileStore, prevTiles: TileStore,
): [OneOnOnePortraitLayout, TileStore] { ): [OneOnOneMobileLayout, TileStore] {
const update = prevTiles.from(media.pip === undefined ? 0 : 1); const update = prevTiles.from(media.pip === undefined ? 0 : 1);
update.registerSpotlight([media.spotlight], true, "transparent"); update.registerSpotlight([media.spotlight], true, "transparent");
if (media.pip !== undefined) update.registerGridTile(media.pip); if (media.pip !== undefined) update.registerGridTile(media.pip);

View File

@@ -45,15 +45,15 @@ export interface SpotlightExpandedLayoutMedia {
pip?: UserMediaViewModel; pip?: UserMediaViewModel;
} }
export interface OneOnOneLandscapeLayoutMedia { export interface OneOnOneDesktopLayoutMedia {
type: "one-on-one-landscape"; type: "one-on-one-desktop";
edgeToEdge: false; edgeToEdge: false;
spotlight: UserMediaViewModel; spotlight: UserMediaViewModel;
pip: LocalUserMediaViewModel | RingingMediaViewModel; pip: LocalUserMediaViewModel | RingingMediaViewModel;
} }
export interface OneOnOnePortraitLayoutMedia { export interface OneOnOneMobileLayoutMedia {
type: "one-on-one-portrait"; type: "one-on-one-mobile";
edgeToEdge: true; edgeToEdge: true;
spotlight: UserMediaViewModel | RingingMediaViewModel; spotlight: UserMediaViewModel | RingingMediaViewModel;
pip?: LocalUserMediaViewModel; pip?: LocalUserMediaViewModel;
@@ -70,8 +70,8 @@ export type LayoutMedia =
| SpotlightLandscapeLayoutMedia | SpotlightLandscapeLayoutMedia
| SpotlightPortraitLayoutMedia | SpotlightPortraitLayoutMedia
| SpotlightExpandedLayoutMedia | SpotlightExpandedLayoutMedia
| OneOnOneLandscapeLayoutMedia | OneOnOneDesktopLayoutMedia
| OneOnOnePortraitLayoutMedia | OneOnOneMobileLayoutMedia
| PipLayoutMedia; | PipLayoutMedia;
export interface Alignment { export interface Alignment {
@@ -108,15 +108,15 @@ export interface SpotlightExpandedLayout {
pipAlignment$: BehaviorSubject<Alignment>; pipAlignment$: BehaviorSubject<Alignment>;
} }
export interface OneOnOneLandscapeLayout { export interface OneOnOneDesktopLayout {
type: "one-on-one-landscape"; type: "one-on-one-desktop";
spotlight: GridTileViewModel; spotlight: GridTileViewModel;
pip: GridTileViewModel; pip: GridTileViewModel;
pipAlignment$: BehaviorSubject<Alignment>; pipAlignment$: BehaviorSubject<Alignment>;
} }
export interface OneOnOnePortraitLayout { export interface OneOnOneMobileLayout {
type: "one-on-one-portrait"; type: "one-on-one-mobile";
spotlight: SpotlightTileViewModel; spotlight: SpotlightTileViewModel;
pip?: GridTileViewModel; pip?: GridTileViewModel;
pipSize$: Behavior<"sm" | "lg">; pipSize$: Behavior<"sm" | "lg">;
@@ -137,6 +137,6 @@ export type Layout =
| SpotlightLandscapeLayout | SpotlightLandscapeLayout
| SpotlightPortraitLayout | SpotlightPortraitLayout
| SpotlightExpandedLayout | SpotlightExpandedLayout
| OneOnOneLandscapeLayout | OneOnOneDesktopLayout
| OneOnOnePortraitLayout | OneOnOneMobileLayout
| PipLayout; | PipLayout;