mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-05 01:10:40 +00:00
* Add option to pick call options for voice calls.
* hook on the right thing
* Fix wrong call being disabled
* update snaps
* Add tests for menus
* more snaps
* snap snap
(cherry picked from commit a352a3838e)
Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
/*
|
||||
Copyright (C) 2025 Element Creations Ltd
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -6,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, useCallback, useMemo, useState } from "react";
|
||||
import React, { type JSX, useCallback, useState } from "react";
|
||||
import { Text, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web";
|
||||
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid";
|
||||
import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call-solid";
|
||||
@@ -29,7 +30,6 @@ import { _t } from "../../../../languageHandler.tsx";
|
||||
import { getPlatformCallTypeProps, useRoomCall } from "../../../../hooks/room/useRoomCall.tsx";
|
||||
import { useRoomThreadNotifications } from "../../../../hooks/room/useRoomThreadNotifications.ts";
|
||||
import { useGlobalNotificationState } from "../../../../hooks/useGlobalNotificationState.ts";
|
||||
import SdkConfig from "../../../../SdkConfig.ts";
|
||||
import { useFeatureEnabled } from "../../../../hooks/useSettings.ts";
|
||||
import { useEncryptionStatus } from "../../../../hooks/useEncryptionStatus.ts";
|
||||
import { E2EStatus } from "../../../../utils/ShieldUtils.ts";
|
||||
@@ -78,16 +78,6 @@ function RoomHeaderButtons({
|
||||
showVoiceCallButton,
|
||||
showVideoCallButton,
|
||||
} = useRoomCall(room);
|
||||
|
||||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||
/**
|
||||
* A special mode where only Element Call is used. In this case we want to
|
||||
* hide the voice call button
|
||||
*/
|
||||
const useElementCallExclusively = useMemo(() => {
|
||||
return SdkConfig.get("element_call").use_exclusively && groupCallsEnabled;
|
||||
}, [groupCallsEnabled]);
|
||||
|
||||
const threadNotifications = useRoomThreadNotifications(room);
|
||||
const globalNotificationState = useGlobalNotificationState();
|
||||
|
||||
@@ -101,6 +91,11 @@ function RoomHeaderButtons({
|
||||
[callOptions, videoCallClick],
|
||||
);
|
||||
|
||||
const voiceClick = useCallback(
|
||||
(ev: React.MouseEvent) => voiceCallClick(ev, callOptions[0]),
|
||||
[callOptions, voiceCallClick],
|
||||
);
|
||||
|
||||
const toggleCallButton = (
|
||||
<Tooltip label={isViewingCall ? _t("voip|minimise_call") : _t("voip|maximise_call")}>
|
||||
<IconButton onClick={toggleCall}>
|
||||
@@ -126,35 +121,50 @@ function RoomHeaderButtons({
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const callIconWithTooltip = (
|
||||
const videoCallIconWithTooltip = (
|
||||
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
|
||||
<VideoCallIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const voiceCallIconWithTooltip = (
|
||||
<Tooltip label={videoCallDisabledReason ?? _t("voip|voice_call")}>
|
||||
<VoiceCallIcon />
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const onOpenChange = useCallback(
|
||||
const [videoMenuOpen, setVideoMenuOpen] = useState(false);
|
||||
|
||||
const onVideoOpenChange = useCallback(
|
||||
(newOpen: boolean) => {
|
||||
if (!videoCallDisabledReason) setMenuOpen(newOpen);
|
||||
if (!videoCallDisabledReason) setVideoMenuOpen(newOpen);
|
||||
},
|
||||
[videoCallDisabledReason],
|
||||
);
|
||||
|
||||
const [voiceMenuOpen, setVoiceMenuOpen] = useState(false);
|
||||
|
||||
const onVoiceOpenChange = useCallback(
|
||||
(newOpen: boolean) => {
|
||||
if (!voiceCallDisabledReason) setVoiceMenuOpen(newOpen);
|
||||
},
|
||||
[voiceCallDisabledReason],
|
||||
);
|
||||
|
||||
const startVideoCallButton = (
|
||||
<>
|
||||
{/* Can be either a menu or just a button depending on the number of call options.*/}
|
||||
{callOptions.length > 1 ? (
|
||||
<Menu
|
||||
open={menuOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
open={videoMenuOpen}
|
||||
onOpenChange={onVideoOpenChange}
|
||||
title={_t("voip|video_call_using")}
|
||||
trigger={
|
||||
<IconButton
|
||||
disabled={!!videoCallDisabledReason}
|
||||
aria-label={videoCallDisabledReason ?? _t("voip|video_call")}
|
||||
>
|
||||
{callIconWithTooltip}
|
||||
{videoCallIconWithTooltip}
|
||||
</IconButton>
|
||||
}
|
||||
side="left"
|
||||
@@ -170,7 +180,7 @@ function RoomHeaderButtons({
|
||||
children={children}
|
||||
className="mx_RoomHeader_videoCallOption"
|
||||
onClick={(ev) => {
|
||||
setMenuOpen(false);
|
||||
setVideoMenuOpen(false);
|
||||
videoCallClick(ev, option);
|
||||
}}
|
||||
Icon={VideoCallIcon}
|
||||
@@ -185,25 +195,61 @@ function RoomHeaderButtons({
|
||||
aria-label={videoCallDisabledReason ?? _t("voip|video_call")}
|
||||
onClick={videoClick}
|
||||
>
|
||||
{callIconWithTooltip}
|
||||
{videoCallIconWithTooltip}
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
let voiceCallButton: JSX.Element | undefined = (
|
||||
<Tooltip label={voiceCallDisabledReason ?? _t("voip|voice_call")}>
|
||||
<IconButton
|
||||
// We need both: isViewingCall and isConnectedToCall
|
||||
// - in the Lobby we are viewing a call but are not connected to it.
|
||||
// - in pip view we are connected to the call but not viewing it.
|
||||
disabled={!!voiceCallDisabledReason || isViewingCall || isConnectedToCall}
|
||||
aria-label={voiceCallDisabledReason ?? _t("voip|voice_call")}
|
||||
onClick={(ev) => voiceCallClick(ev, callOptions[0])}
|
||||
>
|
||||
<VoiceCallIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
const startVoiceCallButton = (
|
||||
<>
|
||||
{/* Can be either a menu or just a button depending on the number of call options.*/}
|
||||
{callOptions.length > 1 ? (
|
||||
<Menu
|
||||
open={voiceMenuOpen}
|
||||
onOpenChange={onVoiceOpenChange}
|
||||
title={_t("voip|voice_call_using")}
|
||||
trigger={
|
||||
<IconButton
|
||||
disabled={!!voiceCallDisabledReason}
|
||||
aria-label={voiceCallDisabledReason ?? _t("voip|voice_call")}
|
||||
>
|
||||
{voiceCallIconWithTooltip}
|
||||
</IconButton>
|
||||
}
|
||||
side="left"
|
||||
align="start"
|
||||
>
|
||||
{callOptions.map((option) => {
|
||||
const { label, children } = getPlatformCallTypeProps(option);
|
||||
return (
|
||||
<MenuItem
|
||||
key={option}
|
||||
label={label}
|
||||
aria-label={label}
|
||||
children={children}
|
||||
className="mx_RoomHeader_videoCallOption"
|
||||
onClick={(ev) => {
|
||||
setVoiceMenuOpen(false);
|
||||
voiceCallClick(ev, option);
|
||||
}}
|
||||
Icon={VoiceCallIcon}
|
||||
onSelect={() => {} /* Dummy handler since we want the click event.*/}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
) : (
|
||||
<IconButton
|
||||
disabled={!!voiceCallDisabledReason}
|
||||
aria-label={voiceCallDisabledReason ?? _t("voip|voice_call")}
|
||||
onClick={voiceClick}
|
||||
>
|
||||
{voiceCallIconWithTooltip}
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const closeLobbyButton = (
|
||||
<Tooltip label={_t("voip|close_lobby")}>
|
||||
<IconButton onClick={toggleCall}>
|
||||
@@ -212,15 +258,19 @@ function RoomHeaderButtons({
|
||||
</Tooltip>
|
||||
);
|
||||
let videoCallButton: JSX.Element | undefined = startVideoCallButton;
|
||||
let voiceCallButton: JSX.Element | undefined = startVoiceCallButton;
|
||||
if (isConnectedToCall) {
|
||||
videoCallButton = toggleCallButton;
|
||||
voiceCallButton = undefined;
|
||||
} else if (isViewingCall) {
|
||||
videoCallButton = closeLobbyButton;
|
||||
voiceCallButton = undefined;
|
||||
}
|
||||
|
||||
if (!showVideoCallButton) {
|
||||
videoCallButton = undefined;
|
||||
}
|
||||
|
||||
if (!showVoiceCallButton) {
|
||||
voiceCallButton = undefined;
|
||||
}
|
||||
@@ -258,7 +308,7 @@ function RoomHeaderButtons({
|
||||
) : (
|
||||
<>
|
||||
{!isVideoRoom && videoCallButton}
|
||||
{!useElementCallExclusively && !isVideoRoom && voiceCallButton}
|
||||
{!isVideoRoom && voiceCallButton}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4103,6 +4103,7 @@
|
||||
"video_call_using": "Video call using:",
|
||||
"voice_call": "Voice call",
|
||||
"voice_call_incoming": "Incoming voice call",
|
||||
"voice_call_using": "Voice call using:",
|
||||
"you_are_presenting": "You are presenting"
|
||||
},
|
||||
"web_default_device_name": "%(appName)s: %(browserName)s on %(osName)s",
|
||||
|
||||
@@ -879,7 +879,6 @@ exports[`RoomView should hide the composer when hideComposer=true 1`] = `
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="_r_5_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -891,6 +890,7 @@ exports[`RoomView should hide the composer when hideComposer=true 1`] = `
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_5_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1303,7 +1303,6 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="_r_5g_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1315,6 +1314,7 @@ exports[`RoomView should hide the pinned message banner when hidePinnedMessageBa
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_5g_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -1665,7 +1665,6 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="_r_2j_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -1677,6 +1676,7 @@ exports[`RoomView should hide the right panel when hideRightPanel=true 1`] = `
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_2j_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -2027,7 +2027,6 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="_r_ag_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -2039,6 +2038,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_ag_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -2239,7 +2239,6 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="_r_ag_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -2251,6 +2250,7 @@ exports[`RoomView should not display the timeline when the room encryption is lo
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_ag_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/*
|
||||
Copyright (C) 2025 Element Creations Ltd
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
@@ -458,7 +459,10 @@ describe("RoomHeader", () => {
|
||||
} as unknown as Call);
|
||||
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([widget]);
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
expect(screen.getByRole("button", { name: "Ongoing call" })).toHaveAttribute("aria-disabled", "true");
|
||||
// Voice and video
|
||||
for (const button of screen.getAllByRole("button", { name: "Ongoing call" })) {
|
||||
expect(button).toHaveAttribute("aria-disabled", "true");
|
||||
}
|
||||
});
|
||||
|
||||
it("clicking on ongoing (unpinned) call re-pins it", async () => {
|
||||
@@ -632,6 +636,41 @@ describe("RoomHeader", () => {
|
||||
|
||||
expect(getByLabelText(document.body, _t("voip|get_call_link"))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("gives the option of element call or legacy calling for video", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 2);
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
if (key === ElementCallMemberEventType.name) return true;
|
||||
return false;
|
||||
});
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
const button = screen.getByRole("button", { name: "Video call" });
|
||||
expect(button).not.toHaveAttribute("aria-disabled", "true");
|
||||
await user.click(button);
|
||||
const elementCallButton = screen.getByRole("menuitem", { name: "Element Call" });
|
||||
const legacyCallButton = screen.getByRole("menuitem", { name: "Legacy Call" });
|
||||
expect(elementCallButton).toBeInTheDocument();
|
||||
expect(legacyCallButton).toBeInTheDocument();
|
||||
});
|
||||
it("gives the option of element call or legacy calling for voice in DM rooms", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 2);
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
if (key === ElementCallMemberEventType.name) return true;
|
||||
return false;
|
||||
});
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
const button = screen.getByRole("button", { name: "Voice call" });
|
||||
expect(button).not.toHaveAttribute("aria-disabled", "true");
|
||||
await user.click(button);
|
||||
const elementCallButton = screen.getByRole("menuitem", { name: "Element Call" });
|
||||
const legacyCallButton = screen.getByRole("menuitem", { name: "Legacy Call" });
|
||||
expect(elementCallButton).toBeInTheDocument();
|
||||
expect(legacyCallButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("public room", () => {
|
||||
|
||||
@@ -56,7 +56,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_14k_"
|
||||
aria-labelledby="_r_17e_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -72,7 +72,6 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
<button
|
||||
aria-disabled="false"
|
||||
aria-label="Voice call"
|
||||
aria-labelledby="_r_14p_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -84,6 +83,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby="_r_17j_"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -98,7 +98,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby="_r_14u_"
|
||||
aria-labelledby="_r_17o_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
@@ -125,7 +125,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby="_r_153_"
|
||||
aria-labelledby="_r_17t_"
|
||||
class="_icon-button_1pz9o_8"
|
||||
data-kind="primary"
|
||||
role="button"
|
||||
|
||||
Reference in New Issue
Block a user