mirror of
https://github.com/vector-im/element-call.git
synced 2026-02-11 04:27:03 +00:00
Split local and remote user media into different classes
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user