Merge pull request #2514 from robintown/mobile-layouts

Improve the layouts on small mobile calls
This commit is contained in:
Robin
2024-08-06 10:10:29 -04:00
committed by GitHub
5 changed files with 115 additions and 94 deletions

View File

@@ -95,7 +95,6 @@ export interface GridArrangement {
const tileMaxAspectRatio = 17 / 9; const tileMaxAspectRatio = 17 / 9;
const tileMinAspectRatio = 4 / 3; const tileMinAspectRatio = 4 / 3;
const tileMobileMinAspectRatio = 2 / 3;
/** /**
* Determine the ideal arrangement of tiles into a grid of a particular size. * Determine the ideal arrangement of tiles into a grid of a particular size.
@@ -138,15 +137,10 @@ export function arrangeTiles(
// Impose a minimum and maximum aspect ratio on the tiles // Impose a minimum and maximum aspect ratio on the tiles
const tileAspectRatio = tileWidth / tileHeight; const tileAspectRatio = tileWidth / tileHeight;
// We enforce a different min aspect ratio in 1:1s on mobile
const minAspectRatio =
tileCount === 1 && width < 600
? tileMobileMinAspectRatio
: tileMinAspectRatio;
if (tileAspectRatio > tileMaxAspectRatio) if (tileAspectRatio > tileMaxAspectRatio)
tileWidth = tileHeight * tileMaxAspectRatio; tileWidth = tileHeight * tileMaxAspectRatio;
else if (tileAspectRatio < minAspectRatio) else if (tileAspectRatio < tileMinAspectRatio)
tileHeight = tileWidth / minAspectRatio; tileHeight = tileWidth / tileMinAspectRatio;
return { tileWidth, tileHeight, gap, columns }; return { tileWidth, tileHeight, gap, columns };
} }

View File

@@ -26,18 +26,11 @@ limitations under the License.
.local { .local {
position: absolute; position: absolute;
inline-size: 135px; inline-size: 180px;
block-size: 160px; block-size: 135px;
inset: var(--cpd-space-4x); inset: var(--cpd-space-4x);
} }
@media (min-width: 600px) {
.local {
inline-size: 170px;
block-size: 110px;
}
}
.spotlight { .spotlight {
position: absolute; position: absolute;
inline-size: 404px; inline-size: 404px;

View File

@@ -25,11 +25,18 @@ limitations under the License.
.pip { .pip {
position: absolute; position: absolute;
inline-size: 180px; inline-size: 135px;
block-size: 135px; block-size: 160px;
inset: var(--cpd-space-4x); inset: var(--cpd-space-4x);
} }
@media (min-width: 600px) {
.pip {
inline-size: 180px;
block-size: 135px;
}
}
.pip[data-block-alignment="start"] { .pip[data-block-alignment="start"] {
inset-block-end: unset; inset-block-end: unset;
} }

View File

@@ -295,7 +295,7 @@ export const InCallView: FC<InCallViewProps> = ({
ref, ref,
) { ) {
const spotlightExpanded = useObservableEagerState(vm.spotlightExpanded); const spotlightExpanded = useObservableEagerState(vm.spotlightExpanded);
const [onToggleExpanded] = useObservableEagerState( const onToggleExpanded = useObservableEagerState(
vm.toggleSpotlightExpanded, vm.toggleSpotlightExpanded,
); );
const showSpeakingIndicatorsValue = useObservableEagerState( const showSpeakingIndicatorsValue = useObservableEagerState(

View File

@@ -46,6 +46,7 @@ import {
shareReplay, shareReplay,
skip, skip,
startWith, startWith,
switchAll,
switchMap, switchMap,
throttleTime, throttleTime,
timer, timer,
@@ -75,6 +76,10 @@ import { duplicateTiles } from "../settings/settings";
// list again // list again
const POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS = 3000; 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 { export interface GridLayout {
type: "grid"; type: "grid";
spotlight?: MediaViewModel[]; spotlight?: MediaViewModel[];
@@ -515,7 +520,7 @@ export class CallViewModel extends ViewModel {
const height = window.innerHeight; const height = window.innerHeight;
const width = window.innerWidth; const width = window.innerWidth;
if (height <= 400 && width <= 340) return "pip"; if (height <= 400 && width <= 340) return "pip";
if (width <= 660) return "narrow"; if (width <= 600) return "narrow";
if (height <= 660) return "flat"; if (height <= 660) return "flat";
return "normal"; return "normal";
}), }),
@@ -561,75 +566,98 @@ export class CallViewModel extends ViewModel {
this.gridModeUserSelection.next(value); this.gridModeUserSelection.next(value);
} }
private readonly oneOnOne: Observable<boolean> = 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<Layout> = combineLatest(
[this.grid, this.spotlight],
(grid, spotlight) => ({
type: "grid",
spotlight: spotlight.some((vm) => vm instanceof ScreenShareViewModel)
? spotlight
: undefined,
grid,
}),
);
private readonly spotlightLandscapeLayout: Observable<Layout> = combineLatest(
[this.grid, this.spotlight],
(grid, spotlight) => ({ type: "spotlight-landscape", spotlight, grid }),
);
private readonly spotlightPortraitLayout: Observable<Layout> = combineLatest(
[this.grid, this.spotlight],
(grid, spotlight) => ({ type: "spotlight-portrait", spotlight, grid }),
);
private readonly spotlightExpandedLayout: Observable<Layout> = combineLatest(
[this.spotlight, this.pip],
(spotlight, pip) => ({
type: "spotlight-expanded",
spotlight,
pip: pip ?? undefined,
}),
);
private readonly oneOnOneLayout: Observable<Layout> = 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<Layout> = this.spotlight.pipe(
map((spotlight): Layout => ({ type: "pip", spotlight })),
);
public readonly layout: Observable<Layout> = this.windowMode.pipe( public readonly layout: Observable<Layout> = this.windowMode.pipe(
switchMap((windowMode) => { 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) { switch (windowMode) {
case "normal": case "normal":
return this.gridMode.pipe( return this.gridMode.pipe(
switchMap((gridMode) => { switchMap((gridMode) => {
switch (gridMode) { switch (gridMode) {
case "grid": case "grid":
return combineLatest( return this.oneOnOne.pipe(
[this.grid, this.spotlight, this.screenShares], switchMap((oneOnOne) =>
(grid, spotlight, screenShares): Layout => oneOnOne ? this.oneOnOneLayout : this.gridLayout,
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,
},
); );
case "spotlight": case "spotlight":
return this.spotlightExpanded.pipe( return this.spotlightExpanded.pipe(
switchMap((expanded) => switchMap((expanded) =>
expanded expanded
? spotlightExpandedLayout ? this.spotlightExpandedLayout
: spotlightLandscapeLayout, : this.spotlightLandscapeLayout,
), ),
); );
} }
}), }),
); );
case "narrow": case "narrow":
return combineLatest( return this.oneOnOne.pipe(
[this.grid, this.spotlight], switchMap((oneOnOne) =>
(grid, spotlight): Layout => ({ oneOnOne
type: "spotlight-portrait", ? // The expanded spotlight layout makes for a better one-on-one
spotlight, // experience in narrow windows
grid, this.spotlightExpandedLayout
}), : combineLatest(
[this.grid, this.spotlight],
(grid, spotlight) =>
grid.length > smallMobileCallThreshold ||
spotlight.some((vm) => vm instanceof ScreenShareViewModel)
? this.spotlightPortraitLayout
: this.gridLayout,
).pipe(switchAll()),
),
); );
case "flat": case "flat":
return this.gridMode.pipe( return this.gridMode.pipe(
@@ -638,16 +666,14 @@ export class CallViewModel extends ViewModel {
case "grid": case "grid":
// Yes, grid mode actually gets you a "spotlight" layout in // Yes, grid mode actually gets you a "spotlight" layout in
// this window mode. // this window mode.
return spotlightLandscapeLayout; return this.spotlightLandscapeLayout;
case "spotlight": case "spotlight":
return spotlightExpandedLayout; return this.spotlightExpandedLayout;
} }
}), }),
); );
case "pip": case "pip":
return this.spotlight.pipe( return this.pipLayout;
map((spotlight): Layout => ({ type: "pip", spotlight })),
);
} }
}), }),
shareReplay(1), shareReplay(1),
@@ -665,24 +691,25 @@ export class CallViewModel extends ViewModel {
shareReplay(1), shareReplay(1),
); );
// To work around https://github.com/crimx/observable-hooks/issues/131 we must public readonly toggleSpotlightExpanded: Observable<(() => void) | null> =
// wrap the emitted function here in a non-function wrapper type this.windowMode.pipe(
public readonly toggleSpotlightExpanded: Observable< switchMap((mode) =>
readonly [(() => void) | null] mode === "normal"
> = this.layout.pipe( ? this.layout.pipe(
map( map(
(l) => (l) =>
l.type === "spotlight-landscape" || l.type === "spotlight-expanded", l.type === "spotlight-landscape" ||
), l.type === "spotlight-expanded",
distinctUntilChanged(), ),
map( )
(enabled) => : of(false),
[ ),
enabled ? (): void => this.spotlightExpandedToggle.next() : null, distinctUntilChanged(),
] as const, map((enabled) =>
), enabled ? (): void => this.spotlightExpandedToggle.next() : null,
shareReplay(1), ),
); shareReplay(1),
);
public constructor( public constructor(
// A call is permanently tied to a single Matrix room and LiveKit room // A call is permanently tied to a single Matrix room and LiveKit room