Revert "Support using Element Call for voice calls in DMs (#30817)"

This reverts commit f3a880f1

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2025-12-03 12:36:34 +00:00
parent 3c6f3f7814
commit 6f72dfefac
No known key found for this signature in database
GPG Key ID: A2B008A5F49F5D0D
25 changed files with 112 additions and 365 deletions

View File

@ -12,7 +12,7 @@ import type { EventType, Preset } from "matrix-js-sdk/src/matrix";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { test, expect } from "../../element-web-test";
import type { Credentials } from "../../plugins/homeserver";
import { Bot } from "../../pages/bot";
import type { Bot } from "../../pages/bot";
// Load a copy of our fake Element Call app, and the latest widget API.
// The fake call app does *just* enough to convince Element Web that a call is ongoing
@ -37,28 +37,27 @@ function assertCommonCallParameters(
expect(hash.get("preload")).toEqual("false");
}
async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "notification", intent?: string) {
async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "notification") {
const resp = await bot.sendStateEvent(
roomId,
"org.matrix.msc3401.call.member",
{
"application": "m.call",
"call_id": "",
"m.call.intent": intent,
"device_id": "OiDFxsZrjz",
"expires": 180000000,
"foci_preferred": [
application: "m.call",
call_id: "",
device_id: "OiDFxsZrjz",
expires: 180000000,
foci_preferred: [
{
livekit_alias: roomId,
livekit_service_url: "https://example.org",
type: "livekit",
},
],
"focus_active": {
focus_active: {
focus_selection: "oldest_membership",
type: "livekit",
},
"scope": "m.room",
scope: "m.room",
},
`_@${bot.credentials.userId}_OiDFxsZrjz_m.call`,
);
@ -75,7 +74,6 @@ async function sendRTCState(bot: Bot, roomId: string, notification?: "ring" | "n
event_id: resp.event_id,
rel_type: "org.matrix.msc4075.rtc.notification.parent",
},
"m.call.intent": intent,
"notification_type": notification,
"sender_ts": 1758611895996,
});
@ -117,21 +115,15 @@ test.describe("Element Call", () => {
});
test.describe("Group Chat", () => {
let charlie: Bot;
test.use({
room: async ({ page, app, user, homeserver, bot }, use) => {
charlie = new Bot(page, homeserver, { displayName: "Charlie" });
await charlie.prepareClient();
const roomId = await app.client.createRoom({
name: "TestRoom",
invite: [bot.credentials.userId, charlie.credentials.userId],
});
room: async ({ page, app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name: "TestRoom", invite: [bot.credentials.userId] });
await use({ roomId });
},
});
test("should be able to start a video call", async ({ page, user, room, app }) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await expect(page.getByText("Bob joined the room")).toBeVisible();
await page.getByRole("button", { name: "Video call" }).click();
await page.getByRole("menuitem", { name: "Element Call" }).click();
@ -146,16 +138,9 @@ test.describe("Element Call", () => {
expect(hash.get("skipLobby")).toEqual(null);
});
test("should NOT be able to start a voice call", async ({ page, user, room, app }) => {
// Voice calls do not exist in group rooms
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await expect(page.getByRole("button", { name: "Voice call" })).not.toBeVisible();
});
test("should be able to skip lobby by holding down shift", async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await expect(page.getByText("Bob joined the room")).toBeVisible();
await page.getByRole("button", { name: "Video call" }).click();
await page.keyboard.down("Shift");
@ -174,8 +159,8 @@ test.describe("Element Call", () => {
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
// Allow bob to create a call
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
await expect(page.getByText("Bob joined the room")).toBeVisible();
// Fake a start of a call
await sendRTCState(bot, room.roomId);
const button = page.getByTestId("join-call-button");
@ -183,6 +168,7 @@ test.describe("Element Call", () => {
// And test joining
await button.click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
console.log(frameUrlStr);
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
@ -194,29 +180,29 @@ test.describe("Element Call", () => {
[true, false].forEach((skipLobbyToggle) => {
test(
`should be able to join a call via incoming video call toast (skipLobby=${skipLobbyToggle})`,
`should be able to join a call via incoming call toast (skipLobby=${skipLobbyToggle})`,
{ tag: ["@screenshot"] },
async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
// Allow bob to create a call
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
await expect(page.getByText("Bob joined the room")).toBeVisible();
// Fake a start of a call
await sendRTCState(bot, room.roomId, "notification", "video");
await sendRTCState(bot, room.roomId, "notification");
const toast = page.locator(".mx_Toast_toast");
const button = toast.getByRole("button", { name: "Join" });
if (skipLobbyToggle) {
await toast.getByRole("switch").check();
await expect(toast).toMatchScreenshot(`incoming-call-group-video-toast-checked.png`);
await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-checked.png");
} else {
await toast.getByRole("switch").uncheck();
await expect(toast).toMatchScreenshot(`incoming-call-group-video-toast-unchecked.png`);
await expect(toast).toMatchScreenshot("incoming-call-group-video-toast-unchecked.png");
}
// And test joining
await button.click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
console.log(frameUrlStr);
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
@ -227,34 +213,6 @@ test.describe("Element Call", () => {
},
);
});
test(
`should be able to join a call via incoming voice call toast`,
{ tag: ["@screenshot"] },
async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
// Allow bob to create a call
await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible();
await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50);
// Fake a start of a call
await sendRTCState(bot, room.roomId, "notification", "audio");
const toast = page.locator(".mx_Toast_toast");
const button = toast.getByRole("button", { name: "Join" });
await expect(toast).toMatchScreenshot(`incoming-call-group-voice-toast.png`);
// And test joining
await button.click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
assertCommonCallParameters(url.searchParams, hash, user, room);
expect(hash.get("intent")).toEqual("join_existing");
expect(hash.get("skipLobby")).toEqual("true");
},
);
});
test.describe("DMs", () => {
@ -307,6 +265,7 @@ test.describe("Element Call", () => {
test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
// Allow bob to create a call
await expect(page.getByText("Bob joined the room")).toBeVisible();
// Fake a start of a call
await sendRTCState(bot, room.roomId);
@ -315,6 +274,7 @@ test.describe("Element Call", () => {
// And test joining
await button.click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
console.log(frameUrlStr);
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
@ -330,31 +290,24 @@ test.describe("Element Call", () => {
{ tag: ["@screenshot"] },
async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
// Allow bob to create a call
await expect(page.getByText("Bob joined the room")).toBeVisible();
// Fake a start of a call
await sendRTCState(bot, room.roomId, "ring", "video");
await sendRTCState(bot, room.roomId, "ring");
const toast = page.locator(".mx_Toast_toast");
const button = toast.getByRole("button", { name: "Accept" });
const button = toast.getByRole("button", { name: "Join" });
if (skipLobbyToggle) {
await toast.getByRole("switch").check();
await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-checked.png");
} else {
await toast.getByRole("switch").uncheck();
await expect(toast).toMatchScreenshot("incoming-call-dm-video-toast-unchecked.png");
}
await expect(toast).toMatchScreenshot(
`incoming-call-dm-video-toast-${skipLobbyToggle ? "checked" : "unchecked"}.png`,
{
// Hide UserId
css: `
.mx_IncomingCallToast_AvatarWithDetails span:nth-child(2) {
opacity: 0;
}
`,
},
);
// And test joining
await button.click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
console.log(frameUrlStr);
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
@ -365,39 +318,6 @@ test.describe("Element Call", () => {
},
);
});
test(
`should be able to join a call via incoming voice call toast`,
{ tag: ["@screenshot"] },
async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
await expect(page.getByText("Bob joined the room")).toBeVisible();
// Fake a start of a call
await sendRTCState(bot, room.roomId, "ring", "audio");
const toast = page.locator(".mx_Toast_toast");
const button = toast.getByRole("button", { name: "Accept" });
await expect(toast).toMatchScreenshot(`incoming-call-dm-voice-toast.png`, {
// Hide UserId
css: `
.mx_IncomingCallToast_AvatarWithDetails span:nth-child(2) {
opacity: 0;
}
`,
});
// And test joining
await button.click();
const frameUrlStr = await page.locator("iframe").getAttribute("src");
await expect(frameUrlStr).toBeDefined();
const url = new URL(frameUrlStr);
const hash = new URLSearchParams(url.hash.slice(1));
assertCommonCallParameters(url.searchParams, hash, user, room);
expect(hash.get("intent")).toEqual("join_existing_dm_voice");
expect(hash.get("skipLobby")).toEqual("true");
},
);
});
test.describe("Video Rooms", () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -25,10 +25,6 @@ Please see LICENSE files in the repository root for full details.
mask-image: url("@vector-im/compound-design-tokens/icons/video-call-solid.svg");
}
&.mx_LiveContentSummary_text_voice::before {
mask-image: url("@vector-im/compound-design-tokens/icons/voice-call-solid.svg");
}
&.mx_LiveContentSummary_text_active {
color: $accent;

View File

@ -7,7 +7,6 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import dispatcher from "../../../dispatcher/dispatcher";
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
@ -20,7 +19,7 @@ import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { useEventEmitter, useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { DefaultTagID } from "../../../stores/room-list/models";
import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall";
import { CallEvent, type ConnectionState } from "../../../models/Call";
import { type ConnectionState } from "../../../models/Call";
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
import DMRoomMap from "../../../utils/DMRoomMap";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
@ -68,10 +67,6 @@ export interface RoomListItemViewState {
* Whether there are participants in the call.
*/
hasParticipantInCall: boolean;
/**
* Whether the call is a voice or video call.
*/
callType: CallType | undefined;
/**
* Pre-rendered and translated preview for the latest message in the room, or undefined
* if no preview should be shown.
@ -128,10 +123,10 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
// EC video call or video room
const call = useCall(room.roomId);
const connectionState = useConnectionState(call);
const participantCount = useParticipantCount(call);
const hasParticipantInCall = useParticipantCount(call) > 0;
const callConnectionState = call ? connectionState : null;
const showNotificationDecoration = hasVisibleNotification || participantCount > 0;
const showNotificationDecoration = hasVisibleNotification || hasParticipantInCall;
// Actions
@ -143,9 +138,6 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
});
}, [room]);
const [callType, setCallType] = useState<CallType>(CallType.Video);
useTypedEventEmitter(call ?? undefined, CallEvent.CallTypeChanged, setCallType);
return {
name,
notificationState,
@ -156,10 +148,9 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
isBold,
isVideoRoom,
callConnectionState,
hasParticipantInCall: participantCount > 0,
hasParticipantInCall,
messagePreview,
showNotificationDecoration,
callType: call ? callType : undefined,
};
}

View File

@ -10,10 +10,12 @@ import React, { type FC } from "react";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
import { type Call } from "../../../models/Call";
import { useParticipantCount } from "../../../hooks/useCall";
export enum LiveContentType {
Video,
Voice,
// More coming soon
}
interface Props {
@ -31,7 +33,6 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
<span
className={classNames("mx_LiveContentSummary_text", {
mx_LiveContentSummary_text_video: type === LiveContentType.Video,
mx_LiveContentSummary_text_voice: type === LiveContentType.Voice,
mx_LiveContentSummary_text_active: active,
})}
>
@ -50,3 +51,16 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
)}
</span>
);
interface LiveContentSummaryWithCallProps {
call: Call;
}
export const LiveContentSummaryWithCall: FC<LiveContentSummaryWithCallProps> = ({ call }) => (
<LiveContentSummary
type={LiveContentType.Video}
text={_t("common|video")}
active={false}
participantCount={useParticipantCount(call)}
/>
);

View File

@ -12,8 +12,6 @@ import NotificationOffIcon from "@vector-im/compound-design-tokens/assets/web/ic
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid";
import EmailIcon from "@vector-im/compound-design-tokens/assets/web/icons/email-solid";
import { UnreadCounter, Unread } from "@vector-im/compound-web";
import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call-solid";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { Flex } from "@element-hq/web-shared-components";
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
@ -26,9 +24,9 @@ interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
*/
notificationState: RoomNotificationState;
/**
* Whether the room has a voice or video call.
* Whether the room has a video call.
*/
callType?: CallType;
hasVideoCall: boolean;
}
/**
@ -36,7 +34,7 @@ interface NotificationDecorationProps extends HTMLProps<HTMLDivElement> {
*/
export function NotificationDecoration({
notificationState,
callType,
hasVideoCall,
...props
}: NotificationDecorationProps): JSX.Element | null {
// Listen to the notification state and update the component when it changes
@ -60,7 +58,7 @@ export function NotificationDecoration({
muted: notificationState.muted,
}));
if (!hasAnyNotificationOrActivity && !muted && !callType) return null;
if (!hasAnyNotificationOrActivity && !muted && !hasVideoCall) return null;
return (
<Flex
@ -71,12 +69,7 @@ export function NotificationDecoration({
data-testid="notification-decoration"
>
{isUnsentMessage && <ErrorIcon width="20px" height="20px" fill="var(--cpd-color-icon-critical-primary)" />}
{callType === CallType.Video && (
<VideoCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />
)}
{callType === CallType.Voice && (
<VoiceCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />
)}
{hasVideoCall && <VideoCallIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
{invited && <EmailIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
{isMention && <MentionIcon width="20px" height="20px" fill="var(--cpd-color-icon-accent-primary)" />}
{(isMention || isNotification) && <UnreadCounter count={count || null} />}

View File

@ -131,7 +131,7 @@ export const RoomListItemView = memo(function RoomListItemView({
<NotificationDecoration
notificationState={vm.notificationState}
aria-hidden={true}
callType={vm.callType}
hasVideoCall={vm.hasParticipantInCall}
/>
)}
</>

View File

@ -121,7 +121,7 @@ export enum Action {
UpdateSystemFont = "update_system_font",
/**
* Changes room based on payload parameters. Should be used with ViewRoomPayload.
* Changes room based on payload parameters. Should be used with JoinRoomPayload.
*/
ViewRoom = "view_room",

View File

@ -39,7 +39,6 @@ interface BaseViewRoomPayload extends Pick<ActionPayload, "action"> {
clear_search?: boolean; // Whether to clear the room list search
view_call?: boolean; // Whether to view the call or call lobby for the room
skipLobby?: boolean; // Whether to skip the call lobby when showing the call (only supported for element calls)
voiceOnly?: boolean; // Whether the call is voice only (only supported for element calls)
opts?: JoinRoomPayload["opts"];
deferred_action?: ActionPayload; // Action to fire after MatrixChat handles this ViewRoom action

View File

@ -142,6 +142,11 @@ export const useRoomCall = (
// If there are multiple options, the user will be prompted to choose.
const callOptions = useMemo((): PlatformCallType[] => {
const options: PlatformCallType[] = [];
if (memberCount <= 2) {
options.push(PlatformCallType.LegacyCall);
} else if (mayEditWidgets || hasJitsiWidget) {
options.push(PlatformCallType.JitsiCall);
}
if (groupCallsEnabled) {
if (hasGroupCall || mayCreateElementCalls) {
options.push(PlatformCallType.ElementCall);
@ -150,11 +155,6 @@ export const useRoomCall = (
return [PlatformCallType.ElementCall];
}
}
if (memberCount <= 2) {
options.push(PlatformCallType.LegacyCall);
} else if (mayEditWidgets || hasJitsiWidget) {
options.push(PlatformCallType.JitsiCall);
}
if (hasGroupCall && WidgetType.CALL.matches(groupCall.widget.type)) {
// only allow joining the ongoing Element call if there is one.
return [PlatformCallType.ElementCall];
@ -231,7 +231,7 @@ export const useRoomCall = (
if (widget && promptPinWidget) {
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top);
} else {
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined, true);
placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined);
}
},
[promptPinWidget, room, widget],
@ -244,7 +244,7 @@ export const useRoomCall = (
} else {
// If we have pressed shift then always skip the lobby, otherwise `undefined` will defer
// to the defaults of the call implementation.
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined, false);
placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined);
}
},
[widget, promptPinWidget, room],
@ -279,13 +279,7 @@ export const useRoomCall = (
const roomDoesNotExist = room instanceof LocalRoom && room.state !== LocalRoomState.CREATED;
// We hide the voice call button if it'd have the same effect as the video call button
let hideVoiceCallButton =
isManagedHybridWidgetEnabled(room) ||
// Disable voice calls if Legacy calls are disabled
(!callOptions.includes(PlatformCallType.LegacyCall) &&
// Disable voice calls in ECall if the room is a group (we only present video calls for groups of users)
(!callOptions.includes(PlatformCallType.ElementCall) || memberCount > 2));
let hideVoiceCallButton = isManagedHybridWidgetEnabled(room) || !callOptions.includes(PlatformCallType.LegacyCall);
let hideVideoCallButton = false;
// We hide both buttons if:
// - they require widgets but widgets are disabled

View File

@ -604,7 +604,6 @@
"video": "Video",
"video_room": "Video room",
"view_message": "View message",
"voice": "Voice",
"warning": "Warning"
},
"composer": {
@ -4098,11 +4097,9 @@
"user_busy_description": "The user you called is busy.",
"user_is_presenting": "%(sharerName)s is presenting",
"video_call": "Video call",
"video_call_incoming": "Incoming video call",
"video_call_started": "Video call started",
"video_call_using": "Video call using:",
"voice_call": "Voice call",
"voice_call_incoming": "Incoming voice call",
"you_are_presenting": "You are presenting"
},
"web_default_device_name": "%(appName)s: %(browserName)s on %(osName)s",

View File

@ -84,7 +84,6 @@ export enum CallEvent {
Participants = "participants",
Close = "close",
Destroy = "destroy",
CallTypeChanged = "call_type_changed",
}
interface CallEventHandlerMap {
@ -95,7 +94,6 @@ interface CallEventHandlerMap {
) => void;
[CallEvent.Close]: () => void;
[CallEvent.Destroy]: () => void;
[CallEvent.CallTypeChanged]: (callType: CallType) => void;
}
/**
@ -105,18 +103,6 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
protected readonly widgetUid: string;
protected readonly room: Room;
private _callType: CallType = CallType.Video;
public get callType(): CallType {
return this._callType;
}
protected set callType(callType: CallType) {
if (this._callType !== callType) {
this.emit(CallEvent.CallTypeChanged, callType);
}
this._callType = callType;
}
/**
* The time after which device member state should be considered expired.
*/
@ -558,24 +544,7 @@ export enum ElementCallIntent {
StartCall = "start_call",
JoinExisting = "join_existing",
StartCallDM = "start_call_dm",
StartCallDMVoice = "start_call_dm_voice",
JoinExistingDM = "join_existing_dm",
JoinExistingDMVoice = "join_existing_dm_voice",
}
/**
* Parameters to be passed during widget creation.
* These parameters are hints only, and may not be accepted by the implementation.
*/
export interface WidgetGenerationParameters {
/**
* Skip showing the lobby screen of a call.
*/
skipLobby?: boolean;
/**
* Does the user intent to start a voice call?
*/
voiceOnly?: boolean;
}
/**
@ -617,12 +586,7 @@ export class ElementCall extends Call {
* @param client The current client.
* @param roomId The room ID for the call.
*/
private static appendRoomParams(
params: URLSearchParams,
client: MatrixClient,
roomId: string,
{ voiceOnly }: WidgetGenerationParameters,
): void {
private static appendRoomParams(params: URLSearchParams, client: MatrixClient, roomId: string): void {
const room = client.getRoom(roomId);
if (!room) {
// If the room isn't known, or the room is a video room then skip setting an intent.
@ -646,17 +610,13 @@ export class ElementCall extends Call {
// is released and upgraded.
if (isDM) {
if (hasCallStarted) {
params.append(
"intent",
voiceOnly ? ElementCallIntent.JoinExistingDMVoice : ElementCallIntent.JoinExistingDM,
);
params.append("intent", ElementCallIntent.JoinExistingDM);
params.append("preload", "false");
} else {
params.append("intent", voiceOnly ? ElementCallIntent.StartCallDMVoice : ElementCallIntent.StartCallDM);
params.append("intent", ElementCallIntent.StartCallDM);
params.append("preload", "false");
}
} else {
// Group chats do not have a voice option.
if (hasCallStarted) {
params.append("intent", ElementCallIntent.JoinExisting);
params.append("preload", "false");
@ -757,7 +717,7 @@ export class ElementCall extends Call {
.forEach((font) => params.append("font", font));
}
this.appendAnalyticsParams(params, client);
this.appendRoomParams(params, client, roomId, opts);
this.appendRoomParams(params, client, roomId);
const replacedUrl = params.toString().replace(/%24/g, "$");
url.hash = `#?${replacedUrl}`;
@ -791,43 +751,11 @@ export class ElementCall extends Call {
);
}
/**
* Get the correct intent for a widget, so that Element Call presents the correct
* default config.
* @param client The matrix client.
* @param roomId
* @param voiceOnly Should the call be voice-only, or video (default).
*/
public static getWidgetIntent(client: MatrixClient, roomId: string, voiceOnly?: boolean): ElementCallIntent {
const room = client.getRoom(roomId);
if (room !== null && !isVideoRoom(room)) {
const isDM = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId);
const oldestCallMember = client.matrixRTC.getRoomSession(room).getOldestMembership();
const hasCallStarted = !!oldestCallMember && oldestCallMember.sender !== client.getSafeUserId();
if (isDM) {
if (hasCallStarted) {
return voiceOnly ? ElementCallIntent.JoinExistingDMVoice : ElementCallIntent.JoinExistingDM;
} else {
return voiceOnly ? ElementCallIntent.StartCallDMVoice : ElementCallIntent.StartCallDM;
}
} else {
if (hasCallStarted) {
return ElementCallIntent.JoinExisting;
} else {
return ElementCallIntent.StartCall;
}
}
}
// If unknown, default to joining an existing call.
return ElementCallIntent.JoinExisting;
}
private static getWidgetData(
client: MatrixClient,
roomId: string,
currentData: IWidgetData,
overwriteData: IWidgetData,
voiceOnly?: boolean,
): IWidgetData {
let perParticipantE2EE = false;
if (
@ -835,13 +763,9 @@ export class ElementCall extends Call {
!SettingsStore.getValue("feature_disable_call_per_sender_encryption")
)
perParticipantE2EE = true;
const intent = ElementCall.getWidgetIntent(client, roomId, voiceOnly);
return {
...currentData,
...overwriteData,
intent,
perParticipantE2EE,
};
}
@ -867,7 +791,7 @@ export class ElementCall extends Call {
this.updateParticipants();
}
public static get(room: Room, voiceOnly?: boolean): ElementCall | null {
public static get(room: Room): ElementCall | null {
const apps = WidgetStore.instance.getApps(room.roomId);
const hasEcWidget = apps.some((app) => WidgetType.CALL.matches(app.type));
const session = room.client.matrixRTC.getRoomSession(room);
@ -950,10 +874,7 @@ export class ElementCall extends Call {
if (this.session.memberships.length === 0 && !this.presented && !this.room.isCallRoom()) this.destroy();
};
private readonly onMembershipChanged = (): void => {
this.updateParticipants();
this.callType = this.session.getConsensusCallIntent() === "audio" ? CallType.Voice : CallType.Video;
};
private readonly onMembershipChanged = (): void => this.updateParticipants();
private updateParticipants(): void {
const participants = new Map<RoomMember, Set<string>>();

View File

@ -381,9 +381,7 @@ export class RoomViewStore extends EventEmitter {
call.presented = true;
// Immediately start the call. This will connect to all required widget events
// and allow the widget to show the lobby.
if (call.connectionState === ConnectionState.Disconnected) {
call.start({ skipLobby: payload.skipLobby, voiceOnly: payload.voiceOnly });
}
if (call.connectionState === ConnectionState.Disconnected) call.start({ skipLobby: payload.skipLobby });
}
// If we switch to a different room from the call, we are no longer presenting it
const prevRoomCall = this.state.roomId ? CallStore.instance.getCall(this.state.roomId) : null;

View File

@ -14,7 +14,6 @@ import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"
import CrossIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
import { logger } from "matrix-js-sdk/src/logger";
import { type IRTCNotificationContent } from "matrix-js-sdk/src/matrixrtc";
import { VoiceCallIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { AvatarWithDetails } from "@element-hq/web-shared-components";
import { _t } from "../languageHandler";
@ -24,8 +23,12 @@ import defaultDispatcher from "../dispatcher/dispatcher";
import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../dispatcher/actions";
import ToastStore from "../stores/ToastStore";
import { LiveContentSummary, LiveContentType } from "../components/views/rooms/LiveContentSummary";
import { useCall, useJoinCallButtonDisabledTooltip, useParticipantCount } from "../hooks/useCall";
import {
LiveContentSummary,
LiveContentSummaryWithCall,
LiveContentType,
} from "../components/views/rooms/LiveContentSummary";
import { useCall, useJoinCallButtonDisabledTooltip } from "../hooks/useCall";
import AccessibleButton, { type ButtonEvent } from "../components/views/elements/AccessibleButton";
import { useDispatcher } from "../hooks/useDispatcher";
import { type ActionPayload } from "../dispatcher/payloads";
@ -33,7 +36,6 @@ import { type Call, CallEvent } from "../models/Call";
import LegacyCallHandler, { AudioID } from "../LegacyCallHandler";
import { useEventEmitter } from "../hooks/useEventEmitter";
import { CallStore, CallStoreEvent } from "../stores/CallStore";
import DMRoomMap from "../utils/DMRoomMap";
/**
* Get the key for the incoming call toast. A combination of the event ID and room ID.
@ -69,15 +71,9 @@ interface JoinCallButtonWithCallProps {
onClick: (e: ButtonEvent) => void;
call: Call | null;
disabledTooltip: string | undefined;
isRinging: boolean;
}
function JoinCallButtonWithCall({
onClick,
call,
disabledTooltip,
isRinging,
}: JoinCallButtonWithCallProps): JSX.Element {
function JoinCallButtonWithCall({ onClick, call, disabledTooltip }: JoinCallButtonWithCallProps): JSX.Element {
let disTooltip = disabledTooltip;
const disabledBecauseFullTooltip = useJoinCallButtonDisabledTooltip(call);
disTooltip = disabledTooltip ?? disabledBecauseFullTooltip ?? undefined;
@ -92,7 +88,7 @@ function JoinCallButtonWithCall({
Icon={CheckIcon}
size="sm"
>
{isRinging ? _t("action|accept") : _t("action|join")}
{_t("action|join")}
</Button>
</Tooltip>
);
@ -156,7 +152,7 @@ export function IncomingCallToast({ notificationEvent }: Props): JSX.Element {
// This section can race, so we use a ref to keep track of whether we have started trying to play.
// This is because `LegacyCallHandler.play` tries to load the sound and then play it asynchonously
// and `LegacyCallHandler.isPlaying` will not be `true` until the sound starts playing.
const isRingToast = notificationContent.notification_type === "ring";
const isRingToast = notificationContent.notification_type == "ring";
if (isRingToast && !soundHasStarted.current && !LegacyCallHandler.instance.isPlaying(AudioID.Ring)) {
// Start ringing if not already.
soundHasStarted.current = true;
@ -247,11 +243,10 @@ export function IncomingCallToast({ notificationEvent }: Props): JSX.Element {
room_id: room?.roomId,
view_call: true,
skipLobby: ("shiftKey" in e && e.shiftKey) || skipLobbyToggle,
voiceOnly: notificationContent["m.call.intent"] === "audio",
metricsTrigger: undefined,
});
},
[room, skipLobbyToggle, notificationContent],
[room, skipLobbyToggle],
);
// Dismiss on closing toast.
@ -267,53 +262,34 @@ export function IncomingCallToast({ notificationEvent }: Props): JSX.Element {
useEventEmitter(CallStore.instance, CallStoreEvent.Call, onCall);
useEventEmitter(call ?? undefined, CallEvent.Participants, onParticipantChange);
useEventEmitter(room, RoomEvent.Timeline, onTimelineChange);
const isVoice = notificationContent["m.call.intent"] === "audio";
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(roomId);
const participantCount = useParticipantCount(call);
const detailsInformation =
notificationContent.notification_type === "ring" ? (
<span>{otherUserId}</span>
) : (
<LiveContentSummary
type={isVoice ? LiveContentType.Voice : LiveContentType.Video}
text={isVoice ? _t("common|voice") : _t("common|video")}
active={false}
participantCount={participantCount}
/>
);
const callLiveContentSummary = call ? (
<LiveContentSummaryWithCall call={call} />
) : (
<LiveContentSummary
type={LiveContentType.Video}
text={_t("common|video")}
active={false}
participantCount={0}
/>
);
return (
<TooltipProvider>
<>
<div className="mx_IncomingCallToast_content">
{isVoice ? (
<div className="mx_IncomingCallToast_message">
<VoiceCallIcon width="20px" height="20px" style={{ position: "relative", top: "4px" }} />{" "}
{_t("voip|voice_call_incoming")}
</div>
) : (
<div className="mx_IncomingCallToast_message">
<VideoCallIcon width="20px" height="20px" style={{ position: "relative", top: "4px" }} />{" "}
{notificationContent.notification_type === "ring"
? _t("voip|video_call_incoming")
: _t("voip|video_call_started")}
</div>
)}
<div className="mx_IncomingCallToast_message">
<VideoCallIcon width="20px" height="20px" style={{ position: "relative", top: "4px" }} />{" "}
{_t("voip|video_call_started")}
</div>
<AvatarWithDetails
avatar={<RoomAvatar room={room ?? undefined} size="32px" />}
details={detailsInformation}
details={callLiveContentSummary}
title={room ? room.name : _t("voip|call_toast_unknown_room")}
className="mx_IncomingCallToast_AvatarWithDetails"
/>
{!isVoice && (
<div className="mx_IncomingCallToast_toggleWithLabel">
<span>{_t("voip|skip_lobby_toggle_option")}</span>
<ToggleInput
onChange={(e) => setSkipLobbyToggle(e.target.checked)}
checked={skipLobbyToggle}
/>
</div>
)}
<div className="mx_IncomingCallToast_toggleWithLabel">
<span>{_t("voip|skip_lobby_toggle_option")}</span>
<ToggleInput onChange={(e) => setSkipLobbyToggle(e.target.checked)} checked={skipLobbyToggle} />
</div>
<div className="mx_IncomingCallToast_buttons">
<DeclineCallButtonWithNotificationEvent
notificationEvent={notificationEvent}
@ -323,7 +299,6 @@ export function IncomingCallToast({ notificationEvent }: Props): JSX.Element {
<JoinCallButtonWithCall
onClick={onJoinClick}
call={call}
isRinging={notificationContent.notification_type === "ring"}
disabledTooltip={otherCallIsOngoing ? "Ongoing call" : undefined}
/>
</div>

View File

@ -27,8 +27,7 @@ export const placeCall = async (
room: Room,
callType: CallType,
platformCallType: PlatformCallType,
skipLobby: boolean | undefined,
voiceOnly: boolean,
skipLobby?: boolean,
): Promise<void> => {
const { analyticsName } = getPlatformCallTypeProps(platformCallType);
PosthogTrackers.trackInteraction(analyticsName);
@ -40,7 +39,6 @@ export const placeCall = async (
action: Action.ViewRoom,
room_id: room.roomId,
view_call: true,
voiceOnly,
skipLobby,
metricsTrigger: undefined,
});

View File

@ -181,7 +181,6 @@ export function setUpClientRoomAndStores(): {
const roomSession = new MockEventEmitter({
memberships: [],
getOldestMembership: jest.fn().mockReturnValue(undefined),
getConsensusCallIntent: jest.fn().mockReturnValue(undefined),
room,
}) as Mocked<MatrixRTCSession>;

View File

@ -7,7 +7,6 @@
import React from "react";
import { render, screen } from "jest-matrix-react";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
import { NotificationDecoration } from "../../../../../src/components/views/rooms/NotificationDecoration";
@ -23,7 +22,7 @@ describe("<NotificationDecoration />", () => {
it("should not render if RoomNotificationState.hasAnyNotificationOrActivity=true", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
render(<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />);
render(<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />);
expect(screen.queryByTestId("notification-decoration")).toBeNull();
});
@ -31,7 +30,7 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "isUnsentMessage", "get").mockReturnValue(true);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
);
expect(asFragment()).toMatchSnapshot();
});
@ -40,7 +39,7 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "invited", "get").mockReturnValue(true);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
);
expect(asFragment()).toMatchSnapshot();
});
@ -50,7 +49,7 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "isMention", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(1);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
);
expect(asFragment()).toMatchSnapshot();
});
@ -60,7 +59,7 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "isNotification", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(1);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
);
expect(asFragment()).toMatchSnapshot();
});
@ -70,7 +69,7 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "isNotification", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "count", "get").mockReturnValue(0);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
);
expect(asFragment()).toMatchSnapshot();
});
@ -79,7 +78,7 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "isActivityNotification", "get").mockReturnValue(true);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
);
expect(asFragment()).toMatchSnapshot();
});
@ -88,21 +87,14 @@ describe("<NotificationDecoration />", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true);
jest.spyOn(roomNotificationState, "muted", "get").mockReturnValue(true);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} callType={undefined} />,
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={false} />,
);
expect(asFragment()).toMatchSnapshot();
});
it("should render the video call decoration", () => {
it("should render the video decoration", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} callType={CallType.Video} />,
);
expect(asFragment()).toMatchSnapshot();
});
it("should render the audio call decoration", () => {
jest.spyOn(roomNotificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(false);
const { asFragment } = render(
<NotificationDecoration notificationState={roomNotificationState} callType={CallType.Voice} />,
<NotificationDecoration notificationState={roomNotificationState} hasVideoCall={true} />,
);
expect(asFragment()).toMatchSnapshot();
});

View File

@ -10,7 +10,6 @@ import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { mkRoom, stubClient, withClientContextRenderOptions } from "../../../../../test-utils";
import { RoomListItemView } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListItemView";
@ -65,7 +64,6 @@ describe("<RoomListItemView />", () => {
isBold: false,
isVideoRoom: false,
callConnectionState: null,
callType: CallType.Video,
hasParticipantInCall: false,
name: room.name,
showNotificationDecoration: false,

View File

@ -103,17 +103,6 @@ exports[`<RoomListItemView /> should display notification decoration 1`] = `
data-testid="notification-decoration"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
>
<svg
fill="var(--cpd-color-icon-accent-primary)"
height="20px"
viewBox="0 0 24 24"
width="20px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
/>
</svg>
<span
class="_unread-counter_1147r_8"
>

View File

@ -16,28 +16,6 @@ exports[`<NotificationDecoration /> should render the activity decoration 1`] =
</DocumentFragment>
`;
exports[`<NotificationDecoration /> should render the audio call decoration 1`] = `
<DocumentFragment>
<div
class="_flex_4dswl_9"
data-testid="notification-decoration"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
>
<svg
fill="var(--cpd-color-icon-accent-primary)"
height="20px"
viewBox="0 0 24 24"
width="20px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m20.958 16.374.039 3.527q0 .427-.33.756-.33.33-.756.33a16 16 0 0 1-6.57-1.105 16.2 16.2 0 0 1-5.563-3.663 16.1 16.1 0 0 1-3.653-5.573 16.3 16.3 0 0 1-1.115-6.56q0-.427.33-.757T4.095 3l3.528.039a1.07 1.07 0 0 1 1.085.93l.543 3.954q.039.271-.039.504a1.1 1.1 0 0 1-.271.426l-1.64 1.64q.505 1.008 1.154 1.909c.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444q.9.65 1.909 1.153l1.64-1.64q.193-.193.426-.27t.504-.04l3.954.543q.406.059.668.359t.262.727"
/>
</svg>
</div>
</DocumentFragment>
`;
exports[`<NotificationDecoration /> should render the invitation decoration 1`] = `
<DocumentFragment>
<div
@ -164,7 +142,7 @@ exports[`<NotificationDecoration /> should render the unset message decoration 1
</DocumentFragment>
`;
exports[`<NotificationDecoration /> should render the video call decoration 1`] = `
exports[`<NotificationDecoration /> should render the video decoration 1`] = `
<DocumentFragment>
<div
class="_flex_4dswl_9"

View File

@ -32,7 +32,6 @@ import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { CallView as _CallView } from "../../../../../src/components/views/voip/CallView";
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
import { CallStore } from "../../../../../src/stores/CallStore";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
const CallView = wrapInMatrixClientContext(_CallView);
@ -51,7 +50,6 @@ describe("CallView", () => {
stubClient();
client = mocked(MatrixClientPeg.safeGet());
DMRoomMap.makeShared(client);
room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,

View File

@ -188,7 +188,6 @@ describe("IncomingCallToast", () => {
room_id: room.roomId,
skipLobby: true,
view_call: true,
voiceOnly: false,
}),
);
await waitFor(() =>
@ -216,7 +215,6 @@ describe("IncomingCallToast", () => {
room_id: room.roomId,
skipLobby: false,
view_call: true,
voiceOnly: false,
}),
);
await waitFor(() =>
@ -241,7 +239,6 @@ describe("IncomingCallToast", () => {
room_id: room.roomId,
skipLobby: true,
view_call: true,
voiceOnly: false,
}),
);
await waitFor(() =>