Complete first pass on call permissions

This commit is contained in:
Half-Shot 2025-10-27 11:21:55 +00:00
parent 4b80d23a1f
commit 0656fff3cb
5 changed files with 96 additions and 107 deletions

View File

@ -28,6 +28,7 @@ const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ 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<ElementCallSwitchProps> = ({ room }) => {
setBusy(false);
}
})();
}, [canStartCall, enableCallInRoom, disableCallInRoom]);
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;

View File

@ -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<RoomPowerLevelsEventContent>();
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<boolean> => {
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<void> => {
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;
}

View File

@ -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<boolean> => {
if (hasElementCallSlot) {
return true;
}
const { finished } = Modal.createDialog(QuestionDialog, {
title: "Do you want to allow calls in this room?",
description: (
<p>
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.
</p>
),
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<void> => {
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,
};
};

View File

@ -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",

View File

@ -641,6 +641,7 @@ export const SETTINGS: Settings = {
[[UNSTABLE_MSC4354_STICKY_EVENTS]],
undefined,
_td("labs|feature_element_call_msc4354_msc_support"),
false,
),
default: false,