* Switch to Compound icons to replace old icons Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Apply same treatment to missed icons Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove duplicated icon Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update icon Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch from css masks to rendering svg in ImageView Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch from css masks to rendering svg in ExtensionsCard Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch from css masks to rendering svg in LegacyRoomListHeader Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch from css masks to rendering svg in ImageSizePanel Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch from css masks to rendering svg in LegacyRoomList Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove icon from CreateSecretStorageDialog title Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch from css masks to rendering svg in LiveContentSummary Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch from css masks to rendering svg in RoomCallBanner Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch from css masks to rendering svg in NonUrgentEchoFailureToast Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch from css masks to rendering svg in LegacyCallViewHeader Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Switch from css masks to rendering svg in CallEvent Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Replace dark-light-mode.svg with Compound Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Draw stop icon using svg rather than square mask Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Replace masks in RoomSublist with SVG icons Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Replace masks with SVG icons in LegacyCall views Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Replace masks with SVG icons in EventTile Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Replace masks with SVG icons in ForwardDialog Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove redundant css style The `::before` has no content so is never rendered Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update playwright tests & screenshots Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Update snapshot Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove snapshot as it causes issues Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * More tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
365 lines
13 KiB
TypeScript
365 lines
13 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
|
Copyright 2019-2021 The Matrix.org Foundation C.I.C.
|
|
Copyright 2015, 2016 OpenMarket Ltd
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import React, { createRef, useState, type Ref, type FC, type JSX } from "react";
|
|
import classNames from "classnames";
|
|
import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
|
import {
|
|
ChevronDownIcon,
|
|
ChevronUpIcon,
|
|
DialPadIcon,
|
|
EndCallIcon,
|
|
ListViewIcon,
|
|
MicOffSolidIcon,
|
|
MicOnSolidIcon,
|
|
OverflowHorizontalIcon,
|
|
ShareScreenSolidIcon,
|
|
SpotlightViewIcon,
|
|
VideoCallOffSolidIcon,
|
|
VideoCallSolidIcon,
|
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
|
|
|
import LegacyCallContextMenu from "../../context_menus/LegacyCallContextMenu";
|
|
import DialpadContextMenu from "../../context_menus/DialpadContextMenu";
|
|
import {
|
|
alwaysMenuProps,
|
|
alwaysAboveRightOf,
|
|
ChevronFace,
|
|
ContextMenuTooltipButton,
|
|
useContextMenu,
|
|
} from "../../../structures/ContextMenu";
|
|
import { _t } from "../../../../languageHandler";
|
|
import DeviceContextMenu from "../../context_menus/DeviceContextMenu";
|
|
import { MediaDeviceKindEnum } from "../../../../MediaDeviceHandler";
|
|
import AccessibleButton, {
|
|
type ButtonEvent,
|
|
type ButtonProps as AccessibleButtonProps,
|
|
} from "../../elements/AccessibleButton";
|
|
|
|
// 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;
|
|
|
|
type ButtonProps = Omit<AccessibleButtonProps<"div">, "title" | "element"> & {
|
|
state: boolean;
|
|
onLabel?: string;
|
|
onIcon: JSX.Element;
|
|
offLabel?: string;
|
|
offIcon: JSX.Element;
|
|
forceHide?: boolean;
|
|
onHover?: (hovering: boolean) => void;
|
|
ref?: Ref<HTMLElement>;
|
|
};
|
|
|
|
const LegacyCallViewToggleButton: FC<ButtonProps> = ({
|
|
children,
|
|
state: isOn,
|
|
className,
|
|
onLabel,
|
|
onIcon,
|
|
offLabel,
|
|
offIcon,
|
|
forceHide,
|
|
onHover,
|
|
ref,
|
|
...props
|
|
}) => {
|
|
const classes = classNames("mx_LegacyCallViewButtons_button", className, {
|
|
mx_LegacyCallViewButtons_button_on: isOn,
|
|
mx_LegacyCallViewButtons_button_off: !isOn,
|
|
});
|
|
|
|
const title = forceHide ? undefined : isOn ? onLabel : offLabel;
|
|
|
|
return (
|
|
<AccessibleButton
|
|
ref={ref}
|
|
className={classes}
|
|
title={title}
|
|
placement="top"
|
|
onTooltipOpenChange={onHover}
|
|
{...props}
|
|
>
|
|
{isOn ? onIcon : offIcon}
|
|
{children}
|
|
</AccessibleButton>
|
|
);
|
|
};
|
|
|
|
interface IDropdownButtonProps extends ButtonProps {
|
|
deviceKinds: MediaDeviceKindEnum[];
|
|
}
|
|
|
|
const LegacyCallViewDropdownButton: React.FC<IDropdownButtonProps> = ({ state, deviceKinds, ...props }) => {
|
|
const [menuDisplayed, buttonRef, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
|
const [hoveringDropdown, setHoveringDropdown] = useState(false);
|
|
|
|
const onClick = (event: ButtonEvent): void => {
|
|
event.stopPropagation();
|
|
openMenu();
|
|
};
|
|
|
|
return (
|
|
<LegacyCallViewToggleButton
|
|
ref={buttonRef}
|
|
forceHide={menuDisplayed || hoveringDropdown}
|
|
state={state}
|
|
{...props}
|
|
>
|
|
<LegacyCallViewToggleButton
|
|
className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_dropdownButton"
|
|
onClick={onClick}
|
|
onIcon={<ChevronUpIcon />}
|
|
offIcon={<ChevronDownIcon />}
|
|
onHover={(hovering) => setHoveringDropdown(hovering)}
|
|
state={state}
|
|
/>
|
|
{menuDisplayed && buttonRef.current && (
|
|
<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 = null;
|
|
|
|
public 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(): React.ReactNode {
|
|
const callControlsClasses = classNames("mx_LegacyCallViewButtons", {
|
|
mx_LegacyCallViewButtons_hidden: !this.state.visible,
|
|
});
|
|
|
|
let dialPad;
|
|
if (this.state.showDialpad && this.dialpadButton.current) {
|
|
dialPad = (
|
|
<DialpadContextMenu
|
|
{...alwaysMenuProps(
|
|
this.dialpadButton.current.getBoundingClientRect(),
|
|
ChevronFace.None,
|
|
CONTEXT_MENU_VPADDING,
|
|
)}
|
|
// We mount the context menus 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 && this.contextMenuButton.current) {
|
|
contextMenu = (
|
|
<LegacyCallContextMenu
|
|
{...alwaysMenuProps(
|
|
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"
|
|
ref={this.dialpadButton}
|
|
onClick={this.onDialpadClick}
|
|
isExpanded={this.state.showDialpad}
|
|
title={_t("voip|dialpad")}
|
|
placement="top"
|
|
>
|
|
<DialPadIcon />
|
|
</ContextMenuTooltipButton>
|
|
)}
|
|
<LegacyCallViewDropdownButton
|
|
state={!this.props.buttonsState.micMuted}
|
|
className="mx_LegacyCallViewButtons_button_mic"
|
|
onLabel={_t("voip|disable_microphone")}
|
|
onIcon={<MicOnSolidIcon />}
|
|
offLabel={_t("voip|enable_microphone")}
|
|
offIcon={<MicOffSolidIcon />}
|
|
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("voip|disable_camera")}
|
|
onIcon={<VideoCallSolidIcon />}
|
|
offLabel={_t("voip|enable_camera")}
|
|
offIcon={<VideoCallOffSolidIcon />}
|
|
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("voip|stop_screenshare")}
|
|
onIcon={<ShareScreenSolidIcon />}
|
|
offLabel={_t("voip|start_screenshare")}
|
|
offIcon={<ShareScreenSolidIcon />}
|
|
onClick={this.props.handlers.onScreenshareClick}
|
|
/>
|
|
)}
|
|
{this.props.buttonsVisibility.sidebar && (
|
|
<LegacyCallViewToggleButton
|
|
state={this.props.buttonsState.sidebarShown}
|
|
className="mx_LegacyCallViewButtons_button_sidebar"
|
|
onLabel={_t("voip|hide_sidebar_button")}
|
|
onIcon={<ListViewIcon />}
|
|
offLabel={_t("voip|show_sidebar_button")}
|
|
offIcon={<SpotlightViewIcon />}
|
|
onClick={this.props.handlers.onToggleSidebarClick}
|
|
/>
|
|
)}
|
|
{this.props.buttonsVisibility.contextMenu && (
|
|
<ContextMenuTooltipButton
|
|
className="mx_LegacyCallViewButtons_button"
|
|
onClick={this.onMoreClick}
|
|
ref={this.contextMenuButton}
|
|
isExpanded={this.state.showMoreMenu}
|
|
title={_t("voip|more_button")}
|
|
placement="top"
|
|
>
|
|
<OverflowHorizontalIcon />
|
|
</ContextMenuTooltipButton>
|
|
)}
|
|
<AccessibleButton
|
|
className="mx_LegacyCallViewButtons_button mx_LegacyCallViewButtons_button_hangup"
|
|
onClick={this.props.handlers.onHangupClick}
|
|
title={_t("voip|hangup")}
|
|
placement="top"
|
|
>
|
|
<EndCallIcon />
|
|
</AccessibleButton>
|
|
</div>
|
|
);
|
|
}
|
|
}
|