Prepare for Element Call integration (#9224)
* Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
This commit is contained in:
parent
50f6986f6c
commit
0d6a550c33
107 changed files with 2573 additions and 2157 deletions
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, useState } from "react";
|
||||
import classNames from "classnames";
|
||||
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
import AccessibleTooltipButton from "../../elements/AccessibleTooltipButton";
|
||||
import LegacyCallContextMenu from "../../context_menus/LegacyCallContextMenu";
|
||||
import DialpadContextMenu from "../../context_menus/DialpadContextMenu";
|
||||
import { Alignment } from "../../elements/Tooltip";
|
||||
import {
|
||||
alwaysAboveLeftOf,
|
||||
alwaysAboveRightOf,
|
||||
ChevronFace,
|
||||
ContextMenuTooltipButton,
|
||||
useContextMenu,
|
||||
} from '../../../structures/ContextMenu';
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import DeviceContextMenu from "../../context_menus/DeviceContextMenu";
|
||||
import { MediaDeviceKindEnum } from "../../../../MediaDeviceHandler";
|
||||
|
||||
// Height of the header duplicated from CSS because we need to subtract it from our max
|
||||
// height to get the max height of the video
|
||||
const CONTEXT_MENU_VPADDING = 8; // How far the context menu sits above the button (px)
|
||||
|
||||
const CONTROLS_HIDE_DELAY = 2000;
|
||||
|
||||
interface IButtonProps extends Omit<React.ComponentProps<typeof AccessibleTooltipButton>, "title"> {
|
||||
state: boolean;
|
||||
className: string;
|
||||
onLabel?: string;
|
||||
offLabel?: string;
|
||||
onClick: (event: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const LegacyCallViewToggleButton: React.FC<IButtonProps> = ({
|
||||
children,
|
||||
state: isOn,
|
||||
className,
|
||||
onLabel,
|
||||
offLabel,
|
||||
...props
|
||||
}) => {
|
||||
const classes = classNames("mx_LegacyCallViewButtons_button", className, {
|
||||
mx_LegacyCallViewButtons_button_on: isOn,
|
||||
mx_LegacyCallViewButtons_button_off: !isOn,
|
||||
});
|
||||
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
className={classes}
|
||||
title={isOn ? onLabel : offLabel}
|
||||
alignment={Alignment.Top}
|
||||
{...props}
|
||||
>
|
||||
{ children }
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface IDropdownButtonProps extends IButtonProps {
|
||||
deviceKinds: MediaDeviceKindEnum[];
|
||||
}
|
||||
|
||||
const LegacyCallViewDropdownButton: React.FC<IDropdownButtonProps> = ({ state, deviceKinds, ...props }) => {
|
||||
const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu();
|
||||
const [hoveringDropdown, setHoveringDropdown] = useState(false);
|
||||
|
||||
const classes = classNames("mx_LegacyCallViewButtons_button", "mx_LegacyCallViewButtons_dropdownButton", {
|
||||
mx_LegacyCallViewButtons_dropdownButton_collapsed: !menuDisplayed,
|
||||
});
|
||||
|
||||
const onClick = (event: React.MouseEvent): void => {
|
||||
event.stopPropagation();
|
||||
openMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
<LegacyCallViewToggleButton inputRef={buttonRef} forceHide={menuDisplayed || hoveringDropdown} state={state} {...props}>
|
||||
<LegacyCallViewToggleButton
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
onHover={(hovering) => setHoveringDropdown(hovering)}
|
||||
state={state}
|
||||
/>
|
||||
{ menuDisplayed && <DeviceContextMenu
|
||||
{...alwaysAboveRightOf(buttonRef.current?.getBoundingClientRect())}
|
||||
|
||||
onFinished={closeMenu}
|
||||
deviceKinds={deviceKinds}
|
||||
/> }
|
||||
</LegacyCallViewToggleButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
call: MatrixCall;
|
||||
pipMode: boolean;
|
||||
handlers: {
|
||||
onHangupClick: () => void;
|
||||
onScreenshareClick: () => void;
|
||||
onToggleSidebarClick: () => void;
|
||||
onMicMuteClick: () => void;
|
||||
onVidMuteClick: () => void;
|
||||
};
|
||||
buttonsState: {
|
||||
micMuted: boolean;
|
||||
vidMuted: boolean;
|
||||
sidebarShown: boolean;
|
||||
screensharing: boolean;
|
||||
};
|
||||
buttonsVisibility: {
|
||||
screensharing: boolean;
|
||||
vidMute: boolean;
|
||||
sidebar: boolean;
|
||||
dialpad: boolean;
|
||||
contextMenu: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface IState {
|
||||
visible: boolean;
|
||||
showDialpad: boolean;
|
||||
hoveringControls: boolean;
|
||||
showMoreMenu: boolean;
|
||||
}
|
||||
|
||||
export default class LegacyCallViewButtons extends React.Component<IProps, IState> {
|
||||
private dialpadButton = createRef<HTMLDivElement>();
|
||||
private contextMenuButton = createRef<HTMLDivElement>();
|
||||
private controlsHideTimer: number = null;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showDialpad: false,
|
||||
hoveringControls: false,
|
||||
showMoreMenu: false,
|
||||
visible: true,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.showControls();
|
||||
}
|
||||
|
||||
public showControls(): void {
|
||||
if (this.state.showMoreMenu || this.state.showDialpad) return;
|
||||
|
||||
if (!this.state.visible) {
|
||||
this.setState({
|
||||
visible: true,
|
||||
});
|
||||
}
|
||||
if (this.controlsHideTimer !== null) {
|
||||
clearTimeout(this.controlsHideTimer);
|
||||
}
|
||||
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
|
||||
}
|
||||
|
||||
private onControlsHideTimer = (): void => {
|
||||
if (this.state.hoveringControls || this.state.showDialpad || this.state.showMoreMenu) return;
|
||||
this.controlsHideTimer = null;
|
||||
this.setState({ visible: false });
|
||||
};
|
||||
|
||||
private onMouseEnter = (): void => {
|
||||
this.setState({ hoveringControls: true });
|
||||
};
|
||||
|
||||
private onMouseLeave = (): void => {
|
||||
this.setState({ hoveringControls: false });
|
||||
};
|
||||
|
||||
private onDialpadClick = (): void => {
|
||||
if (!this.state.showDialpad) {
|
||||
this.setState({ showDialpad: true });
|
||||
this.showControls();
|
||||
} else {
|
||||
this.setState({ showDialpad: false });
|
||||
}
|
||||
};
|
||||
|
||||
private onMoreClick = (): void => {
|
||||
this.setState({ showMoreMenu: true });
|
||||
this.showControls();
|
||||
};
|
||||
|
||||
private closeDialpad = (): void => {
|
||||
this.setState({ showDialpad: false });
|
||||
};
|
||||
|
||||
private closeContextMenu = (): void => {
|
||||
this.setState({ showMoreMenu: false });
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const callControlsClasses = classNames("mx_LegacyCallViewButtons", {
|
||||
mx_LegacyCallViewButtons_hidden: !this.state.visible,
|
||||
});
|
||||
|
||||
let dialPad;
|
||||
if (this.state.showDialpad) {
|
||||
dialPad = <DialpadContextMenu
|
||||
{...alwaysAboveLeftOf(
|
||||
this.dialpadButton.current.getBoundingClientRect(),
|
||||
ChevronFace.None,
|
||||
CONTEXT_MENU_VPADDING,
|
||||
)}
|
||||
// We mount the context menus as a as a child typically in order to include the
|
||||
// context menus when fullscreening the call content.
|
||||
// However, this does not work as well when the call is embedded in a
|
||||
// picture-in-picture frame. Thus, only mount as child when we are *not* in PiP.
|
||||
mountAsChild={!this.props.pipMode}
|
||||
onFinished={this.closeDialpad}
|
||||
call={this.props.call}
|
||||
/>;
|
||||
}
|
||||
|
||||
let contextMenu;
|
||||
if (this.state.showMoreMenu) {
|
||||
contextMenu = <LegacyCallContextMenu
|
||||
{...alwaysAboveLeftOf(
|
||||
this.contextMenuButton.current.getBoundingClientRect(),
|
||||
ChevronFace.None,
|
||||
CONTEXT_MENU_VPADDING,
|
||||
)}
|
||||
mountAsChild={!this.props.pipMode}
|
||||
onFinished={this.closeContextMenu}
|
||||
call={this.props.call}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={callControlsClasses}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
{ dialPad }
|
||||
{ contextMenu }
|
||||
|
||||
{ this.props.buttonsVisibility.dialpad && <ContextMenuTooltipButton
|
||||
className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_dialpad"
|
||||
inputRef={this.dialpadButton}
|
||||
onClick={this.onDialpadClick}
|
||||
isExpanded={this.state.showDialpad}
|
||||
title={_t("Dialpad")}
|
||||
alignment={Alignment.Top}
|
||||
/> }
|
||||
<LegacyCallViewDropdownButton
|
||||
state={!this.props.buttonsState.micMuted}
|
||||
className="mx_LegacyCallViewButtons_button_mic"
|
||||
onLabel={_t("Mute the microphone")}
|
||||
offLabel={_t("Unmute the microphone")}
|
||||
onClick={this.props.handlers.onMicMuteClick}
|
||||
deviceKinds={[MediaDeviceKindEnum.AudioInput, MediaDeviceKindEnum.AudioOutput]}
|
||||
/>
|
||||
{ this.props.buttonsVisibility.vidMute && <LegacyCallViewDropdownButton
|
||||
state={!this.props.buttonsState.vidMuted}
|
||||
className="mx_LegacyCallViewButtons_button_vid"
|
||||
onLabel={_t("Stop the camera")}
|
||||
offLabel={_t("Start the camera")}
|
||||
onClick={this.props.handlers.onVidMuteClick}
|
||||
deviceKinds={[MediaDeviceKindEnum.VideoInput]}
|
||||
/> }
|
||||
{ this.props.buttonsVisibility.screensharing && <LegacyCallViewToggleButton
|
||||
state={this.props.buttonsState.screensharing}
|
||||
className="mx_LegacyCallViewButtons_button_screensharing"
|
||||
onLabel={_t("Stop sharing your screen")}
|
||||
offLabel={_t("Start sharing your screen")}
|
||||
onClick={this.props.handlers.onScreenshareClick}
|
||||
/> }
|
||||
{ this.props.buttonsVisibility.sidebar && <LegacyCallViewToggleButton
|
||||
state={this.props.buttonsState.sidebarShown}
|
||||
className="mx_LegacyCallViewButtons_button_sidebar"
|
||||
onLabel={_t("Hide sidebar")}
|
||||
offLabel={_t("Show sidebar")}
|
||||
onClick={this.props.handlers.onToggleSidebarClick}
|
||||
/> }
|
||||
{ this.props.buttonsVisibility.contextMenu && <ContextMenuTooltipButton
|
||||
className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_more"
|
||||
onClick={this.onMoreClick}
|
||||
inputRef={this.contextMenuButton}
|
||||
isExpanded={this.state.showMoreMenu}
|
||||
title={_t("More")}
|
||||
alignment={Alignment.Top}
|
||||
/> }
|
||||
<AccessibleTooltipButton
|
||||
className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_hangup"
|
||||
onClick={this.props.handlers.onHangupClick}
|
||||
title={_t("Hangup")}
|
||||
alignment={Alignment.Top}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
Copyright 2021 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import React from 'react';
|
||||
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import RoomAvatar from '../../avatars/RoomAvatar';
|
||||
import AccessibleTooltipButton from '../../elements/AccessibleTooltipButton';
|
||||
|
||||
interface LegacyCallControlsProps {
|
||||
onExpand?: () => void;
|
||||
onPin?: () => void;
|
||||
onMaximize?: () => void;
|
||||
}
|
||||
|
||||
const LegacyCallViewHeaderControls: React.FC<LegacyCallControlsProps> = ({ onExpand, onPin, onMaximize }) => {
|
||||
return <div className="mx_LegacyCallViewHeader_controls">
|
||||
{ onMaximize && <AccessibleTooltipButton
|
||||
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_fullscreen"
|
||||
onClick={onMaximize}
|
||||
title={_t("Fill Screen")}
|
||||
/> }
|
||||
{ onPin && <AccessibleTooltipButton
|
||||
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_pin"
|
||||
onClick={onPin}
|
||||
title={_t("Pin")}
|
||||
/> }
|
||||
{ onExpand && <AccessibleTooltipButton
|
||||
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_expand"
|
||||
onClick={onExpand}
|
||||
title={_t("Return to call")}
|
||||
/> }
|
||||
</div>;
|
||||
};
|
||||
|
||||
interface ISecondaryCallInfoProps {
|
||||
callRoom: Room;
|
||||
}
|
||||
|
||||
const SecondaryCallInfo: React.FC<ISecondaryCallInfoProps> = ({ callRoom }) => {
|
||||
return <span className="mx_LegacyCallViewHeader_secondaryCallInfo">
|
||||
<RoomAvatar room={callRoom} height={16} width={16} />
|
||||
<span className="mx_LegacyCallView_secondaryCall_roomName">
|
||||
{ _t("%(name)s on hold", { name: callRoom.name }) }
|
||||
</span>
|
||||
</span>;
|
||||
};
|
||||
|
||||
interface LegacyCallViewHeaderProps {
|
||||
pipMode: boolean;
|
||||
callRooms?: Room[];
|
||||
onPipMouseDown: (event: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
onExpand?: () => void;
|
||||
onPin?: () => void;
|
||||
onMaximize?: () => void;
|
||||
}
|
||||
|
||||
const LegacyCallViewHeader: React.FC<LegacyCallViewHeaderProps> = ({
|
||||
pipMode = false,
|
||||
callRooms = [],
|
||||
onPipMouseDown,
|
||||
onExpand,
|
||||
onPin,
|
||||
onMaximize,
|
||||
}) => {
|
||||
const [callRoom, onHoldCallRoom] = callRooms;
|
||||
const callRoomName = callRoom.name;
|
||||
|
||||
if (!pipMode) {
|
||||
return <div className="mx_LegacyCallViewHeader">
|
||||
<div className="mx_LegacyCallViewHeader_icon" />
|
||||
<span className="mx_LegacyCallViewHeader_text">{ _t("Call") }</span>
|
||||
<LegacyCallViewHeaderControls onMaximize={onMaximize} />
|
||||
</div>;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="mx_LegacyCallViewHeader mx_LegacyCallViewHeader_pip"
|
||||
onMouseDown={onPipMouseDown}
|
||||
>
|
||||
<RoomAvatar room={callRoom} height={32} width={32} />
|
||||
<div className="mx_LegacyCallViewHeader_callInfo">
|
||||
<div className="mx_LegacyCallViewHeader_roomName">{ callRoomName }</div>
|
||||
{ onHoldCallRoom && <SecondaryCallInfo callRoom={onHoldCallRoom} /> }
|
||||
</div>
|
||||
<LegacyCallViewHeaderControls onExpand={onExpand} onPin={onPin} onMaximize={onMaximize} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LegacyCallViewHeader;
|
Loading…
Add table
Add a link
Reference in a new issue