Fix the wrong layout being used until window size changes
While looking into what had regressed https://github.com/element-hq/element-call/issues/3588, I found that 28047217b8 had filled in a couple of behaviors with non-reactive default values, the "natural window mode" behavior being among them. This meant that the app would no longer determine the correct window mode upon joining a call, instead always guessing "normal" as the value. This change restores its reactivity.
This commit is contained in:
@@ -502,6 +502,48 @@ describe("CallViewModel", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("layout reacts to window size", () => {
|
||||||
|
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||||
|
const windowSizeInputMarbles = "abc";
|
||||||
|
const expectedLayoutMarbles = " abc";
|
||||||
|
withCallViewModel(
|
||||||
|
{
|
||||||
|
remoteParticipants$: constant([aliceParticipant]),
|
||||||
|
rtcMembers$: constant([localRtcMember, aliceRtcMember]),
|
||||||
|
windowSize$: behavior(windowSizeInputMarbles, {
|
||||||
|
a: { width: 300, height: 600 }, // 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
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
(vm) => {
|
||||||
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
|
expectedLayoutMarbles,
|
||||||
|
{
|
||||||
|
a: {
|
||||||
|
// This is the expected one-on-one layout for a narrow window
|
||||||
|
type: "spotlight-expanded",
|
||||||
|
spotlight: [`${aliceId}:0`],
|
||||||
|
pip: `${localId}:0`,
|
||||||
|
},
|
||||||
|
b: {
|
||||||
|
// In a larger window, expect the normal one-on-one layout
|
||||||
|
type: "one-on-one",
|
||||||
|
local: `${localId}:0`,
|
||||||
|
remote: `${aliceId}:0`,
|
||||||
|
},
|
||||||
|
c: {
|
||||||
|
// In a PiP-sized window, we of course expect a PiP layout
|
||||||
|
type: "pip",
|
||||||
|
spotlight: [`${aliceId}:0`],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("spotlight speakers swap places", () => {
|
test("spotlight speakers swap places", () => {
|
||||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||||
// Go immediately into spotlight mode for the test
|
// Go immediately into spotlight mode for the test
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ export interface CallViewModelOptions {
|
|||||||
livekitRoomFactory?: (options?: RoomOptions) => LivekitRoom;
|
livekitRoomFactory?: (options?: RoomOptions) => LivekitRoom;
|
||||||
/** Optional behavior overriding the local connection state, mainly for testing purposes. */
|
/** Optional behavior overriding the local connection state, mainly for testing purposes. */
|
||||||
connectionState$?: Behavior<ConnectionState>;
|
connectionState$?: Behavior<ConnectionState>;
|
||||||
|
/** Optional behavior overriding the computed window size, mainly for testing purposes. */
|
||||||
|
windowSize$?: Behavior<{ width: number; height: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not play any sounds if the participant count has exceeded this
|
// Do not play any sounds if the participant count has exceeded this
|
||||||
@@ -949,11 +951,19 @@ export function createCallViewModel$(
|
|||||||
|
|
||||||
const pipEnabled$ = scope.behavior(setPipEnabled$, false);
|
const pipEnabled$ = scope.behavior(setPipEnabled$, false);
|
||||||
|
|
||||||
|
const windowSize$ =
|
||||||
|
options.windowSize$ ??
|
||||||
|
scope.behavior<{ width: number; height: number }>(
|
||||||
|
fromEvent(window, "resize").pipe(
|
||||||
|
startWith(null),
|
||||||
|
map(() => ({ width: window.innerWidth, height: window.innerHeight })),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// A guess at what the window's mode should be based on its size and shape.
|
||||||
const naturalWindowMode$ = scope.behavior<WindowMode>(
|
const naturalWindowMode$ = scope.behavior<WindowMode>(
|
||||||
fromEvent(window, "resize").pipe(
|
windowSize$.pipe(
|
||||||
map(() => {
|
map(({ width, height }) => {
|
||||||
const height = window.innerHeight;
|
|
||||||
const width = window.innerWidth;
|
|
||||||
if (height <= 400 && width <= 340) return "pip";
|
if (height <= 400 && width <= 340) return "pip";
|
||||||
// Our layouts for flat windows are better at adapting to a small width
|
// Our layouts for flat windows are better at adapting to a small width
|
||||||
// than our layouts for narrow windows are at adapting to a small height,
|
// than our layouts for narrow windows are at adapting to a small height,
|
||||||
@@ -963,7 +973,6 @@ export function createCallViewModel$(
|
|||||||
return "normal";
|
return "normal";
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
"normal",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export interface CallViewModelInputs {
|
|||||||
speaking: Map<Participant, Observable<boolean>>;
|
speaking: Map<Participant, Observable<boolean>>;
|
||||||
mediaDevices: MediaDevices;
|
mediaDevices: MediaDevices;
|
||||||
initialSyncState: SyncState;
|
initialSyncState: SyncState;
|
||||||
|
windowSize$: Behavior<{ width: number; height: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const localParticipant = mockLocalParticipant({ identity: "" });
|
const localParticipant = mockLocalParticipant({ identity: "" });
|
||||||
@@ -89,6 +90,7 @@ export function withCallViewModel(
|
|||||||
speaking = new Map(),
|
speaking = new Map(),
|
||||||
mediaDevices = mockMediaDevices({}),
|
mediaDevices = mockMediaDevices({}),
|
||||||
initialSyncState = SyncState.Syncing,
|
initialSyncState = SyncState.Syncing,
|
||||||
|
windowSize$ = constant({ width: 1000, height: 800 }),
|
||||||
}: Partial<CallViewModelInputs> = {},
|
}: Partial<CallViewModelInputs> = {},
|
||||||
continuation: (
|
continuation: (
|
||||||
vm: CallViewModel,
|
vm: CallViewModel,
|
||||||
@@ -173,6 +175,7 @@ export function withCallViewModel(
|
|||||||
setE2EEEnabled: async () => Promise.resolve(),
|
setE2EEEnabled: async () => Promise.resolve(),
|
||||||
}),
|
}),
|
||||||
connectionState$,
|
connectionState$,
|
||||||
|
windowSize$,
|
||||||
},
|
},
|
||||||
raisedHands$,
|
raisedHands$,
|
||||||
reactions$,
|
reactions$,
|
||||||
|
|||||||
Reference in New Issue
Block a user