From d99745938447060a0a461cb45f29e8ae2725b052 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 24 Oct 2025 09:22:07 +0100 Subject: [PATCH 01/11] Refactor calls to use slots --- src/@types/matrix-js-sdk.d.ts | 1 + .../tabs/room/VoipRoomSettingsTab.tsx | 75 ++++--------- src/hooks/room/useElementCallPermissions.tsx | 105 ++++++++++++++++++ src/hooks/room/useRoomCall.tsx | 80 ++++++++++++- src/i18n/strings/en_EN.json | 2 + src/models/Call.ts | 5 + src/settings/Settings.tsx | 20 +++- 7 files changed, 229 insertions(+), 59 deletions(-) create mode 100644 src/hooks/room/useElementCallPermissions.tsx diff --git a/src/@types/matrix-js-sdk.d.ts b/src/@types/matrix-js-sdk.d.ts index ad75ca95f05..ff2e3952c43 100644 --- a/src/@types/matrix-js-sdk.d.ts +++ b/src/@types/matrix-js-sdk.d.ts @@ -36,6 +36,7 @@ declare module "matrix-js-sdk/src/types" { export interface StateEvents { // Jitsi-backed video room state events [JitsiCallMemberEventType]: JitsiCallMemberContent; + "org.matrix.msc4143.rtc.slot": any; // Unstable widgets state events "im.vector.modular.widgets": IWidget | EmptyObject; diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 177d4953664..440df901abf 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -6,69 +6,40 @@ 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, { useCallback, useMemo, useState } from "react"; -import { JoinRule, EventType, type RoomState, type Room } from "matrix-js-sdk/src/matrix"; -import { type RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types"; +import React, { useCallback, useState } from "react"; +import { type Room } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../../../languageHandler"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; import { SettingsSubsection } from "../../shared/SettingsSubsection"; import SettingsTab from "../SettingsTab"; -import { useRoomState } from "../../../../../hooks/useRoomState"; import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig"; import { SettingsSection } from "../../shared/SettingsSection"; -import { ElementCallEventType, ElementCallMemberEventType } from "../../../../../call-types"; +import { useElementCallPermissions } from "../../../../../hooks/room/useElementCallPermissions"; interface ElementCallSwitchProps { room: Room; } const ElementCallSwitch: React.FC = ({ room }) => { - const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]); - const [content, maySend] = useRoomState( - room, - useCallback( - (state: RoomState) => { - const content = state - ?.getStateEvents(EventType.RoomPowerLevels, "") - ?.getContent(); - return [ - content ?? {}, - state?.maySendStateEvent(EventType.RoomPowerLevels, room.client.getSafeUserId()), - ] as const; - }, - [room.client], - ), - ); - - const [elementCallEnabled, setElementCallEnabled] = useState(() => { - return content.events?.[ElementCallMemberEventType.name] === 0; - }); - - const onChange = useCallback( - (enabled: boolean): void => { - setElementCallEnabled(enabled); - - // Take a copy to avoid mutating the original - const newContent = { events: {}, ...content }; - - if (enabled) { - const userLevel = newContent.events[EventType.RoomMessage] ?? content.users_default ?? 0; - const moderatorLevel = content.kick ?? 50; - - 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[ElementCallEventType.name] = adminLevel; - newContent.events[ElementCallMemberEventType.name] = adminLevel; + // For MSC4356 only. + const {canStartCall, canAdjustCallPermissions, enableCallInRoom, disableCallInRoom} = useElementCallPermissions(room); + const [busy, setBusy] = useState(); + const onToggle = useCallback(() => { + setBusy(true) + void (async () => { + try { + if (canStartCall) { + await disableCallInRoom(); + } else { + await enableCallInRoom(); + } + } finally { + setBusy(false); } + })(); - room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); - }, - [room.client, room.roomId, content, isPublic], - ); + }, [canStartCall, enableCallInRoom, disableCallInRoom]); const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand; @@ -79,10 +50,10 @@ const ElementCallSwitch: React.FC = ({ room }) => { caption={_t("room_settings|voip|enable_element_call_caption", { brand, })} - value={elementCallEnabled} - onChange={onChange} - disabled={!maySend} - tooltip={_t("room_settings|voip|enable_element_call_no_permissions_tooltip")} + value={canStartCall} + onChange={onToggle} + disabled={busy || !canAdjustCallPermissions} + tooltip={canAdjustCallPermissions ? undefined : _t("room_settings|voip|enable_element_call_no_permissions_tooltip")} /> ); }; diff --git a/src/hooks/room/useElementCallPermissions.tsx b/src/hooks/room/useElementCallPermissions.tsx new file mode 100644 index 00000000000..fb85226f3dd --- /dev/null +++ b/src/hooks/room/useElementCallPermissions.tsx @@ -0,0 +1,105 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2023 The Matrix.org Foundation C.I.C. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import { JoinRule, type Room } from "matrix-js-sdk/src/matrix"; +import { useCallback, useMemo } from "react"; + +import type React from "react"; +import { useFeatureEnabled } from "../useSettings"; +import { useRoomState } from "../useRoomState"; +import { _t } from "../../languageHandler"; +import { ElementCallMemberEventType } from "../../call-types"; +import { LocalRoom } from "../../models/LocalRoom"; +import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; +import Modal from "../../Modal"; + + +const useLegacyCallPermissions = (room: Room | LocalRoom): ReturnType { + +} + +/** + * Hook for adjusting permissions for enabling Element Call. + * @param room the room to track + * @returns the call button attributes for the given room + */ +const useSlotsCallPermissions = ( + room: Room | LocalRoom, +): { + canStartCall: boolean; + canAdjustCallPermissions: boolean; + enableCallInRoom(): void; + disableCallInRoom(): void; +} => { + // Use sticky events + const isMSC4354Enabled = useFeatureEnabled("feature_element_call_msc4354"); + const [mayCreateElementCallState, maySendSlot, hasRoomSlot, isPublic] = useRoomState<[boolean, boolean, boolean, boolean, boolean]>(room, () => [ + room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client), + room.currentState.mayClientSendStateEvent(ElementCallMemberEventType.name, room.client), + room.currentState.mayClientSendStateEvent("org.matrix.msc4143.rtc.slot", room.client), + // TODO: Replace with proper const + room.currentState.getStateEvents("org.matrix.msc4143.rtc.slot", "m.call#ROOM")?.getContent()?.application?.type === 'm.call', + room.getJoinRule() === JoinRule.Public, + ]); + + // TODO: Check that we are allowed to create audio/video calls, when the telephony PR lands. + const hasElementCallSlot = !isMSC4354Enabled || hasRoomSlot; + + const mayCreateElementCalls = useMemo(() => { + if (isMSC4354Enabled) { + return hasElementCallSlot || maySendSlot + } + return mayCreateElementCallState; + }, [isMSC4354Enabled, mayCreateElementCallState, maySendSlot, hasElementCallSlot]); + + const createElementCallSlot = useCallback(async (): Promise => { + if (hasElementCallSlot) { + return true; + } + const { finished } = Modal.createDialog(QuestionDialog, { + title: "Do you want to allow calls in this room?", + description: ( +

+ This room doesn't currently permit calling. If you continue, other users will + be able to place calls in the future. You may turn this off in the Room Settings. +

+ ), + button: _t("action|continue"), + }); + const [confirmed] = await finished; + if (!confirmed) { + return false; + } + await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { + "application": { + "type": "m.call", + // + "m.call.id": "i_dont_know_what_this_should_be", + } + }, "m.call#ROOM"); + return true; + }, [room, hasElementCallSlot]); + + const removeElementCallSlot = useCallback(async (): Promise => { + if (hasElementCallSlot) { + await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { }, "m.call#ROOM"); + } + }, [room, hasElementCallSlot]); + + + return { + canStartCall: mayCreateElementCalls, + canAdjustCallPermissions: maySendSlot, + enableCallInRoom: createElementCallSlot, + disableCallInRoom: removeElementCallSlot, + }; +}; + +const useLegacyCallPermissions = (room: Room | LocalRoom): ReturnType { + +} \ No newline at end of file diff --git a/src/hooks/room/useRoomCall.tsx b/src/hooks/room/useRoomCall.tsx index 92f68d02f83..d5541ac9e2a 100644 --- a/src/hooks/room/useRoomCall.tsx +++ b/src/hooks/room/useRoomCall.tsx @@ -37,6 +37,8 @@ import { UIFeature } from "../../settings/UIFeature"; import { type InteractionName } from "../../PosthogTrackers"; import { ElementCallMemberEventType } from "../../call-types"; import { LocalRoom, LocalRoomState } from "../../models/LocalRoom"; +import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; +import Modal from "../../Modal"; export enum PlatformCallType { ElementCall, @@ -97,6 +99,11 @@ export const useRoomCall = ( callOptions: PlatformCallType[]; showVideoCallButton: boolean; showVoiceCallButton: boolean; + + hasElementCallSlot: boolean; + canAdjustElementCallSlot: boolean; + createElementCallSlot(): void; + removeElementCallSlot(): void; } => { // settings const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); @@ -105,6 +112,8 @@ export const useRoomCall = ( const useElementCallExclusively = useMemo(() => { return SdkConfig.get("element_call").use_exclusively; }, []); + // Use sticky events + const isMSC4354Enabled = useFeatureEnabled("feature_element_call_msc4354"); const hasLegacyCall = useEventEmitterState( LegacyCallHandler.instance, @@ -132,17 +141,30 @@ export const useRoomCall = ( // room const memberCount = useRoomMemberCount(room); - const [mayEditWidgets, mayCreateElementCalls] = useRoomState(room, () => [ + const [mayEditWidgets, mayCreateElementCallState, maySendSlot, hasRoomSlot] = useRoomState<[boolean, boolean, boolean, boolean]>(room, () => [ room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client), room.currentState.mayClientSendStateEvent(ElementCallMemberEventType.name, room.client), + room.currentState.mayClientSendStateEvent("org.matrix.msc4143.rtc.slot", room.client), + // TODO: Replace with proper const + room.currentState.getStateEvents("org.matrix.msc4143.rtc.slot", "m.call#ROOM")?.getContent()?.application?.type === 'm.call' ]); + // TODO: Check that we are allowed to create audio/video calls, when the telephony PR lands. + const hasElementCallSlot = !isMSC4354Enabled || hasRoomSlot; + + const mayCreateElementCalls = useMemo(() => { + if (isMSC4354Enabled) { + return hasElementCallSlot || maySendSlot + } + return mayCreateElementCallState; + }, [isMSC4354Enabled, mayCreateElementCallState, maySendSlot, hasElementCallSlot]); + // The options provided to the RoomHeader. // 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); + // options.push(PlatformCallType.LegacyCall); } else if (mayEditWidgets || hasJitsiWidget) { options.push(PlatformCallType.JitsiCall); } @@ -224,16 +246,54 @@ export const useRoomCall = ( room.roomId, ]); + const createElementCallSlot = useCallback(async (): Promise => { + if (hasElementCallSlot) { + return true; + } + const { finished } = Modal.createDialog(QuestionDialog, { + title: "Do you want to allow calls in this room?", + description: ( +

+ This room doesn't currently permit calling. If you continue, other users will + be able to place calls in the future. You may turn this off in the Room Settings. +

+ ), + button: _t("action|continue"), + }); + const [confirmed] = await finished; + if (!confirmed) { + return false; + } + await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { + "application": { + "type": "m.call", + // + "m.call.id": "i_dont_know_what_this_should_be", + } + }, "m.call#ROOM"); + return true; + }, [room, hasElementCallSlot]); + + const removeElementCallSlot = useCallback(async (): Promise => { + if (hasElementCallSlot) { + await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { }, "m.call#ROOM"); + } + }, [room, hasElementCallSlot]); + const voiceCallClick = useCallback( (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => { evt?.stopPropagation(); if (widget && promptPinWidget) { WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top); } else { - placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined); + void (async () => { + if (callPlatformType !== PlatformCallType.ElementCall || await createElementCallSlot()) { + await placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined); + } + })(); } }, - [promptPinWidget, room, widget], + [promptPinWidget, room, widget, createElementCallSlot], ); const videoCallClick = useCallback( (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => { @@ -243,10 +303,14 @@ 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); + void (async () => { + if (callPlatformType !== PlatformCallType.ElementCall || await createElementCallSlot()) { + await placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined); + } + })(); } }, - [widget, promptPinWidget, room], + [widget, promptPinWidget, room, createElementCallSlot], ); let voiceCallDisabledReason: string | null; @@ -304,5 +368,9 @@ export const useRoomCall = ( callOptions, showVoiceCallButton: !hideVoiceCallButton, showVideoCallButton: !hideVideoCallButton, + hasElementCallSlot, + canAdjustElementCallSlot: maySendSlot, + createElementCallSlot, + removeElementCallSlot, }; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3ba40d89fe5..56a168fb0e0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1500,6 +1500,8 @@ "experimental_description": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. Learn more.", "experimental_section": "Early previews", "extended_profiles_msc_support": "Requires your server to support MSC4133", + "feature_element_call_msc4354": "Use sticky events for Element Call", + "feature_element_call_msc4354_msc_support": "Requires your server to support MSC4354 (Sticky Events)", "feature_disable_call_per_sender_encryption": "Disable per-sender encryption for Element Call", "feature_wysiwyg_composer_description": "Use rich text instead of Markdown in the message composer.", "group_calls": "New group call experience", diff --git a/src/models/Call.ts b/src/models/Call.ts index 356aef1f3bc..3338d5d2d4c 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -556,6 +556,7 @@ export interface WidgetGenerationParameters { * Skip showing the lobby screen of a call. */ skipLobby?: boolean; + useMSC4354?: boolean; } /** @@ -708,6 +709,10 @@ export class ElementCall extends Call { params.append("allowVoipWithNoMedia", "true"); } + if (SettingsStore.getValue("feature_element_call_msc4354")) { + params.append("useMSC4354", "true"); + } + // Set custom fonts if (SettingsStore.getValue("useSystemFont")) { SettingsStore.getValue("systemFont") diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index f390544abe2..3e68cc114af 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -50,6 +50,7 @@ import { SortingAlgorithm } from "../stores/room-list-v3/skip-list/sorters/index import MediaPreviewConfigController from "./controllers/MediaPreviewConfigController.ts"; import InviteRulesConfigController from "./controllers/InviteRulesConfigController.ts"; import { type ComputedInviteConfig } from "../@types/invite-rules.ts"; +import { UNSTABLE_MSC4354_STICKY_EVENTS } from "matrix-js-sdk/src/matrix"; export const defaultWatchManager = new WatchManager(); @@ -220,6 +221,7 @@ export interface Settings { "feature_sliding_sync": IBaseSetting; "feature_simplified_sliding_sync": IFeature; "feature_element_call_video_rooms": IFeature; + "feature_element_call_msc4354": IFeature; "feature_group_calls": IFeature; "feature_disable_call_per_sender_encryption": IFeature; "feature_allow_screen_share_only_mode": IFeature; @@ -627,13 +629,29 @@ export const SETTINGS: Settings = { controller: new ReloadOnChangeController(), default: false, }, + "feature_element_call_msc4354": { + isFeature: true, + labsGroup: LabGroup.VoiceAndVideo, + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, + supportedLevelsAreOrdered: true, + displayName: _td("labs|feature_element_call_msc4354"), + controller: new ServerSupportUnstableFeatureController( + "feature_element_call_msc4354", + defaultWatchManager, + [[UNSTABLE_MSC4354_STICKY_EVENTS]], + undefined, + _td("labs|feature_element_call_msc4354_msc_support"), + ), + default: false, + + }, "feature_group_calls": { isFeature: true, labsGroup: LabGroup.VoiceAndVideo, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, supportedLevelsAreOrdered: true, - displayName: _td("labs|group_calls"), controller: new ReloadOnChangeController(), + displayName: _td("labs|group_calls"), default: false, }, "feature_disable_call_per_sender_encryption": { From 4b80d23a1feeafc3b37f90919ad7ef88b8b836c9 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 24 Oct 2025 10:38:48 +0100 Subject: [PATCH 02/11] map out hook --- src/hooks/room/useElementCallPermissions.tsx | 34 +++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/hooks/room/useElementCallPermissions.tsx b/src/hooks/room/useElementCallPermissions.tsx index fb85226f3dd..94c256ed942 100644 --- a/src/hooks/room/useElementCallPermissions.tsx +++ b/src/hooks/room/useElementCallPermissions.tsx @@ -6,7 +6,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 { JoinRule, type Room } from "matrix-js-sdk/src/matrix"; +import { type Room } from "matrix-js-sdk/src/matrix"; import { useCallback, useMemo } from "react"; import type React from "react"; @@ -18,9 +18,21 @@ import { LocalRoom } from "../../models/LocalRoom"; import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; import Modal from "../../Modal"; +type ElementCallPermissions = { + canStartCall: boolean; + canAdjustCallPermissions: boolean; + enableCallInRoom(): void; + disableCallInRoom(): void; +} -const useLegacyCallPermissions = (room: Room | LocalRoom): ReturnType { +function useLegacyCallPermissions(room: Room | LocalRoom): ElementCallPermissions { + return { + canStartCall: true, + canAdjustCallPermissions: true, + enableCallInRoom: () => {}, + disableCallInRoom: () => {}, + } } /** @@ -30,21 +42,15 @@ const useLegacyCallPermissions = (room: Room | LocalRoom): ReturnType { +): ElementCallPermissions => { // Use sticky events const isMSC4354Enabled = useFeatureEnabled("feature_element_call_msc4354"); - const [mayCreateElementCallState, maySendSlot, hasRoomSlot, isPublic] = useRoomState<[boolean, boolean, boolean, boolean, boolean]>(room, () => [ + const [mayCreateElementCallState, maySendSlot, hasRoomSlot] = useRoomState(room, () => [ room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client), room.currentState.mayClientSendStateEvent(ElementCallMemberEventType.name, room.client), room.currentState.mayClientSendStateEvent("org.matrix.msc4143.rtc.slot", room.client), // TODO: Replace with proper const room.currentState.getStateEvents("org.matrix.msc4143.rtc.slot", "m.call#ROOM")?.getContent()?.application?.type === 'm.call', - room.getJoinRule() === JoinRule.Public, ]); // TODO: Check that we are allowed to create audio/video calls, when the telephony PR lands. @@ -100,6 +106,10 @@ const useSlotsCallPermissions = ( }; }; -const useLegacyCallPermissions = (room: Room | LocalRoom): ReturnType { - +export function useElementCallPermissions (room: Room | LocalRoom): ElementCallPermissions { + const isMSC4354Enabled = useFeatureEnabled("feature_element_call_msc4354"); + if (isMSC4354Enabled) { + return useSlotsCallPermissions(room); + } + return useLegacyCallPermissions(room); } \ No newline at end of file From 0656fff3cbf700aaca31030a4803632f8914619f Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 27 Oct 2025 11:21:55 +0000 Subject: [PATCH 03/11] Complete first pass on call permissions --- .../tabs/room/VoipRoomSettingsTab.tsx | 2 +- src/hooks/room/useElementCallPermissions.tsx | 108 ++++++++++++------ src/hooks/room/useRoomCall.tsx | 91 +++------------ src/i18n/strings/en_EN.json | 1 + src/settings/Settings.tsx | 1 + 5 files changed, 96 insertions(+), 107 deletions(-) diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 440df901abf..7c30abea7b7 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -28,6 +28,7 @@ const ElementCallSwitch: React.FC = ({ room }) => { const onToggle = useCallback(() => { setBusy(true) void (async () => { + console.log({canStartCall, canAdjustCallPermissions}); try { if (canStartCall) { await disableCallInRoom(); @@ -38,7 +39,6 @@ const ElementCallSwitch: React.FC = ({ room }) => { setBusy(false); } })(); - }, [canStartCall, enableCallInRoom, disableCallInRoom]); const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand; diff --git a/src/hooks/room/useElementCallPermissions.tsx b/src/hooks/room/useElementCallPermissions.tsx index 94c256ed942..59109131680 100644 --- a/src/hooks/room/useElementCallPermissions.tsx +++ b/src/hooks/room/useElementCallPermissions.tsx @@ -6,65 +6,98 @@ 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 { type Room } from "matrix-js-sdk/src/matrix"; -import { useCallback, useMemo } from "react"; +import { EventType, JoinRule, RoomState, type Room } from "matrix-js-sdk/src/matrix"; +import { useCallback } from "react"; import type React from "react"; import { useFeatureEnabled } from "../useSettings"; import { useRoomState } from "../useRoomState"; import { _t } from "../../languageHandler"; -import { ElementCallMemberEventType } from "../../call-types"; +import { ElementCallEventType, ElementCallMemberEventType } from "../../call-types"; import { LocalRoom } from "../../models/LocalRoom"; import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; import Modal from "../../Modal"; +import { RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types"; type ElementCallPermissions = { canStartCall: boolean; canAdjustCallPermissions: boolean; enableCallInRoom(): void; disableCallInRoom(): void; -} +} + + +/** + * Hook for adjusting permissions for enabling Element Call. + * This uses the legacy state controlled system. + * @param room the room to track + */ +function useLegacyCallPermissions(room: Room| LocalRoom): ElementCallPermissions { + const [powerLevelContent, maySend, elementCallEnabled] = useRoomState( + room, + useCallback( + (state: RoomState) => { + const content = state + ?.getStateEvents(EventType.RoomPowerLevels, "") + ?.getContent(); + return [ + content ?? {}, + state?.maySendStateEvent(EventType.RoomPowerLevels, room.client.getSafeUserId()), + content?.events?.[ElementCallMemberEventType.name] === 0 + ] as const; + }, + [room.client], + ), + ); + + const enableCallInRoom = useCallback(() => { + console.log('Enabling call'); + const newContent = { events: {}, ...powerLevelContent }; + const userLevel = newContent.events[EventType.RoomMessage] ?? powerLevelContent.users_default ?? 0; + const moderatorLevel = powerLevelContent.kick ?? 50; + const isPublic = room.getJoinRule() === JoinRule.Public; + console.log(newContent.events); + newContent.events[ElementCallEventType.name] = isPublic ? moderatorLevel : userLevel; + newContent.events[ElementCallMemberEventType.name] = userLevel; + room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); + },[room, powerLevelContent]); -function useLegacyCallPermissions(room: Room | LocalRoom): ElementCallPermissions { + const disableCallInRoom = useCallback(() => { + console.log('Disabling call'); + const newContent = { events: {}, ...powerLevelContent }; + const adminLevel = newContent.events[EventType.RoomPowerLevels] ?? powerLevelContent.state_default ?? 100; + newContent.events[ElementCallEventType.name] = adminLevel; + newContent.events[ElementCallMemberEventType.name] = adminLevel; + room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); + },[room, powerLevelContent]); + return { - canStartCall: true, - canAdjustCallPermissions: true, - enableCallInRoom: () => {}, - disableCallInRoom: () => {}, - } + canStartCall: elementCallEnabled, + canAdjustCallPermissions: maySend, + enableCallInRoom, + disableCallInRoom, + }; } /** * Hook for adjusting permissions for enabling Element Call. + * This requires MSC4354 (Sticky events) to work. * @param room the room to track - * @returns the call button attributes for the given room */ const useSlotsCallPermissions = ( room: Room | LocalRoom, ): ElementCallPermissions => { - // Use sticky events - const isMSC4354Enabled = useFeatureEnabled("feature_element_call_msc4354"); - const [mayCreateElementCallState, maySendSlot, hasRoomSlot] = useRoomState(room, () => [ - room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client), - room.currentState.mayClientSendStateEvent(ElementCallMemberEventType.name, room.client), + const [maySendSlot, hasRoomSlot] = useRoomState(room, () => [ room.currentState.mayClientSendStateEvent("org.matrix.msc4143.rtc.slot", room.client), // TODO: Replace with proper const room.currentState.getStateEvents("org.matrix.msc4143.rtc.slot", "m.call#ROOM")?.getContent()?.application?.type === 'm.call', ]); // TODO: Check that we are allowed to create audio/video calls, when the telephony PR lands. - const hasElementCallSlot = !isMSC4354Enabled || hasRoomSlot; - - const mayCreateElementCalls = useMemo(() => { - if (isMSC4354Enabled) { - return hasElementCallSlot || maySendSlot - } - return mayCreateElementCallState; - }, [isMSC4354Enabled, mayCreateElementCallState, maySendSlot, hasElementCallSlot]); - const createElementCallSlot = useCallback(async (): Promise => { - if (hasElementCallSlot) { + console.log('createElementCallSlot', { hasRoomSlot }); + if (hasRoomSlot) { return true; } const { finished } = Modal.createDialog(QuestionDialog, { @@ -89,27 +122,34 @@ const useSlotsCallPermissions = ( } }, "m.call#ROOM"); return true; - }, [room, hasElementCallSlot]); + }, [room, hasRoomSlot]); const removeElementCallSlot = useCallback(async (): Promise => { - if (hasElementCallSlot) { + console.log('removeElementCallSlot', { hasRoomSlot }); + if (hasRoomSlot) { await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { }, "m.call#ROOM"); } - }, [room, hasElementCallSlot]); + }, [room, hasRoomSlot]); return { - canStartCall: mayCreateElementCalls, + canStartCall: hasRoomSlot, canAdjustCallPermissions: maySendSlot, enableCallInRoom: createElementCallSlot, disableCallInRoom: removeElementCallSlot, }; }; +/** + * Get and set whether an Element Call session may take place. If MSC4354 is enabled, + * this will use the new slots flow. Otherwise, this will fallback to the older state-based permissions. + * @param room + * @returns + */ export function useElementCallPermissions (room: Room | LocalRoom): ElementCallPermissions { + // We load both, to avoid conditional hook rendering on settings change. + const slotsPerms = useSlotsCallPermissions(room); + const legacyPerms = useLegacyCallPermissions(room); const isMSC4354Enabled = useFeatureEnabled("feature_element_call_msc4354"); - if (isMSC4354Enabled) { - return useSlotsCallPermissions(room); - } - return useLegacyCallPermissions(room); + return isMSC4354Enabled ? slotsPerms : legacyPerms; } \ No newline at end of file diff --git a/src/hooks/room/useRoomCall.tsx b/src/hooks/room/useRoomCall.tsx index d5541ac9e2a..6bb7bcb5b3e 100644 --- a/src/hooks/room/useRoomCall.tsx +++ b/src/hooks/room/useRoomCall.tsx @@ -35,10 +35,8 @@ import { CallStore, CallStoreEvent } from "../../stores/CallStore"; import { isVideoRoom } from "../../utils/video-rooms"; import { UIFeature } from "../../settings/UIFeature"; import { type InteractionName } from "../../PosthogTrackers"; -import { ElementCallMemberEventType } from "../../call-types"; import { LocalRoom, LocalRoomState } from "../../models/LocalRoom"; -import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; -import Modal from "../../Modal"; +import { useElementCallPermissions } from "./useElementCallPermissions"; export enum PlatformCallType { ElementCall, @@ -75,6 +73,7 @@ export const getPlatformCallTypeProps = ( const enum State { NoCall, NoPermission, + CallingDisabled, Unpinned, Ongoing, NotJoined, @@ -99,11 +98,6 @@ export const useRoomCall = ( callOptions: PlatformCallType[]; showVideoCallButton: boolean; showVoiceCallButton: boolean; - - hasElementCallSlot: boolean; - canAdjustElementCallSlot: boolean; - createElementCallSlot(): void; - removeElementCallSlot(): void; } => { // settings const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); @@ -112,8 +106,7 @@ export const useRoomCall = ( const useElementCallExclusively = useMemo(() => { return SdkConfig.get("element_call").use_exclusively; }, []); - // Use sticky events - const isMSC4354Enabled = useFeatureEnabled("feature_element_call_msc4354"); + const { canStartCall: mayCreateElementCalls } = useElementCallPermissions(room); const hasLegacyCall = useEventEmitterState( LegacyCallHandler.instance, @@ -141,24 +134,10 @@ export const useRoomCall = ( // room const memberCount = useRoomMemberCount(room); - const [mayEditWidgets, mayCreateElementCallState, maySendSlot, hasRoomSlot] = useRoomState<[boolean, boolean, boolean, boolean]>(room, () => [ + const [mayEditWidgets] = useRoomState<[boolean]>(room, () => [ room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client), - room.currentState.mayClientSendStateEvent(ElementCallMemberEventType.name, room.client), - room.currentState.mayClientSendStateEvent("org.matrix.msc4143.rtc.slot", room.client), - // TODO: Replace with proper const - room.currentState.getStateEvents("org.matrix.msc4143.rtc.slot", "m.call#ROOM")?.getContent()?.application?.type === 'm.call' ]); - // TODO: Check that we are allowed to create audio/video calls, when the telephony PR lands. - const hasElementCallSlot = !isMSC4354Enabled || hasRoomSlot; - - const mayCreateElementCalls = useMemo(() => { - if (isMSC4354Enabled) { - return hasElementCallSlot || maySendSlot - } - return mayCreateElementCallState; - }, [isMSC4354Enabled, mayCreateElementCallState, maySendSlot, hasElementCallSlot]); - // The options provided to the RoomHeader. // If there are multiple options, the user will be prompted to choose. const callOptions = useMemo((): PlatformCallType[] => { @@ -172,7 +151,7 @@ export const useRoomCall = ( if (hasGroupCall || mayCreateElementCalls) { options.push(PlatformCallType.ElementCall); } - if (useElementCallExclusively && !hasJitsiWidget) { + if (useElementCallExclusively && mayCreateElementCalls && !hasJitsiWidget) { return [PlatformCallType.ElementCall]; } } @@ -229,9 +208,14 @@ export const useRoomCall = ( return State.Ongoing; } + if (callOptions.length === 0 && !mayCreateElementCalls) { + return State.CallingDisabled; + } + if (!callOptions.includes(PlatformCallType.LegacyCall) && !mayCreateElementCalls && !mayEditWidgets) { return State.NoPermission; } + return State.NoCall; }, [ callOptions, @@ -246,40 +230,6 @@ export const useRoomCall = ( room.roomId, ]); - const createElementCallSlot = useCallback(async (): Promise => { - if (hasElementCallSlot) { - return true; - } - const { finished } = Modal.createDialog(QuestionDialog, { - title: "Do you want to allow calls in this room?", - description: ( -

- This room doesn't currently permit calling. If you continue, other users will - be able to place calls in the future. You may turn this off in the Room Settings. -

- ), - button: _t("action|continue"), - }); - const [confirmed] = await finished; - if (!confirmed) { - return false; - } - await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { - "application": { - "type": "m.call", - // - "m.call.id": "i_dont_know_what_this_should_be", - } - }, "m.call#ROOM"); - return true; - }, [room, hasElementCallSlot]); - - const removeElementCallSlot = useCallback(async (): Promise => { - if (hasElementCallSlot) { - await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { }, "m.call#ROOM"); - } - }, [room, hasElementCallSlot]); - const voiceCallClick = useCallback( (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => { evt?.stopPropagation(); @@ -287,13 +237,11 @@ export const useRoomCall = ( WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top); } else { void (async () => { - if (callPlatformType !== PlatformCallType.ElementCall || await createElementCallSlot()) { - await placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined); - } + await placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined); })(); } }, - [promptPinWidget, room, widget, createElementCallSlot], + [promptPinWidget, room, widget], ); const videoCallClick = useCallback( (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => { @@ -304,18 +252,19 @@ export const useRoomCall = ( // If we have pressed shift then always skip the lobby, otherwise `undefined` will defer // to the defaults of the call implementation. void (async () => { - if (callPlatformType !== PlatformCallType.ElementCall || await createElementCallSlot()) { - await placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined); - } + await placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined); })(); } }, - [widget, promptPinWidget, room, createElementCallSlot], + [widget, promptPinWidget, room], ); let voiceCallDisabledReason: string | null; let videoCallDisabledReason: string | null; switch (state) { + case State.CallingDisabled: + voiceCallDisabledReason = videoCallDisabledReason = _t("voip|disabled_branded_call", { brand: SdkConfig.get("element_call").brand }); + break; case State.NoPermission: voiceCallDisabledReason = _t("voip|disabled_no_perms_start_voice_call"); videoCallDisabledReason = _t("voip|disabled_no_perms_start_video_call"); @@ -353,6 +302,8 @@ export const useRoomCall = ( hideVideoCallButton = true; } + console.log("useRoomCall", { voiceCallDisabledReason, videoCallDisabledReason, callOptions, hideVideoCallButton, hideVoiceCallButton, mayCreateElementCalls }); + /** * We've gone through all the steps */ @@ -368,9 +319,5 @@ export const useRoomCall = ( callOptions, showVoiceCallButton: !hideVoiceCallButton, showVideoCallButton: !hideVideoCallButton, - hasElementCallSlot, - canAdjustElementCallSlot: maySendSlot, - createElementCallSlot, - removeElementCallSlot, }; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 56a168fb0e0..e6acd0129c4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -4008,6 +4008,7 @@ "disable_microphone": "Mute microphone", "disabled_no_perms_start_video_call": "You do not have permission to start video calls", "disabled_no_perms_start_voice_call": "You do not have permission to start voice calls", + "disabled_branded_call": "Enable %(brand)s in the room settings", "disabled_ongoing_call": "Ongoing call", "element_call": "Element Call", "enable_camera": "Turn on camera", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 3e68cc114af..ef19a6d4888 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -641,6 +641,7 @@ export const SETTINGS: Settings = { [[UNSTABLE_MSC4354_STICKY_EVENTS]], undefined, _td("labs|feature_element_call_msc4354_msc_support"), + false, ), default: false, From adfefdc7685b8097e4c428960942243aae178f92 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 27 Oct 2025 11:33:41 +0000 Subject: [PATCH 04/11] useStickyEvents --- src/models/Call.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/Call.ts b/src/models/Call.ts index 3338d5d2d4c..32b50bf0635 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -710,7 +710,7 @@ export class ElementCall extends Call { } if (SettingsStore.getValue("feature_element_call_msc4354")) { - params.append("useMSC4354", "true"); + params.append("useStickyEvents", "true"); } // Set custom fonts From 06471e12160bf86737d32732cf8104e756c64020 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 27 Oct 2025 11:48:43 +0000 Subject: [PATCH 05/11] updates --- src/hooks/room/useElementCallPermissions.tsx | 3 +-- src/models/Call.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/hooks/room/useElementCallPermissions.tsx b/src/hooks/room/useElementCallPermissions.tsx index 59109131680..163e78d3583 100644 --- a/src/hooks/room/useElementCallPermissions.tsx +++ b/src/hooks/room/useElementCallPermissions.tsx @@ -117,8 +117,7 @@ const useSlotsCallPermissions = ( await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { "application": { "type": "m.call", - // - "m.call.id": "i_dont_know_what_this_should_be", + // m.call.id is not specified here, as we want this slot to be general purpose } }, "m.call#ROOM"); return true; diff --git a/src/models/Call.ts b/src/models/Call.ts index 32b50bf0635..f72032da1f3 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -710,7 +710,7 @@ export class ElementCall extends Call { } if (SettingsStore.getValue("feature_element_call_msc4354")) { - params.append("useStickyEvents", "true"); + params.append("preferStickyEvents", "true"); } // Set custom fonts From df55a4836fb64cff633718d384458937fd48d6ca Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 27 Oct 2025 12:31:37 +0000 Subject: [PATCH 06/11] Cleanup --- src/@types/matrix-js-sdk.d.ts | 1 + .../views/settings/tabs/room/VoipRoomSettingsTab.tsx | 2 +- src/hooks/room/useElementCallPermissions.tsx | 11 ++--------- src/hooks/room/useRoomCall.tsx | 6 ++---- src/models/Call.ts | 2 +- src/settings/Settings.tsx | 3 ++- 6 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/@types/matrix-js-sdk.d.ts b/src/@types/matrix-js-sdk.d.ts index ff2e3952c43..16074e19843 100644 --- a/src/@types/matrix-js-sdk.d.ts +++ b/src/@types/matrix-js-sdk.d.ts @@ -1,4 +1,5 @@ /* +Copyright 2025 Element Creations Ltd. Copyright 2024, 2025 New Vector Ltd. Copyright 2024 The Matrix.org Foundation C.I.C. diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 7c30abea7b7..25d4fd0a33d 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -1,4 +1,5 @@ /* +Copyright 2025 Element Creations Ltd. Copyright 2024 New Vector Ltd. Copyright 2022 The Matrix.org Foundation C.I.C. @@ -28,7 +29,6 @@ const ElementCallSwitch: React.FC = ({ room }) => { const onToggle = useCallback(() => { setBusy(true) void (async () => { - console.log({canStartCall, canAdjustCallPermissions}); try { if (canStartCall) { await disableCallInRoom(); diff --git a/src/hooks/room/useElementCallPermissions.tsx b/src/hooks/room/useElementCallPermissions.tsx index 163e78d3583..2dab3049fa5 100644 --- a/src/hooks/room/useElementCallPermissions.tsx +++ b/src/hooks/room/useElementCallPermissions.tsx @@ -1,15 +1,13 @@ /* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. +Copyright 2025 Element Creations Ltd. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ import { EventType, JoinRule, RoomState, type Room } from "matrix-js-sdk/src/matrix"; -import { useCallback } from "react"; +import React, { useCallback } from "react"; -import type React from "react"; import { useFeatureEnabled } from "../useSettings"; import { useRoomState } from "../useRoomState"; import { _t } from "../../languageHandler"; @@ -51,12 +49,10 @@ function useLegacyCallPermissions(room: Room| LocalRoom): ElementCallPermissions ); const enableCallInRoom = useCallback(() => { - console.log('Enabling call'); const newContent = { events: {}, ...powerLevelContent }; const userLevel = newContent.events[EventType.RoomMessage] ?? powerLevelContent.users_default ?? 0; const moderatorLevel = powerLevelContent.kick ?? 50; const isPublic = room.getJoinRule() === JoinRule.Public; - console.log(newContent.events); newContent.events[ElementCallEventType.name] = isPublic ? moderatorLevel : userLevel; newContent.events[ElementCallMemberEventType.name] = userLevel; room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); @@ -64,7 +60,6 @@ function useLegacyCallPermissions(room: Room| LocalRoom): ElementCallPermissions const disableCallInRoom = useCallback(() => { - console.log('Disabling call'); const newContent = { events: {}, ...powerLevelContent }; const adminLevel = newContent.events[EventType.RoomPowerLevels] ?? powerLevelContent.state_default ?? 100; newContent.events[ElementCallEventType.name] = adminLevel; @@ -96,7 +91,6 @@ const useSlotsCallPermissions = ( // TODO: Check that we are allowed to create audio/video calls, when the telephony PR lands. const createElementCallSlot = useCallback(async (): Promise => { - console.log('createElementCallSlot', { hasRoomSlot }); if (hasRoomSlot) { return true; } @@ -124,7 +118,6 @@ const useSlotsCallPermissions = ( }, [room, hasRoomSlot]); const removeElementCallSlot = useCallback(async (): Promise => { - console.log('removeElementCallSlot', { hasRoomSlot }); if (hasRoomSlot) { await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { }, "m.call#ROOM"); } diff --git a/src/hooks/room/useRoomCall.tsx b/src/hooks/room/useRoomCall.tsx index 6bb7bcb5b3e..b186284cca3 100644 --- a/src/hooks/room/useRoomCall.tsx +++ b/src/hooks/room/useRoomCall.tsx @@ -1,4 +1,5 @@ /* +Copyright 2025 Element Creations Ltd. Copyright 2024 New Vector Ltd. Copyright 2023 The Matrix.org Foundation C.I.C. @@ -143,7 +144,7 @@ export const useRoomCall = ( const callOptions = useMemo((): PlatformCallType[] => { const options: PlatformCallType[] = []; if (memberCount <= 2) { - // options.push(PlatformCallType.LegacyCall); + options.push(PlatformCallType.LegacyCall); } else if (mayEditWidgets || hasJitsiWidget) { options.push(PlatformCallType.JitsiCall); } @@ -215,7 +216,6 @@ export const useRoomCall = ( if (!callOptions.includes(PlatformCallType.LegacyCall) && !mayCreateElementCalls && !mayEditWidgets) { return State.NoPermission; } - return State.NoCall; }, [ callOptions, @@ -302,8 +302,6 @@ export const useRoomCall = ( hideVideoCallButton = true; } - console.log("useRoomCall", { voiceCallDisabledReason, videoCallDisabledReason, callOptions, hideVideoCallButton, hideVoiceCallButton, mayCreateElementCalls }); - /** * We've gone through all the steps */ diff --git a/src/models/Call.ts b/src/models/Call.ts index f72032da1f3..9d483e7af7b 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -1,4 +1,5 @@ /* +Copyright 2025 Element Creations Ltd. Copyright 2024 New Vector Ltd. Copyright 2022 The Matrix.org Foundation C.I.C. @@ -556,7 +557,6 @@ export interface WidgetGenerationParameters { * Skip showing the lobby screen of a call. */ skipLobby?: boolean; - useMSC4354?: boolean; } /** diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index ef19a6d4888..0c24c739d33 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -1,4 +1,5 @@ /* +Copyright 2025 Element Creations Ltd. Copyright 2024, 2025 New Vector Ltd. Copyright 2018-2024 The Matrix.org Foundation C.I.C. Copyright 2017 Travis Ralston @@ -651,8 +652,8 @@ export const SETTINGS: Settings = { labsGroup: LabGroup.VoiceAndVideo, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, supportedLevelsAreOrdered: true, - controller: new ReloadOnChangeController(), displayName: _td("labs|group_calls"), + controller: new ReloadOnChangeController(), default: false, }, "feature_disable_call_per_sender_encryption": { From 5697eec4dc6b696c589c99d52711989d4dcb8add Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 27 Oct 2025 12:32:45 +0000 Subject: [PATCH 07/11] lint --- src/settings/Settings.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 0c24c739d33..158c6794ee2 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -645,7 +645,6 @@ export const SETTINGS: Settings = { false, ), default: false, - }, "feature_group_calls": { isFeature: true, From e3ef27c31de1d400417b71d35d388cdce6ee20d0 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 28 Oct 2025 09:46:48 +0000 Subject: [PATCH 08/11] Cleanup --- src/Notifier.ts | 11 +++++++++-- .../tabs/room/RolesRoomSettingsTab.tsx | 8 ++++++-- src/hooks/room/useElementCallPermissions.tsx | 18 +++++++++--------- src/i18n/strings/en_EN.json | 5 +++-- src/models/Call.ts | 4 ++-- src/settings/Settings.tsx | 10 +++++----- test/unit-tests/Notifier-test.ts | 2 +- test/unit-tests/createRoom-test.ts | 2 +- test/unit-tests/models/Call-test.ts | 5 +++-- 9 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/Notifier.ts b/src/Notifier.ts index aa68b386a34..e502585df38 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -25,7 +25,11 @@ import { } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { type PermissionChanged as PermissionChangedEvent } from "@matrix-org/analytics-events/types/typescript/PermissionChanged"; -import { type IRTCNotificationContent, MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc"; +import { + DefaultCallApplicationSlot, + type IRTCNotificationContent, + MatrixRTCSession, +} from "matrix-js-sdk/src/matrixrtc"; import { MatrixClientPeg } from "./MatrixClientPeg"; import { PosthogAnalytics } from "./PosthogAnalytics"; @@ -487,7 +491,10 @@ class NotifierClass extends TypedEventEmitter m.sender === cli.getUserId()); + room && + MatrixRTCSession.sessionMembershipsForSlot(room, DefaultCallApplicationSlot).some( + (m) => m.sender === cli.getUserId(), + ); if (EventType.RTCNotification === ev.getType() && !thisUserHasConnectedDevice) { const content = ev.getContent() as IRTCNotificationContent; diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index a7e0e3755bd..82f01d1525e 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -296,8 +296,12 @@ export default class RolesRoomSettingsTab extends React.Component { + const slotId = slotDescriptionToId({id: "", application: "m.call"}); const [maySendSlot, hasRoomSlot] = useRoomState(room, () => [ - room.currentState.mayClientSendStateEvent("org.matrix.msc4143.rtc.slot", room.client), + room.currentState.mayClientSendStateEvent(EventType.RTCSlot, room.client), // TODO: Replace with proper const - room.currentState.getStateEvents("org.matrix.msc4143.rtc.slot", "m.call#ROOM")?.getContent()?.application?.type === 'm.call', + room.currentState.getStateEvents(EventType.RTCSlot, slotId)?.getContent().application.type === DefaultCallApplicationSlot.application.type, ]); // TODO: Check that we are allowed to create audio/video calls, when the telephony PR lands. @@ -109,17 +112,14 @@ const useSlotsCallPermissions = ( return false; } await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { - "application": { - "type": "m.call", - // m.call.id is not specified here, as we want this slot to be general purpose - } - }, "m.call#ROOM"); + "application": DefaultCallApplicationSlot.application + }, slotId); return true; }, [room, hasRoomSlot]); const removeElementCallSlot = useCallback(async (): Promise => { if (hasRoomSlot) { - await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { }, "m.call#ROOM"); + await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { }, slotId); } }, [room, hasRoomSlot]); @@ -142,6 +142,6 @@ export function useElementCallPermissions (room: Room | LocalRoom): ElementCallP // We load both, to avoid conditional hook rendering on settings change. const slotsPerms = useSlotsCallPermissions(room); const legacyPerms = useLegacyCallPermissions(room); - const isMSC4354Enabled = useFeatureEnabled("feature_element_call_msc4354"); + const isMSC4354Enabled = useFeatureEnabled("feature_element_call_nextgen"); return isMSC4354Enabled ? slotsPerms : legacyPerms; } \ No newline at end of file diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e6acd0129c4..c2048f8b170 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1500,8 +1500,8 @@ "experimental_description": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. Learn more.", "experimental_section": "Early previews", "extended_profiles_msc_support": "Requires your server to support MSC4133", - "feature_element_call_msc4354": "Use sticky events for Element Call", - "feature_element_call_msc4354_msc_support": "Requires your server to support MSC4354 (Sticky Events)", + "feature_element_call_nextgen": "Enable MultiSFU support for Element Call", + "feature_element_call_nextgen_msc_support": "Requires your server to support MSC4354 (Sticky Events)", "feature_disable_call_per_sender_encryption": "Disable per-sender encryption for Element Call", "feature_wysiwyg_composer_description": "Use rich text instead of Markdown in the message composer.", "group_calls": "New group call experience", @@ -2369,6 +2369,7 @@ "m.room.tombstone": "Upgrade the room", "m.room.topic": "Change topic", "m.room.topic_space": "Change description", + "m.rtc.slot": "Enable or disable %(brand)s calls", "m.space.child": "Manage rooms in this space", "m.widget": "Modify widgets", "muted_users_section": "Muted Users", diff --git a/src/models/Call.ts b/src/models/Call.ts index 9d483e7af7b..e66ef55192c 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -709,8 +709,8 @@ export class ElementCall extends Call { params.append("allowVoipWithNoMedia", "true"); } - if (SettingsStore.getValue("feature_element_call_msc4354")) { - params.append("preferStickyEvents", "true"); + if (SettingsStore.getValue("feature_element_call_nextgen")) { + params.append("multiSFU", "true"); } // Set custom fonts diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 158c6794ee2..c69ba07579c 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -222,7 +222,7 @@ export interface Settings { "feature_sliding_sync": IBaseSetting; "feature_simplified_sliding_sync": IFeature; "feature_element_call_video_rooms": IFeature; - "feature_element_call_msc4354": IFeature; + "feature_element_call_nextgen": IFeature; "feature_group_calls": IFeature; "feature_disable_call_per_sender_encryption": IFeature; "feature_allow_screen_share_only_mode": IFeature; @@ -630,18 +630,18 @@ export const SETTINGS: Settings = { controller: new ReloadOnChangeController(), default: false, }, - "feature_element_call_msc4354": { + "feature_element_call_nextgen": { isFeature: true, labsGroup: LabGroup.VoiceAndVideo, supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, supportedLevelsAreOrdered: true, - displayName: _td("labs|feature_element_call_msc4354"), + displayName: _td("labs|feature_element_call_nextgen"), controller: new ServerSupportUnstableFeatureController( - "feature_element_call_msc4354", + "feature_element_call_nextgen", defaultWatchManager, [[UNSTABLE_MSC4354_STICKY_EVENTS]], undefined, - _td("labs|feature_element_call_msc4354_msc_support"), + _td("labs|feature_element_call_nextgen_msc_support"), false, ), default: false, diff --git a/test/unit-tests/Notifier-test.ts b/test/unit-tests/Notifier-test.ts index 5ff4b0bf771..87c724b1145 100644 --- a/test/unit-tests/Notifier-test.ts +++ b/test/unit-tests/Notifier-test.ts @@ -433,7 +433,7 @@ describe("Notifier", () => { }); it("should not show toast when group call is already connected", () => { - const spyCallMemberships = jest.spyOn(MatrixRTCSession, "callMembershipsForRoom").mockReturnValue([ + const spyCallMemberships = jest.spyOn(MatrixRTCSession, "sessionMembershipsForSlot").mockReturnValue([ new CallMembership( mkEvent({ event: true, diff --git a/test/unit-tests/createRoom-test.ts b/test/unit-tests/createRoom-test.ts index 06fb43cb4cf..133b47b9853 100644 --- a/test/unit-tests/createRoom-test.ts +++ b/test/unit-tests/createRoom-test.ts @@ -125,7 +125,7 @@ describe("createRoom", () => { it("sets up Element video rooms correctly", async () => { const userId = client.getUserId()!; const createCallSpy = jest.spyOn(ElementCall, "create"); - const callMembershipSpy = jest.spyOn(MatrixRTCSession, "callMembershipsForRoom"); + const callMembershipSpy = jest.spyOn(MatrixRTCSession, "sessionMembershipsForSlot"); callMembershipSpy.mockReturnValue([]); const roomId = await createRoom(client, { roomType: RoomType.UnstableCall }); diff --git a/test/unit-tests/models/Call-test.ts b/test/unit-tests/models/Call-test.ts index 5d83aa335d2..1f441da4b51 100644 --- a/test/unit-tests/models/Call-test.ts +++ b/test/unit-tests/models/Call-test.ts @@ -25,6 +25,7 @@ import { MatrixRTCSessionManagerEvents, MatrixRTCSession, MatrixRTCSessionEvent, + DefaultCallApplicationSlot, } from "matrix-js-sdk/src/matrixrtc"; import type { Mocked } from "jest-mock"; @@ -933,8 +934,8 @@ describe("ElementCall", () => { setRoomMembers(["@user:example.com", "@user2:example.com", "@user4:example.com"]); }); it("don't sent notify event if there are existing room call members", async () => { - jest.spyOn(MatrixRTCSession, "callMembershipsForRoom").mockReturnValue([ - { application: "m.call", callId: "" } as unknown as CallMembership, + jest.spyOn(MatrixRTCSession, "sessionMembershipsForSlot").mockReturnValue([ + DefaultCallApplicationSlot as unknown as CallMembership, ]); const sendEventSpy = jest.spyOn(room.client, "sendEvent"); ElementCall.create(room); From 147f2ba0649fe22aad10caabbf522d03b98afd5e Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 28 Oct 2025 12:38:32 +0000 Subject: [PATCH 09/11] lint --- src/hooks/room/useElementCallPermissions.tsx | 58 ++++++++++---------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/hooks/room/useElementCallPermissions.tsx b/src/hooks/room/useElementCallPermissions.tsx index dc0dac23738..854ab9dcb8f 100644 --- a/src/hooks/room/useElementCallPermissions.tsx +++ b/src/hooks/room/useElementCallPermissions.tsx @@ -16,22 +16,26 @@ import { LocalRoom } from "../../models/LocalRoom"; import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; import Modal from "../../Modal"; import { RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types"; -import { DefaultCallApplicationSlot, RtcSlotEventContent, slotDescriptionToId } from "matrix-js-sdk/src/matrixrtc"; +import { + DefaultCallApplicationDescription, + DefaultCallApplicationSlot, + MatrixRTCSession, +} from "matrix-js-sdk/src/matrixrtc"; +import { slotDescriptionToId } from "matrix-js-sdk/src/matrixrtc"; type ElementCallPermissions = { canStartCall: boolean; canAdjustCallPermissions: boolean; enableCallInRoom(): void; disableCallInRoom(): void; -} - +}; /** * Hook for adjusting permissions for enabling Element Call. * This uses the legacy state controlled system. * @param room the room to track */ -function useLegacyCallPermissions(room: Room| LocalRoom): ElementCallPermissions { +function useLegacyCallPermissions(room: Room | LocalRoom): ElementCallPermissions { const [powerLevelContent, maySend, elementCallEnabled] = useRoomState( room, useCallback( @@ -42,7 +46,7 @@ function useLegacyCallPermissions(room: Room| LocalRoom): ElementCallPermissions return [ content ?? {}, state?.maySendStateEvent(EventType.RoomPowerLevels, room.client.getSafeUserId()), - content?.events?.[ElementCallMemberEventType.name] === 0 + content?.events?.[ElementCallMemberEventType.name] === 0, ] as const; }, [room.client], @@ -57,8 +61,7 @@ function useLegacyCallPermissions(room: Room| LocalRoom): ElementCallPermissions newContent.events[ElementCallEventType.name] = isPublic ? moderatorLevel : userLevel; newContent.events[ElementCallMemberEventType.name] = userLevel; room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); - },[room, powerLevelContent]); - + }, [room, powerLevelContent]); const disableCallInRoom = useCallback(() => { const newContent = { events: {}, ...powerLevelContent }; @@ -66,7 +69,7 @@ function useLegacyCallPermissions(room: Room| LocalRoom): ElementCallPermissions newContent.events[ElementCallEventType.name] = adminLevel; newContent.events[ElementCallMemberEventType.name] = adminLevel; room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); - },[room, powerLevelContent]); + }, [room, powerLevelContent]); return { canStartCall: elementCallEnabled, @@ -76,20 +79,17 @@ function useLegacyCallPermissions(room: Room| LocalRoom): ElementCallPermissions }; } - /** * Hook for adjusting permissions for enabling Element Call. * This requires MSC4354 (Sticky events) to work. * @param room the room to track */ -const useSlotsCallPermissions = ( - room: Room | LocalRoom, -): ElementCallPermissions => { - const slotId = slotDescriptionToId({id: "", application: "m.call"}); +const useSlotsCallPermissions = (room: Room | LocalRoom): ElementCallPermissions => { + const slotId = slotDescriptionToId(DefaultCallApplicationDescription); const [maySendSlot, hasRoomSlot] = useRoomState(room, () => [ room.currentState.mayClientSendStateEvent(EventType.RTCSlot, room.client), // TODO: Replace with proper const - room.currentState.getStateEvents(EventType.RTCSlot, slotId)?.getContent().application.type === DefaultCallApplicationSlot.application.type, + MatrixRTCSession.getRtcSlot(room, DefaultCallApplicationDescription) !== null, ]); // TODO: Check that we are allowed to create audio/video calls, when the telephony PR lands. @@ -101,8 +101,8 @@ const useSlotsCallPermissions = ( title: "Do you want to allow calls in this room?", description: (

- This room doesn't currently permit calling. If you continue, other users will - be able to place calls in the future. You may turn this off in the Room Settings. + This room doesn't currently permit calling. If you continue, other users will be able to place calls + in the future. You may turn this off in the Room Settings.

), button: _t("action|continue"), @@ -111,18 +111,20 @@ const useSlotsCallPermissions = ( if (!confirmed) { return false; } - await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { - "application": DefaultCallApplicationSlot.application - }, slotId); + await room.client.sendStateEvent( + room.roomId, + "org.matrix.msc4143.rtc.slot", + { + application: DefaultCallApplicationSlot.application, + }, + slotId, + ); return true; }, [room, hasRoomSlot]); const removeElementCallSlot = useCallback(async (): Promise => { - if (hasRoomSlot) { - await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", { }, slotId); - } - }, [room, hasRoomSlot]); - + await room.client.sendStateEvent(room.roomId, "org.matrix.msc4143.rtc.slot", {}, slotId); + }, [room]); return { canStartCall: hasRoomSlot, @@ -135,13 +137,13 @@ const useSlotsCallPermissions = ( /** * Get and set whether an Element Call session may take place. If MSC4354 is enabled, * this will use the new slots flow. Otherwise, this will fallback to the older state-based permissions. - * @param room - * @returns + * @param room + * @returns */ -export function useElementCallPermissions (room: Room | LocalRoom): ElementCallPermissions { +export function useElementCallPermissions(room: Room | LocalRoom): ElementCallPermissions { // We load both, to avoid conditional hook rendering on settings change. const slotsPerms = useSlotsCallPermissions(room); const legacyPerms = useLegacyCallPermissions(room); const isMSC4354Enabled = useFeatureEnabled("feature_element_call_nextgen"); return isMSC4354Enabled ? slotsPerms : legacyPerms; -} \ No newline at end of file +} From 34f19f0c20eeb714291dfccccaba67f49d479d40 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 28 Oct 2025 17:18:35 +0000 Subject: [PATCH 10/11] Refactor EncryptionCard -> InformationCard --- res/css/_common.pcss | 2 +- .../dialogs/_ConfirmKeyStorageOffDialog.pcss | 2 +- .../security/_AccessSecretStorageDialog.pcss | 4 +-- .../settings/encryption/_EncryptionCard.pcss | 22 ++++++------ .../_EncryptionCardEmphasisedContent.pcss | 2 +- .../InformationCard.tsx} | 25 +++++++++++--- .../structures/auth/SetupEncryptionBody.tsx | 14 ++++---- .../auth/InteractiveAuthEntryComponents.tsx | 11 +++--- .../dialogs/ConfirmKeyStorageOffDialog.tsx | 11 +++--- .../security/AccessSecretStorageDialog.tsx | 11 +++--- .../settings/encryption/ChangeRecoveryKey.tsx | 19 +++++------ .../encryption/DeleteKeyStoragePanel.tsx | 11 +++--- .../encryption/EncryptionCardButtons.tsx | 16 --------- .../EncryptionCardEmphasisedContent.tsx | 2 +- .../settings/encryption/ResetIdentityBody.tsx | 11 +++--- .../CompleteSecurity-test.tsx.snap | 16 ++++----- ...teractiveAuthEntryComponents-test.tsx.snap | 6 ++-- .../ConfirmKeyStorageOffDialog-test.tsx.snap | 6 ++-- .../encryption/EncryptionCard-test.tsx | 8 ++--- .../ChangeRecoveryKey-test.tsx.snap | 30 ++++++++-------- .../DeleteKeyStoragePanel-test.tsx.snap | 8 ++--- .../EncryptionCard-test.tsx.snap | 6 ++-- .../ResetIdentityPanel-test.tsx.snap | 34 +++++++++---------- .../EncryptionUserSettingsTab-test.tsx.snap | 8 ++--- 24 files changed, 139 insertions(+), 146 deletions(-) rename src/components/{views/settings/encryption/EncryptionCard.tsx => structures/InformationCard.tsx} (63%) delete mode 100644 src/components/views/settings/encryption/EncryptionCardButtons.tsx diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 908da2bdda6..10d3439c493 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -593,7 +593,7 @@ legend { .mx_Dialog button:not( .mx_EncryptionUserSettingsTab button, - .mx_EncryptionCard button, + .mx_InformationCard button, .mx_UserProfileSettings button, .mx_ShareDialog button, .mx_UnpinAllDialog button, diff --git a/res/css/views/dialogs/_ConfirmKeyStorageOffDialog.pcss b/res/css/views/dialogs/_ConfirmKeyStorageOffDialog.pcss index 5ac53c7b706..95fc3c5e2f9 100644 --- a/res/css/views/dialogs/_ConfirmKeyStorageOffDialog.pcss +++ b/res/css/views/dialogs/_ConfirmKeyStorageOffDialog.pcss @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. width: 600px; } - .mx_EncryptionCard { + .mx_InformationCard { text-align: center; } } diff --git a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss index 2c78a62f8d1..25576aaa718 100644 --- a/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss +++ b/res/css/views/dialogs/security/_AccessSecretStorageDialog.pcss @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ .mx_AccessSecretStorageDialog { - &.mx_EncryptionCard { + &.mx_InformationCard { /* override some styles that we don't need */ border: 0px none; box-shadow: none; @@ -56,7 +56,7 @@ Please see LICENSE files in the repository root for full details. } } - .mx_EncryptionCard_buttons { + .mx_InformationCard_buttons { margin-top: var(--cpd-space-20x); } } diff --git a/res/css/views/settings/encryption/_EncryptionCard.pcss b/res/css/views/settings/encryption/_EncryptionCard.pcss index 5aba3fe7d75..49284ff5641 100644 --- a/res/css/views/settings/encryption/_EncryptionCard.pcss +++ b/res/css/views/settings/encryption/_EncryptionCard.pcss @@ -5,17 +5,18 @@ * Please see LICENSE files in the repository root for full details. */ -.mx_EncryptionCard { +.mx_InformationCard { display: flex; flex-direction: column; gap: var(--cpd-space-8x); - padding: var(--cpd-space-10x); border-radius: var(--cpd-space-4x); /* From figma */ - box-shadow: 0 1.2px 2.4px 0 rgb(27, 29, 34, 0.15); - border: 1px solid var(--cpd-color-gray-400); - .mx_EncryptionCard_header { + border: 0px none; + box-shadow: none; + padding: 0px; + + .mx_InformationCard_header { display: flex; flex-direction: column; gap: var(--cpd-space-4x); @@ -31,15 +32,14 @@ } } - /* extra class for specifying that we don't need a border */ - &.mx_EncryptionCard_noBorder { - border: 0px none; - box-shadow: none; - padding: 0px; + &.mx_InformationCard_border { + padding: var(--cpd-space-10x); + box-shadow: 0 1.2px 2.4px 0 rgb(27, 29, 34, 0.15); + border: 1px solid var(--cpd-color-gray-400); } } -.mx_EncryptionCard_buttons { +.mx_InformationCard_buttons { display: flex; flex-direction: column; gap: var(--cpd-space-4x); diff --git a/res/css/views/settings/encryption/_EncryptionCardEmphasisedContent.pcss b/res/css/views/settings/encryption/_EncryptionCardEmphasisedContent.pcss index 6b18fcff650..4c8573a26b6 100644 --- a/res/css/views/settings/encryption/_EncryptionCardEmphasisedContent.pcss +++ b/res/css/views/settings/encryption/_EncryptionCardEmphasisedContent.pcss @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -.mx_EncryptionCard_emphasisedContent { +.mx_InformationCard_emphasisedContent { span { font: var(--cpd-font-body-md-medium); text-align: center; diff --git a/src/components/views/settings/encryption/EncryptionCard.tsx b/src/components/structures/InformationCard.tsx similarity index 63% rename from src/components/views/settings/encryption/EncryptionCard.tsx rename to src/components/structures/InformationCard.tsx index 89ce2cbb17b..1aed8d3bdb3 100644 --- a/src/components/views/settings/encryption/EncryptionCard.tsx +++ b/src/components/structures/InformationCard.tsx @@ -9,7 +9,7 @@ import React, { type JSX, type PropsWithChildren, type ComponentType, type SVGAt import { BigIcon, Heading } from "@vector-im/compound-web"; import classNames from "classnames"; -interface EncryptionCardProps { +interface InformationCardProps { /** * CSS class name to apply to the card. */ @@ -26,26 +26,41 @@ interface EncryptionCardProps { * Whether this icon shows a destructive action. */ destructive?: boolean; + /** + * Whether the component should have a border + */ + border?: boolean; /** * The icon to display. */ Icon: ComponentType>; } +/** + * A component to present action buttons at the bottom of an {@link EncryptionCard} + * (mostly as somewhere for the common CSS to live). + */ +export function InformationCardButtons({ children }: PropsWithChildren): JSX.Element { + return
{children}
; +} + + /** * A styled card for encryption settings. + * Note: This was previously known as the EncryptionCard */ -export function EncryptionCard({ +export function InformationCard({ title, description, className, + border = true, destructive = false, Icon, children, -}: PropsWithChildren): JSX.Element { +}: PropsWithChildren): JSX.Element { return ( -
-
+
+
diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 5098265e67f..c0fb423bd40 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -22,8 +22,7 @@ import EncryptionPanel from "../../views/right_panel/EncryptionPanel"; import AccessibleButton from "../../views/elements/AccessibleButton"; import Spinner from "../../views/elements/Spinner"; import { ResetIdentityDialog } from "../../views/dialogs/ResetIdentityDialog"; -import { EncryptionCard } from "../../views/settings/encryption/EncryptionCard"; -import { EncryptionCardButtons } from "../../views/settings/encryption/EncryptionCardButtons"; +import { InformationCard, InformationCardButtons } from "../../structures/InformationCard"; import { EncryptionCardEmphasisedContent } from "../../views/settings/encryption/EncryptionCardEmphasisedContent"; import ExternalLink from "../../views/elements/ExternalLink"; import dispatcher from "../../../dispatcher/dispatcher"; @@ -196,10 +195,11 @@ export default class SetupEncryptionBody extends React.Component } return ( - {_t("encryption|verification|confirm_identity_description")} @@ -209,15 +209,15 @@ export default class SetupEncryptionBody extends React.Component - + {verifyButton} {useRecoveryKeyButton} {signOutButton} - - + + ); } else if (phase === Phase.Done) { let message: JSX.Element; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index a9bf8b75970..cc95ac69f31 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -23,8 +23,7 @@ import Field from "../elements/Field"; import Spinner from "../elements/Spinner"; import CaptchaForm from "./CaptchaForm"; import { pickBestPolicyLanguage } from "../../../Terms.ts"; -import { EncryptionCardButtons } from "../settings/encryption/EncryptionCardButtons.tsx"; -import { EncryptionCard } from "../settings/encryption/EncryptionCard.tsx"; +import { InformationCard, InformationCardButtons } from "../../structures/InformationCard.tsx"; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -973,14 +972,14 @@ export class MasUnlockCrossSigningAuthEntry extends FallbackAuthEntry<{ public render(): React.ReactNode { return ( - - + - - + + ); } } diff --git a/src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx b/src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx index d6a5f79aebb..76199d67e78 100644 --- a/src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx +++ b/src/components/views/dialogs/ConfirmKeyStorageOffDialog.tsx @@ -12,8 +12,7 @@ import { PopOutIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { EncryptionCard } from "../settings/encryption/EncryptionCard"; -import { EncryptionCardButtons } from "../settings/encryption/EncryptionCardButtons"; +import { InformationCard, InformationCardButtons } from "../../structures/InformationCard"; import { type OpenToTabPayload } from "../../../dispatcher/payloads/OpenToTabPayload"; import { Action } from "../../../dispatcher/actions"; import { UserTab } from "./UserTab"; @@ -52,7 +51,7 @@ export default class ConfirmKeyStorageOffDialog extends React.Component { public render(): React.ReactNode { return ( - { ), })} - + - - + + ); } } diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 94d9a30e547..b0bac0ec5e8 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -15,8 +15,7 @@ import { type SecretStorage } from "matrix-js-sdk/src/matrix"; import { Flex } from "../../../../../packages/shared-components/src/utils/Flex"; import { _t } from "../../../../languageHandler"; -import { EncryptionCard } from "../../settings/encryption/EncryptionCard"; -import { EncryptionCardButtons } from "../../settings/encryption/EncryptionCardButtons"; +import { InformationCard, InformationCardButtons } from "../../../structures/InformationCard"; import BaseDialog from "../BaseDialog"; // Don't shout at the user that their key is invalid every time they type a key: wait a short time @@ -192,14 +191,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent
{recoveryKeyFeedback} - + - +
); @@ -208,14 +207,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent - {content} - + ); } diff --git a/src/components/views/settings/encryption/ChangeRecoveryKey.tsx b/src/components/views/settings/encryption/ChangeRecoveryKey.tsx index ae568efc627..174471a8860 100644 --- a/src/components/views/settings/encryption/ChangeRecoveryKey.tsx +++ b/src/components/views/settings/encryption/ChangeRecoveryKey.tsx @@ -21,13 +21,12 @@ import CopyIcon from "@vector-im/compound-design-tokens/assets/web/icons/copy"; import KeyIcon from "@vector-im/compound-design-tokens/assets/web/icons/key-solid"; import { _t } from "../../../../languageHandler"; -import { EncryptionCard } from "./EncryptionCard"; +import { InformationCard, InformationCardButtons } from "../../../structures/InformationCard.tsx"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; import { useAsyncMemo } from "../../../../hooks/useAsyncMemo"; import { copyPlaintext } from "../../../../utils/strings"; import { initialiseDehydrationIfEnabled } from "../../../../utils/device/dehydration.ts"; import { withSecretStorageKeyCache } from "../../../../SecurityManager"; -import { EncryptionCardButtons } from "./EncryptionCardButtons"; import { logErrorAndShowErrorDialog } from "../../../../utils/ErrorUtils.tsx"; import { RECOVERY_ACCOUNT_DATA_KEY } from "../../../../DeviceListener"; @@ -166,14 +165,14 @@ export function ChangeRecoveryKey({ pages={pages} onPageClick={onCancelClick} /> - {content} - + ); } @@ -245,12 +244,12 @@ function InformationPanel({ onContinueClick, onCancelClick }: InformationPanelPr {_t("settings|encryption|recovery|set_up_recovery_secondary_description")} - + - + ); } @@ -292,12 +291,12 @@ function KeyPanel({ recoveryKey, onConfirmClick, onCancelClick }: KeyPanelProps)
- + - + ); } @@ -367,12 +366,12 @@ function KeyForm({ onCancelClick, onSubmit, recoveryKey, submitButtonLabel }: Ke {_t("settings|encryption|recovery|enter_key_error")} )} - + - + ); } diff --git a/src/components/views/settings/encryption/DeleteKeyStoragePanel.tsx b/src/components/views/settings/encryption/DeleteKeyStoragePanel.tsx index d4f80d7eeef..143293d1a87 100644 --- a/src/components/views/settings/encryption/DeleteKeyStoragePanel.tsx +++ b/src/components/views/settings/encryption/DeleteKeyStoragePanel.tsx @@ -11,10 +11,9 @@ import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error- import React, { type JSX, useCallback, useState } from "react"; import { _t } from "../../../../languageHandler"; -import { EncryptionCard } from "./EncryptionCard"; +import { InformationCard, InformationCardButtons } from "../../../structures/InformationCard.tsx"; import { useKeyStoragePanelViewModel } from "../../../viewmodels/settings/encryption/KeyStoragePanelViewModel"; import SdkConfig from "../../../../SdkConfig"; -import { EncryptionCardButtons } from "./EncryptionCardButtons"; import { EncryptionCardEmphasisedContent } from "./EncryptionCardEmphasisedContent"; interface Props { @@ -49,7 +48,7 @@ export function DeleteKeyStoragePanel({ onFinish }: Props): JSX.Element { pages={[_t("settings|encryption|title"), _t("settings|encryption|delete_key_storage|breadcrumb_page")]} onPageClick={onFinish} /> - - + - - + + ); } diff --git a/src/components/views/settings/encryption/EncryptionCardButtons.tsx b/src/components/views/settings/encryption/EncryptionCardButtons.tsx deleted file mode 100644 index 06e5ef60d7f..00000000000 --- a/src/components/views/settings/encryption/EncryptionCardButtons.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only - * Please see LICENSE files in the repository root for full details. - */ - -import React, { type JSX, type PropsWithChildren } from "react"; - -/** - * A component to present action buttons at the bottom of an {@link EncryptionCard} - * (mostly as somewhere for the common CSS to live). - */ -export function EncryptionCardButtons({ children }: PropsWithChildren): JSX.Element { - return
{children}
; -} diff --git a/src/components/views/settings/encryption/EncryptionCardEmphasisedContent.tsx b/src/components/views/settings/encryption/EncryptionCardEmphasisedContent.tsx index 4ad472ee823..d78ee2bb932 100644 --- a/src/components/views/settings/encryption/EncryptionCardEmphasisedContent.tsx +++ b/src/components/views/settings/encryption/EncryptionCardEmphasisedContent.tsx @@ -19,7 +19,7 @@ export function EncryptionCardEmphasisedContent({ children }: PropsWithChildren) direction="column" gap="var(--cpd-space-3x)" align="normal" - className="mx_EncryptionCard_emphasisedContent" + className="mx_InformationCard_emphasisedContent" > {children} diff --git a/src/components/views/settings/encryption/ResetIdentityBody.tsx b/src/components/views/settings/encryption/ResetIdentityBody.tsx index 91b7d65b5d4..3b3c59ac73e 100644 --- a/src/components/views/settings/encryption/ResetIdentityBody.tsx +++ b/src/components/views/settings/encryption/ResetIdentityBody.tsx @@ -12,9 +12,8 @@ import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error- import React, { type JSX, useState } from "react"; import { _t } from "../../../../languageHandler"; -import { EncryptionCard } from "./EncryptionCard"; +import { InformationCard, InformationCardButtons } from "../../../structures/InformationCard.tsx"; import { uiAuthCallback } from "../../../../CreateCrossSigning"; -import { EncryptionCardButtons } from "./EncryptionCardButtons"; import { EncryptionCardEmphasisedContent } from "./EncryptionCardEmphasisedContent"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; @@ -68,7 +67,7 @@ export function ResetIdentityBody({ onCancelClick, onReset, variant }: ResetIden const [inProgress, setInProgress] = useState(false); return ( - + @@ -83,7 +82,7 @@ export function ResetIdentityBody({ onCancelClick, onReset, variant }: ResetIden {variant === "compromised" && {_t("settings|encryption|advanced|breadcrumb_warning")}} - +
should display the reset identity panel w
should display the reset identity panel w
    should display the reset identity panel w
+ + + + +} \ No newline at end of file diff --git a/src/hooks/room/useElementCallPermissions.tsx b/src/hooks/room/useElementCallPermissions.tsx index 854ab9dcb8f..bd0bf4956dc 100644 --- a/src/hooks/room/useElementCallPermissions.tsx +++ b/src/hooks/room/useElementCallPermissions.tsx @@ -6,15 +6,13 @@ Please see LICENSE files in the repository root for full details. */ import { EventType, JoinRule, RoomState, type Room } from "matrix-js-sdk/src/matrix"; -import React, { useCallback } from "react"; +import { useCallback } from "react"; import { useFeatureEnabled } from "../useSettings"; import { useRoomState } from "../useRoomState"; import { _t } from "../../languageHandler"; import { ElementCallEventType, ElementCallMemberEventType } from "../../call-types"; import { LocalRoom } from "../../models/LocalRoom"; -import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; -import Modal from "../../Modal"; import { RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types"; import { DefaultCallApplicationDescription, @@ -23,11 +21,27 @@ import { } from "matrix-js-sdk/src/matrixrtc"; import { slotDescriptionToId } from "matrix-js-sdk/src/matrixrtc"; +export enum ElementCallPromptAction { + /** + * Do not allow calls to be started without permission being set. + */ + NoPrompt, + /** + * Prompt before allowing a call to be started. + */ + Prompt, + /** + * Do not prompt, just configure permissions automatically. + */ + AutoAllow, +} + type ElementCallPermissions = { canStartCall: boolean; canAdjustCallPermissions: boolean; - enableCallInRoom(): void; - disableCallInRoom(): void; + permissionsPromptAction: ElementCallPromptAction; + enableCallInRoom(): Promise; + disableCallInRoom(): Promise; }; /** @@ -53,27 +67,28 @@ function useLegacyCallPermissions(room: Room | LocalRoom): ElementCallPermission ), ); - const enableCallInRoom = useCallback(() => { + const enableCallInRoom = useCallback(async () => { const newContent = { events: {}, ...powerLevelContent }; const userLevel = newContent.events[EventType.RoomMessage] ?? powerLevelContent.users_default ?? 0; const moderatorLevel = powerLevelContent.kick ?? 50; const isPublic = room.getJoinRule() === JoinRule.Public; newContent.events[ElementCallEventType.name] = isPublic ? moderatorLevel : userLevel; newContent.events[ElementCallMemberEventType.name] = userLevel; - room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); + await room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); }, [room, powerLevelContent]); - const disableCallInRoom = useCallback(() => { + const disableCallInRoom = useCallback(async () => { const newContent = { events: {}, ...powerLevelContent }; const adminLevel = newContent.events[EventType.RoomPowerLevels] ?? powerLevelContent.state_default ?? 100; newContent.events[ElementCallEventType.name] = adminLevel; newContent.events[ElementCallMemberEventType.name] = adminLevel; - room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); + await room.client.sendStateEvent(room.roomId, EventType.RoomPowerLevels, newContent); }, [room, powerLevelContent]); return { canStartCall: elementCallEnabled, canAdjustCallPermissions: maySend, + permissionsPromptAction: ElementCallPromptAction.NoPrompt, enableCallInRoom, disableCallInRoom, }; @@ -86,31 +101,15 @@ function useLegacyCallPermissions(room: Room | LocalRoom): ElementCallPermission */ const useSlotsCallPermissions = (room: Room | LocalRoom): ElementCallPermissions => { const slotId = slotDescriptionToId(DefaultCallApplicationDescription); - const [maySendSlot, hasRoomSlot] = useRoomState(room, () => [ + const [maySendSlot, hasRoomSlot, canEveryoneAdjustPermissions] = useRoomState(room, () => [ room.currentState.mayClientSendStateEvent(EventType.RTCSlot, room.client), // TODO: Replace with proper const MatrixRTCSession.getRtcSlot(room, DefaultCallApplicationDescription) !== null, + !room.getJoinedMembers().some((v) => !room.currentState.maySendStateEvent(EventType.RTCSlot, v.userId)), ]); // TODO: Check that we are allowed to create audio/video calls, when the telephony PR lands. - const createElementCallSlot = useCallback(async (): Promise => { - if (hasRoomSlot) { - return true; - } - const { finished } = Modal.createDialog(QuestionDialog, { - title: "Do you want to allow calls in this room?", - description: ( -

- This room doesn't currently permit calling. If you continue, other users will be able to place calls - in the future. You may turn this off in the Room Settings. -

- ), - button: _t("action|continue"), - }); - const [confirmed] = await finished; - if (!confirmed) { - return false; - } + const createElementCallSlot = useCallback(async (): Promise => { await room.client.sendStateEvent( room.roomId, "org.matrix.msc4143.rtc.slot", @@ -119,7 +118,6 @@ const useSlotsCallPermissions = (room: Room | LocalRoom): ElementCallPermissions }, slotId, ); - return true; }, [room, hasRoomSlot]); const removeElementCallSlot = useCallback(async (): Promise => { @@ -129,6 +127,9 @@ const useSlotsCallPermissions = (room: Room | LocalRoom): ElementCallPermissions return { canStartCall: hasRoomSlot, canAdjustCallPermissions: maySendSlot, + permissionsPromptAction: canEveryoneAdjustPermissions + ? ElementCallPromptAction.AutoAllow + : ElementCallPromptAction.Prompt, enableCallInRoom: createElementCallSlot, disableCallInRoom: removeElementCallSlot, }; diff --git a/src/hooks/room/useRoomCall.tsx b/src/hooks/room/useRoomCall.tsx index b186284cca3..1eae2ca4892 100644 --- a/src/hooks/room/useRoomCall.tsx +++ b/src/hooks/room/useRoomCall.tsx @@ -11,7 +11,7 @@ import { type Room } from "matrix-js-sdk/src/matrix"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; -import type React from "react"; +import React from "react"; import { useFeatureEnabled, useSettingValue } from "../useSettings"; import SdkConfig from "../../SdkConfig"; import { useEventEmitter, useEventEmitterState } from "../useEventEmitter"; @@ -37,7 +37,11 @@ import { isVideoRoom } from "../../utils/video-rooms"; import { UIFeature } from "../../settings/UIFeature"; import { type InteractionName } from "../../PosthogTrackers"; import { LocalRoom, LocalRoomState } from "../../models/LocalRoom"; -import { useElementCallPermissions } from "./useElementCallPermissions"; +import { ElementCallPromptAction, useElementCallPermissions } from "./useElementCallPermissions"; +import Modal from "../../Modal"; +import QuestionDialog from "../../components/views/dialogs/QuestionDialog"; +import { logger } from "matrix-js-sdk/src/logger"; +import ConfirmEnableCallDialog from "../../components/views/dialogs/ConfirmEnableCallDialog"; export enum PlatformCallType { ElementCall, @@ -107,7 +111,12 @@ export const useRoomCall = ( const useElementCallExclusively = useMemo(() => { return SdkConfig.get("element_call").use_exclusively; }, []); - const { canStartCall: mayCreateElementCalls } = useElementCallPermissions(room); + const { + canStartCall: mayCreateElementCalls, + permissionsPromptAction: elementCallPromptAction, + canAdjustCallPermissions: canAdjustElementCallPermissions, + enableCallInRoom, + } = useElementCallPermissions(room); const hasLegacyCall = useEventEmitterState( LegacyCallHandler.instance, @@ -144,13 +153,18 @@ export const useRoomCall = ( const callOptions = useMemo((): PlatformCallType[] => { const options: PlatformCallType[] = []; if (memberCount <= 2) { - options.push(PlatformCallType.LegacyCall); + // options.push(PlatformCallType.LegacyCall); } else if (mayEditWidgets || hasJitsiWidget) { options.push(PlatformCallType.JitsiCall); } if (groupCallsEnabled) { if (hasGroupCall || mayCreateElementCalls) { options.push(PlatformCallType.ElementCall); + } else if ( + canAdjustElementCallPermissions && + elementCallPromptAction !== ElementCallPromptAction.NoPrompt + ) { + options.push(PlatformCallType.ElementCall); } if (useElementCallExclusively && mayCreateElementCalls && !hasJitsiWidget) { return [PlatformCallType.ElementCall]; @@ -210,7 +224,12 @@ export const useRoomCall = ( } if (callOptions.length === 0 && !mayCreateElementCalls) { - return State.CallingDisabled; + // Element call is not enabled for this room and we are not meant to prompt it to be enabled. + if (!canAdjustElementCallPermissions || elementCallPromptAction === ElementCallPromptAction.NoPrompt) { + return State.NoPermission; + } + // Otherwise, we can prompt. + return State.NoCall; } if (!callOptions.includes(PlatformCallType.LegacyCall) && !mayCreateElementCalls && !mayEditWidgets) { @@ -225,45 +244,63 @@ export const useRoomCall = ( hasLegacyCall, hasManagedHybridWidget, mayCreateElementCalls, + canAdjustElementCallPermissions, mayEditWidgets, promptPinWidget, room.roomId, ]); - const voiceCallClick = useCallback( - (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => { - evt?.stopPropagation(); + const onPlaceCall = useCallback( + (callType: CallType, platform: PlatformCallType, skipLobby?: boolean) => { if (widget && promptPinWidget) { WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top); - } else { - void (async () => { - await placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined); - })(); + return; } + + void (async () => { + // If we are placing an Element Call, we may not have permission to place it yet. + // MatrixRTC requires a slot to be in the room first, so prompt for this. + if (platform === PlatformCallType.ElementCall && !mayCreateElementCalls) { + if (elementCallPromptAction === ElementCallPromptAction.NoPrompt) { + throw Error("Should not have got to this stage"); + } else if (elementCallPromptAction === ElementCallPromptAction.Prompt) { + const { finished } = Modal.createDialog(ConfirmEnableCallDialog); + if (!(await finished)[0]) { + return; + } + } // otherwise, this is set to AutoAllow and we can just continue. + await enableCallInRoom(); + } + await placeCall(room, callType, platform, skipLobby); + })().catch((ex) => { + logger.error("Failed to place call", ex); + }); }, - [promptPinWidget, room, widget], + [room, widget, promptPinWidget, mayCreateElementCalls, elementCallPromptAction, enableCallInRoom], + ); + + const voiceCallClick = useCallback( + (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => { + evt?.stopPropagation(); + onPlaceCall(CallType.Voice, callPlatformType, evt?.shiftKey || undefined); + }, + [onPlaceCall], ); const videoCallClick = useCallback( (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => { evt?.stopPropagation(); - if (widget && promptPinWidget) { - WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Top); - } else { - // If we have pressed shift then always skip the lobby, otherwise `undefined` will defer - // to the defaults of the call implementation. - void (async () => { - await placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined); - })(); - } + onPlaceCall(CallType.Video, callPlatformType, evt?.shiftKey || undefined); }, - [widget, promptPinWidget, room], + [onPlaceCall], ); let voiceCallDisabledReason: string | null; let videoCallDisabledReason: string | null; switch (state) { case State.CallingDisabled: - voiceCallDisabledReason = videoCallDisabledReason = _t("voip|disabled_branded_call", { brand: SdkConfig.get("element_call").brand }); + voiceCallDisabledReason = videoCallDisabledReason = _t("voip|disabled_branded_call", { + brand: SdkConfig.get("element_call").brand, + }); break; case State.NoPermission: voiceCallDisabledReason = _t("voip|disabled_no_perms_start_voice_call"); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c2048f8b170..37239f35293 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -4078,7 +4078,12 @@ "video_call_started": "Video call started", "video_call_using": "Video call using:", "voice_call": "Voice call", - "you_are_presenting": "You are presenting" + "you_are_presenting": "You are presenting", + "enable_call_dialog": { + "title": "Allow video calling in this room?", + "description": "Any members in this room will be able to start a video call. You can always change this by going to Room Settings > Voice & video", + "accept_button": "Allow video calls" + } }, "web_default_device_name": "%(appName)s: %(browserName)s on %(osName)s", "welcome_to_element": "Welcome to Element",