diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index dbfe05f00..6538dd985 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -46,6 +46,7 @@ import { shareReplay, skip, startWith, + switchAll, switchMap, throttleTime, timer, @@ -75,6 +76,10 @@ import { duplicateTiles } from "../settings/settings"; // list again const POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS = 3000; +// This is the number of participants that we think constitutes a "small" call +// on mobile. No spotlight tile should be shown below this threshold. +const smallMobileCallThreshold = 3; + export interface GridLayout { type: "grid"; spotlight?: MediaViewModel[]; @@ -571,75 +576,96 @@ export class CallViewModel extends ViewModel { this.gridModeUserSelection.next(value); } + private readonly oneOnOne: Observable = combineLatest( + [this.grid, this.screenShares], + (grid, screenShares) => + grid.length == 2 && + // There might not be a remote tile if only the local user is in the call + // and they're using the duplicate tiles option + grid.some((vm) => !vm.local) && + screenShares.length === 0, + ); + + private readonly gridLayout: Observable = combineLatest( + [this.grid, this.spotlight], + (grid, spotlight) => ({ + type: "grid", + spotlight: spotlight.some((vm) => vm instanceof ScreenShareViewModel) + ? spotlight + : undefined, + grid, + }), + ); + + private readonly spotlightLandscapeLayout: Observable = combineLatest( + [this.grid, this.spotlight], + (grid, spotlight) => ({ type: "spotlight-landscape", spotlight, grid }), + ); + + private readonly spotlightPortraitLayout: Observable = combineLatest( + [this.grid, this.spotlight], + (grid, spotlight) => ({ type: "spotlight-portrait", spotlight, grid }), + ); + + private readonly spotlightExpandedLayout: Observable = combineLatest( + [this.spotlight, this.pip], + (spotlight, pip) => ({ + type: "spotlight-expanded", + spotlight, + pip: pip ?? undefined, + }), + ); + + private readonly oneOnOneLayout: Observable = this.grid.pipe( + map((grid) => ({ + type: "one-on-one", + local: grid.find((vm) => vm.local) as LocalUserMediaViewModel, + remote: grid.find((vm) => !vm.local) as RemoteUserMediaViewModel, + })), + ); + + private readonly pipLayout: Observable = this.spotlight.pipe( + map((spotlight): Layout => ({ type: "pip", spotlight })), + ); + public readonly layout: Observable = this.windowMode.pipe( switchMap((windowMode) => { - const spotlightLandscapeLayout = combineLatest( - [this.grid, this.spotlight], - (grid, spotlight): Layout => ({ - type: "spotlight-landscape", - spotlight, - grid, - }), - ); - const spotlightExpandedLayout = combineLatest( - [this.spotlight, this.pip], - (spotlight, pip): Layout => ({ - type: "spotlight-expanded", - spotlight, - pip: pip ?? undefined, - }), - ); - switch (windowMode) { case "normal": return this.gridMode.pipe( switchMap((gridMode) => { switch (gridMode) { case "grid": - return combineLatest( - [this.grid, this.spotlight, this.screenShares], - (grid, spotlight, screenShares): Layout => - grid.length == 2 && - // There might not be a remote tile if only the local user - // is in the call and they're using the duplicate tiles - // option - grid.some((vm) => !vm.local) && - screenShares.length === 0 - ? { - type: "one-on-one", - local: grid.find( - (vm) => vm.local, - ) as LocalUserMediaViewModel, - remote: grid.find( - (vm) => !vm.local, - ) as RemoteUserMediaViewModel, - } - : { - type: "grid", - spotlight: - screenShares.length > 0 ? spotlight : undefined, - grid, - }, + return this.oneOnOne.pipe( + switchMap((oneOnOne) => + oneOnOne ? this.oneOnOneLayout : this.gridLayout, + ), ); case "spotlight": return this.spotlightExpanded.pipe( switchMap((expanded) => expanded - ? spotlightExpandedLayout - : spotlightLandscapeLayout, + ? this.spotlightExpandedLayout + : this.spotlightLandscapeLayout, ), ); } }), ); case "narrow": - return combineLatest( - [this.grid, this.spotlight], - (grid, spotlight): Layout => ({ - type: "spotlight-portrait", - spotlight, - grid, - }), + return this.oneOnOne.pipe( + switchMap((oneOnOne) => + oneOnOne + ? this.oneOnOneLayout + : combineLatest( + [this.grid, this.spotlight], + (grid, spotlight) => + grid.length > smallMobileCallThreshold || + spotlight.some((vm) => vm instanceof ScreenShareViewModel) + ? this.spotlightPortraitLayout + : this.gridLayout, + ).pipe(switchAll()), + ), ); case "flat": return this.gridMode.pipe( @@ -648,16 +674,14 @@ export class CallViewModel extends ViewModel { case "grid": // Yes, grid mode actually gets you a "spotlight" layout in // this window mode. - return spotlightLandscapeLayout; + return this.spotlightLandscapeLayout; case "spotlight": - return spotlightExpandedLayout; + return this.spotlightExpandedLayout; } }), ); case "pip": - return this.spotlight.pipe( - map((spotlight): Layout => ({ type: "pip", spotlight })), - ); + return this.pipLayout; } }), shareReplay(1),