diff --git a/src/state/CallViewModel.test.ts b/src/state/CallViewModel.test.ts index f6a5892a..21a398d4 100644 --- a/src/state/CallViewModel.test.ts +++ b/src/state/CallViewModel.test.ts @@ -675,6 +675,21 @@ test("spotlight speakers swap places", () => { }, }, ); + + // While we expect the media on tiles to change, layout$ itself should + // *never* meaningfully change. That is, we expect there to be no layout + // shifts as the spotlight speaker changes; instead, the same tiles + // should be reused for the whole duration of the test and simply have + // their media swapped out. This is meaningful for keeping the interface + // not too visually distracting during back-and-forth conversations, + // while still animating tiles to express people joining, leaving, etc. + expectObservable( + vm.layout$.pipe( + distinctUntilChanged(deepCompare), + debounceTime(0), + map(() => "x"), + ), + ).toBe("x"); // Expect just one emission }, ); }); diff --git a/src/state/TileStore.ts b/src/state/TileStore.ts index 85bf8bc7..9465a709 100644 --- a/src/state/TileStore.ts +++ b/src/state/TileStore.ts @@ -118,10 +118,11 @@ export class TileStore { */ export class TileStoreBuilder { private spotlight: SpotlightTileData | null = null; - private readonly prevSpotlightSpeaker = + private readonly prevSpotlightSpeaker: UserMediaViewModel | null = this.prevSpotlight?.media.length === 1 && - "speaking" in this.prevSpotlight.media[0] && - this.prevSpotlight.media[0]; + "speaking$" in this.prevSpotlight.media[0] + ? this.prevSpotlight.media[0] + : null; private readonly prevGridByMedia: Map< MediaViewModel, @@ -201,7 +202,7 @@ export class TileStoreBuilder { if ( media === this.prevSpotlightSpeaker && this.spotlight.media.length === 1 && - "speaking" in this.spotlight.media[0] && + "speaking$" in this.spotlight.media[0] && this.prevSpotlightSpeaker !== this.spotlight.media[0] ) { const prev = this.prevGridByMedia.get(this.spotlight.media[0]);