From 0656fff3cbf700aaca31030a4803632f8914619f Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 27 Oct 2025 11:21:55 +0000 Subject: [PATCH] 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 440df901ab..7c30abea7b 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 94c256ed94..5910913168 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; -} +} -function useLegacyCallPermissions(room: Room | LocalRoom): ElementCallPermissions { +/** + * 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]); + + + 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 d5541ac9e2..6bb7bcb5b3 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 56a168fb0e..e6acd0129c 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 3e68cc114a..ef19a6d488 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,