mirror of
https://github.com/vector-im/element-call.git
synced 2026-06-30 18:02:56 +00:00
Create a proper one-on-one call layout for portrait screens
This commit is contained in:
@@ -50,6 +50,7 @@ import {
|
||||
aliceParticipant,
|
||||
aliceRtcMember,
|
||||
aliceUserId,
|
||||
bob,
|
||||
bobId,
|
||||
bobRtcMember,
|
||||
local,
|
||||
@@ -133,12 +134,19 @@ export interface SpotlightExpandedLayoutSummary {
|
||||
pip?: string;
|
||||
}
|
||||
|
||||
export interface OneOnOneLayoutSummary {
|
||||
type: "one-on-one";
|
||||
export interface OneOnOneLandscapeLayoutSummary {
|
||||
type: "one-on-one-landscape";
|
||||
spotlight: string;
|
||||
pip: string;
|
||||
}
|
||||
|
||||
export interface OneOnOnePortraitLayoutSummary {
|
||||
type: "one-on-one-portrait";
|
||||
spotlight: string[];
|
||||
pip?: string;
|
||||
pipSize: "sm" | "lg";
|
||||
}
|
||||
|
||||
export interface PipLayoutSummary {
|
||||
type: "pip";
|
||||
spotlight: string[];
|
||||
@@ -149,7 +157,8 @@ export type LayoutSummary =
|
||||
| SpotlightLandscapeLayoutSummary
|
||||
| SpotlightPortraitLayoutSummary
|
||||
| SpotlightExpandedLayoutSummary
|
||||
| OneOnOneLayoutSummary
|
||||
| OneOnOneLandscapeLayoutSummary
|
||||
| OneOnOnePortraitLayoutSummary
|
||||
| PipLayoutSummary;
|
||||
|
||||
function summarizeLayout$(l$: Observable<Layout>): Observable<LayoutSummary> {
|
||||
@@ -187,7 +196,7 @@ function summarizeLayout$(l$: Observable<Layout>): Observable<LayoutSummary> {
|
||||
pip: pip?.id,
|
||||
}),
|
||||
);
|
||||
case "one-on-one":
|
||||
case "one-on-one-landscape":
|
||||
return combineLatest(
|
||||
[l.spotlight.media$, l.pip.media$],
|
||||
(spotlight, pip) => ({
|
||||
@@ -196,6 +205,20 @@ function summarizeLayout$(l$: Observable<Layout>): Observable<LayoutSummary> {
|
||||
pip: pip.id,
|
||||
}),
|
||||
);
|
||||
case "one-on-one-portrait":
|
||||
return combineLatest(
|
||||
[
|
||||
l.spotlight.media$,
|
||||
l.pip?.media$ ?? constant(undefined),
|
||||
l.pipSize$,
|
||||
],
|
||||
(spotlight, pip, pipSize) => ({
|
||||
type: l.type,
|
||||
spotlight: spotlight.map((vm) => vm.id),
|
||||
pip: pip?.id,
|
||||
pipSize,
|
||||
}),
|
||||
);
|
||||
case "pip":
|
||||
return l.spotlight.media$.pipe(
|
||||
map((spotlight) => ({
|
||||
@@ -405,7 +428,7 @@ describe.each([
|
||||
expectedLayoutMarbles,
|
||||
{
|
||||
a: {
|
||||
type: "one-on-one",
|
||||
type: "one-on-one-landscape",
|
||||
pip: `${localId}:0`,
|
||||
spotlight: `${aliceId}:0`,
|
||||
},
|
||||
@@ -421,6 +444,85 @@ describe.each([
|
||||
});
|
||||
});
|
||||
|
||||
test("one-on-one portrait layout shows local tile when video is enabled", () => {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
// Local participant enables their video, then disables it
|
||||
const videoInputMarbles = " ny--n";
|
||||
// While tile is shown, tap the screen twice
|
||||
const tapScreenInputMarbles = "--aa-";
|
||||
// Layout should show local tile, make it small, enlarge it again, then hide it
|
||||
const expectedLayoutMarbles = "abcba";
|
||||
|
||||
withCallViewModel(
|
||||
{
|
||||
remoteParticipants$: constant([aliceParticipant]),
|
||||
roomMembers: [local, alice],
|
||||
rtcMembers$: constant([localRtcMember, aliceRtcMember]),
|
||||
videoEnabled: new Map([
|
||||
[localParticipant, behavior(videoInputMarbles, yesNo)],
|
||||
]),
|
||||
windowSize$: constant({ width: 380, height: 700 }), // Mobile phone in portrait
|
||||
},
|
||||
(vm) => {
|
||||
schedule(tapScreenInputMarbles, { a: () => vm.tapScreen() });
|
||||
|
||||
expectObservable(vm.edgeToEdge$).toBe("y", yesNo); // Edge-to-edge-layout
|
||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||
expectedLayoutMarbles,
|
||||
{
|
||||
a: {
|
||||
type: "one-on-one-portrait",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
pip: undefined,
|
||||
pipSize: "lg",
|
||||
},
|
||||
b: {
|
||||
type: "one-on-one-portrait",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
pip: `${localId}:0`,
|
||||
pipSize: "lg",
|
||||
},
|
||||
c: {
|
||||
type: "one-on-one-portrait",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
pip: `${localId}:0`,
|
||||
pipSize: "sm",
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("one-on-one portrait layout shows name tags in room with 3 members", () => {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
withCallViewModel(
|
||||
{
|
||||
remoteParticipants$: constant([aliceParticipant]),
|
||||
// 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
|
||||
},
|
||||
(vm) => {
|
||||
// Uses one-on-one portrait layout
|
||||
expectObservable(summarizeLayout$(vm.layout$)).toBe("a", {
|
||||
a: {
|
||||
type: "one-on-one-portrait",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
pip: undefined,
|
||||
pipSize: "lg",
|
||||
},
|
||||
});
|
||||
// It wouldn't be clear whether Alice or Bob is the remote video tile,
|
||||
// so the interface must put a name tag on it
|
||||
expectObservable(vm.showNameTags$).toBe("y", yesNo);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("participants stay in the same order unless to appear/disappear", () => {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
const visibilityInputMarbles = "a";
|
||||
@@ -576,7 +678,7 @@ describe.each([
|
||||
});
|
||||
|
||||
test("layout reacts to window size", () => {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
const windowSizeInputMarbles = "abc";
|
||||
const expectedLayoutMarbles = " abc";
|
||||
withCallViewModel(
|
||||
@@ -584,7 +686,7 @@ describe.each([
|
||||
remoteParticipants$: constant([aliceParticipant]),
|
||||
rtcMembers$: constant([localRtcMember, aliceRtcMember]),
|
||||
windowSize$: behavior(windowSizeInputMarbles, {
|
||||
a: { width: 300, height: 600 }, // Start very narrow, like a phone
|
||||
a: { width: 380, height: 700 }, // Start very narrow, like a phone
|
||||
b: { width: 1000, height: 800 }, // Go to normal desktop window size
|
||||
c: { width: 200, height: 180 }, // Go to PiP size
|
||||
}),
|
||||
@@ -595,13 +697,14 @@ describe.each([
|
||||
{
|
||||
a: {
|
||||
// This is the expected one-on-one layout for a narrow window
|
||||
type: "spotlight-expanded",
|
||||
type: "one-on-one-portrait",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
pip: `${localId}:0`,
|
||||
pip: undefined,
|
||||
pipSize: "lg",
|
||||
},
|
||||
b: {
|
||||
// In a larger window, expect the normal one-on-one layout
|
||||
type: "one-on-one",
|
||||
type: "one-on-one-landscape",
|
||||
pip: `${localId}:0`,
|
||||
spotlight: `${aliceId}:0`,
|
||||
},
|
||||
@@ -956,7 +1059,7 @@ describe.each([
|
||||
grid: [`${localId}:0`],
|
||||
},
|
||||
b: {
|
||||
type: "one-on-one",
|
||||
type: "one-on-one-landscape",
|
||||
pip: `${localId}:0`,
|
||||
spotlight: `${aliceId}:0`,
|
||||
},
|
||||
@@ -999,7 +1102,7 @@ describe.each([
|
||||
grid: [`${localId}:0`],
|
||||
},
|
||||
b: {
|
||||
type: "one-on-one",
|
||||
type: "one-on-one-landscape",
|
||||
pip: `${localId}:0`,
|
||||
spotlight: `${aliceId}:0`,
|
||||
},
|
||||
@@ -1009,7 +1112,7 @@ describe.each([
|
||||
grid: [`${localId}:0`, `${aliceId}:0`, `${daveId}:0`],
|
||||
},
|
||||
d: {
|
||||
type: "one-on-one",
|
||||
type: "one-on-one-landscape",
|
||||
pip: `${localId}:0`,
|
||||
spotlight: `${daveId}:0`,
|
||||
},
|
||||
@@ -1227,7 +1330,7 @@ describe.each([
|
||||
// ringing the entire time (even once timed out)
|
||||
expectObservable(summarizeLayout$(vm.layout$)).toBe("a", {
|
||||
a: {
|
||||
type: "one-on-one",
|
||||
type: "one-on-one-landscape",
|
||||
spotlight: `${localId}:0`,
|
||||
pip: `ringing:${aliceUserId}`,
|
||||
},
|
||||
@@ -1266,12 +1369,12 @@ describe.each([
|
||||
// ringing the entire time
|
||||
expectObservable(summarizeLayout$(vm.layout$)).toBe("a 20ms b", {
|
||||
a: {
|
||||
type: "one-on-one",
|
||||
type: "one-on-one-landscape",
|
||||
spotlight: `${localId}:0`,
|
||||
pip: `ringing:${aliceUserId}`,
|
||||
},
|
||||
b: {
|
||||
type: "one-on-one",
|
||||
type: "one-on-one-landscape",
|
||||
spotlight: `${aliceId}:0`,
|
||||
pip: `${localId}:0`,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user