From b42191163a0dd31e27b6247732b05ccdc714ec49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 4 Oct 2022 13:56:43 +0200 Subject: [PATCH 01/26] Correctly setup EC perms on room creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/createRoom.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/createRoom.ts b/src/createRoom.ts index 19d92164a02..82d7d0a9cfe 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -168,6 +168,15 @@ export default async function createRoom(opts: IOpts): Promise { }, }; } + } else { + createOpts.power_level_content_override = { + events: { + // Element Call should be disabled by default + [ElementCall.MEMBER_EVENT_TYPE.name]: 100, + // Make sure only admins can enable it + [ElementCall.CALL_EVENT_TYPE.name]: 100, + }, + }; } // By default, view the room after creating it From 055faf6baa326c57366b664ff061962582879ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 4 Oct 2022 13:57:14 +0200 Subject: [PATCH 02/26] Add `VoipRoomSettingsTab` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/room/VoipRoomSettingsTab.tsx | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx new file mode 100644 index 00000000000..729dc7fcf33 --- /dev/null +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -0,0 +1,96 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useCallback, useMemo, useState } from 'react'; +import { logger } from "matrix-js-sdk/src/logger"; +import { JoinRule } from "matrix-js-sdk/src/@types/partials"; +import { EventType } from "matrix-js-sdk/src/@types/event"; + +import { _t } from "../../../../../languageHandler"; +import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; +import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; +import SettingsSubsection from "../../shared/SettingsSubsection"; +import SettingsTab from "../SettingsTab"; +import Modal from "../../../../../Modal"; +import ErrorDialog from "../../../dialogs/ErrorDialog"; +import { ElementCall } from "../../../../../models/Call"; +import { useRoomState } from "../../../../../hooks/useRoomState"; + +interface ElementCallSwitchProps { + roomId: string; +} + +const ElementCallSwitch: React.FC = ({ roomId }) => { + const room = useMemo(() => MatrixClientPeg.get().getRoom(roomId), [roomId]); + const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]); + const [content, events] = useRoomState(room, useCallback((state) => { + const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent(); + return [content ?? {}, content?.["events"] ?? {}]; + }, [])); + + const [elementCallEnabled, setElementCallEnabled] = useState(() => { + return events[ElementCall.MEMBER_EVENT_TYPE.name] === 0; + }); + + const onChange = useCallback((enabled: boolean): void => { + setElementCallEnabled(enabled); + + if (enabled) { + events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? 50 : 0; + events[ElementCall.MEMBER_EVENT_TYPE.name] = 0; + } else { + events[ElementCall.CALL_EVENT_TYPE.name] = 100; + events[ElementCall.MEMBER_EVENT_TYPE.name] = 100; + } + + MatrixClientPeg.get().sendStateEvent(roomId, EventType.RoomPowerLevels, { + "events": events, + ...content, + }).catch(e => { + logger.error(e); + + Modal.createDialog(ErrorDialog, { + title: _t('Error changing power level requirement'), + description: _t( + "An error occurred changing the room's power level requirements. Ensure you have sufficient " + + "permissions and try again.", + ), + }); + }); + }, [roomId, content, events, isPublic]); + + return ; +}; + +interface Props { + roomId: string; +} + +export const VoipRoomSettingsTab: React.FC = ({ roomId }) => { + return + + + + ; +}; From 0cdf7ca72ffb79ab52e0fca55e03f08514ea60d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 4 Oct 2022 13:57:43 +0200 Subject: [PATCH 03/26] Show `VoipRoomSettingsTab` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/dialogs/_RoomSettingsDialog.pcss | 4 ++++ src/components/views/dialogs/RoomSettingsDialog.tsx | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/res/css/views/dialogs/_RoomSettingsDialog.pcss b/res/css/views/dialogs/_RoomSettingsDialog.pcss index a242a99596b..8631ec5d7d5 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.pcss +++ b/res/css/views/dialogs/_RoomSettingsDialog.pcss @@ -21,6 +21,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/settings.svg'); } +.mx_RoomSettingsDialog_voiceIcon::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); +} + .mx_RoomSettingsDialog_securityIcon::before { mask-image: url('$(res)/img/element-icons/security.svg'); } diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index ce8d24cd3fd..6454bb32fb3 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -32,8 +32,10 @@ import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import BaseDialog from "./BaseDialog"; import { Action } from '../../../dispatcher/actions'; +import { VoipRoomSettingsTab } from "../settings/tabs/room/VoipRoomSettingsTab"; export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB"; +export const ROOM_VOIP_TAB = "ROOM_VOIP_TAB"; export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB"; export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB"; export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB"; @@ -96,6 +98,13 @@ export default class RoomSettingsDialog extends React.Component , "RoomSettingsGeneral", )); + tabs.push(new Tab( + ROOM_VOIP_TAB, + _td("Voice & Video"), + "mx_RoomSettingsDialog_voiceIcon", + , + "RoomSettingsVoip", + )); tabs.push(new Tab( ROOM_SECURITY_TAB, _td("Security & Privacy"), From ebcd2d533a41676cb1935b82696728b24218eccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 4 Oct 2022 13:58:03 +0200 Subject: [PATCH 04/26] Show MSC3401 perms in room settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/settings/tabs/room/RolesRoomSettingsTab.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index bab69042435..5e7da467cc5 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -31,6 +31,7 @@ import PowerSelector from "../../../elements/PowerSelector"; import SettingsFieldset from '../../SettingsFieldset'; import SettingsStore from "../../../../../settings/SettingsStore"; import { VoiceBroadcastInfoEventType } from '../../../../../voice-broadcast'; +import { ElementCall } from "../../../../../models/Call"; interface IEventShowOpts { isState?: boolean; @@ -60,6 +61,10 @@ const plEventsToShow: Record = { [EventType.Reaction]: { isState: false, hideForSpace: true }, [EventType.RoomRedaction]: { isState: false, hideForSpace: true }, + // MSC33401: Native Group VoIP signalling + [ElementCall.CALL_EVENT_TYPE.name]: { isState: true, hideForSpace: true }, + [ElementCall.MEMBER_EVENT_TYPE.name]: { isState: true, hideForSpace: true }, + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": { isState: true, hideForSpace: true }, [VoiceBroadcastInfoEventType]: { isState: true, hideForSpace: true }, @@ -244,6 +249,10 @@ export default class RolesRoomSettingsTab extends React.Component { [EventType.Reaction]: _td("Send reactions"), [EventType.RoomRedaction]: _td("Remove messages sent by me"), + // MSC33401: Native Group VoIP signalling + [ElementCall.CALL_EVENT_TYPE.name]: _td("Start Element calls"), + [ElementCall.MEMBER_EVENT_TYPE.name]: _td("Join Element calls"), + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": isSpaceRoom ? null : _td("Modify widgets"), [VoiceBroadcastInfoEventType]: _td("Voice broadcasts"), From 88c3666c8abbb42bb8966a9dc2713797f420baae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 4 Oct 2022 13:58:08 +0200 Subject: [PATCH 05/26] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ce14860df6a..1dedc6a7fa1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1644,6 +1644,8 @@ "Change server ACLs": "Change server ACLs", "Send reactions": "Send reactions", "Remove messages sent by me": "Remove messages sent by me", + "Start Element calls": "Start Element calls", + "Join Element calls": "Join Element calls", "Modify widgets": "Modify widgets", "Voice broadcasts": "Voice broadcasts", "Manage pinned events": "Manage pinned events", @@ -1686,6 +1688,9 @@ "Security & Privacy": "Security & Privacy", "Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.", "Encrypted": "Encrypted", + "Enable Element Call as an additional calling option in this room": "Enable Element Call as an additional calling option in this room", + "Element Call is end-to-end encrypted, but is currently limited to smaller numbers of users.": "Element Call is end-to-end encrypted, but is currently limited to smaller numbers of users.", + "Call type": "Call type", "Unable to revoke sharing for email address": "Unable to revoke sharing for email address", "Unable to share email address": "Unable to share email address", "Your email address hasn't been verified yet": "Your email address hasn't been verified yet", From 761e5285dcd3873ba952821522c98606aa4f74dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 4 Oct 2022 14:07:56 +0200 Subject: [PATCH 06/26] Remove `ScreenName` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/dialogs/RoomSettingsDialog.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 6454bb32fb3..ab5e90d00db 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -103,7 +103,6 @@ export default class RoomSettingsDialog extends React.Component _td("Voice & Video"), "mx_RoomSettingsDialog_voiceIcon", , - "RoomSettingsVoip", )); tabs.push(new Tab( ROOM_SECURITY_TAB, From adf9f1931e370ac3bf649f83f25d7993342fe927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 4 Oct 2022 14:41:48 +0200 Subject: [PATCH 07/26] Hide behind labs flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/dialogs/RoomSettingsDialog.tsx | 14 ++++++++------ .../settings/tabs/room/RolesRoomSettingsTab.tsx | 9 +++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index ab5e90d00db..d20aca98d9c 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -98,12 +98,14 @@ export default class RoomSettingsDialog extends React.Component , "RoomSettingsGeneral", )); - tabs.push(new Tab( - ROOM_VOIP_TAB, - _td("Voice & Video"), - "mx_RoomSettingsDialog_voiceIcon", - , - )); + if (SettingsStore.getValue("feature_group_calls")) { + tabs.push(new Tab( + ROOM_VOIP_TAB, + _td("Voice & Video"), + "mx_RoomSettingsDialog_voiceIcon", + , + )); + } tabs.push(new Tab( ROOM_SECURITY_TAB, _td("Security & Privacy"), diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 5e7da467cc5..07e65b07f7f 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -249,10 +249,6 @@ export default class RolesRoomSettingsTab extends React.Component { [EventType.Reaction]: _td("Send reactions"), [EventType.RoomRedaction]: _td("Remove messages sent by me"), - // MSC33401: Native Group VoIP signalling - [ElementCall.CALL_EVENT_TYPE.name]: _td("Start Element calls"), - [ElementCall.MEMBER_EVENT_TYPE.name]: _td("Join Element calls"), - // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": isSpaceRoom ? null : _td("Modify widgets"), [VoiceBroadcastInfoEventType]: _td("Voice broadcasts"), @@ -261,6 +257,11 @@ export default class RolesRoomSettingsTab extends React.Component { if (SettingsStore.getValue("feature_pinning")) { plEventsToLabels[EventType.RoomPinnedEvents] = _td("Manage pinned events"); } + // MSC33401: Native Group VoIP signalling + if (SettingsStore.getValue("feature_group_calls")) { + plEventsToLabels[ElementCall.CALL_EVENT_TYPE.name] = _td("Start Element calls"); + plEventsToLabels[ElementCall.MEMBER_EVENT_TYPE.name] = _td("Join Element calls"); + } const powerLevelDescriptors: Record = { "users_default": { From b92a576be461cb9504b23e8c30bac2af2e060aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 4 Oct 2022 16:04:30 +0200 Subject: [PATCH 08/26] Add `testid` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/LabelledToggleSwitch.tsx | 2 +- src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/LabelledToggleSwitch.tsx b/src/components/views/elements/LabelledToggleSwitch.tsx index 90b419c735a..c9531025c07 100644 --- a/src/components/views/elements/LabelledToggleSwitch.tsx +++ b/src/components/views/elements/LabelledToggleSwitch.tsx @@ -66,7 +66,7 @@ export default class LabelledToggleSwitch extends React.PureComponent { "mx_SettingsFlag_toggleInFront": this.props.toggleInFront, }); return ( -
+
{ firstPart } { secondPart }
diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 729dc7fcf33..afa4ea7aaaa 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -73,6 +73,7 @@ const ElementCallSwitch: React.FC = ({ roomId }) => { }, [roomId, content, events, isPublic]); return Date: Tue, 4 Oct 2022 16:04:57 +0200 Subject: [PATCH 09/26] Add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/room/RolesRoomSettingsTab-test.tsx | 107 ++++++++++++- .../tabs/room/VoipRoomSettingsTab-test.tsx | 141 ++++++++++++++++++ test/createRoom-test.ts | 16 ++ 3 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 test/components/views/settings/tabs/room/VoipRoomSettingsTab-test.tsx diff --git a/test/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx b/test/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx index 9c041de4c0d..7522f8d7441 100644 --- a/test/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx @@ -16,30 +16,35 @@ limitations under the License. import React from "react"; import { fireEvent, render, RenderResult } from "@testing-library/react"; -import { EventType, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { EventType } from "matrix-js-sdk/src/@types/event"; import RolesRoomSettingsTab from "../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab"; import { mkStubRoom, stubClient } from "../../../../../test-utils"; import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; import { VoiceBroadcastInfoEventType } from "../../../../../../src/voice-broadcast"; +import SettingsStore from "../../../../../../src/settings/SettingsStore"; +import { ElementCall } from "../../../../../../src/models/Call"; describe("RolesRoomSettingsTab", () => { const roomId = "!room:example.com"; - let rolesRoomSettingsTab: RenderResult; let cli: MatrixClient; + const renderTab = (): RenderResult => { + return render(); + }; + const getVoiceBroadcastsSelect = () => { - return rolesRoomSettingsTab.container.querySelector("select[label='Voice broadcasts']"); + return renderTab().container.querySelector("select[label='Voice broadcasts']"); }; const getVoiceBroadcastsSelectedOption = () => { - return rolesRoomSettingsTab.container.querySelector("select[label='Voice broadcasts'] option:checked"); + return renderTab().container.querySelector("select[label='Voice broadcasts'] option:checked"); }; beforeEach(() => { stubClient(); cli = MatrixClientPeg.get(); - rolesRoomSettingsTab = render(); mkStubRoom(roomId, "test room", cli); }); @@ -66,4 +71,96 @@ describe("RolesRoomSettingsTab", () => { ); }); }); + + describe("Element Call", () => { + const setGroupCallsEnabled = (val: boolean): void => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { + if (name === "feature_group_calls") return val; + }); + }; + + const getStartCallSelect = (tab: RenderResult) => { + return tab.container.querySelector("select[label='Start Element calls']"); + }; + + const getStartCallSelectedOption = (tab: RenderResult) => { + return tab.container.querySelector("select[label='Start Element calls'] option:checked"); + }; + + const getJoinCallSelect = (tab: RenderResult) => { + return tab.container.querySelector("select[label='Join Element calls']"); + }; + + const getJoinCallSelectedOption = (tab: RenderResult) => { + return tab.container.querySelector("select[label='Join Element calls'] option:checked"); + }; + + describe("Element Call enabled", () => { + beforeEach(() => { + setGroupCallsEnabled(true); + }); + + describe("Join Element calls", () => { + it("defaults to moderator for joining calls", () => { + expect(getJoinCallSelectedOption(renderTab())?.textContent).toBe("Moderator"); + }); + + it("can change joining calls power level", () => { + const tab = renderTab(); + + fireEvent.change(getJoinCallSelect(tab), { + target: { value: 0 }, + }); + + expect(getJoinCallSelectedOption(tab)?.textContent).toBe("Default"); + expect(cli.sendStateEvent).toHaveBeenCalledWith( + roomId, + EventType.RoomPowerLevels, + { + events: { + [ElementCall.MEMBER_EVENT_TYPE.name]: 0, + }, + }, + ); + }); + }); + + describe("Start Element calls", () => { + it("defaults to moderator for starting calls", () => { + expect(getStartCallSelectedOption(renderTab())?.textContent).toBe("Moderator"); + }); + + it("can change starting calls power level", () => { + const tab = renderTab(); + + fireEvent.change(getStartCallSelect(tab), { + target: { value: 0 }, + }); + + expect(getStartCallSelectedOption(tab)?.textContent).toBe("Default"); + expect(cli.sendStateEvent).toHaveBeenCalledWith( + roomId, + EventType.RoomPowerLevels, + { + events: { + [ElementCall.CALL_EVENT_TYPE.name]: 0, + }, + }, + ); + }); + }); + }); + + it("hides when group calls disabled", () => { + setGroupCallsEnabled(false); + + const tab = renderTab(); + + expect(getStartCallSelect(tab)).toBeFalsy(); + expect(getStartCallSelectedOption(tab)).toBeFalsy(); + + expect(getJoinCallSelect(tab)).toBeFalsy(); + expect(getJoinCallSelectedOption(tab)).toBeFalsy(); + }); + }); }); diff --git a/test/components/views/settings/tabs/room/VoipRoomSettingsTab-test.tsx b/test/components/views/settings/tabs/room/VoipRoomSettingsTab-test.tsx new file mode 100644 index 00000000000..45e61385c6f --- /dev/null +++ b/test/components/views/settings/tabs/room/VoipRoomSettingsTab-test.tsx @@ -0,0 +1,141 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { fireEvent, render, RenderResult, waitFor } from "@testing-library/react"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { JoinRule } from "matrix-js-sdk/src/@types/partials"; + +import { mkStubRoom, stubClient } from "../../../../../test-utils"; +import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; +import { VoipRoomSettingsTab } from "../../../../../../src/components/views/settings/tabs/room/VoipRoomSettingsTab"; +import { ElementCall } from "../../../../../../src/models/Call"; + +describe("RolesRoomSettingsTab", () => { + const roomId = "!room:example.com"; + let cli: MatrixClient; + let room: Room; + + const renderTab = (): RenderResult => { + return render(); + }; + + beforeEach(() => { + stubClient(); + cli = MatrixClientPeg.get(); + room = mkStubRoom(roomId, "test room", cli); + + jest.spyOn(cli, "sendStateEvent"); + jest.spyOn(cli, "getRoom").mockReturnValue(room); + }); + + describe("Element Call", () => { + const mockPowerLevels = (events): void => { + jest.spyOn(room.currentState, "getStateEvents").mockReturnValue({ + getContent: () => ({ + events, + }), + } as unknown as MatrixEvent); + }; + + const getElementCallSwitch = (tab: RenderResult): HTMLElement => { + return tab.container.querySelector("[data-testid='element-call-switch']"); + }; + + describe("correct state", () => { + it("shows enabled when call member power level is 0", () => { + mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 0 }); + + const tab = renderTab(); + + expect(getElementCallSwitch(tab).querySelector("[aria-checked='true']")).toBeTruthy(); + }); + + it.each([1, 50, 100])("shows disabled when call member power level is 0", (level: number) => { + mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: level }); + + const tab = renderTab(); + + expect(getElementCallSwitch(tab).querySelector("[aria-checked='false']")).toBeTruthy(); + }); + }); + + describe("enabling/disabling", () => { + describe("enabling Element calls", () => { + beforeEach(() => { + mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 100 }); + }); + + it("enables Element calls in public room", async () => { + jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public); + + const tab = renderTab(); + + fireEvent.click(getElementCallSwitch(tab).querySelector(".mx_ToggleSwitch")); + await waitFor(() => expect(cli.sendStateEvent).toHaveBeenCalledWith( + room.roomId, + EventType.RoomPowerLevels, + expect.objectContaining({ + events: { + [ElementCall.CALL_EVENT_TYPE.name]: 50, + [ElementCall.MEMBER_EVENT_TYPE.name]: 0, + }, + }), + )); + }); + + it("enables Element calls in private room", async () => { + jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite); + + const tab = renderTab(); + + fireEvent.click(getElementCallSwitch(tab).querySelector(".mx_ToggleSwitch")); + await waitFor(() => expect(cli.sendStateEvent).toHaveBeenCalledWith( + room.roomId, + EventType.RoomPowerLevels, + expect.objectContaining({ + events: { + [ElementCall.CALL_EVENT_TYPE.name]: 0, + [ElementCall.MEMBER_EVENT_TYPE.name]: 0, + }, + }), + )); + }); + }); + + it("disabled Element calls", async () => { + mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 0 }); + + const tab = renderTab(); + + fireEvent.click(getElementCallSwitch(tab).querySelector(".mx_ToggleSwitch")); + await waitFor(() => expect(cli.sendStateEvent).toHaveBeenCalledWith( + room.roomId, + EventType.RoomPowerLevels, + expect.objectContaining({ + events: { + [ElementCall.CALL_EVENT_TYPE.name]: 100, + [ElementCall.MEMBER_EVENT_TYPE.name]: 100, + }, + }), + )); + }); + }); + }); +}); diff --git a/test/createRoom-test.ts b/test/createRoom-test.ts index 7dbd4a2a41c..7e44061c3de 100644 --- a/test/createRoom-test.ts +++ b/test/createRoom-test.ts @@ -109,6 +109,22 @@ describe("createRoom", () => { expect(createJitsiCallSpy).not.toHaveBeenCalled(); expect(createElementCallSpy).not.toHaveBeenCalled(); }); + + it("correctly sets up MSC3401 power levels", async () => { + await createRoom({}); + + const [[{ + power_level_content_override: { + events: { + [ElementCall.CALL_EVENT_TYPE.name]: callPower, + [ElementCall.MEMBER_EVENT_TYPE.name]: callMemberPower, + }, + }, + }]] = client.createRoom.mock.calls as any; // no good type + + expect(callPower).toBe(100); + expect(callMemberPower).toBe(100); + }); }); describe("canEncryptToAllUsers", () => { From eb8b2425f873435b3ccea5df7198b773c36dfedc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 4 Oct 2022 16:08:50 +0200 Subject: [PATCH 10/26] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1dedc6a7fa1..d191810b3d0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1644,11 +1644,11 @@ "Change server ACLs": "Change server ACLs", "Send reactions": "Send reactions", "Remove messages sent by me": "Remove messages sent by me", - "Start Element calls": "Start Element calls", - "Join Element calls": "Join Element calls", "Modify widgets": "Modify widgets", "Voice broadcasts": "Voice broadcasts", "Manage pinned events": "Manage pinned events", + "Start Element calls": "Start Element calls", + "Join Element calls": "Join Element calls", "Default role": "Default role", "Send messages": "Send messages", "Invite users": "Invite users", From 1b1949167afeb71d8d370a4ea7ce325be35df601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 5 Oct 2022 17:05:18 +0200 Subject: [PATCH 11/26] Don't mix `"` and `'` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index afa4ea7aaaa..1d510b3dbf6 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -63,7 +63,7 @@ const ElementCallSwitch: React.FC = ({ roomId }) => { logger.error(e); Modal.createDialog(ErrorDialog, { - title: _t('Error changing power level requirement'), + title: _t("Error changing power level requirement"), description: _t( "An error occurred changing the room's power level requirements. Ensure you have sufficient " + "permissions and try again.", From 1e56fbcb87e2d8dfab74ad2ad7b71ba688977652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 5 Oct 2022 17:05:47 +0200 Subject: [PATCH 12/26] Fix indent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 1d510b3dbf6..428b6742107 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -77,7 +77,7 @@ const ElementCallSwitch: React.FC = ({ roomId }) => { label={_t("Enable Element Call as an additional calling option in this room")} caption={_t( "Element Call is end-to-end encrypted, " + - "but is currently limited to smaller numbers of users.", + "but is currently limited to smaller numbers of users.", )} value={elementCallEnabled} onChange={onChange} From 278f0fddefde9c79d39bbef7c054237bfdab8e19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 5 Oct 2022 17:09:42 +0200 Subject: [PATCH 13/26] Fix test name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/settings/tabs/room/VoipRoomSettingsTab-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/settings/tabs/room/VoipRoomSettingsTab-test.tsx b/test/components/views/settings/tabs/room/VoipRoomSettingsTab-test.tsx index 45e61385c6f..0295170ab37 100644 --- a/test/components/views/settings/tabs/room/VoipRoomSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/VoipRoomSettingsTab-test.tsx @@ -119,7 +119,7 @@ describe("RolesRoomSettingsTab", () => { }); }); - it("disabled Element calls", async () => { + it("disables Element calls", async () => { mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 0 }); const tab = renderTab(); From 9983434517df20a115018e88b3b59c85e45701b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 5 Oct 2022 17:55:50 +0200 Subject: [PATCH 14/26] Try to use existing power levels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../settings/tabs/room/VoipRoomSettingsTab.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 428b6742107..12ea7bf42c5 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -49,11 +49,16 @@ const ElementCallSwitch: React.FC = ({ roomId }) => { setElementCallEnabled(enabled); if (enabled) { - events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? 50 : 0; - events[ElementCall.MEMBER_EVENT_TYPE.name] = 0; + const userLevel = events[EventType.RoomMessage] ?? content.users_default ?? 0; + const moderatorLevel = content.kick ?? 50; + + events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel; + events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel; } else { - events[ElementCall.CALL_EVENT_TYPE.name] = 100; - events[ElementCall.MEMBER_EVENT_TYPE.name] = 100; + const adminLevel = events[EventType.RoomPowerLevels] ?? content.state_default ?? 100; + + events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel; + events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel; } MatrixClientPeg.get().sendStateEvent(roomId, EventType.RoomPowerLevels, { From d4f0789c36f579db51d5f5d2e9fc576761a1b317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Oct 2022 15:28:47 +0200 Subject: [PATCH 15/26] Fix MSC number Co-authored-by: Robin --- .../views/settings/tabs/room/RolesRoomSettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 07e65b07f7f..c27f9befd8e 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -61,7 +61,7 @@ const plEventsToShow: Record = { [EventType.Reaction]: { isState: false, hideForSpace: true }, [EventType.RoomRedaction]: { isState: false, hideForSpace: true }, - // MSC33401: Native Group VoIP signalling + // MSC3401: Native Group VoIP signaling [ElementCall.CALL_EVENT_TYPE.name]: { isState: true, hideForSpace: true }, [ElementCall.MEMBER_EVENT_TYPE.name]: { isState: true, hideForSpace: true }, From 940095b44e682cbe3464c376a47984d4c37f8157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Oct 2022 15:28:55 +0200 Subject: [PATCH 16/26] Fix MSC number Co-authored-by: Robin --- .../views/settings/tabs/room/RolesRoomSettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index c27f9befd8e..34bbb4c535e 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -257,7 +257,7 @@ export default class RolesRoomSettingsTab extends React.Component { if (SettingsStore.getValue("feature_pinning")) { plEventsToLabels[EventType.RoomPinnedEvents] = _td("Manage pinned events"); } - // MSC33401: Native Group VoIP signalling + // MSC3401: Native Group VoIP signaling if (SettingsStore.getValue("feature_group_calls")) { plEventsToLabels[ElementCall.CALL_EVENT_TYPE.name] = _td("Start Element calls"); plEventsToLabels[ElementCall.MEMBER_EVENT_TYPE.name] = _td("Join Element calls"); From deabd80d9972f7717cce654ed2e0b6db273f92ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Oct 2022 15:30:11 +0200 Subject: [PATCH 17/26] Gate behind flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/createRoom.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/createRoom.ts b/src/createRoom.ts index 82d7d0a9cfe..cdbf4bda313 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -46,6 +46,7 @@ import { findDMForUser } from "./utils/dm/findDMForUser"; import { privateShouldBeEncrypted } from "./utils/rooms"; import { waitForMember } from "./utils/membership"; import { PreferredRoomVersions } from "./utils/PreferredRoomVersions"; +import SettingsStore from "./settings/SettingsStore"; // we define a number of interfaces which take their names from the js-sdk /* eslint-disable camelcase */ @@ -168,7 +169,7 @@ export default async function createRoom(opts: IOpts): Promise { }, }; } - } else { + } else if (SettingsStore.getValue("feature_group_calls")) { createOpts.power_level_content_override = { events: { // Element Call should be disabled by default From 512b07a1f16a0db62cf0bf558a46e1292fdd199a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Oct 2022 15:32:46 +0200 Subject: [PATCH 18/26] Add missing `DEFAULT_EVENT_POWER_LEVELS` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/createRoom.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/createRoom.ts b/src/createRoom.ts index cdbf4bda313..88e3f8ef9f6 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -172,6 +172,7 @@ export default async function createRoom(opts: IOpts): Promise { } else if (SettingsStore.getValue("feature_group_calls")) { createOpts.power_level_content_override = { events: { + ...DEFAULT_EVENT_POWER_LEVELS, // Element Call should be disabled by default [ElementCall.MEMBER_EVENT_TYPE.name]: 100, // Make sure only admins can enable it From 938498f406ea446b8e2c05443707f8e579a5505e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Oct 2022 16:02:46 +0200 Subject: [PATCH 19/26] Mock `SettingsStore` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- test/createRoom-test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/createRoom-test.ts b/test/createRoom-test.ts index 7e44061c3de..f4e844721bf 100644 --- a/test/createRoom-test.ts +++ b/test/createRoom-test.ts @@ -25,6 +25,7 @@ import WidgetStore from "../src/stores/WidgetStore"; import WidgetUtils from "../src/utils/WidgetUtils"; import { JitsiCall, ElementCall } from "../src/models/Call"; import createRoom, { canEncryptToAllUsers } from '../src/createRoom'; +import SettingsStore from "../src/settings/SettingsStore"; describe("createRoom", () => { mockPlatformPeg(); @@ -111,6 +112,10 @@ describe("createRoom", () => { }); it("correctly sets up MSC3401 power levels", async () => { + jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => { + if (name === "feature_group_calls") return true; + }); + await createRoom({}); const [[{ From 2a6bbda7e4d0d1146245b6e46ffd01c257025f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Oct 2022 17:55:08 +0200 Subject: [PATCH 20/26] Disable when insufficient perms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../elements/AccessibleTooltipButton.tsx | 4 ++-- .../views/elements/LabelledToggleSwitch.tsx | 3 +++ .../views/elements/ToggleSwitch.tsx | 12 ++++++---- .../tabs/room/VoipRoomSettingsTab.tsx | 23 +++++++------------ src/i18n/strings/en_EN.json | 1 + 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 0f52879cc8d..6aa09d74e9a 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -21,7 +21,7 @@ import AccessibleButton from "./AccessibleButton"; import Tooltip, { Alignment } from './Tooltip'; interface IProps extends React.ComponentProps { - title: string; + title?: string; tooltip?: React.ReactNode; label?: string; tooltipClassName?: string; @@ -78,7 +78,7 @@ export default class AccessibleTooltipButton extends React.PureComponent { disabled={this.props.disabled} onChange={this.props.onChange} aria-label={this.props.label} + tooltip={this.props.tooltip} />; if (this.props.toggleInFront) { diff --git a/src/components/views/elements/ToggleSwitch.tsx b/src/components/views/elements/ToggleSwitch.tsx index f56633786a9..0190f28a387 100644 --- a/src/components/views/elements/ToggleSwitch.tsx +++ b/src/components/views/elements/ToggleSwitch.tsx @@ -18,7 +18,7 @@ limitations under the License. import React from "react"; import classNames from "classnames"; -import AccessibleButton from "./AccessibleButton"; +import AccessibleTooltipButton from "./AccessibleTooltipButton"; interface IProps { // Whether or not this toggle is in the 'on' position. @@ -27,12 +27,15 @@ interface IProps { // Whether or not the user can interact with the switch disabled?: boolean; + // Tooltip to show + tooltip?: string; + // Called when the checked state changes. First argument will be the new state. onChange(checked: boolean): void; } // Controlled Toggle Switch element, written with Accessibility in mind -export default ({ checked, disabled = false, onChange, ...props }: IProps) => { +export default ({ checked, disabled = false, tooltip, onChange, ...props }: IProps) => { const _onClick = () => { if (disabled) return; onChange(!checked); @@ -45,14 +48,15 @@ export default ({ checked, disabled = false, onChange, ...props }: IProps) => { }); return ( -
- + ); }; diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index 12ea7bf42c5..a3ac57d0458 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import React, { useCallback, useMemo, useState } from 'react'; -import { logger } from "matrix-js-sdk/src/logger"; import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { EventType } from "matrix-js-sdk/src/@types/event"; @@ -24,8 +23,6 @@ import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; import SettingsSubsection from "../../shared/SettingsSubsection"; import SettingsTab from "../SettingsTab"; -import Modal from "../../../../../Modal"; -import ErrorDialog from "../../../dialogs/ErrorDialog"; import { ElementCall } from "../../../../../models/Call"; import { useRoomState } from "../../../../../hooks/useRoomState"; @@ -36,9 +33,13 @@ interface ElementCallSwitchProps { const ElementCallSwitch: React.FC = ({ roomId }) => { const room = useMemo(() => MatrixClientPeg.get().getRoom(roomId), [roomId]); const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]); - const [content, events] = useRoomState(room, useCallback((state) => { + const [content, events, maySend] = useRoomState(room, useCallback((state) => { const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent(); - return [content ?? {}, content?.["events"] ?? {}]; + return [ + content ?? {}, + content?.["events"] ?? {}, + state?.maySendStateEvent(EventType.RoomPowerLevels, MatrixClientPeg.get().getUserId()), + ]; }, [])); const [elementCallEnabled, setElementCallEnabled] = useState(() => { @@ -64,16 +65,6 @@ const ElementCallSwitch: React.FC = ({ roomId }) => { MatrixClientPeg.get().sendStateEvent(roomId, EventType.RoomPowerLevels, { "events": events, ...content, - }).catch(e => { - logger.error(e); - - Modal.createDialog(ErrorDialog, { - title: _t("Error changing power level requirement"), - description: _t( - "An error occurred changing the room's power level requirements. Ensure you have sufficient " + - "permissions and try again.", - ), - }); }); }, [roomId, content, events, isPublic]); @@ -86,6 +77,8 @@ const ElementCallSwitch: React.FC = ({ roomId }) => { )} value={elementCallEnabled} onChange={onChange} + disabled={!maySend} + tooltip={_t("You do not have sufficient permissions to change this.")} />; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d191810b3d0..d702935ffcf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1690,6 +1690,7 @@ "Encrypted": "Encrypted", "Enable Element Call as an additional calling option in this room": "Enable Element Call as an additional calling option in this room", "Element Call is end-to-end encrypted, but is currently limited to smaller numbers of users.": "Element Call is end-to-end encrypted, but is currently limited to smaller numbers of users.", + "You do not have sufficient permissions to change this.": "You do not have sufficient permissions to change this.", "Call type": "Call type", "Unable to revoke sharing for email address": "Unable to revoke sharing for email address", "Unable to share email address": "Unable to share email address", From 756bdaf898294667ff8d321a4bd871047279e719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 6 Oct 2022 18:42:51 +0200 Subject: [PATCH 21/26] Fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../elements/AccessibleTooltipButton.tsx | 10 +-- .../views/elements/LabelledToggleSwitch.tsx | 2 +- .../views/elements/SettingsFlag.tsx | 2 +- .../views/elements/ToggleSwitch.tsx | 6 +- .../views/settings/devices/DeviceDetails.tsx | 2 +- .../LocationShareMenu-test.tsx.snap | 43 +++++++--- .../__snapshots__/Notifications-test.tsx.snap | 86 +++++++++++++------ 7 files changed, 103 insertions(+), 48 deletions(-) diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 6aa09d74e9a..8fd3a17096d 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -86,11 +86,11 @@ export default class AccessibleTooltipButton extends React.PureComponent { children } { this.props.label } diff --git a/src/components/views/elements/LabelledToggleSwitch.tsx b/src/components/views/elements/LabelledToggleSwitch.tsx index 2a6ff9ea644..eb251d1bd62 100644 --- a/src/components/views/elements/LabelledToggleSwitch.tsx +++ b/src/components/views/elements/LabelledToggleSwitch.tsx @@ -55,7 +55,7 @@ export default class LabelledToggleSwitch extends React.PureComponent { checked={this.props.value} disabled={this.props.disabled} onChange={this.props.onChange} - aria-label={this.props.label} + title={this.props.label} tooltip={this.props.tooltip} />; diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index e369b29c183..76348342a9b 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -114,7 +114,7 @@ export default class SettingsFlag extends React.Component { checked={this.state.value} onChange={this.onChange} disabled={this.props.disabled || !canChange} - aria-label={label} + title={label} />
); diff --git a/src/components/views/elements/ToggleSwitch.tsx b/src/components/views/elements/ToggleSwitch.tsx index 0190f28a387..6a95b5d9a09 100644 --- a/src/components/views/elements/ToggleSwitch.tsx +++ b/src/components/views/elements/ToggleSwitch.tsx @@ -24,6 +24,9 @@ interface IProps { // Whether or not this toggle is in the 'on' position. checked: boolean; + // Title to use + title?: string; + // Whether or not the user can interact with the switch disabled?: boolean; @@ -35,7 +38,7 @@ interface IProps { } // Controlled Toggle Switch element, written with Accessibility in mind -export default ({ checked, disabled = false, tooltip, onChange, ...props }: IProps) => { +export default ({ checked, disabled = false, title, tooltip, onChange, ...props }: IProps) => { const _onClick = () => { if (disabled) return; onChange(!checked); @@ -54,6 +57,7 @@ export default ({ checked, disabled = false, tooltip, onChange, ...props }: IPro role="switch" aria-checked={checked} aria-disabled={disabled} + title={title} tooltip={tooltip} >
diff --git a/src/components/views/settings/devices/DeviceDetails.tsx b/src/components/views/settings/devices/DeviceDetails.tsx index afd97938e2c..41428798ede 100644 --- a/src/components/views/settings/devices/DeviceDetails.tsx +++ b/src/components/views/settings/devices/DeviceDetails.tsx @@ -131,7 +131,7 @@ const DeviceDetails: React.FC = ({ checked={isPushNotificationsEnabled(pusher, localNotificationSettings)} disabled={isCheckboxDisabled(pusher, localNotificationSettings)} onChange={checked => setPushNotifications?.(device.device_id, checked)} - aria-label={_t("Toggle push notifications on this session.")} + title={_t("Toggle push notifications on this session.")} data-testid='device-detail-push-notification-checkbox' />

diff --git a/test/components/views/location/__snapshots__/LocationShareMenu-test.tsx.snap b/test/components/views/location/__snapshots__/LocationShareMenu-test.tsx.snap index 76f50889c05..f30cdac00a6 100644 --- a/test/components/views/location/__snapshots__/LocationShareMenu-test.tsx.snap +++ b/test/components/views/location/__snapshots__/LocationShareMenu-test.tsx.snap @@ -42,36 +42,53 @@ exports[` with live location disabled goes to labs flag scr Enable live location sharing <_default - aria-label="Enable live location sharing" checked={false} onChange={[Function]} + title="Enable live location sharing" > - -

-
- + aria-checked={false} + aria-disabled={false} + aria-label="Enable live location sharing" + className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + onMouseLeave={[Function]} + onMouseOver={[Function]} + role="switch" + tabIndex={0} + > +
+
+ +
diff --git a/test/components/views/settings/__snapshots__/Notifications-test.tsx.snap b/test/components/views/settings/__snapshots__/Notifications-test.tsx.snap index f9f4bcd58a3..c00d74b56aa 100644 --- a/test/components/views/settings/__snapshots__/Notifications-test.tsx.snap +++ b/test/components/views/settings/__snapshots__/Notifications-test.tsx.snap @@ -18,37 +18,54 @@ exports[` main notification switches email switches renders ema Enable email notifications for tester@test.com <_default - aria-label="Enable email notifications for tester@test.com" checked={false} disabled={false} onChange={[Function]} + title="Enable email notifications for tester@test.com" > - -
-
- + aria-checked={false} + aria-disabled={false} + aria-label="Enable email notifications for tester@test.com" + className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + onMouseLeave={[Function]} + onMouseOver={[Function]} + role="switch" + tabIndex={0} + > +
+
+ +
@@ -84,37 +101,54 @@ exports[` main notification switches renders only enable notifi <_default - aria-label="Enable notifications for this account" checked={false} disabled={false} onChange={[Function]} + title="Enable notifications for this account" > - -
-
- + aria-checked={false} + aria-disabled={false} + aria-label="Enable notifications for this account" + className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + onMouseLeave={[Function]} + onMouseOver={[Function]} + role="switch" + tabIndex={0} + > +
+
+ +
From 14d9bab42615dbade71a11081345dade91f887ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 7 Oct 2022 15:24:34 +0200 Subject: [PATCH 22/26] Don't cast as `any` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- test/createRoom-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/createRoom-test.ts b/test/createRoom-test.ts index f4e844721bf..842449c6875 100644 --- a/test/createRoom-test.ts +++ b/test/createRoom-test.ts @@ -86,7 +86,7 @@ describe("createRoom", () => { [ElementCall.MEMBER_EVENT_TYPE.name]: callMemberPower, }, }, - }]] = client.createRoom.mock.calls as any; // no good type + }]] = client.createRoom.mock.calls; // We should have had enough power to be able to set up the call expect(userPower).toBeGreaterThanOrEqual(callPower); @@ -125,7 +125,7 @@ describe("createRoom", () => { [ElementCall.MEMBER_EVENT_TYPE.name]: callMemberPower, }, }, - }]] = client.createRoom.mock.calls as any; // no good type + }]] = client.createRoom.mock.calls; expect(callPower).toBe(100); expect(callMemberPower).toBe(100); From 2bcdbf76e8f6f7a90ddc106f607ac8f4888cf5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 7 Oct 2022 16:09:48 +0200 Subject: [PATCH 23/26] Add EC brand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/IConfigOptions.ts | 1 + src/SdkConfig.ts | 1 + src/components/views/rooms/RoomHeader.tsx | 3 ++- .../views/settings/tabs/room/RolesRoomSettingsTab.tsx | 8 +++++--- .../views/settings/tabs/room/VoipRoomSettingsTab.tsx | 8 ++++++-- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index a6f7d0cdb4d..9739cc88702 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -119,6 +119,7 @@ export interface IConfigOptions { element_call: { url: string; use_exclusively: boolean; + brand: string; }; logout_redirect_url?: string; diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 14fd0694d02..2e1e4a44724 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -33,6 +33,7 @@ export const DEFAULTS: IConfigOptions = { element_call: { url: "https://call.element.io", use_exclusively: false, + brand: "Element Call", }, // @ts-ignore - we deliberately use the camelCase version here so we trigger diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index f0c55b6988c..6f0eac864f5 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -195,10 +195,11 @@ const VideoCallButton: FC = ({ room, busy, setBusy, behavi let menu: JSX.Element | null = null; if (menuOpen) { const buttonRect = buttonRef.current!.getBoundingClientRect(); + const brand = SdkConfig.get("element_call").brand; menu = - + ; } diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 34bbb4c535e..1c70c3ea39f 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -32,6 +32,7 @@ import SettingsFieldset from '../../SettingsFieldset'; import SettingsStore from "../../../../../settings/SettingsStore"; import { VoiceBroadcastInfoEventType } from '../../../../../voice-broadcast'; import { ElementCall } from "../../../../../models/Call"; +import SdkConfig from "../../../../../SdkConfig"; interface IEventShowOpts { isState?: boolean; @@ -259,8 +260,8 @@ export default class RolesRoomSettingsTab extends React.Component { } // MSC3401: Native Group VoIP signaling if (SettingsStore.getValue("feature_group_calls")) { - plEventsToLabels[ElementCall.CALL_EVENT_TYPE.name] = _td("Start Element calls"); - plEventsToLabels[ElementCall.MEMBER_EVENT_TYPE.name] = _td("Join Element calls"); + plEventsToLabels[ElementCall.CALL_EVENT_TYPE.name] = _td("Start %(brand)s calls"); + plEventsToLabels[ElementCall.MEMBER_EVENT_TYPE.name] = _td("Join %(brand)s calls"); } const powerLevelDescriptors: Record = { @@ -445,7 +446,8 @@ export default class RolesRoomSettingsTab extends React.Component { let label = plEventsToLabels[eventType]; if (label) { - label = _t(label); + const brand = SdkConfig.get("element_call").brand; + label = _t(label, { brand }); } else { label = _t("Send %(eventType)s events", { eventType }); } diff --git a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx index a3ac57d0458..29863b64aab 100644 --- a/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/VoipRoomSettingsTab.tsx @@ -25,6 +25,7 @@ import SettingsSubsection from "../../shared/SettingsSubsection"; import SettingsTab from "../SettingsTab"; import { ElementCall } from "../../../../../models/Call"; import { useRoomState } from "../../../../../hooks/useRoomState"; +import SdkConfig from "../../../../../SdkConfig"; interface ElementCallSwitchProps { roomId: string; @@ -68,12 +69,15 @@ const ElementCallSwitch: React.FC = ({ roomId }) => { }); }, [roomId, content, events, isPublic]); + const brand = SdkConfig.get("element_call").brand; + return Date: Fri, 7 Oct 2022 16:09:56 +0200 Subject: [PATCH 24/26] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index aec7c1bc5a0..8da63749df6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1650,8 +1650,8 @@ "Modify widgets": "Modify widgets", "Voice broadcasts": "Voice broadcasts", "Manage pinned events": "Manage pinned events", - "Start Element calls": "Start Element calls", - "Join Element calls": "Join Element calls", + "Start %(brand)s calls": "Start %(brand)s calls", + "Join %(brand)s calls": "Join %(brand)s calls", "Default role": "Default role", "Send messages": "Send messages", "Invite users": "Invite users", @@ -1691,8 +1691,8 @@ "Security & Privacy": "Security & Privacy", "Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.", "Encrypted": "Encrypted", - "Enable Element Call as an additional calling option in this room": "Enable Element Call as an additional calling option in this room", - "Element Call is end-to-end encrypted, but is currently limited to smaller numbers of users.": "Element Call is end-to-end encrypted, but is currently limited to smaller numbers of users.", + "Enable %(brand)s as an additional calling option in this room": "Enable %(brand)s as an additional calling option in this room", + "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.", "You do not have sufficient permissions to change this.": "You do not have sufficient permissions to change this.", "Call type": "Call type", "Unable to revoke sharing for email address": "Unable to revoke sharing for email address", @@ -1898,7 +1898,7 @@ "Recently visited rooms": "Recently visited rooms", "No recently visited rooms": "No recently visited rooms", "Video call (Jitsi)": "Video call (Jitsi)", - "Video call (Element Call)": "Video call (Element Call)", + "Video call (%(brand)s)": "Video call (%(brand)s)", "Ongoing call": "Ongoing call", "You do not have permission to start video calls": "You do not have permission to start video calls", "There's no one here to call": "There's no one here to call", From d5ee3d6f3ce100164118b4adac625a407a186999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 7 Oct 2022 16:18:46 +0200 Subject: [PATCH 25/26] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- test/components/views/rooms/RoomHeader-test.tsx | 12 +++++++++--- test/utils/device/clientInformation-test.ts | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/test/components/views/rooms/RoomHeader-test.tsx b/test/components/views/rooms/RoomHeader-test.tsx index e75502eff44..c278bfa1b43 100644 --- a/test/components/views/rooms/RoomHeader-test.tsx +++ b/test/components/views/rooms/RoomHeader-test.tsx @@ -505,7 +505,9 @@ describe("RoomHeader (React Testing Library)", () => { + "and there's an ongoing call", async () => { mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - SdkConfig.put({ element_call: { url: "https://call.element.io", use_exclusively: true } }); + SdkConfig.put( + { element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" } }, + ); await ElementCall.create(room); renderHeader(); @@ -519,7 +521,9 @@ describe("RoomHeader (React Testing Library)", () => { + "use Element Call exclusively", async () => { mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - SdkConfig.put({ element_call: { url: "https://call.element.io", use_exclusively: true } }); + SdkConfig.put( + { element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" } }, + ); renderHeader(); expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull(); @@ -541,7 +545,9 @@ describe("RoomHeader (React Testing Library)", () => { + "and the user lacks permission", () => { mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - SdkConfig.put({ element_call: { url: "https://call.element.io", use_exclusively: true } }); + SdkConfig.put( + { element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" } }, + ); mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100 }); renderHeader(); diff --git a/test/utils/device/clientInformation-test.ts b/test/utils/device/clientInformation-test.ts index 0f1d030e791..24355d49c85 100644 --- a/test/utils/device/clientInformation-test.ts +++ b/test/utils/device/clientInformation-test.ts @@ -36,7 +36,7 @@ describe('recordClientInformation()', () => { const sdkConfig: IConfigOptions = { brand: 'Test Brand', - element_call: { url: '', use_exclusively: false }, + element_call: { url: '', use_exclusively: false, brand: "Element Call" }, }; const platform = { From dfd40315310ae127644a99c864337023acae4768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 7 Oct 2022 16:34:31 +0200 Subject: [PATCH 26/26] Fix tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../settings/tabs/room/RolesRoomSettingsTab-test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx b/test/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx index 7522f8d7441..dc2427e1436 100644 --- a/test/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx @@ -80,19 +80,19 @@ describe("RolesRoomSettingsTab", () => { }; const getStartCallSelect = (tab: RenderResult) => { - return tab.container.querySelector("select[label='Start Element calls']"); + return tab.container.querySelector("select[label='Start Element Call calls']"); }; const getStartCallSelectedOption = (tab: RenderResult) => { - return tab.container.querySelector("select[label='Start Element calls'] option:checked"); + return tab.container.querySelector("select[label='Start Element Call calls'] option:checked"); }; const getJoinCallSelect = (tab: RenderResult) => { - return tab.container.querySelector("select[label='Join Element calls']"); + return tab.container.querySelector("select[label='Join Element Call calls']"); }; const getJoinCallSelectedOption = (tab: RenderResult) => { - return tab.container.querySelector("select[label='Join Element calls'] option:checked"); + return tab.container.querySelector("select[label='Join Element Call calls'] option:checked"); }; describe("Element Call enabled", () => {