Files
ThreadNet-Web/src/components/views/voip/LegacyCallView/LegacyCallViewButtons.tsx
T
Michael TelatynskiandGitHub 466f60ead5 Update the way we render icons for accessibility (#31731)
* 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>
2026-01-16 11:21:57 +00:00

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>
);
}
}