From 0cc4f4e1bc4d17ed5c58994d38e8c33d84f59d8b Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Mon, 19 Sep 2022 08:42:29 +0200 Subject: [PATCH] Add voice broadcast permissions (#9284) * Add Voice Broadcast labs setting and composer button * Implement strict typing * Extend MessageComposer-test * Extend tests * Revert some strict type fixex * Implement voice broadcast permissions * Update variable casing --- src/components/structures/RoomView.tsx | 12 +++- .../views/rooms/MessageComposer.tsx | 8 ++- .../tabs/room/RolesRoomSettingsTab.tsx | 3 + src/contexts/RoomContext.ts | 1 + src/i18n/strings/en_EN.json | 1 + src/voice-broadcast/index.ts | 17 +++++ .../views/rooms/MessageComposer-test.tsx | 13 +++- .../rooms/MessageComposerButtons-test.tsx | 1 + .../views/rooms/SendMessageComposer-test.tsx | 1 + .../tabs/room/RolesRoomSettingsTab-test.tsx | 69 +++++++++++++++++++ 10 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 src/voice-broadcast/index.ts create mode 100644 test/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 780619c7e4..b94c8d4ea4 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -119,6 +119,7 @@ import { isLocalRoom } from '../../utils/localRoom/isLocalRoom'; import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload"; import { RoomStatusBarUnsentMessages } from './RoomStatusBarUnsentMessages'; import { LargeLoader } from './LargeLoader'; +import { VoiceBroadcastInfoEventType } from '../../voice-broadcast'; import { isVideoRoom } from '../../utils/video-rooms'; const DEBUG = false; @@ -200,6 +201,7 @@ export interface IRoomState { upgradeRecommendation?: IRecommendedVersion; canReact: boolean; canSendMessages: boolean; + canSendVoiceBroadcasts: boolean; tombstone?: MatrixEvent; resizing: boolean; layout: Layout; @@ -402,6 +404,7 @@ export class RoomView extends React.Component { statusBarVisible: false, canReact: false, canSendMessages: false, + canSendVoiceBroadcasts: false, resizing: false, layout: SettingsStore.getValue("layout"), lowBandwidth: SettingsStore.getValue("lowBandwidth"), @@ -1346,8 +1349,14 @@ export class RoomView extends React.Component { ); const canSendMessages = room.maySendMessage(); const canSelfRedact = room.currentState.maySendEvent(EventType.RoomRedaction, me); + const canSendVoiceBroadcasts = room.currentState.maySendEvent(VoiceBroadcastInfoEventType, me); - this.setState({ canReact, canSendMessages, canSelfRedact }); + this.setState({ + canReact, + canSendMessages, + canSendVoiceBroadcasts, + canSelfRedact, + }); } } @@ -2220,6 +2229,7 @@ export class RoomView extends React.Component { resizeNotifier={this.props.resizeNotifier} replyToEvent={this.state.replyToEvent} permalinkCreator={this.permalinkCreator} + showVoiceBroadcastButton={this.state.canSendVoiceBroadcasts} />; } diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index fb8dbc827e..3f75e9f16d 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -79,6 +79,7 @@ interface IProps { relation?: IEventRelation; e2eStatus?: E2EStatus; compact?: boolean; + showVoiceBroadcastButton?: boolean; } interface IState { @@ -107,6 +108,7 @@ export default class MessageComposer extends React.Component { public static defaultProps = { compact: false, + showVoiceBroadcastButton: false, }; public constructor(props: IProps) { @@ -368,6 +370,10 @@ export default class MessageComposer extends React.Component { return this.state.showStickersButton && !isLocalRoom(this.props.room); } + private get showVoiceBroadcastButton(): boolean { + return this.props.showVoiceBroadcastButton && this.state.showVoiceBroadcastButton; + } + public render() { const controls = [ this.props.e2eStatus ? @@ -495,7 +501,7 @@ export default class MessageComposer extends React.Component { showPollsButton={this.state.showPollsButton} showStickersButton={this.showStickersButton} toggleButtonMenu={this.toggleButtonMenu} - showVoiceBroadcastButton={this.state.showVoiceBroadcastButton} + showVoiceBroadcastButton={this.showVoiceBroadcastButton} onStartVoiceBroadcastClick={() => { // Sends a voice message. To be replaced by voice broadcast during development. this.voiceRecordingButton.current?.onRecordStartEndClick(); diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index ec1c9a49fd..bab6904243 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -30,6 +30,7 @@ import ErrorDialog from '../../../dialogs/ErrorDialog'; import PowerSelector from "../../../elements/PowerSelector"; import SettingsFieldset from '../../SettingsFieldset'; import SettingsStore from "../../../../../settings/SettingsStore"; +import { VoiceBroadcastInfoEventType } from '../../../../../voice-broadcast'; interface IEventShowOpts { isState?: boolean; @@ -61,6 +62,7 @@ const plEventsToShow: Record = { // 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 }, }; // parse a string as an integer; if the input is undefined, or cannot be parsed @@ -244,6 +246,7 @@ export default class RolesRoomSettingsTab extends React.Component { // 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"), }; if (SettingsStore.getValue("feature_pinning")) { diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 6ca5ed60c2..a66749a0cd 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -45,6 +45,7 @@ const RoomContext = createContext({ canReact: false, canSelfRedact: false, canSendMessages: false, + canSendVoiceBroadcasts: false, resizing: false, layout: Layout.Group, lowBandwidth: false, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0aab906f3d..1534c4d51f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1642,6 +1642,7 @@ "Send reactions": "Send reactions", "Remove messages sent by me": "Remove messages sent by me", "Modify widgets": "Modify widgets", + "Voice broadcasts": "Voice broadcasts", "Manage pinned events": "Manage pinned events", "Default role": "Default role", "Send messages": "Send messages", diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts new file mode 100644 index 0000000000..473bbf73ef --- /dev/null +++ b/src/voice-broadcast/index.ts @@ -0,0 +1,17 @@ +/* +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. +*/ + +export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info"; diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index aa4245cebc..7f5e9715a6 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -137,7 +137,7 @@ describe("MessageComposer", () => { beforeEach(() => { SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value); - wrapper = wrapAndRender({ room }); + wrapper = wrapAndRender({ room, showVoiceBroadcastButton: true }); }); it(`should pass the prop ${prop} = ${value}`, () => { @@ -164,6 +164,17 @@ describe("MessageComposer", () => { }); }); + [false, undefined].forEach((value) => { + it(`should pass showVoiceBroadcastButton = false if the MessageComposer prop is ${value}`, () => { + SettingsStore.setValue(Features.VoiceBroadcast, null, SettingLevel.DEVICE, true); + const wrapper = wrapAndRender({ + room, + showVoiceBroadcastButton: value, + }); + expect(wrapper.find(MessageComposerButtons).props().showVoiceBroadcastButton).toBe(false); + }); + }); + it("should not render the send button", () => { const wrapper = wrapAndRender({ room }); expect(wrapper.find("SendButton")).toHaveLength(0); diff --git a/test/components/views/rooms/MessageComposerButtons-test.tsx b/test/components/views/rooms/MessageComposerButtons-test.tsx index ade14f752e..4939663c8e 100644 --- a/test/components/views/rooms/MessageComposerButtons-test.tsx +++ b/test/components/views/rooms/MessageComposerButtons-test.tsx @@ -250,6 +250,7 @@ function createRoomState(room: Room, narrow: boolean): IRoomState { statusBarVisible: false, canReact: false, canSendMessages: false, + canSendVoiceBroadcasts: false, layout: Layout.Group, lowBandwidth: false, alwaysShowTimestamps: false, diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx index 6f6f846c24..5cc0f508b3 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -72,6 +72,7 @@ describe('', () => { statusBarVisible: false, canReact: false, canSendMessages: false, + canSendVoiceBroadcasts: false, layout: Layout.Group, lowBandwidth: false, alwaysShowTimestamps: false, diff --git a/test/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx b/test/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx new file mode 100644 index 0000000000..9c041de4c0 --- /dev/null +++ b/test/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx @@ -0,0 +1,69 @@ +/* +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 } from "@testing-library/react"; +import { EventType, MatrixClient } from "matrix-js-sdk/src/matrix"; + +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"; + +describe("RolesRoomSettingsTab", () => { + const roomId = "!room:example.com"; + let rolesRoomSettingsTab: RenderResult; + let cli: MatrixClient; + + const getVoiceBroadcastsSelect = () => { + return rolesRoomSettingsTab.container.querySelector("select[label='Voice broadcasts']"); + }; + + const getVoiceBroadcastsSelectedOption = () => { + return rolesRoomSettingsTab.container.querySelector("select[label='Voice broadcasts'] option:checked"); + }; + + beforeEach(() => { + stubClient(); + cli = MatrixClientPeg.get(); + rolesRoomSettingsTab = render(); + mkStubRoom(roomId, "test room", cli); + }); + + it("should initially show »Moderator« permission for »Voice broadcasts«", () => { + expect(getVoiceBroadcastsSelectedOption().textContent).toBe("Moderator"); + }); + + describe("when setting »Default« permission for »Voice broadcasts«", () => { + beforeEach(() => { + fireEvent.change(getVoiceBroadcastsSelect(), { + target: { value: 0 }, + }); + }); + + it("should update the power levels", () => { + expect(cli.sendStateEvent).toHaveBeenCalledWith( + roomId, + EventType.RoomPowerLevels, + { + events: { + [VoiceBroadcastInfoEventType]: 0, + }, + }, + ); + }); + }); +});