diff --git a/package.json b/package.json index 1f9c2d2fa5..a4ef8e9c33 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "@babel/preset-typescript": "^7.12.7", "@babel/runtime": "^7.12.5", "@casualbot/jest-sonar-reporter": "2.2.7", - "@element-hq/element-call-embedded": "0.13.1", + "@element-hq/element-call-embedded": "0.14.1", "@element-hq/element-web-playwright-common": "^1.4.4", "@peculiar/webcrypto": "^1.4.3", "@playwright/test": "^1.50.1", diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index b63e5b2a00..604dd2142f 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -36,9 +36,9 @@ import { RoomSettingsTab } from "./components/views/dialogs/RoomSettingsDialog"; import AccessibleButton from "./components/views/elements/AccessibleButton"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; -import { ElementCall } from "./models/Call"; import { getSenderName } from "./utils/event/getSenderName"; import PosthogTrackers from "./PosthogTrackers.ts"; +import { ElementCallEventType } from "./call-types.ts"; function getRoomMemberDisplayname(client: MatrixClient, event: MatrixEvent, userId = event.getSender()): string { const roomId = event.getRoomId(); @@ -922,7 +922,7 @@ for (const evType of ALL_RULE_TYPES) { } // Add both stable and unstable m.call events -for (const evType of ElementCall.CALL_EVENT_TYPE.names) { +for (const evType of ElementCallEventType.names) { stateHandlers[evType] = textForCallEvent; } diff --git a/src/call-types.ts b/src/call-types.ts index 6586bcf3b9..9ee870945f 100644 --- a/src/call-types.ts +++ b/src/call-types.ts @@ -6,6 +6,9 @@ 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 { EventType } from "matrix-js-sdk/src/matrix"; +import { NamespacedValue } from "matrix-js-sdk/src/NamespacedValue"; + export const JitsiCallMemberEventType = "io.element.video.member"; export interface JitsiCallMemberContent { @@ -14,3 +17,9 @@ export interface JitsiCallMemberContent { // Time at which this state event should be considered stale expires_ts: number; } + +// Element Call no longer sends this event type; it only exists to support timeline rendering of +// group calls from a previous iteration of the group VoIP MSCs (MSC3401) which used it. +export const ElementCallEventType = new NamespacedValue(null, EventType.GroupCallPrefix); + +export const ElementCallMemberEventType = new NamespacedValue(null, EventType.GroupCallMemberPrefix); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index accf22873d..44420d9efc 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -76,13 +76,13 @@ import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary"; import { ReadReceiptGroup } from "./ReadReceiptGroup"; import { type ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom"; -import { ElementCall } from "../../../models/Call"; import { UnreadNotificationBadge } from "./NotificationBadge/UnreadNotificationBadge"; import { EventTileThreadToolbar } from "./EventTile/EventTileThreadToolbar"; import { getLateEventInfo } from "../../structures/grouper/LateEventGrouper"; import PinningUtils from "../../../utils/PinningUtils"; import { PinnedMessageBadge } from "../messages/PinnedMessageBadge"; import { EventPreview } from "./EventPreview"; +import { ElementCallEventType } from "../../../call-types"; export type GetRelationsForEvent = ( eventId: string, @@ -984,7 +984,7 @@ export class UnwrappedEventTile extends React.Component mx_EventTile_highlight: this.shouldHighlight(), mx_EventTile_selected: this.props.isSelectedEvent || this.state.contextMenu, mx_EventTile_continuation: - isContinuation || eventType === EventType.CallInvite || ElementCall.CALL_EVENT_TYPE.matches(eventType), + isContinuation || eventType === EventType.CallInvite || ElementCallEventType.matches(eventType), mx_EventTile_last: this.props.last, mx_EventTile_lastInSection: this.props.lastInSection, mx_EventTile_contextual: this.props.contextual, @@ -1037,7 +1037,7 @@ export class UnwrappedEventTile extends React.Component } else if ( (this.props.continuation && this.context.timelineRenderingType !== TimelineRenderingType.File) || eventType === EventType.CallInvite || - ElementCall.CALL_EVENT_TYPE.matches(eventType) + ElementCallEventType.matches(eventType) ) { // no avatar or sender profile for continuation messages and call tiles avatarSize = null; diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index c44812d618..091c01e2b9 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -26,13 +26,13 @@ import ErrorDialog from "../../../dialogs/ErrorDialog"; import PowerSelector from "../../../elements/PowerSelector"; import SettingsFieldset from "../../SettingsFieldset"; import SettingsStore from "../../../../../settings/SettingsStore"; -import { ElementCall } from "../../../../../models/Call"; import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig"; import { AddPrivilegedUsers } from "../../AddPrivilegedUsers"; import SettingsTab from "../SettingsTab"; import { SettingsSection } from "../../shared/SettingsSection"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; import { PowerLevelSelector } from "../../PowerLevelSelector"; +import { ElementCallEventType, ElementCallMemberEventType } from "../../../../../call-types"; interface IEventShowOpts { isState?: boolean; @@ -63,8 +63,8 @@ const plEventsToShow: Record = { [EventType.RoomRedaction]: { isState: false, hideForSpace: true }, // MSC3401: Native Group VoIP signaling - [ElementCall.CALL_EVENT_TYPE.name]: { isState: true, hideForSpace: true }, - [ElementCall.MEMBER_EVENT_TYPE.name]: { isState: true, hideForSpace: true }, + [ElementCallEventType.name]: { isState: true, hideForSpace: true }, + [ElementCallMemberEventType.name]: { isState: true, hideForSpace: true }, // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": { isState: true, hideForSpace: true }, @@ -298,8 +298,8 @@ export default class RolesRoomSettingsTab extends React.Component = { diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 3583179b92..177d495366 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -14,10 +14,10 @@ import { _t } from "../../../../../languageHandler"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; import { SettingsSubsection } from "../../shared/SettingsSubsection"; import SettingsTab from "../SettingsTab"; -import { ElementCall } from "../../../../../models/Call"; import { useRoomState } from "../../../../../hooks/useRoomState"; import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig"; import { SettingsSection } from "../../shared/SettingsSection"; +import { ElementCallEventType, ElementCallMemberEventType } from "../../../../../call-types"; interface ElementCallSwitchProps { room: Room; @@ -42,7 +42,7 @@ const ElementCallSwitch: React.FC = ({ room }) => { ); const [elementCallEnabled, setElementCallEnabled] = useState(() => { - return content.events?.[ElementCall.MEMBER_EVENT_TYPE.name] === 0; + return content.events?.[ElementCallMemberEventType.name] === 0; }); const onChange = useCallback( @@ -56,13 +56,13 @@ const ElementCallSwitch: React.FC = ({ room }) => { const userLevel = newContent.events[EventType.RoomMessage] ?? content.users_default ?? 0; const moderatorLevel = content.kick ?? 50; - newContent.events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel; - newContent.events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel; + newContent.events[ElementCallEventType.name] = isPublic ? moderatorLevel : userLevel; + newContent.events[ElementCallMemberEventType.name] = userLevel; } else { const adminLevel = newContent.events[EventType.RoomPowerLevels] ?? content.state_default ?? 100; - newContent.events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel; - newContent.events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel; + newContent.events[ElementCallEventType.name] = adminLevel; + newContent.events[ElementCallMemberEventType.name] = adminLevel; } room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); diff --git a/src/createRoom.ts b/src/createRoom.ts index df0f90b91f..13dcb4e7af 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -42,6 +42,7 @@ import { waitForMember } from "./utils/membership"; import { PreferredRoomVersions } from "./utils/PreferredRoomVersions"; import SettingsStore from "./settings/SettingsStore"; import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto"; +import { ElementCallEventType, ElementCallMemberEventType } from "./call-types"; // we define a number of interfaces which take their names from the js-sdk /* eslint-disable camelcase */ @@ -165,9 +166,9 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro events: { ...DEFAULT_EVENT_POWER_LEVELS, // Allow all users to send call membership updates - [ElementCall.MEMBER_EVENT_TYPE.name]: 0, + [ElementCallMemberEventType.name]: 0, // Make calls immutable, even to admins - [ElementCall.CALL_EVENT_TYPE.name]: 200, + [ElementCallEventType.name]: 200, }, users: { // Temporarily give ourselves the power to set up a call @@ -180,9 +181,9 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro events: { ...DEFAULT_EVENT_POWER_LEVELS, // It should always (including non video rooms) be possible to join a group call. - [ElementCall.MEMBER_EVENT_TYPE.name]: 0, + [ElementCallMemberEventType.name]: 0, // Make sure only admins can enable it (DEPRECATED) - [ElementCall.CALL_EVENT_TYPE.name]: 100, + [ElementCallEventType.name]: 100, }, }; } diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx index 9187e05546..57cafcbab1 100644 --- a/src/events/EventTileFactory.tsx +++ b/src/events/EventTileFactory.tsx @@ -40,11 +40,11 @@ import { getMessageModerationState, MessageModerationState } from "../utils/Even import HiddenBody from "../components/views/messages/HiddenBody"; import ViewSourceEvent from "../components/views/messages/ViewSourceEvent"; import { shouldDisplayAsBeaconTile } from "../utils/beacon/timeline"; -import { ElementCall } from "../models/Call"; import { type IBodyProps } from "../components/views/messages/IBodyProps"; import ModuleApi from "../modules/Api"; import { TextualEventViewModel } from "../viewmodels/event-tiles/TextualEventViewModel"; import { TextualEventView } from "../shared-components/event-tiles/TextualEventView"; +import { ElementCallEventType } from "../call-types"; // Subset of EventTile's IProps plus some mixins export interface EventTileTypeProps @@ -122,7 +122,7 @@ const STATE_EVENT_TILE_TYPES = new Map([ [EventType.RoomGuestAccess, TextualEventFactory], ]); -for (const evType of ElementCall.CALL_EVENT_TYPE.names) { +for (const evType of ElementCallEventType.names) { STATE_EVENT_TILE_TYPES.set(evType, CallEventFactory); } @@ -444,9 +444,7 @@ export function haveRendererForEvent( const dynamicPredecessorsEnabled = SettingsStore.getValue("feature_dynamic_room_predecessors"); const predecessor = matrixClient.getRoom(mxEvent.getRoomId())?.findPredecessor(dynamicPredecessorsEnabled); return Boolean(predecessor); - } else if ( - ElementCall.CALL_EVENT_TYPE.names.some((eventType) => handler === STATE_EVENT_TILE_TYPES.get(eventType)) - ) { + } else if (ElementCallEventType.names.some((eventType) => handler === STATE_EVENT_TILE_TYPES.get(eventType))) { const intent = mxEvent.getContent()["m.intent"]; const newlyStarted = Object.keys(mxEvent.getPrevContent()).length === 0; // Only interested in events that mark the start of a non-room call diff --git a/src/hooks/room/useRoomCall.tsx b/src/hooks/room/useRoomCall.tsx index 8d7667aa7d..d3e76809cd 100644 --- a/src/hooks/room/useRoomCall.tsx +++ b/src/hooks/room/useRoomCall.tsx @@ -18,7 +18,7 @@ import { useWidgets } from "../../utils/WidgetUtils"; import { WidgetType } from "../../widgets/WidgetType"; import { useCall, useConnectionState, useParticipantCount } from "../useCall"; import { useRoomMemberCount } from "../useRoomMembers"; -import { ConnectionState, ElementCall } from "../../models/Call"; +import { ConnectionState } from "../../models/Call"; import { placeCall } from "../../utils/room/placeCall"; import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore"; import { useRoomState } from "../useRoomState"; @@ -36,6 +36,7 @@ import { useGuestAccessInformation } from "./useGuestAccessInformation"; import { UIFeature } from "../../settings/UIFeature"; import { BetaPill } from "../../components/views/beta/BetaCard"; import { type InteractionName } from "../../PosthogTrackers"; +import { ElementCallMemberEventType } from "../../call-types"; export enum PlatformCallType { ElementCall, @@ -135,7 +136,7 @@ export const useRoomCall = ( const [mayEditWidgets, mayCreateElementCalls] = useRoomState(room, () => [ room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client), - room.currentState.mayClientSendStateEvent(ElementCall.MEMBER_EVENT_TYPE.name, room.client), + room.currentState.mayClientSendStateEvent(ElementCallMemberEventType.name, room.client), ]); // The options provided to the RoomHeader. diff --git a/src/models/Call.ts b/src/models/Call.ts index 2e1f0f1f10..df22abc9bd 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -10,7 +10,6 @@ import { TypedEventEmitter, RoomEvent, RoomStateEvent, - EventType, type MatrixClient, type IMyDevice, type Room, @@ -20,14 +19,12 @@ import { KnownMembership, type Membership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { secureRandomString } from "matrix-js-sdk/src/randomstring"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; -import { NamespacedValue } from "matrix-js-sdk/src/NamespacedValue"; import { type IWidgetApiRequest, type ClientWidgetApi, type IWidgetData } from "matrix-widget-api"; import { - MatrixRTCSession, + type MatrixRTCSession, MatrixRTCSessionEvent, type CallMembership, MatrixRTCSessionManagerEvents, - type ICallNotifyContent, } from "matrix-js-sdk/src/matrixrtc"; import type EventEmitter from "events"; @@ -44,11 +41,12 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../stores/ActiveWidge import { getCurrentLanguage } from "../languageHandler"; import { Anonymity, PosthogAnalytics } from "../PosthogAnalytics"; import { UPDATE_EVENT } from "../stores/AsyncStore"; -import { getJoinedNonFunctionalMembers } from "../utils/room/getJoinedNonFunctionalMembers"; import { isVideoRoom } from "../utils/video-rooms"; import { FontWatcher } from "../settings/watchers/FontWatcher"; import { type JitsiCallMemberContent, JitsiCallMemberEventType } from "../call-types"; import SdkConfig from "../SdkConfig.ts"; +import RoomListStore from "../stores/room-list/RoomListStore.ts"; +import { DefaultTagID } from "../stores/room-list/models.ts"; const TIMEOUT_MS = 16000; @@ -643,10 +641,6 @@ export class JitsiCall extends Call { * (somewhat cheekily named) */ export class ElementCall extends Call { - // TODO this is only there to support backwards compatibility in timeline rendering - // this should not be part of this class since it has nothing to do with it. - public static readonly CALL_EVENT_TYPE = new NamespacedValue(null, EventType.GroupCallPrefix); - public static readonly MEMBER_EVENT_TYPE = new NamespacedValue(null, EventType.GroupCallMemberPrefix); public readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour private settingsStoreCallEncryptionWatcher?: string; @@ -684,6 +678,14 @@ export class ElementCall extends Call { theme: "$org.matrix.msc2873.client_theme", }); + const room = client.getRoom(roomId); + if (room !== null && !isVideoRoom(room)) { + params.append( + "sendNotificationType", + RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.DM) ? "ring" : "notification", + ); + } + const rageshakeSubmitUrl = SdkConfig.get("bug_report_endpoint_url"); if (rageshakeSubmitUrl) { params.append("rageshakeSubmitUrl", rageshakeSubmitUrl); @@ -858,31 +860,6 @@ export class ElementCall extends Call { ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, isVideoRoom(room)); } - protected async sendCallNotify(): Promise { - const room = this.room; - const existingOtherRoomCallMembers = MatrixRTCSession.callMembershipsForRoom(room).filter( - // filter all memberships where the application is m.call and the call_id is "" - (m) => { - const isRoomCallMember = m.application === "m.call" && m.callId === ""; - const isThisDevice = m.deviceId === this.client.deviceId; - return isRoomCallMember && !isThisDevice; - }, - ); - - const memberCount = getJoinedNonFunctionalMembers(room).length; - if (!isVideoRoom(room) && existingOtherRoomCallMembers.length === 0) { - // send ringing event - const content: ICallNotifyContent = { - "application": "m.call", - "m.mentions": { user_ids: [], room: true }, - "notify_type": memberCount == 2 ? "ring" : "notify", - "call_id": "", - }; - - await room.client.sendEvent(room.roomId, EventType.CallNotify, content); - } - } - protected async performConnection( audioInput: MediaDeviceInfo | null, videoInput: MediaDeviceInfo | null, @@ -891,9 +868,8 @@ export class ElementCall extends Call { this.messaging!.once(`action:${ElementWidgetActions.Close}`, this.onClose); this.messaging!.on(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute); - // TODO: if the widget informs us when the join button is clicked (widget action), so we can - // - set state to connecting - // - send call notify + // TODO: Watch for a widget action telling us that the join button was clicked, rather than + // relying on the MatrixRTC session state, to set the state to connecting const session = this.client.matrixRTC.getActiveRoomSession(this.room); if (session) { await waitForEvent( @@ -912,7 +888,6 @@ export class ElementCall extends Call { false, // allow user to wait as long as they want (no timeout) ); } - this.sendCallNotify(); } protected async performDisconnection(): Promise { diff --git a/src/utils/EventRenderingUtils.ts b/src/utils/EventRenderingUtils.ts index 5ac6629b8b..b99e01a07b 100644 --- a/src/utils/EventRenderingUtils.ts +++ b/src/utils/EventRenderingUtils.ts @@ -20,7 +20,7 @@ import { import SettingsStore from "../settings/SettingsStore"; import { haveRendererForEvent, JitsiEventFactory, JSONEventFactory, pickFactory } from "../events/EventTileFactory"; import { getMessageModerationState, isLocationEvent, MessageModerationState } from "./EventUtils"; -import { ElementCall } from "../models/Call"; +import { ElementCallEventType } from "../call-types"; const calcIsInfoMessage = ( eventType: EventType | string, @@ -82,7 +82,7 @@ export function getEventDisplayInfo( eventType === EventType.RoomEncryption || factory === JitsiEventFactory; const isLeftAlignedBubbleMessage = - !isBubbleMessage && (eventType === EventType.CallInvite || ElementCall.CALL_EVENT_TYPE.matches(eventType)); + !isBubbleMessage && (eventType === EventType.CallInvite || ElementCallEventType.matches(eventType)); let isInfoMessage = calcIsInfoMessage(eventType, content, isBubbleMessage, isLeftAlignedBubbleMessage); // Some non-info messages want to be rendered in the appropriate bubble column but without the bubble background const noBubbleEvent = diff --git a/test/unit-tests/TextForEvent-test.ts b/test/unit-tests/TextForEvent-test.ts index a505d6510f..4799b0fd5d 100644 --- a/test/unit-tests/TextForEvent-test.ts +++ b/test/unit-tests/TextForEvent-test.ts @@ -26,8 +26,8 @@ import SettingsStore from "../../src/settings/SettingsStore"; import { createTestClient, stubClient } from "../test-utils"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import UserIdentifierCustomisations from "../../src/customisations/UserIdentifier"; -import { ElementCall } from "../../src/models/Call"; import { getSenderName } from "../../src/utils/event/getSenderName"; +import { ElementCallEventType } from "../../src/call-types"; jest.mock("../../src/settings/SettingsStore"); jest.mock("../../src/customisations/UserIdentifier", () => ({ @@ -471,7 +471,7 @@ describe("TextForEvent", () => { } as unknown as MatrixEvent; }); - describe.each(ElementCall.CALL_EVENT_TYPE.names)("eventType=%s", (eventType: string) => { + describe.each(ElementCallEventType.names)("eventType=%s", (eventType: string) => { beforeEach(() => { mocked(callEvent).getType.mockReturnValue(eventType); }); diff --git a/test/unit-tests/components/views/rooms/RoomHeader/RoomHeader-test.tsx b/test/unit-tests/components/views/rooms/RoomHeader/RoomHeader-test.tsx index b93a26c412..4c3ae9262f 100644 --- a/test/unit-tests/components/views/rooms/RoomHeader/RoomHeader-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomHeader/RoomHeader-test.tsx @@ -48,7 +48,7 @@ import SettingsStore from "../../../../../../src/settings/SettingsStore"; import SdkConfig from "../../../../../../src/SdkConfig"; import dispatcher from "../../../../../../src/dispatcher/dispatcher"; import { CallStore } from "../../../../../../src/stores/CallStore"; -import { type Call, ElementCall } from "../../../../../../src/models/Call"; +import { type Call } from "../../../../../../src/models/Call"; import * as ShieldUtils from "../../../../../../src/utils/ShieldUtils"; import { Container, WidgetLayoutStore } from "../../../../../../src/stores/widgets/WidgetLayoutStore"; import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; @@ -58,6 +58,7 @@ import { SdkContextClass } from "../../../../../../src/contexts/SDKContext"; import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore"; import { UIFeature } from "../../../../../../src/settings/UIFeature"; import { SettingLevel } from "../../../../../../src/settings/SettingLevel"; +import { ElementCallMemberEventType } from "../../../../../../src/call-types"; jest.mock("../../../../../../src/utils/ShieldUtils"); jest.mock("../../../../../../src/hooks/right-panel/useCurrentPhase", () => ({ @@ -599,7 +600,7 @@ describe("RoomHeader", () => { mockRoomMembers(room, 3); jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => { - if (key === ElementCall.MEMBER_EVENT_TYPE.name) return true; + if (key === ElementCallMemberEventType.name) return true; return false; }); diff --git a/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx index afd84ab33a..7681ec0c7b 100644 --- a/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx @@ -24,7 +24,7 @@ import RolesRoomSettingsTab from "../../../../../../../src/components/views/sett import { mkStubRoom, withClientContextRenderOptions, stubClient } from "../../../../../../test-utils"; import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg"; import SettingsStore from "../../../../../../../src/settings/SettingsStore"; -import { ElementCall } from "../../../../../../../src/models/Call"; +import { ElementCallEventType, ElementCallMemberEventType } from "../../../../../../../src/call-types"; describe("RolesRoomSettingsTab", () => { const userId = "@alice:server.org"; @@ -116,7 +116,7 @@ describe("RolesRoomSettingsTab", () => { expect(getJoinCallSelectedOption(tab)?.textContent).toBe("Default"); expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPowerLevels, { events: { - [ElementCall.MEMBER_EVENT_TYPE.name]: 0, + [ElementCallMemberEventType.name]: 0, }, }); }); @@ -137,7 +137,7 @@ describe("RolesRoomSettingsTab", () => { expect(getStartCallSelectedOption(tab)?.textContent).toBe("Default"); expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPowerLevels, { events: { - [ElementCall.CALL_EVENT_TYPE.name]: 0, + [ElementCallEventType.name]: 0, }, }); }); diff --git a/test/unit-tests/components/views/settings/tabs/room/VoipRoomSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/room/VoipRoomSettingsTab-test.tsx index 4184e76360..1008aadbc9 100644 --- a/test/unit-tests/components/views/settings/tabs/room/VoipRoomSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/room/VoipRoomSettingsTab-test.tsx @@ -13,7 +13,7 @@ import { type MatrixClient, type Room, type MatrixEvent, EventType, JoinRule } f import { mkStubRoom, stubClient } from "../../../../../../test-utils"; import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg"; import { VoipRoomSettingsTab } from "../../../../../../../src/components/views/settings/tabs/room/VoipRoomSettingsTab"; -import { ElementCall } from "../../../../../../../src/models/Call"; +import { ElementCallEventType, ElementCallMemberEventType } from "../../../../../../../src/call-types"; describe("VoipRoomSettingsTab", () => { const roomId = "!room:example.com"; @@ -48,7 +48,7 @@ describe("VoipRoomSettingsTab", () => { describe("correct state", () => { it("shows enabled when call member power level is 0", () => { - mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 0 }); + mockPowerLevels({ [ElementCallMemberEventType.name]: 0 }); const tab = renderTab(); @@ -56,7 +56,7 @@ describe("VoipRoomSettingsTab", () => { }); it.each([1, 50, 100])("shows disabled when call member power level is 0", (level: number) => { - mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: level }); + mockPowerLevels({ [ElementCallMemberEventType.name]: level }); const tab = renderTab(); @@ -67,7 +67,7 @@ describe("VoipRoomSettingsTab", () => { describe("enabling/disabling", () => { describe("enabling Element calls", () => { beforeEach(() => { - mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 100 }); + mockPowerLevels({ [ElementCallMemberEventType.name]: 100 }); }); it("enables Element calls in public room", async () => { @@ -82,8 +82,8 @@ describe("VoipRoomSettingsTab", () => { EventType.RoomPowerLevels, expect.objectContaining({ events: { - [ElementCall.CALL_EVENT_TYPE.name]: 50, - [ElementCall.MEMBER_EVENT_TYPE.name]: 0, + [ElementCallEventType.name]: 50, + [ElementCallMemberEventType.name]: 0, }, }), ), @@ -102,8 +102,8 @@ describe("VoipRoomSettingsTab", () => { EventType.RoomPowerLevels, expect.objectContaining({ events: { - [ElementCall.CALL_EVENT_TYPE.name]: 0, - [ElementCall.MEMBER_EVENT_TYPE.name]: 0, + [ElementCallEventType.name]: 0, + [ElementCallMemberEventType.name]: 0, }, }), ), @@ -112,7 +112,7 @@ describe("VoipRoomSettingsTab", () => { }); it("disables Element calls", async () => { - mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 0 }); + mockPowerLevels({ [ElementCallMemberEventType.name]: 0 }); const tab = renderTab(); @@ -123,8 +123,8 @@ describe("VoipRoomSettingsTab", () => { EventType.RoomPowerLevels, expect.objectContaining({ events: { - [ElementCall.CALL_EVENT_TYPE.name]: 100, - [ElementCall.MEMBER_EVENT_TYPE.name]: 100, + [ElementCallEventType.name]: 100, + [ElementCallMemberEventType.name]: 100, }, }), ), diff --git a/test/unit-tests/createRoom-test.ts b/test/unit-tests/createRoom-test.ts index c75bebc0fb..d06e19cc83 100644 --- a/test/unit-tests/createRoom-test.ts +++ b/test/unit-tests/createRoom-test.ts @@ -18,6 +18,8 @@ import WidgetUtils from "../../src/utils/WidgetUtils"; import { JitsiCall, ElementCall } from "../../src/models/Call"; import createRoom, { checkUserIsAllowedToChangeEncryption, canEncryptToAllUsers } from "../../src/createRoom"; import SettingsStore from "../../src/settings/SettingsStore"; +import { ElementCallEventType, ElementCallMemberEventType } from "../../src/call-types"; +import DMRoomMap from "../../src/utils/DMRoomMap"; describe("createRoom", () => { mockPlatformPeg(); @@ -26,6 +28,7 @@ describe("createRoom", () => { beforeEach(() => { stubClient(); client = mocked(MatrixClientPeg.safeGet()); + DMRoomMap.makeShared(client); }); afterEach(() => jest.clearAllMocks()); @@ -75,11 +78,9 @@ describe("createRoom", () => { const userPower = client.createRoom.mock.calls[0][0].power_level_content_override?.users?.[userId]; const callPower = - client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCall.CALL_EVENT_TYPE.name]; + client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallEventType.name]; const callMemberPower = - client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ - ElementCall.MEMBER_EVENT_TYPE.name - ]; + client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallMemberEventType.name]; // We should have had enough power to be able to set up the call expect(userPower).toBeGreaterThanOrEqual(callPower!); @@ -112,11 +113,9 @@ describe("createRoom", () => { await createRoom(client, {}); const callPower = - client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCall.CALL_EVENT_TYPE.name]; + client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallEventType.name]; const callMemberPower = - client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ - ElementCall.MEMBER_EVENT_TYPE.name - ]; + client.createRoom.mock.calls[0][0].power_level_content_override?.events?.[ElementCallMemberEventType.name]; expect(callPower).toBe(100); expect(callMemberPower).toBe(0); diff --git a/test/unit-tests/models/Call-test.ts b/test/unit-tests/models/Call-test.ts index de5c42fc9c..8fea3ee83b 100644 --- a/test/unit-tests/models/Call-test.ts +++ b/test/unit-tests/models/Call-test.ts @@ -51,6 +51,9 @@ import SettingsStore from "../../../src/settings/SettingsStore"; import { Anonymity, PosthogAnalytics } from "../../../src/PosthogAnalytics"; import { type SettingKey } from "../../../src/settings/Settings.tsx"; import SdkConfig from "../../../src/SdkConfig.ts"; +import RoomListStore from "../../../src/stores/room-list/RoomListStore.ts"; +import { DefaultTagID } from "../../../src/stores/room-list/models.ts"; +import DMRoomMap from "../../../src/utils/DMRoomMap.ts"; jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({ [MediaDeviceKindEnum.AudioInput]: [ @@ -78,6 +81,7 @@ const setUpClientRoomAndStores = (): { } => { stubClient(); const client = mocked(MatrixClientPeg.safeGet()); + DMRoomMap.makeShared(client); const room = new Room("!1:example.org", client, "@alice:example.org", { pendingEventOrdering: PendingEventOrdering.Detached, @@ -674,6 +678,8 @@ describe("ElementCall", () => { }); describe("get", () => { + afterEach(() => Call.get(room)?.destroy()); + it("finds no calls", () => { expect(Call.get(room)).toBeNull(); }); @@ -681,7 +687,6 @@ describe("ElementCall", () => { it("finds calls", async () => { ElementCall.create(room); expect(Call.get(room)).toBeInstanceOf(ElementCall); - Call.get(room)?.destroy(); }); it("should use element call URL from developer settings if present", async () => { @@ -698,7 +703,6 @@ describe("ElementCall", () => { const call = ElementCall.get(room); expect(call?.widget.url.startsWith("https://call.element.dev/")).toBeTruthy(); SettingsStore.getValue = originalGetValue; - call?.destroy(); }); it("finds ongoing calls that are created by the session manager", async () => { @@ -710,7 +714,6 @@ describe("ElementCall", () => { } as unknown as MatrixRTCSession); const call = Call.get(room); if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); - call.destroy(); }); it("passes font settings through widget URL", async () => { @@ -772,7 +775,6 @@ describe("ElementCall", () => { const urlParams2 = new URLSearchParams(new URL(call2.widget.url).hash.slice(1)); expect(urlParams2.has("allowIceFallback")).toBe(true); - call2.destroy(); SettingsStore.getValue = originalGetValue; }); @@ -799,7 +801,6 @@ describe("ElementCall", () => { expect(urlParams.get("posthogUserId")).toBe("123456789987654321"); expect(urlParams.get("posthogApiHost")).toBe("https://posthog"); expect(urlParams.get("posthogApiKey")).toBe("DEADBEEF"); - call.destroy(); }); it("does not pass analyticsID if `pseudonymousAnalyticsOptIn` set to false", async () => { @@ -817,7 +818,6 @@ describe("ElementCall", () => { const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); expect(urlParams.get("analyticsID")).toBeFalsy(); - call.destroy(); }); it("passes feature_allow_screen_share_only_mode setting to allowVoipWithNoMedia url param", async () => { @@ -840,7 +840,6 @@ describe("ElementCall", () => { const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); expect(urlParams.get("allowVoipWithNoMedia")).toBe("true"); SettingsStore.getValue = originalGetValue; - call.destroy(); }); it("passes empty analyticsID if the id is not in the account data", async () => { @@ -857,6 +856,30 @@ describe("ElementCall", () => { const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); expect(urlParams.get("analyticsID")).toBeFalsy(); }); + + it("requests ringing notifications in DMs", async () => { + const tagsSpy = jest.spyOn(RoomListStore.instance, "getTagsForRoom"); + try { + tagsSpy.mockReturnValue([DefaultTagID.DM]); + ElementCall.create(room); + const call = Call.get(room); + if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); + + const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); + expect(urlParams.get("sendNotificationType")).toBe("ring"); + } finally { + tagsSpy.mockRestore(); + } + }); + + it("requests visual notifications in non-DMs", async () => { + ElementCall.create(room); + const call = Call.get(room); + if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); + + const urlParams = new URLSearchParams(new URL(call.widget.url).hash.slice(1)); + expect(urlParams.get("sendNotificationType")).toBe("notification"); + }); }); describe("instance in a non-video room", () => { @@ -1019,31 +1042,6 @@ describe("ElementCall", () => { roomSpy.mockRestore(); addWidgetSpy.mockRestore(); }); - - it("sends notify event on connect in a room with more than two members", async () => { - const sendEventSpy = jest.spyOn(room.client, "sendEvent"); - ElementCall.create(room); - await callConnectProcedure(Call.get(room) as ElementCall); - expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", { - "application": "m.call", - "call_id": "", - "m.mentions": { room: true, user_ids: [] }, - "notify_type": "notify", - }); - }); - it("sends ring on create in a DM (two participants) room", async () => { - setRoomMembers(["@user:example.com", "@user2:example.com"]); - - const sendEventSpy = jest.spyOn(room.client, "sendEvent"); - ElementCall.create(room); - await callConnectProcedure(Call.get(room) as ElementCall); - expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", { - "application": "m.call", - "call_id": "", - "m.mentions": { room: true, user_ids: [] }, - "notify_type": "ring", - }); - }); }); describe("instance in a video room", () => { diff --git a/yarn.lock b/yarn.lock index b9809cc12c..e833706f6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1677,10 +1677,10 @@ resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b" integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg== -"@element-hq/element-call-embedded@0.13.1": - version "0.13.1" - resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.13.1.tgz#9161f657f7bebdb5339847b7a8f0a3149a36f95c" - integrity sha512-6RGZPdx+gOCzpJNe+dbftEyiWuNx+2H+uXiZp7QN8SOZ3dl/yjg0JcK60wsC48i7gXy/6ERdbwTgaL9ez8mvhA== +"@element-hq/element-call-embedded@0.14.1": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@element-hq/element-call-embedded/-/element-call-embedded-0.14.1.tgz#358c537e147ff3d48028cfb65d414cfe89ac1371" + integrity sha512-1ODnohNvg7bgR8tg+rIF81MYGChNXVD96lBWkCI96ygjGg7U+HqqA8sY0YsRN5oJ9aLDQPicSr09XwLEXSPmjQ== "@element-hq/element-web-module-api@1.3.0": version "1.3.0"