Start refactoring some business logic into view models
As Element Call grows in complexity, it has become a pain point that our business logic remains so tightly coupled to the UI code. In particular, this has made testing difficult, and the complex semantics of React hooks are not a great match for arbitrary business logic. Here, I show the beginnings of what it would look like for us to adopt the MVVM pattern. I've created a CallViewModel and TileViewModel that expose their state to the UI as rxjs Observables, as well as a couple of helper functions for consuming view models in React code. This should contain no user-visible changes, but we need to watch out for regressions particularly around focus switching and promotion of speakers, because this was the logic I chose to refactor first.
This commit is contained in:
@@ -45,6 +45,11 @@ import { useReactiveState } from "../useReactiveState";
|
||||
import { AudioButton, FullscreenButton } from "../button/Button";
|
||||
import { VideoTileSettingsModal } from "./VideoTileSettingsModal";
|
||||
import { MatrixInfo } from "../room/VideoPreview";
|
||||
import {
|
||||
ScreenShareTileViewModel,
|
||||
TileViewModel,
|
||||
UserMediaTileViewModel,
|
||||
} from "../state/TileViewModel";
|
||||
|
||||
export interface ItemData {
|
||||
id: string;
|
||||
@@ -59,7 +64,7 @@ export enum TileContent {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: ItemData;
|
||||
vm: TileViewModel;
|
||||
maximised: boolean;
|
||||
fullscreen: boolean;
|
||||
onToggleFullscreen: (itemId: string) => void;
|
||||
@@ -78,7 +83,7 @@ interface Props {
|
||||
export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
(
|
||||
{
|
||||
data,
|
||||
vm,
|
||||
maximised,
|
||||
fullscreen,
|
||||
onToggleFullscreen,
|
||||
@@ -94,7 +99,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { content, sfuParticipant, member } = data;
|
||||
const { id, sfuParticipant, member } = vm;
|
||||
|
||||
// Handle display name changes.
|
||||
const [displayName, setDisplayName] = useReactiveState(
|
||||
@@ -115,13 +120,13 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
}, [member, setDisplayName]);
|
||||
|
||||
const audioInfo = useMediaTrack(
|
||||
content === TileContent.UserMedia
|
||||
vm instanceof UserMediaTileViewModel
|
||||
? Track.Source.Microphone
|
||||
: Track.Source.ScreenShareAudio,
|
||||
sfuParticipant,
|
||||
);
|
||||
const videoInfo = useMediaTrack(
|
||||
content === TileContent.UserMedia
|
||||
vm instanceof UserMediaTileViewModel
|
||||
? Track.Source.Camera
|
||||
: Track.Source.ScreenShare,
|
||||
sfuParticipant,
|
||||
@@ -134,8 +139,8 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
const MicIcon = muted ? MicOffSolidIcon : MicOnSolidIcon;
|
||||
|
||||
const onFullscreen = useCallback(() => {
|
||||
onToggleFullscreen(data.id);
|
||||
}, [data, onToggleFullscreen]);
|
||||
onToggleFullscreen(id);
|
||||
}, [id, onToggleFullscreen]);
|
||||
|
||||
const [videoTileSettingsModalOpen, setVideoTileSettingsModalOpen] =
|
||||
useState(false);
|
||||
@@ -159,7 +164,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
/>,
|
||||
);
|
||||
|
||||
if (content === TileContent.ScreenShare) {
|
||||
if (vm instanceof ScreenShareTileViewModel) {
|
||||
toolbarButtons.push(
|
||||
<FullscreenButton
|
||||
key="fullscreen"
|
||||
@@ -177,9 +182,9 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
[styles.isLocal]: sfuParticipant.isLocal,
|
||||
[styles.speaking]:
|
||||
sfuParticipant.isSpeaking &&
|
||||
content === TileContent.UserMedia &&
|
||||
vm instanceof UserMediaTileViewModel &&
|
||||
showSpeakingIndicator,
|
||||
[styles.screenshare]: content === TileContent.ScreenShare,
|
||||
[styles.screenshare]: vm instanceof ScreenShareTileViewModel,
|
||||
[styles.maximised]: maximised,
|
||||
})}
|
||||
style={style}
|
||||
@@ -189,7 +194,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
{toolbarButtons.length > 0 && (!maximised || fullscreen) && (
|
||||
<div className={classNames(styles.toolbar)}>{toolbarButtons}</div>
|
||||
)}
|
||||
{content === TileContent.UserMedia &&
|
||||
{vm instanceof UserMediaTileViewModel &&
|
||||
!sfuParticipant.isCameraEnabled && (
|
||||
<>
|
||||
<div className={styles.videoMutedOverlay} />
|
||||
@@ -203,7 +208,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{content === TileContent.ScreenShare ? (
|
||||
{vm instanceof ScreenShareTileViewModel ? (
|
||||
<div className={styles.presenterLabel}>
|
||||
<span>{t("video_tile.presenter_label", { displayName })}</span>
|
||||
</div>
|
||||
@@ -245,7 +250,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
<VideoTrack
|
||||
participant={sfuParticipant}
|
||||
source={
|
||||
content === TileContent.UserMedia
|
||||
vm instanceof UserMediaTileViewModel
|
||||
? Track.Source.Camera
|
||||
: Track.Source.ScreenShare
|
||||
}
|
||||
@@ -260,9 +265,14 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
// eslint-disable-next-line react/no-unknown-property
|
||||
disablepictureinpicture="true"
|
||||
/>
|
||||
{!maximised && (
|
||||
{!maximised && sfuParticipant instanceof RemoteParticipant && (
|
||||
<VideoTileSettingsModal
|
||||
data={data}
|
||||
participant={sfuParticipant}
|
||||
media={
|
||||
vm instanceof UserMediaTileViewModel
|
||||
? "user media"
|
||||
: "screen share"
|
||||
}
|
||||
open={videoTileSettingsModalOpen}
|
||||
onDismiss={closeVideoTileSettingsModal}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user