Split local and remote user media into different classes

This commit is contained in:
Robin
2024-05-16 12:32:18 -04:00
parent a534356dd9
commit e33fbd77d1
5 changed files with 292 additions and 210 deletions

View File

@@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ComponentProps, forwardRef, useCallback, useState } from "react";
import {
ComponentProps,
ReactNode,
forwardRef,
useCallback,
useState,
} from "react";
import { animated } from "@react-spring/web";
import classNames from "classnames";
import { useTranslation } from "react-i18next";
@@ -33,7 +39,7 @@ import {
ToggleMenuItem,
Menu,
} from "@vector-im/compound-web";
import { useStateObservable } from "@react-rxjs/core";
import { useObservableEagerState } from "observable-hooks";
import styles from "./GridTile.module.css";
import {
@@ -41,71 +47,92 @@ import {
MediaViewModel,
UserMediaViewModel,
useNameData,
LocalUserMediaViewModel,
RemoteUserMediaViewModel,
} from "../state/MediaViewModel";
import { subscribe } from "../state/subscribe";
import { Slider } from "../Slider";
import { MediaView } from "./MediaView";
interface UserMediaTileProps {
vm: UserMediaViewModel;
interface TileProps {
className?: string;
style?: ComponentProps<typeof animated.div>["style"];
targetWidth: number;
targetHeight: number;
maximised: boolean;
onOpenProfile: () => void;
showSpeakingIndicator: boolean;
displayName: string;
nameTag: string;
}
const UserMediaTile = subscribe<UserMediaTileProps, HTMLDivElement>(
interface MediaTileProps
extends TileProps,
Omit<ComponentProps<typeof animated.div>, "className"> {
vm: MediaViewModel;
videoEnabled: boolean;
videoFit: "contain" | "cover";
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}
mirror={false}
member={vm.member}
unencryptedWarning={unencryptedWarning}
{...props}
/>
);
},
);
MediaTile.displayName = "MediaTile";
interface UserMediaTileProps extends TileProps {
vm: UserMediaViewModel;
showSpeakingIndicators: boolean;
menuStart?: ReactNode;
menuEnd?: ReactNode;
}
const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
(
{
vm,
showSpeakingIndicators,
menuStart,
menuEnd,
className,
style,
targetWidth,
targetHeight,
maximised,
onOpenProfile,
showSpeakingIndicator,
nameTag,
...props
},
ref,
) => {
const { t } = useTranslation();
const { displayName, nameTag } = useNameData(vm);
const video = useStateObservable(vm.video);
const audioEnabled = useStateObservable(vm.audioEnabled);
const videoEnabled = useStateObservable(vm.videoEnabled);
const unencryptedWarning = useStateObservable(vm.unencryptedWarning);
const mirror = useStateObservable(vm.mirror);
const speaking = useStateObservable(vm.speaking);
const locallyMuted = useStateObservable(vm.locallyMuted);
const cropVideo = useStateObservable(vm.cropVideo);
const localVolume = useStateObservable(vm.localVolume);
const onChangeMute = useCallback(() => vm.toggleLocallyMuted(), [vm]);
const audioEnabled = useObservableEagerState(vm.audioEnabled);
const videoEnabled = useObservableEagerState(vm.videoEnabled);
const speaking = useObservableEagerState(vm.speaking);
const cropVideo = useObservableEagerState(vm.cropVideo);
const onChangeFitContain = useCallback(() => vm.toggleFitContain(), [vm]);
const onSelectMute = useCallback((e: Event) => e.preventDefault(), []);
const onSelectFitContain = useCallback(
(e: Event) => e.preventDefault(),
[],
);
const onChangeLocalVolume = useCallback(
(v: number) => vm.setLocalVolume(v),
[vm],
);
const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;
const VolumeIcon = locallyMuted ? VolumeOffIcon : VolumeOnIcon;
const [menuOpen, setMenuOpen] = useState(false);
const menu = vm.local ? (
const menu = (
<>
<MenuItem
Icon={UserProfileIcon}
label={t("common.profile")}
onSelect={onOpenProfile}
/>
{menuStart}
<ToggleMenuItem
Icon={ExpandIcon}
label={t("video_tile.change_fit_contain")}
@@ -113,55 +140,19 @@ const UserMediaTile = subscribe<UserMediaTileProps, HTMLDivElement>(
onChange={onChangeFitContain}
onSelect={onSelectFitContain}
/>
</>
) : (
<>
<ToggleMenuItem
Icon={MicOffIcon}
label={t("video_tile.mute_for_me")}
checked={locallyMuted}
onChange={onChangeMute}
onSelect={onSelectMute}
/>
<ToggleMenuItem
Icon={ExpandIcon}
label={t("video_tile.change_fit_contain")}
checked={cropVideo}
onChange={onChangeFitContain}
onSelect={onSelectFitContain}
/>
{/* TODO: Figure out how to make this slider keyboard accessible */}
<MenuItem as="div" Icon={VolumeIcon} label={null} onSelect={null}>
<Slider
className={styles.volumeSlider}
label={t("video_tile.volume")}
value={localVolume}
onValueChange={onChangeLocalVolume}
min={0.1}
max={1}
step={0.01}
disabled={locallyMuted}
/>
</MenuItem>
{menuEnd}
</>
);
const tile = (
<MediaView
<MediaTile
ref={ref}
className={classNames(className, styles.tile, {
[styles.speaking]: showSpeakingIndicator && speaking,
})}
data-maximised={maximised}
style={style}
targetWidth={targetWidth}
targetHeight={targetHeight}
video={video}
videoFit={cropVideo ? "cover" : "contain"}
mirror={mirror}
member={vm.member}
vm={vm}
videoEnabled={videoEnabled}
unencryptedWarning={unencryptedWarning}
videoFit={cropVideo ? "cover" : "contain"}
className={classNames(className, {
[styles.speaking]: showSpeakingIndicators && speaking,
})}
nameTagLeadingIcon={
<MicIcon
width={20}
@@ -172,7 +163,6 @@ const UserMediaTile = subscribe<UserMediaTileProps, HTMLDivElement>(
/>
}
nameTag={nameTag}
displayName={displayName}
primaryButton={
<Menu
open={menuOpen}
@@ -189,6 +179,7 @@ const UserMediaTile = subscribe<UserMediaTileProps, HTMLDivElement>(
{menu}
</Menu>
}
{...props}
/>
);
@@ -202,35 +193,102 @@ const UserMediaTile = subscribe<UserMediaTileProps, HTMLDivElement>(
UserMediaTile.displayName = "UserMediaTile";
interface ScreenShareTileProps {
interface LocalUserMediaTileProps extends TileProps {
vm: LocalUserMediaViewModel;
onOpenProfile: () => void;
showSpeakingIndicators: boolean;
}
const LocalUserMediaTile = forwardRef<HTMLDivElement, LocalUserMediaTileProps>(
({ vm, onOpenProfile, className, ...props }, ref) => {
const { t } = useTranslation();
const mirror = useObservableEagerState(vm.mirror);
return (
<UserMediaTile
ref={ref}
vm={vm}
menuStart={
<MenuItem
Icon={UserProfileIcon}
label={t("common.profile")}
onSelect={onOpenProfile}
/>
}
className={classNames(className, { [styles.mirror]: mirror })}
{...props}
/>
);
},
);
LocalUserMediaTile.displayName = "LocalUserMediaTile";
interface RemoteUserMediaTileProps extends TileProps {
vm: RemoteUserMediaViewModel;
showSpeakingIndicators: boolean;
}
const RemoteUserMediaTile = forwardRef<
HTMLDivElement,
RemoteUserMediaTileProps
>(({ vm, ...props }, ref) => {
const { t } = useTranslation();
const locallyMuted = useObservableEagerState(vm.locallyMuted);
const localVolume = useObservableEagerState(vm.localVolume);
const onChangeMute = useCallback(() => vm.toggleLocallyMuted(), [vm]);
const onSelectMute = useCallback((e: Event) => e.preventDefault(), []);
const onChangeLocalVolume = useCallback(
(v: number) => vm.setLocalVolume(v),
[vm],
);
const VolumeIcon = locallyMuted ? VolumeOffIcon : VolumeOnIcon;
return (
<UserMediaTile
ref={ref}
vm={vm}
menuStart={
<>
<ToggleMenuItem
Icon={MicOffIcon}
label={t("video_tile.mute_for_me")}
checked={locallyMuted}
onChange={onChangeMute}
onSelect={onSelectMute}
/>
{/* TODO: Figure out how to make this slider keyboard accessible */}
<MenuItem as="div" Icon={VolumeIcon} label={null} onSelect={null}>
<Slider
className={styles.volumeSlider}
label={t("video_tile.volume")}
value={localVolume}
onValueChange={onChangeLocalVolume}
min={0.1}
max={1}
step={0.01}
disabled={locallyMuted}
/>
</MenuItem>
</>
}
{...props}
/>
);
});
RemoteUserMediaTile.displayName = "RemoteUserMediaTile";
interface ScreenShareTileProps extends TileProps {
vm: ScreenShareViewModel;
className?: string;
style?: ComponentProps<typeof animated.div>["style"];
targetWidth: number;
targetHeight: number;
maximised: boolean;
fullscreen: boolean;
onToggleFullscreen: (itemId: string) => void;
}
const ScreenShareTile = subscribe<ScreenShareTileProps, HTMLDivElement>(
(
{
vm,
className,
style,
targetWidth,
targetHeight,
maximised,
fullscreen,
onToggleFullscreen,
},
ref,
) => {
const ScreenShareTile = forwardRef<HTMLDivElement, ScreenShareTileProps>(
({ vm, fullscreen, onToggleFullscreen, ...props }, ref) => {
const { t } = useTranslation();
const { displayName, nameTag } = useNameData(vm);
const video = useStateObservable(vm.video);
const unencryptedWarning = useStateObservable(vm.unencryptedWarning);
const onClickFullScreen = useCallback(
() => onToggleFullscreen(vm.id),
[onToggleFullscreen, vm],
@@ -239,23 +297,10 @@ const ScreenShareTile = subscribe<ScreenShareTileProps, HTMLDivElement>(
const FullScreenIcon = fullscreen ? CollapseIcon : ExpandIcon;
return (
<MediaView
<MediaTile
ref={ref}
className={classNames(className, styles.tile, {
[styles.maximised]: maximised,
})}
data-maximised={maximised}
style={style}
targetWidth={targetWidth}
targetHeight={targetHeight}
video={video}
vm={vm}
videoFit="contain"
mirror={false}
member={vm.member}
videoEnabled
unencryptedWarning={unencryptedWarning}
nameTag={nameTag}
displayName={displayName}
primaryButton={
!vm.local && (
<button
@@ -270,6 +315,8 @@ const ScreenShareTile = subscribe<ScreenShareTileProps, HTMLDivElement>(
</button>
)
}
videoEnabled
{...props}
/>
);
},
@@ -277,7 +324,7 @@ const ScreenShareTile = subscribe<ScreenShareTileProps, HTMLDivElement>(
ScreenShareTile.displayName = "ScreenShareTile";
interface Props {
interface GridTileProps {
vm: MediaViewModel;
maximised: boolean;
fullscreen: boolean;
@@ -287,51 +334,34 @@ interface Props {
targetHeight: number;
className?: string;
style?: ComponentProps<typeof animated.div>["style"];
showSpeakingIndicator: boolean;
showSpeakingIndicators: boolean;
}
export const GridTile = forwardRef<HTMLDivElement, Props>(
(
{
vm,
maximised,
fullscreen,
onToggleFullscreen,
onOpenProfile,
className,
style,
targetWidth,
targetHeight,
showSpeakingIndicator,
},
ref,
) => {
if (vm instanceof UserMediaViewModel) {
export const GridTile = forwardRef<HTMLDivElement, GridTileProps>(
({ vm, fullscreen, onToggleFullscreen, onOpenProfile, ...props }, ref) => {
const nameData = useNameData(vm);
if (vm instanceof LocalUserMediaViewModel) {
return (
<UserMediaTile
<LocalUserMediaTile
ref={ref}
className={className}
style={style}
vm={vm}
targetWidth={targetWidth}
targetHeight={targetHeight}
maximised={maximised}
onOpenProfile={onOpenProfile}
showSpeakingIndicator={showSpeakingIndicator}
{...props}
{...nameData}
/>
);
} else if (vm instanceof RemoteUserMediaViewModel) {
return <RemoteUserMediaTile ref={ref} vm={vm} {...props} {...nameData} />;
} else {
return (
<ScreenShareTile
ref={ref}
className={className}
style={style}
vm={vm}
targetWidth={targetWidth}
targetHeight={targetHeight}
maximised={maximised}
fullscreen={fullscreen}
onToggleFullscreen={onToggleFullscreen}
{...props}
{...nameData}
/>
);
}

View File

@@ -37,8 +37,9 @@ import { MediaView } from "./MediaView";
import styles from "./SpotlightTile.module.css";
import { subscribe } from "../state/subscribe";
import {
LocalUserMediaViewModel,
MediaViewModel,
UserMediaViewModel,
RemoteUserMediaViewModel,
useNameData,
} from "../state/MediaViewModel";
import { useInitial } from "../useInitial";
@@ -48,11 +49,11 @@ import { useReactiveState } from "../useReactiveState";
import { useLatest } from "../useLatest";
// Screen share video is always enabled
const screenShareVideoEnabled = state(of(true));
const videoEnabledDefault = state(of(true));
// Never mirror screen share video
const screenShareMirror = state(of(false));
const mirrorDefault = state(of(false));
// Never crop screen share video
const screenShareCropVideo = state(of(false));
const cropVideoDefault = state(of(false));
interface SpotlightItemProps {
vm: MediaViewModel;
@@ -72,15 +73,19 @@ const SpotlightItem = subscribe<SpotlightItemProps, HTMLDivElement>(
const { displayName, nameTag } = useNameData(vm);
const video = useStateObservable(vm.video);
const videoEnabled = useStateObservable(
vm instanceof UserMediaViewModel
vm instanceof LocalUserMediaViewModel ||
vm instanceof RemoteUserMediaViewModel
? vm.videoEnabled
: screenShareVideoEnabled,
: videoEnabledDefault,
);
const mirror = useStateObservable(
vm instanceof UserMediaViewModel ? vm.mirror : screenShareMirror,
vm instanceof LocalUserMediaViewModel ? vm.mirror : mirrorDefault,
);
const cropVideo = useStateObservable(
vm instanceof UserMediaViewModel ? vm.cropVideo : screenShareCropVideo,
vm instanceof LocalUserMediaViewModel ||
vm instanceof RemoteUserMediaViewModel
? vm.cropVideo
: cropVideoDefault,
);
const unencryptedWarning = useStateObservable(vm.unencryptedWarning);