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:
Robin
2023-11-30 22:59:19 -05:00
parent 445c7c4e0c
commit 169ccd9de5
23 changed files with 847 additions and 531 deletions

View File

@@ -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}
/>