Delete the legacy grid system

This commit is contained in:
Robin
2024-06-07 17:29:48 -04:00
parent 7979493371
commit 45c89a2298
10 changed files with 102 additions and 1892 deletions

View File

@@ -162,6 +162,5 @@
"mute_for_me": "Mute for me",
"sfu_participant_local": "You",
"volume": "Volume"
},
"waiting_for_participants": "Waiting for other participants…"
}
}

View File

@@ -17,7 +17,7 @@ limitations under the License.
import { BehaviorSubject, Observable } from "rxjs";
import { ComponentType } from "react";
import { MediaViewModel } from "../state/MediaViewModel";
import { MediaViewModel, UserMediaViewModel } from "../state/MediaViewModel";
import { LayoutProps } from "./Grid";
export interface Bounds {
@@ -53,7 +53,7 @@ export interface CallLayoutInputs {
export interface GridTileModel {
type: "grid";
vm: MediaViewModel;
vm: UserMediaViewModel;
}
export interface SpotlightTileModel {

View File

@@ -41,7 +41,6 @@ import styles from "./Grid.module.css";
import { useMergedRefs } from "../useMergedRefs";
import { TileWrapper } from "./TileWrapper";
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
import { TileSpringUpdate } from "./LegacyGrid";
import { useInitial } from "../useInitial";
interface Rect {
@@ -69,6 +68,13 @@ interface TileSpring {
height: number;
}
interface TileSpringUpdate extends Partial<TileSpring> {
from?: Partial<TileSpring>;
reset?: boolean;
immediate?: boolean | ((key: string) => boolean);
delay?: (key: string) => number;
}
interface DragState {
tileId: string;
tileX: number;

View File

@@ -1,22 +0,0 @@
/*
Copyright 2022-2024 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.grid {
position: relative;
overflow: hidden;
flex: 1;
touch-action: none;
}

File diff suppressed because it is too large Load Diff

View File

@@ -18,10 +18,9 @@ import {
RoomAudioRenderer,
RoomContext,
useLocalParticipant,
useTracks,
} from "@livekit/components-react";
import { usePreventScroll } from "@react-aria/overlays";
import { ConnectionState, Room, Track } from "livekit-client";
import { ConnectionState, Room } from "livekit-client";
import { MatrixClient } from "matrix-js-sdk/src/client";
import {
FC,
@@ -38,7 +37,6 @@ import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import classNames from "classnames";
import { BehaviorSubject, map } from "rxjs";
import { useObservableEagerState } from "observable-hooks";
import { useTranslation } from "react-i18next";
import LogoMark from "../icons/LogoMark.svg?react";
import LogoType from "../icons/LogoType.svg?react";
@@ -51,10 +49,8 @@ import {
SettingsButton,
} from "../button";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import { LegacyGrid, useLegacyGridLayout } from "../grid/LegacyGrid";
import { useUrlParams } from "../UrlParams";
import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
import { ElementWidgetActions, widget } from "../widget";
import styles from "./InCallView.module.css";
import { GridTile } from "../tile/GridTile";
@@ -72,14 +68,8 @@ import { InviteButton } from "../button/InviteButton";
import { LayoutToggle } from "./LayoutToggle";
import { ECConnectionState } from "../livekit/useECConnectionState";
import { useOpenIDSFU } from "../livekit/openIDSFU";
import {
GridMode,
Layout,
TileDescriptor,
useCallViewModel,
} from "../state/CallViewModel";
import { GridMode, Layout, useCallViewModel } from "../state/CallViewModel";
import { Grid, TileProps } from "../grid/Grid";
import { MediaViewModel } from "../state/MediaViewModel";
import { useObservable } from "../state/useObservable";
import { useInitial } from "../useInitial";
import { SpotlightTile } from "../tile/SpotlightTile";
@@ -89,7 +79,6 @@ import { makeGridLayout } from "../grid/GridLayout";
import { makeSpotlightLayout } from "../grid/SpotlightLayout";
import {
CallLayout,
GridTileModel,
TileModel,
defaultPipAlignment,
defaultSpotlightAlignment,
@@ -98,10 +87,6 @@ import { makeOneOnOneLayout } from "../grid/OneOnOneLayout";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
const dummySpotlightItem = {
id: "spotlight",
} as TileDescriptor<MediaViewModel>;
export interface ActiveCallProps
extends Omit<InCallViewProps, "livekitRoom" | "connState"> {
e2eeSystem: EncryptionSystem;
@@ -155,11 +140,9 @@ export const InCallView: FC<InCallViewProps> = ({
participantCount,
onLeave,
hideHeader,
otelGroupCallMembership,
connState,
onShareClick,
}) => {
const { t } = useTranslation();
usePreventScroll();
useWakeLock();
@@ -177,15 +160,6 @@ export const InCallView: FC<InCallViewProps> = ({
// Merge the refs so they can attach to the same element
const containerRef = useMergedRefs(containerRef1, containerRef2);
const screenSharingTracks = useTracks(
[{ source: Track.Source.ScreenShare, withPlaceholder: false }],
{
room: livekitRoom,
},
);
const { layout: legacyLayout, setLayout: setLegacyLayout } =
useLegacyGridLayout(screenSharingTracks.length > 0);
const { hideScreensharing, showControls } = useUrlParams();
const { isScreenShareEnabled, localParticipant } = useLocalParticipant({
@@ -210,42 +184,6 @@ export const InCallView: FC<InCallViewProps> = ({
(muted) => muteStates.audio.setEnabled?.(!muted),
);
useEffect(() => {
widget?.api.transport.send(
legacyLayout === "grid"
? ElementWidgetActions.TileLayout
: ElementWidgetActions.SpotlightLayout,
{},
);
}, [legacyLayout]);
useEffect(() => {
if (widget) {
const onTileLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
setLegacyLayout("grid");
widget!.api.transport.reply(ev.detail, {});
};
const onSpotlightLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
setLegacyLayout("spotlight");
widget!.api.transport.reply(ev.detail, {});
};
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
widget.lazyActions.on(
ElementWidgetActions.SpotlightLayout,
onSpotlightLayout,
);
return (): void => {
widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
widget!.lazyActions.off(
ElementWidgetActions.SpotlightLayout,
onSpotlightLayout,
);
};
}
}, [setLegacyLayout]);
const mobile = boundsValid && bounds.width <= 660;
const reducedControls = boundsValid && bounds.width <= 340;
const noControls = reducedControls && bounds.height <= 400;
@@ -256,15 +194,12 @@ export const InCallView: FC<InCallViewProps> = ({
matrixInfo.e2eeSystem.kind !== E2eeType.NONE,
connState,
);
const items = useObservableEagerState(vm.tiles);
const layout = useObservableEagerState(vm.layout);
const gridMode = useObservableEagerState(vm.gridMode);
const hasSpotlight = layout.spotlight !== undefined;
// Hack: We insert a dummy "spotlight" tile into the tiles we pass to
// useFullscreen so that we can control the fullscreen state of the
// spotlight tile in the new layouts with this same hook.
const fullscreenItems = useMemo(
() => (hasSpotlight ? [...items, dummySpotlightItem] : items),
[items, hasSpotlight],
() => (hasSpotlight ? ["spotlight"] : []),
[hasSpotlight],
);
const { fullscreenItem, toggleFullscreen, exitFullscreen } =
useFullscreen(fullscreenItems);
@@ -274,18 +209,9 @@ export const InCallView: FC<InCallViewProps> = ({
);
// The maximised participant: either the participant that the user has
// manually put in fullscreen, or the focused (active) participant if the
// window is too small to show everyone
const maximisedParticipant = useMemo(
() =>
fullscreenItem ??
(noControls
? items.find((item) => item.isSpeaker) ?? items.at(0) ?? null
: null),
[fullscreenItem, noControls, items],
);
const prefersReducedMotion = usePrefersReducedMotion();
// manually put in fullscreen, or (TODO) the spotlight if the window is too
// small to show everyone
const maximisedParticipant = fullscreenItem;
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const [settingsTab, setSettingsTab] = useState(defaultSettingsTab);
@@ -339,7 +265,7 @@ export const InCallView: FC<InCallViewProps> = ({
makeLayout = makeSpotlightLayout as CallLayout<Layout>;
else if (l.type === "one-on-one")
makeLayout = makeOneOnOneLayout as CallLayout<Layout>;
else return null; // Not yet implemented
else throw new Error(`Unimplemented layout: ${l.type}`);
return makeLayout({
minBounds: gridBoundsObservable,
@@ -352,13 +278,46 @@ export const InCallView: FC<InCallViewProps> = ({
);
const setGridMode = useCallback(
(mode: GridMode) => {
setLegacyLayout(mode);
vm.setGridMode(mode);
},
[setLegacyLayout, vm],
(mode: GridMode) => vm.setGridMode(mode),
[vm],
);
useEffect(() => {
widget?.api.transport.send(
gridMode === "grid"
? ElementWidgetActions.TileLayout
: ElementWidgetActions.SpotlightLayout,
{},
);
}, [gridMode]);
useEffect(() => {
if (widget) {
const onTileLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
setGridMode("grid");
widget!.api.transport.reply(ev.detail, {});
};
const onSpotlightLayout = (ev: CustomEvent<IWidgetApiRequest>): void => {
setGridMode("spotlight");
widget!.api.transport.reply(ev.detail, {});
};
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
widget.lazyActions.on(
ElementWidgetActions.SpotlightLayout,
onSpotlightLayout,
);
return (): void => {
widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
widget!.lazyActions.off(
ElementWidgetActions.SpotlightLayout,
onSpotlightLayout,
);
};
}
}, [setGridMode]);
const showSpotlightIndicators = useObservable(layout.type === "spotlight");
const showSpeakingIndicators = useObservable(
layout.type === "spotlight" ||
@@ -419,33 +378,10 @@ export const InCallView: FC<InCallViewProps> = ({
],
);
const LegacyTile = useMemo(
() =>
forwardRef<
HTMLDivElement,
PropsWithoutRef<TileProps<MediaViewModel, HTMLDivElement>>
>(function LegacyTile({ model: legacyModel, ...props }, ref) {
const model: GridTileModel = useMemo(
() => ({ type: "grid", vm: legacyModel }),
[legacyModel],
);
return <Tile ref={ref} model={model} {...props} />;
}),
[Tile],
);
const renderContent = (): JSX.Element => {
if (items.length === 0) {
return (
<div className={styles.centerMessage}>
<p>{t("waiting_for_participants")}</p>
</div>
);
}
if (maximisedParticipant !== null) {
const fullscreen = maximisedParticipant === fullscreenItem;
if (maximisedParticipant.id === "spotlight") {
if (maximisedParticipant === "spotlight") {
return (
<SpotlightTile
className={classNames(styles.tile, styles.maximised)}
@@ -459,55 +395,28 @@ export const InCallView: FC<InCallViewProps> = ({
/>
);
}
return (
<GridTile
className={classNames(styles.tile, styles.maximised)}
vm={maximisedParticipant.data}
maximised={true}
fullscreen={fullscreen}
onToggleFullscreen={toggleFullscreen}
targetHeight={gridBounds.height}
targetWidth={gridBounds.width}
key={maximisedParticipant.id}
showSpeakingIndicators={false}
onOpenProfile={openProfile}
/>
);
}
if (layoutSystem === null) {
// This new layout doesn't yet have an implemented layout system, so fall
// back to the legacy grid system
return (
<LegacyGrid
items={items}
layout={legacyLayout}
disableAnimations={prefersReducedMotion}
Tile={LegacyTile}
return (
<>
<Grid
className={styles.scrollingGrid}
model={layout}
Layout={layoutSystem.scrolling}
Tile={Tile}
/>
);
} else {
return (
<>
<Grid
className={styles.scrollingGrid}
model={layout}
Layout={layoutSystem.scrolling}
Tile={Tile}
/>
<Grid
className={styles.fixedGrid}
style={{
insetBlockStart: headerBounds.bottom,
height: gridBounds.height,
}}
model={layout}
Layout={layoutSystem.fixed}
Tile={Tile}
/>
</>
);
}
<Grid
className={styles.fixedGrid}
style={{
insetBlockStart: headerBounds.bottom,
height: gridBounds.height,
}}
model={layout}
Layout={layoutSystem.fixed}
Tile={Tile}
/>
</>
);
};
const rageshakeRequestModalProps = useRageshakeRequestModal(
@@ -596,7 +505,7 @@ export const InCallView: FC<InCallViewProps> = ({
{!mobile && !hideHeader && showControls && (
<LayoutToggle
className={styles.layout}
layout={legacyLayout}
layout={gridMode}
setLayout={setGridMode}
/>
)}

View File

@@ -20,7 +20,6 @@ import { useCallback, useLayoutEffect, useRef } from "react";
import { useReactiveState } from "../useReactiveState";
import { useEventTarget } from "../useEvents";
import { TileDescriptor } from "../state/CallViewModel";
const isFullscreen = (): boolean =>
Boolean(document.fullscreenElement) ||
@@ -55,31 +54,30 @@ function useFullscreenChange(onFullscreenChange: () => void): void {
* Provides callbacks for controlling the full-screen view, which can hold one
* item at a time.
*/
export function useFullscreen<T>(items: TileDescriptor<T>[]): {
fullscreenItem: TileDescriptor<T> | null;
// TODO: Simplify this. Nowadays we only allow the spotlight to be fullscreen,
// so we don't need to bother with multiple items.
export function useFullscreen(items: string[]): {
fullscreenItem: string | null;
toggleFullscreen: (itemId: string) => void;
exitFullscreen: () => void;
} {
const [fullscreenItem, setFullscreenItem] =
useReactiveState<TileDescriptor<T> | null>(
(prevItem) =>
prevItem == null
? null
: items.find((i) => i.id === prevItem.id) ?? null,
[items],
);
const [fullscreenItem, setFullscreenItem] = useReactiveState<string | null>(
(prevItem) =>
prevItem == null ? null : items.find((i) => i === prevItem) ?? null,
[items],
);
const latestItems = useRef<TileDescriptor<T>[]>(items);
const latestItems = useRef<string[]>(items);
latestItems.current = items;
const latestFullscreenItem = useRef<TileDescriptor<T> | null>(fullscreenItem);
const latestFullscreenItem = useRef<string | null>(fullscreenItem);
latestFullscreenItem.current = fullscreenItem;
const toggleFullscreen = useCallback(
(itemId: string) => {
setFullscreenItem(
latestFullscreenItem.current === null
? latestItems.current.find((i) => i.id === itemId) ?? null
? latestItems.current.find((i) => i === itemId) ?? null
: null,
);
},

View File

@@ -74,22 +74,6 @@ import { ObservableScope } from "./ObservableScope";
// list again
const POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS = 3000;
// Represents something that should get a tile on the layout,
// ie. a user's video feed or a screen share feed.
// TODO: This exposes too much information to the view layer, let's keep this
// information internal to the view model and switch to using Tile<T> instead
export interface TileDescriptor<T> {
id: string;
focused: boolean;
isPresenter: boolean;
isSpeaker: boolean;
hasVideo: boolean;
local: boolean;
largeBaseSize: boolean;
placeNear?: string;
data: T;
}
export interface GridLayout {
type: "grid";
spotlight?: MediaViewModel[];
@@ -548,108 +532,6 @@ export class CallViewModel extends ViewModel {
shareReplay(1),
);
/**
* The media tiles to be displayed in the call view.
*/
// TODO: Get rid of this field, replacing it with the 'layout' field above
// which keeps more details of the layout order internal to the view model
public readonly tiles: Observable<TileDescriptor<MediaViewModel>[]> =
combineLatest([
this.remoteParticipants,
observeParticipantMedia(this.livekitRoom.localParticipant),
]).pipe(
scan((ts, [remoteParticipants, { participant: localParticipant }]) => {
const ps = [localParticipant, ...remoteParticipants];
const tilesById = new Map(ts.map((t) => [t.id, t]));
const now = Date.now();
let allGhosts = true;
const newTiles = ps.flatMap((p) => {
const userMediaId = p.identity;
const member = findMatrixMember(this.matrixRoom, userMediaId);
allGhosts &&= member === undefined;
const spokeRecently =
p.lastSpokeAt !== undefined && now - +p.lastSpokeAt <= 10000;
// We always start with a local participant with the empty string as
// their ID before we're connected, this is fine and we'll be in
// "all ghosts" mode.
if (userMediaId !== "" && member === undefined) {
logger.warn(
`Ruh, roh! No matrix member found for SFU participant '${userMediaId}': creating g-g-g-ghost!`,
);
}
const userMediaVm =
tilesById.get(userMediaId)?.data ??
(p instanceof LocalParticipant
? new LocalUserMediaViewModel(
userMediaId,
member,
p,
this.encrypted,
)
: new RemoteUserMediaViewModel(
userMediaId,
member,
p,
this.encrypted,
));
tilesById.delete(userMediaId);
const userMediaTile: TileDescriptor<MediaViewModel> = {
id: userMediaId,
focused: false,
isPresenter: p.isScreenShareEnabled,
isSpeaker: (p.isSpeaking || spokeRecently) && !p.isLocal,
hasVideo: p.isCameraEnabled,
local: p.isLocal,
largeBaseSize: false,
data: userMediaVm,
};
if (p.isScreenShareEnabled) {
const screenShareId = `${userMediaId}:screen-share`;
const screenShareVm =
tilesById.get(screenShareId)?.data ??
new ScreenShareViewModel(
screenShareId,
member,
p,
this.encrypted,
);
tilesById.delete(screenShareId);
const screenShareTile: TileDescriptor<MediaViewModel> = {
id: screenShareId,
focused: true,
isPresenter: false,
isSpeaker: false,
hasVideo: true,
local: p.isLocal,
largeBaseSize: true,
placeNear: userMediaId,
data: screenShareVm,
};
return [userMediaTile, screenShareTile];
} else {
return [userMediaTile];
}
});
// Any tiles left in the map are unused and should be destroyed
for (const t of tilesById.values()) t.data.destroy();
// If every item is a ghost, that probably means we're still connecting
// and shouldn't bother showing anything yet
return allGhosts ? [] : newTiles;
}, [] as TileDescriptor<MediaViewModel>[]),
finalizeValue((ts) => {
for (const t of ts) t.data.destroy();
}),
shareReplay(1),
);
public constructor(
// A call is permanently tied to a single Matrix room and LiveKit room
private readonly matrixRoom: MatrixRoom,

View File

@@ -33,7 +33,6 @@ import VolumeOffIcon from "@vector-im/compound-design-tokens/icons/volume-off.sv
import VisibilityOnIcon from "@vector-im/compound-design-tokens/icons/visibility-on.svg?react";
import UserProfileIcon from "@vector-im/compound-design-tokens/icons/user-profile.svg?react";
import ExpandIcon from "@vector-im/compound-design-tokens/icons/expand.svg?react";
import CollapseIcon from "@vector-im/compound-design-tokens/icons/collapse.svg?react";
import {
ContextMenu,
MenuItem,
@@ -44,8 +43,6 @@ import { useObservableEagerState } from "observable-hooks";
import styles from "./GridTile.module.css";
import {
ScreenShareViewModel,
MediaViewModel,
UserMediaViewModel,
useNameData,
LocalUserMediaViewModel,
@@ -63,45 +60,12 @@ interface TileProps {
maximised: boolean;
displayName: string;
nameTag: string;
showSpeakingIndicators: boolean;
}
interface MediaTileProps
extends TileProps,
Omit<ComponentProps<typeof animated.div>, "className"> {
vm: MediaViewModel;
videoEnabled: boolean;
videoFit: "contain" | "cover";
mirror: boolean;
nameTagLeadingIcon?: ReactNode;
primaryButton: ReactNode;
secondaryButton?: ReactNode;
}
const MediaTile = forwardRef<HTMLDivElement, MediaTileProps>(
({ vm, className, maximised, ...props }, ref) => {
const video = useObservableEagerState(vm.video);
const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning);
return (
<MediaView
ref={ref}
className={classNames(className, styles.tile)}
data-maximised={maximised}
video={video}
member={vm.member}
unencryptedWarning={unencryptedWarning}
{...props}
/>
);
},
);
MediaTile.displayName = "MediaTile";
interface UserMediaTileProps extends TileProps {
vm: UserMediaViewModel;
mirror: boolean;
showSpeakingIndicators: boolean;
menuStart?: ReactNode;
menuEnd?: ReactNode;
}
@@ -115,11 +79,14 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
menuEnd,
className,
nameTag,
maximised,
...props
},
ref,
) => {
const { t } = useTranslation();
const video = useObservableEagerState(vm.video);
const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning);
const audioEnabled = useObservableEagerState(vm.audioEnabled);
const videoEnabled = useObservableEagerState(vm.videoEnabled);
const speaking = useObservableEagerState(vm.speaking);
@@ -148,12 +115,14 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
);
const tile = (
<MediaTile
<MediaView
ref={ref}
vm={vm}
video={video}
member={vm.member}
unencryptedWarning={unencryptedWarning}
videoEnabled={videoEnabled}
videoFit={cropVideo ? "cover" : "contain"}
className={classNames(className, {
className={classNames(className, styles.tile, {
[styles.speaking]: showSpeakingIndicators && speaking,
})}
nameTagLeadingIcon={
@@ -182,6 +151,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
{menu}
</Menu>
}
data-maximised={maximised}
{...props}
/>
);
@@ -199,7 +169,6 @@ UserMediaTile.displayName = "UserMediaTile";
interface LocalUserMediaTileProps extends TileProps {
vm: LocalUserMediaViewModel;
onOpenProfile: () => void;
showSpeakingIndicators: boolean;
}
const LocalUserMediaTile = forwardRef<HTMLDivElement, LocalUserMediaTileProps>(
@@ -248,7 +217,6 @@ LocalUserMediaTile.displayName = "LocalUserMediaTile";
interface RemoteUserMediaTileProps extends TileProps {
vm: RemoteUserMediaViewModel;
showSpeakingIndicators: boolean;
}
const RemoteUserMediaTile = forwardRef<
@@ -303,53 +271,8 @@ const RemoteUserMediaTile = forwardRef<
RemoteUserMediaTile.displayName = "RemoteUserMediaTile";
interface ScreenShareTileProps extends TileProps {
vm: ScreenShareViewModel;
fullscreen: boolean;
onToggleFullscreen: (itemId: string) => void;
}
const ScreenShareTile = forwardRef<HTMLDivElement, ScreenShareTileProps>(
({ vm, fullscreen, onToggleFullscreen, ...props }, ref) => {
const { t } = useTranslation();
const onClickFullScreen = useCallback(
() => onToggleFullscreen(vm.id),
[onToggleFullscreen, vm],
);
const FullScreenIcon = fullscreen ? CollapseIcon : ExpandIcon;
return (
<MediaTile
ref={ref}
vm={vm}
videoEnabled
videoFit="contain"
mirror={false}
primaryButton={
!vm.local && (
<button
aria-label={
fullscreen
? t("video_tile.full_screen")
: t("video_tile.exit_full_screen")
}
onClick={onClickFullScreen}
>
<FullScreenIcon aria-hidden width={20} height={20} />
</button>
)
}
{...props}
/>
);
},
);
ScreenShareTile.displayName = "ScreenShareTile";
interface GridTileProps {
vm: MediaViewModel;
vm: UserMediaViewModel;
maximised: boolean;
fullscreen: boolean;
onToggleFullscreen: (itemId: string) => void;
@@ -375,19 +298,8 @@ export const GridTile = forwardRef<HTMLDivElement, GridTileProps>(
{...nameData}
/>
);
} else if (vm instanceof RemoteUserMediaViewModel) {
return <RemoteUserMediaTile ref={ref} vm={vm} {...props} {...nameData} />;
} else {
return (
<ScreenShareTile
ref={ref}
vm={vm}
fullscreen={fullscreen}
onToggleFullscreen={onToggleFullscreen}
{...props}
{...nameData}
/>
);
return <RemoteUserMediaTile ref={ref} vm={vm} {...props} {...nameData} />;
}
},
);

View File

@@ -1,69 +0,0 @@
/*
Copyright 2023-2024 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { TileDescriptor } from "../../src/state/CallViewModel";
import { Tile, reorderTiles } from "../../src/grid/LegacyGrid";
const alice: Tile<unknown> = {
key: "alice",
order: 0,
item: { local: false } as unknown as TileDescriptor<unknown>,
remove: false,
focused: false,
isPresenter: false,
isSpeaker: false,
hasVideo: true,
};
const bob: Tile<unknown> = {
key: "bob",
order: 1,
item: { local: false } as unknown as TileDescriptor<unknown>,
remove: false,
focused: false,
isPresenter: false,
isSpeaker: false,
hasVideo: false,
};
test("reorderTiles does not promote a non-speaker", () => {
const tiles = [{ ...alice }, { ...bob }];
reorderTiles(tiles, "spotlight", 1);
expect(tiles).toEqual([
expect.objectContaining({ key: "alice", order: 0 }),
expect.objectContaining({ key: "bob", order: 1 }),
]);
});
test("reorderTiles promotes a speaker into the visible area", () => {
const tiles = [{ ...alice }, { ...bob, isSpeaker: true }];
reorderTiles(tiles, "spotlight", 1);
expect(tiles).toEqual([
expect.objectContaining({ key: "alice", order: 1 }),
expect.objectContaining({ key: "bob", order: 0 }),
]);
});
test("reorderTiles keeps a promoted speaker in the visible area", () => {
const tiles = [
{ ...alice, order: 1 },
{ ...bob, isSpeaker: true, order: 0 },
];
reorderTiles(tiles, "spotlight", 1);
expect(tiles).toEqual([
expect.objectContaining({ key: "alice", order: 1 }),
expect.objectContaining({ key: "bob", order: 0 }),
]);
});