Replace react-rxjs with observable-hooks
react-rxjs is the library we've been using to connect our React components to view models and consume observables. However, after spending some time with react-rxjs, I feel that it's a very heavy-handed solution. It requires us to sprinkle <Subscribe /> and <RemoveSubscribe /> components all throughout the code, and makes React go through an extra render cycle whenever we mount a component that binds to a view model. What I really want is a lightweight React hook that just gets the current value out of a plain observable, without any extra setup. Luckily the observable-hooks library with its useObservableEagerState hook seems to do just that—and it's more actively maintained, too!
This commit is contained in:
@@ -35,8 +35,8 @@ import {
|
||||
import useMeasure from "react-use-measure";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import classNames from "classnames";
|
||||
import { state, useStateObservable } from "@react-rxjs/core";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import LogoMark from "../icons/LogoMark.svg?react";
|
||||
@@ -76,7 +76,6 @@ import {
|
||||
TileDescriptor,
|
||||
useCallViewModel,
|
||||
} from "../state/CallViewModel";
|
||||
import { subscribe } from "../state/subscribe";
|
||||
import { Grid, TileProps } from "../grid/Grid";
|
||||
import { MediaViewModel } from "../state/MediaViewModel";
|
||||
import { gridLayoutSystems } from "../grid/GridLayout";
|
||||
@@ -143,458 +142,449 @@ export interface InCallViewProps {
|
||||
onShareClick: (() => void) | null;
|
||||
}
|
||||
|
||||
export const InCallView: FC<InCallViewProps> = subscribe(
|
||||
({
|
||||
client,
|
||||
matrixInfo,
|
||||
rtcSession,
|
||||
livekitRoom,
|
||||
muteStates,
|
||||
participantCount,
|
||||
onLeave,
|
||||
hideHeader,
|
||||
otelGroupCallMembership,
|
||||
connState,
|
||||
onShareClick,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
usePreventScroll();
|
||||
useWakeLock();
|
||||
export const InCallView: FC<InCallViewProps> = ({
|
||||
client,
|
||||
matrixInfo,
|
||||
rtcSession,
|
||||
livekitRoom,
|
||||
muteStates,
|
||||
participantCount,
|
||||
onLeave,
|
||||
hideHeader,
|
||||
otelGroupCallMembership,
|
||||
connState,
|
||||
onShareClick,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
usePreventScroll();
|
||||
useWakeLock();
|
||||
|
||||
useEffect(() => {
|
||||
if (connState === ConnectionState.Disconnected) {
|
||||
// annoyingly we don't get the disconnection reason this way,
|
||||
// only by listening for the emitted event
|
||||
onLeave(new Error("Disconnected from call server"));
|
||||
}
|
||||
}, [connState, onLeave]);
|
||||
useEffect(() => {
|
||||
if (connState === ConnectionState.Disconnected) {
|
||||
// annoyingly we don't get the disconnection reason this way,
|
||||
// only by listening for the emitted event
|
||||
onLeave(new Error("Disconnected from call server"));
|
||||
}
|
||||
}, [connState, onLeave]);
|
||||
|
||||
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||
const [containerRef2, bounds] = useMeasure();
|
||||
const boundsValid = bounds.height > 0;
|
||||
// Merge the refs so they can attach to the same element
|
||||
const containerRef = useMergedRefs(containerRef1, containerRef2);
|
||||
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||
const [containerRef2, bounds] = useMeasure();
|
||||
const boundsValid = bounds.height > 0;
|
||||
// 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({
|
||||
const screenSharingTracks = useTracks(
|
||||
[{ source: Track.Source.ScreenShare, withPlaceholder: false }],
|
||||
{
|
||||
room: livekitRoom,
|
||||
});
|
||||
},
|
||||
);
|
||||
const { layout: legacyLayout, setLayout: setLegacyLayout } =
|
||||
useLegacyGridLayout(screenSharingTracks.length > 0);
|
||||
|
||||
const toggleMicrophone = useCallback(
|
||||
() => muteStates.audio.setEnabled?.((e) => !e),
|
||||
[muteStates],
|
||||
);
|
||||
const toggleCamera = useCallback(
|
||||
() => muteStates.video.setEnabled?.((e) => !e),
|
||||
[muteStates],
|
||||
);
|
||||
const { hideScreensharing, showControls } = useUrlParams();
|
||||
|
||||
// This function incorrectly assumes that there is a camera and microphone, which is not always the case.
|
||||
// TODO: Make sure that this module is resilient when it comes to camera/microphone availability!
|
||||
useCallViewKeyboardShortcuts(
|
||||
containerRef1,
|
||||
toggleMicrophone,
|
||||
toggleCamera,
|
||||
(muted) => muteStates.audio.setEnabled?.(!muted),
|
||||
);
|
||||
const { isScreenShareEnabled, localParticipant } = useLocalParticipant({
|
||||
room: livekitRoom,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
widget?.api.transport.send(
|
||||
legacyLayout === "grid"
|
||||
? ElementWidgetActions.TileLayout
|
||||
: ElementWidgetActions.SpotlightLayout,
|
||||
{},
|
||||
const toggleMicrophone = useCallback(
|
||||
() => muteStates.audio.setEnabled?.((e) => !e),
|
||||
[muteStates],
|
||||
);
|
||||
const toggleCamera = useCallback(
|
||||
() => muteStates.video.setEnabled?.((e) => !e),
|
||||
[muteStates],
|
||||
);
|
||||
|
||||
// This function incorrectly assumes that there is a camera and microphone, which is not always the case.
|
||||
// TODO: Make sure that this module is resilient when it comes to camera/microphone availability!
|
||||
useCallViewKeyboardShortcuts(
|
||||
containerRef1,
|
||||
toggleMicrophone,
|
||||
toggleCamera,
|
||||
(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,
|
||||
);
|
||||
}, [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(
|
||||
return (): void => {
|
||||
widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
|
||||
widget!.lazyActions.off(
|
||||
ElementWidgetActions.SpotlightLayout,
|
||||
onSpotlightLayout,
|
||||
);
|
||||
};
|
||||
}
|
||||
}, [setLegacyLayout]);
|
||||
|
||||
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;
|
||||
|
||||
const mobile = boundsValid && bounds.width <= 660;
|
||||
const reducedControls = boundsValid && bounds.width <= 340;
|
||||
const noControls = reducedControls && bounds.height <= 400;
|
||||
const vm = useCallViewModel(
|
||||
rtcSession.room,
|
||||
livekitRoom,
|
||||
matrixInfo.e2eeSystem.kind !== E2eeType.NONE,
|
||||
connState,
|
||||
);
|
||||
const items = useObservableEagerState(vm.tiles);
|
||||
const layout = useObservableEagerState(vm.layout);
|
||||
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],
|
||||
);
|
||||
const { fullscreenItem, toggleFullscreen, exitFullscreen } =
|
||||
useFullscreen(fullscreenItems);
|
||||
const toggleSpotlightFullscreen = useCallback(
|
||||
() => toggleFullscreen("spotlight"),
|
||||
[toggleFullscreen],
|
||||
);
|
||||
|
||||
const vm = useCallViewModel(
|
||||
rtcSession.room,
|
||||
livekitRoom,
|
||||
matrixInfo.e2eeSystem.kind !== E2eeType.NONE,
|
||||
connState,
|
||||
);
|
||||
const items = useStateObservable(vm.tiles);
|
||||
const layout = useStateObservable(vm.layout);
|
||||
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],
|
||||
);
|
||||
const { fullscreenItem, toggleFullscreen, exitFullscreen } =
|
||||
useFullscreen(fullscreenItems);
|
||||
const toggleSpotlightFullscreen = useCallback(
|
||||
() => toggleFullscreen("spotlight"),
|
||||
[toggleFullscreen],
|
||||
);
|
||||
// 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],
|
||||
);
|
||||
|
||||
// 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();
|
||||
|
||||
const prefersReducedMotion = usePrefersReducedMotion();
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
const [settingsTab, setSettingsTab] = useState(defaultSettingsTab);
|
||||
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
const [settingsTab, setSettingsTab] = useState(defaultSettingsTab);
|
||||
const openSettings = useCallback(
|
||||
() => setSettingsModalOpen(true),
|
||||
[setSettingsModalOpen],
|
||||
);
|
||||
const closeSettings = useCallback(
|
||||
() => setSettingsModalOpen(false),
|
||||
[setSettingsModalOpen],
|
||||
);
|
||||
|
||||
const openSettings = useCallback(
|
||||
() => setSettingsModalOpen(true),
|
||||
[setSettingsModalOpen],
|
||||
);
|
||||
const closeSettings = useCallback(
|
||||
() => setSettingsModalOpen(false),
|
||||
[setSettingsModalOpen],
|
||||
);
|
||||
const openProfile = useCallback(() => {
|
||||
setSettingsTab("profile");
|
||||
setSettingsModalOpen(true);
|
||||
}, [setSettingsTab, setSettingsModalOpen]);
|
||||
|
||||
const openProfile = useCallback(() => {
|
||||
setSettingsTab("profile");
|
||||
setSettingsModalOpen(true);
|
||||
}, [setSettingsTab, setSettingsModalOpen]);
|
||||
const [headerRef, headerBounds] = useMeasure();
|
||||
const [footerRef, footerBounds] = useMeasure();
|
||||
const gridBounds = useMemo(
|
||||
() => ({
|
||||
width: footerBounds.width,
|
||||
height: bounds.height - headerBounds.height - footerBounds.height,
|
||||
}),
|
||||
[
|
||||
footerBounds.width,
|
||||
bounds.height,
|
||||
headerBounds.height,
|
||||
footerBounds.height,
|
||||
],
|
||||
);
|
||||
const gridBoundsObservable = useObservable(gridBounds);
|
||||
const floatingAlignment = useInitial(
|
||||
() => new BehaviorSubject(defaultAlignment),
|
||||
);
|
||||
const { fixed, scrolling } = useInitial(() =>
|
||||
gridLayoutSystems(gridBoundsObservable, floatingAlignment),
|
||||
);
|
||||
|
||||
const [headerRef, headerBounds] = useMeasure();
|
||||
const [footerRef, footerBounds] = useMeasure();
|
||||
const gridBounds = useMemo(
|
||||
() => ({
|
||||
width: footerBounds.width,
|
||||
height: bounds.height - headerBounds.height - footerBounds.height,
|
||||
}),
|
||||
[
|
||||
footerBounds.width,
|
||||
bounds.height,
|
||||
headerBounds.height,
|
||||
footerBounds.height,
|
||||
],
|
||||
);
|
||||
const gridBoundsObservable = useObservable(gridBounds);
|
||||
const floatingAlignment = useInitial(
|
||||
() => new BehaviorSubject(defaultAlignment),
|
||||
);
|
||||
const { fixed, scrolling } = useInitial(() =>
|
||||
gridLayoutSystems(state(gridBoundsObservable), floatingAlignment),
|
||||
);
|
||||
const setGridMode = useCallback(
|
||||
(mode: GridMode) => {
|
||||
setLegacyLayout(mode);
|
||||
vm.setGridMode(mode);
|
||||
},
|
||||
[setLegacyLayout, vm],
|
||||
);
|
||||
|
||||
const setGridMode = useCallback(
|
||||
(mode: GridMode) => {
|
||||
setLegacyLayout(mode);
|
||||
vm.setGridMode(mode);
|
||||
},
|
||||
[setLegacyLayout, vm],
|
||||
);
|
||||
const showSpeakingIndicators =
|
||||
layout.type === "spotlight" ||
|
||||
(layout.type === "grid" && layout.grid.length > 2);
|
||||
|
||||
const showSpeakingIndicators =
|
||||
layout.type === "spotlight" ||
|
||||
(layout.type === "grid" && layout.grid.length > 2);
|
||||
|
||||
const SpotlightTileView = useMemo(
|
||||
() =>
|
||||
forwardRef<HTMLDivElement, TileProps<MediaViewModel[], HTMLDivElement>>(
|
||||
function SpotlightTileView(
|
||||
{ className, style, targetWidth, targetHeight, model },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<SpotlightTile
|
||||
ref={ref}
|
||||
vms={model}
|
||||
maximised={false}
|
||||
fullscreen={false}
|
||||
onToggleFullscreen={toggleSpotlightFullscreen}
|
||||
targetWidth={targetWidth}
|
||||
targetHeight={targetHeight}
|
||||
className={className}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
},
|
||||
),
|
||||
[toggleSpotlightFullscreen],
|
||||
);
|
||||
const GridTileView = useMemo(
|
||||
() =>
|
||||
forwardRef<HTMLDivElement, TileProps<MediaViewModel, HTMLDivElement>>(
|
||||
function GridTileView(
|
||||
{ className, style, targetWidth, targetHeight, model },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<GridTile
|
||||
ref={ref}
|
||||
vm={model}
|
||||
maximised={false}
|
||||
fullscreen={false}
|
||||
onToggleFullscreen={toggleFullscreen}
|
||||
onOpenProfile={openProfile}
|
||||
targetWidth={targetWidth}
|
||||
targetHeight={targetHeight}
|
||||
className={className}
|
||||
style={style}
|
||||
showSpeakingIndicators={showSpeakingIndicators}
|
||||
/>
|
||||
);
|
||||
},
|
||||
),
|
||||
[toggleFullscreen, openProfile, showSpeakingIndicators],
|
||||
);
|
||||
|
||||
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") {
|
||||
const SpotlightTileView = useMemo(
|
||||
() =>
|
||||
forwardRef<HTMLDivElement, TileProps<MediaViewModel[], HTMLDivElement>>(
|
||||
function SpotlightTileView(
|
||||
{ className, style, targetWidth, targetHeight, model },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<SpotlightTile
|
||||
vms={layout.spotlight!}
|
||||
maximised={true}
|
||||
fullscreen={fullscreen}
|
||||
ref={ref}
|
||||
vms={model}
|
||||
maximised={false}
|
||||
fullscreen={false}
|
||||
onToggleFullscreen={toggleSpotlightFullscreen}
|
||||
targetWidth={gridBounds.height}
|
||||
targetHeight={gridBounds.width}
|
||||
targetWidth={targetWidth}
|
||||
targetHeight={targetHeight}
|
||||
className={className}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<GridTile
|
||||
vm={maximisedParticipant.data}
|
||||
maximised={true}
|
||||
fullscreen={fullscreen}
|
||||
onToggleFullscreen={toggleFullscreen}
|
||||
targetHeight={gridBounds.height}
|
||||
targetWidth={gridBounds.width}
|
||||
key={maximisedParticipant.id}
|
||||
showSpeakingIndicators={false}
|
||||
onOpenProfile={openProfile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// The only new layout we've implemented so far is grid layout for non-1:1
|
||||
// calls. All other layouts use the legacy grid system for now.
|
||||
if (
|
||||
legacyLayout === "grid" &&
|
||||
layout.type === "grid" &&
|
||||
!(layout.grid.length === 2 && layout.spotlight === undefined)
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
className={styles.scrollingGrid}
|
||||
model={layout}
|
||||
system={scrolling}
|
||||
Tile={GridTileView}
|
||||
},
|
||||
),
|
||||
[toggleSpotlightFullscreen],
|
||||
);
|
||||
const GridTileView = useMemo(
|
||||
() =>
|
||||
forwardRef<HTMLDivElement, TileProps<MediaViewModel, HTMLDivElement>>(
|
||||
function GridTileView(
|
||||
{ className, style, targetWidth, targetHeight, model },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<GridTile
|
||||
ref={ref}
|
||||
vm={model}
|
||||
maximised={false}
|
||||
fullscreen={false}
|
||||
onToggleFullscreen={toggleFullscreen}
|
||||
onOpenProfile={openProfile}
|
||||
targetWidth={targetWidth}
|
||||
targetHeight={targetHeight}
|
||||
className={className}
|
||||
style={style}
|
||||
showSpeakingIndicators={showSpeakingIndicators}
|
||||
/>
|
||||
<Grid
|
||||
className={styles.fixedGrid}
|
||||
style={{ insetBlockStart: headerBounds.bottom }}
|
||||
model={layout}
|
||||
system={fixed}
|
||||
Tile={SpotlightTileView}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<LegacyGrid
|
||||
items={items}
|
||||
layout={legacyLayout}
|
||||
disableAnimations={prefersReducedMotion}
|
||||
Tile={GridTileView}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const rageshakeRequestModalProps = useRageshakeRequestModal(
|
||||
rtcSession.room.roomId,
|
||||
);
|
||||
|
||||
const toggleScreensharing = useCallback(async () => {
|
||||
exitFullscreen();
|
||||
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, {
|
||||
audio: true,
|
||||
selfBrowserSurface: "include",
|
||||
surfaceSwitching: "include",
|
||||
systemAudio: "include",
|
||||
});
|
||||
}, [localParticipant, isScreenShareEnabled, exitFullscreen]);
|
||||
|
||||
let footer: JSX.Element | null;
|
||||
|
||||
if (noControls) {
|
||||
footer = null;
|
||||
} else {
|
||||
const buttons: JSX.Element[] = [];
|
||||
|
||||
buttons.push(
|
||||
<MicButton
|
||||
key="1"
|
||||
muted={!muteStates.audio.enabled}
|
||||
onPress={toggleMicrophone}
|
||||
disabled={muteStates.audio.setEnabled === null}
|
||||
data-testid="incall_mute"
|
||||
/>,
|
||||
<VideoButton
|
||||
key="2"
|
||||
muted={!muteStates.video.enabled}
|
||||
onPress={toggleCamera}
|
||||
disabled={muteStates.video.setEnabled === null}
|
||||
data-testid="incall_videomute"
|
||||
/>,
|
||||
);
|
||||
|
||||
if (!reducedControls) {
|
||||
if (canScreenshare && !hideScreensharing) {
|
||||
buttons.push(
|
||||
<ScreenshareButton
|
||||
key="3"
|
||||
enabled={isScreenShareEnabled}
|
||||
onPress={toggleScreensharing}
|
||||
data-testid="incall_screenshare"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
buttons.push(<SettingsButton key="4" onPress={openSettings} />);
|
||||
}
|
||||
},
|
||||
),
|
||||
[toggleFullscreen, openProfile, showSpeakingIndicators],
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
<HangupButton
|
||||
key="6"
|
||||
onPress={function (): void {
|
||||
onLeave();
|
||||
}}
|
||||
data-testid="incall_leave"
|
||||
/>,
|
||||
);
|
||||
footer = (
|
||||
<div
|
||||
ref={footerRef}
|
||||
className={classNames(
|
||||
showControls
|
||||
? styles.footer
|
||||
: hideHeader
|
||||
? [styles.footer, styles.footerHidden]
|
||||
: [styles.footer, styles.footerThin],
|
||||
)}
|
||||
>
|
||||
{!mobile && !hideHeader && (
|
||||
<div className={styles.logo}>
|
||||
<LogoMark width={24} height={24} aria-hidden />
|
||||
<LogoType
|
||||
width={80}
|
||||
height={11}
|
||||
aria-label={import.meta.env.VITE_PRODUCT_NAME || "Element Call"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showControls && <div className={styles.buttons}>{buttons}</div>}
|
||||
{!mobile && !hideHeader && showControls && (
|
||||
<LayoutToggle
|
||||
className={styles.layout}
|
||||
layout={legacyLayout}
|
||||
setLayout={setGridMode}
|
||||
/>
|
||||
)}
|
||||
const renderContent = (): JSX.Element => {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className={styles.centerMessage}>
|
||||
<p>{t("waiting_for_participants")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.inRoom} ref={containerRef}>
|
||||
{!hideHeader && maximisedParticipant === null && (
|
||||
<Header className={styles.header} ref={headerRef}>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo
|
||||
id={matrixInfo.roomId}
|
||||
name={matrixInfo.roomName}
|
||||
avatarUrl={matrixInfo.roomAvatar}
|
||||
encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE}
|
||||
participantCount={participantCount}
|
||||
/>
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
{!reducedControls && showControls && onShareClick !== null && (
|
||||
<InviteButton onClick={onShareClick} />
|
||||
)}
|
||||
</RightNav>
|
||||
</Header>
|
||||
)}
|
||||
<RoomAudioRenderer />
|
||||
{renderContent()}
|
||||
{footer}
|
||||
{!noControls && (
|
||||
<RageshakeRequestModal {...rageshakeRequestModalProps} />
|
||||
)}
|
||||
<SettingsModal
|
||||
client={client}
|
||||
roomId={rtcSession.room.roomId}
|
||||
open={settingsModalOpen}
|
||||
onDismiss={closeSettings}
|
||||
tab={settingsTab}
|
||||
onTabChange={setSettingsTab}
|
||||
if (maximisedParticipant !== null) {
|
||||
const fullscreen = maximisedParticipant === fullscreenItem;
|
||||
if (maximisedParticipant.id === "spotlight") {
|
||||
return (
|
||||
<SpotlightTile
|
||||
vms={layout.spotlight!}
|
||||
maximised={true}
|
||||
fullscreen={fullscreen}
|
||||
onToggleFullscreen={toggleSpotlightFullscreen}
|
||||
targetWidth={gridBounds.height}
|
||||
targetHeight={gridBounds.width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<GridTile
|
||||
vm={maximisedParticipant.data}
|
||||
maximised={true}
|
||||
fullscreen={fullscreen}
|
||||
onToggleFullscreen={toggleFullscreen}
|
||||
targetHeight={gridBounds.height}
|
||||
targetWidth={gridBounds.width}
|
||||
key={maximisedParticipant.id}
|
||||
showSpeakingIndicators={false}
|
||||
onOpenProfile={openProfile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// The only new layout we've implemented so far is grid layout for non-1:1
|
||||
// calls. All other layouts use the legacy grid system for now.
|
||||
if (
|
||||
legacyLayout === "grid" &&
|
||||
layout.type === "grid" &&
|
||||
!(layout.grid.length === 2 && layout.spotlight === undefined)
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
className={styles.scrollingGrid}
|
||||
model={layout}
|
||||
system={scrolling}
|
||||
Tile={GridTileView}
|
||||
/>
|
||||
<Grid
|
||||
className={styles.fixedGrid}
|
||||
style={{ insetBlockStart: headerBounds.bottom }}
|
||||
model={layout}
|
||||
system={fixed}
|
||||
Tile={SpotlightTileView}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<LegacyGrid
|
||||
items={items}
|
||||
layout={legacyLayout}
|
||||
disableAnimations={prefersReducedMotion}
|
||||
Tile={GridTileView}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const rageshakeRequestModalProps = useRageshakeRequestModal(
|
||||
rtcSession.room.roomId,
|
||||
);
|
||||
|
||||
const toggleScreensharing = useCallback(async () => {
|
||||
exitFullscreen();
|
||||
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, {
|
||||
audio: true,
|
||||
selfBrowserSurface: "include",
|
||||
surfaceSwitching: "include",
|
||||
systemAudio: "include",
|
||||
});
|
||||
}, [localParticipant, isScreenShareEnabled, exitFullscreen]);
|
||||
|
||||
let footer: JSX.Element | null;
|
||||
|
||||
if (noControls) {
|
||||
footer = null;
|
||||
} else {
|
||||
const buttons: JSX.Element[] = [];
|
||||
|
||||
buttons.push(
|
||||
<MicButton
|
||||
key="1"
|
||||
muted={!muteStates.audio.enabled}
|
||||
onPress={toggleMicrophone}
|
||||
disabled={muteStates.audio.setEnabled === null}
|
||||
data-testid="incall_mute"
|
||||
/>,
|
||||
<VideoButton
|
||||
key="2"
|
||||
muted={!muteStates.video.enabled}
|
||||
onPress={toggleCamera}
|
||||
disabled={muteStates.video.setEnabled === null}
|
||||
data-testid="incall_videomute"
|
||||
/>,
|
||||
);
|
||||
|
||||
if (!reducedControls) {
|
||||
if (canScreenshare && !hideScreensharing) {
|
||||
buttons.push(
|
||||
<ScreenshareButton
|
||||
key="3"
|
||||
enabled={isScreenShareEnabled}
|
||||
onPress={toggleScreensharing}
|
||||
data-testid="incall_screenshare"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
buttons.push(<SettingsButton key="4" onPress={openSettings} />);
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
<HangupButton
|
||||
key="6"
|
||||
onPress={function (): void {
|
||||
onLeave();
|
||||
}}
|
||||
data-testid="incall_leave"
|
||||
/>,
|
||||
);
|
||||
footer = (
|
||||
<div
|
||||
ref={footerRef}
|
||||
className={classNames(
|
||||
showControls
|
||||
? styles.footer
|
||||
: hideHeader
|
||||
? [styles.footer, styles.footerHidden]
|
||||
: [styles.footer, styles.footerThin],
|
||||
)}
|
||||
>
|
||||
{!mobile && !hideHeader && (
|
||||
<div className={styles.logo}>
|
||||
<LogoMark width={24} height={24} aria-hidden />
|
||||
<LogoType
|
||||
width={80}
|
||||
height={11}
|
||||
aria-label={import.meta.env.VITE_PRODUCT_NAME || "Element Call"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showControls && <div className={styles.buttons}>{buttons}</div>}
|
||||
{!mobile && !hideHeader && showControls && (
|
||||
<LayoutToggle
|
||||
className={styles.layout}
|
||||
layout={legacyLayout}
|
||||
setLayout={setGridMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.inRoom} ref={containerRef}>
|
||||
{!hideHeader && maximisedParticipant === null && (
|
||||
<Header className={styles.header} ref={headerRef}>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo
|
||||
id={matrixInfo.roomId}
|
||||
name={matrixInfo.roomName}
|
||||
avatarUrl={matrixInfo.roomAvatar}
|
||||
encrypted={matrixInfo.e2eeSystem.kind !== E2eeType.NONE}
|
||||
participantCount={participantCount}
|
||||
/>
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
{!reducedControls && showControls && onShareClick !== null && (
|
||||
<InviteButton onClick={onShareClick} />
|
||||
)}
|
||||
</RightNav>
|
||||
</Header>
|
||||
)}
|
||||
<RoomAudioRenderer />
|
||||
{renderContent()}
|
||||
{footer}
|
||||
{!noControls && <RageshakeRequestModal {...rageshakeRequestModalProps} />}
|
||||
<SettingsModal
|
||||
client={client}
|
||||
roomId={rtcSession.room.roomId}
|
||||
open={settingsModalOpen}
|
||||
onDismiss={closeSettings}
|
||||
tab={settingsTab}
|
||||
onTabChange={setSettingsTab}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user