Merge pull request #2514 from robintown/mobile-layouts
Improve the layouts on small mobile calls
This commit is contained in:
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user