mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-05 19:59:20 +00:00
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:
@@ -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,
|
||||||
@@ -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"] {
|
||||||
@@ -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,
|
||||||
@@ -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$]);
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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$,
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user