From bb0c175b7e46b0245ffab226cd112d4b34e0ad4e Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Wed, 19 Oct 2022 15:01:14 +0200 Subject: [PATCH] Display info dialogs if unable to start voice broadcasts (#9453) --- src/components/structures/RoomView.tsx | 6 - .../views/rooms/MessageComposer.tsx | 9 +- src/contexts/RoomContext.ts | 1 - src/i18n/strings/en_EN.json | 4 + src/voice-broadcast/index.ts | 1 + .../utils/hasRoomLiveVoiceBroadcast.ts | 54 +++++++ .../utils/startNewVoiceBroadcastRecording.ts | 76 --------- .../utils/startNewVoiceBroadcastRecording.tsx | 136 ++++++++++++++++ .../views/rooms/MessageComposer-test.tsx | 13 +- .../rooms/MessageComposerButtons-test.tsx | 1 - .../views/rooms/SendMessageComposer-test.tsx | 1 - .../wysiwyg_composer/WysiwygComposer-test.tsx | 1 - .../rooms/wysiwyg_composer/message-test.ts | 1 - ...artNewVoiceBroadcastRecording-test.ts.snap | 70 ++++++++ .../utils/hasRoomLiveVoiceBroadcast-test.ts | 144 +++++++++++++++++ .../startNewVoiceBroadcastRecording-test.ts | 151 +++++++++++------- test/voice-broadcast/utils/test-utils.ts | 37 +++++ 17 files changed, 546 insertions(+), 160 deletions(-) create mode 100644 src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts delete mode 100644 src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts create mode 100644 src/voice-broadcast/utils/startNewVoiceBroadcastRecording.tsx create mode 100644 test/voice-broadcast/utils/__snapshots__/startNewVoiceBroadcastRecording-test.ts.snap create mode 100644 test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts create mode 100644 test/voice-broadcast/utils/test-utils.ts diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 77245e0eb8..2dfe61aefa 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -113,7 +113,6 @@ 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'; import { SDKContext } from '../../contexts/SDKContext'; import { CallStore, CallStoreEvent } from "../../stores/CallStore"; @@ -199,7 +198,6 @@ export interface IRoomState { upgradeRecommendation?: IRecommendedVersion; canReact: boolean; canSendMessages: boolean; - canSendVoiceBroadcasts: boolean; tombstone?: MatrixEvent; resizing: boolean; layout: Layout; @@ -404,7 +402,6 @@ export class RoomView extends React.Component { statusBarVisible: false, canReact: false, canSendMessages: false, - canSendVoiceBroadcasts: false, resizing: false, layout: SettingsStore.getValue("layout"), lowBandwidth: SettingsStore.getValue("lowBandwidth"), @@ -1377,12 +1374,10 @@ 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, - canSendVoiceBroadcasts, canSelfRedact, }); } @@ -2253,7 +2248,6 @@ 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 9783e30756..6c83b75b87 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -85,7 +85,6 @@ interface IProps { relation?: IEventRelation; e2eStatus?: E2EStatus; compact?: boolean; - showVoiceBroadcastButton?: boolean; } interface IState { @@ -384,10 +383,6 @@ 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 isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); const controls = [ @@ -532,10 +527,10 @@ export default class MessageComposer extends React.Component { showPollsButton={this.state.showPollsButton} showStickersButton={this.showStickersButton} toggleButtonMenu={this.toggleButtonMenu} - showVoiceBroadcastButton={this.showVoiceBroadcastButton} + showVoiceBroadcastButton={this.state.showVoiceBroadcastButton} onStartVoiceBroadcastClick={() => { startNewVoiceBroadcastRecording( - this.props.room.roomId, + this.props.room, MatrixClientPeg.get(), VoiceBroadcastRecordingsStore.instance(), ); diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 5bc648e736..8193c83ccc 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -45,7 +45,6 @@ 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 3f69088f78..f322c5de8d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -637,6 +637,10 @@ "Send %(msgtype)s messages as you in your active room": "Send %(msgtype)s messages as you in your active room", "See %(msgtype)s messages posted to this room": "See %(msgtype)s messages posted to this room", "See %(msgtype)s messages posted to your active room": "See %(msgtype)s messages posted to your active room", + "Can't start a new voice broadcast": "Can't start a new voice broadcast", + "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.", + "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.", + "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.", "Stop live broadcasting?": "Stop live broadcasting?", "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.", "Yes, stop broadcast": "Yes, stop broadcast", diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index 7262382b0c..8f01c089c6 100644 --- a/src/voice-broadcast/index.ts +++ b/src/voice-broadcast/index.ts @@ -35,6 +35,7 @@ export * from "./components/molecules/VoiceBroadcastRecordingPip"; export * from "./hooks/useVoiceBroadcastRecording"; export * from "./stores/VoiceBroadcastPlaybacksStore"; export * from "./stores/VoiceBroadcastRecordingsStore"; +export * from "./utils/hasRoomLiveVoiceBroadcast"; export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile"; export * from "./utils/shouldDisplayAsVoiceBroadcastTile"; export * from "./utils/startNewVoiceBroadcastRecording"; diff --git a/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts b/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts new file mode 100644 index 0000000000..577b9ed880 --- /dev/null +++ b/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts @@ -0,0 +1,54 @@ +/* +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 { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; + +import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; + +interface Result { + hasBroadcast: boolean; + startedByUser: boolean; +} + +/** + * Finds out whether there is a live broadcast in a room. + * Also returns if the user started the broadcast (if any). + */ +export const hasRoomLiveVoiceBroadcast = (room: Room, userId: string): Result => { + let hasBroadcast = false; + let startedByUser = false; + + const stateEvents = room.currentState.getStateEvents(VoiceBroadcastInfoEventType); + stateEvents.forEach((event: MatrixEvent) => { + const state = event.getContent()?.state; + + if (state && state !== VoiceBroadcastInfoState.Stopped) { + hasBroadcast = true; + + // state key = sender's MXID + if (event.getStateKey() === userId) { + startedByUser = true; + // break here, because more than true / true is not possible + return false; + } + } + }); + + return { + hasBroadcast, + startedByUser, + }; +}; diff --git a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts deleted file mode 100644 index 272958e5d0..0000000000 --- a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* -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 { ISendEventResponse, MatrixClient, RoomStateEvent } from "matrix-js-sdk/src/matrix"; -import { defer } from "matrix-js-sdk/src/utils"; - -import { - VoiceBroadcastInfoEventContent, - VoiceBroadcastInfoEventType, - VoiceBroadcastInfoState, - VoiceBroadcastRecordingsStore, - VoiceBroadcastRecording, -} from ".."; - -/** - * Starts a new Voice Broadcast Recording. - * Sends a voice_broadcast_info state event and waits for the event to actually appear in the room state. - */ -export const startNewVoiceBroadcastRecording = async ( - roomId: string, - client: MatrixClient, - recordingsStore: VoiceBroadcastRecordingsStore, -): Promise => { - const room = client.getRoom(roomId); - const { promise, resolve } = defer(); - let result: ISendEventResponse = null; - - const onRoomStateEvents = () => { - if (!result) return; - - const voiceBroadcastEvent = room.currentState.getStateEvents( - VoiceBroadcastInfoEventType, - client.getUserId(), - ); - - if (voiceBroadcastEvent?.getId() === result.event_id) { - room.off(RoomStateEvent.Events, onRoomStateEvents); - const recording = new VoiceBroadcastRecording( - voiceBroadcastEvent, - client, - ); - recordingsStore.setCurrent(recording); - recording.start(); - resolve(recording); - } - }; - - room.on(RoomStateEvent.Events, onRoomStateEvents); - - // XXX Michael W: refactor to live event - result = await client.sendStateEvent( - roomId, - VoiceBroadcastInfoEventType, - { - device_id: client.getDeviceId(), - state: VoiceBroadcastInfoState.Started, - chunk_length: 300, - } as VoiceBroadcastInfoEventContent, - client.getUserId(), - ); - - return promise; -}; diff --git a/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.tsx b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.tsx new file mode 100644 index 0000000000..cff195c668 --- /dev/null +++ b/src/voice-broadcast/utils/startNewVoiceBroadcastRecording.tsx @@ -0,0 +1,136 @@ +/* +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 { ISendEventResponse, MatrixClient, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { defer } from "matrix-js-sdk/src/utils"; + +import { _t } from "../../languageHandler"; +import InfoDialog from "../../components/views/dialogs/InfoDialog"; +import Modal from "../../Modal"; +import { + VoiceBroadcastInfoEventContent, + VoiceBroadcastInfoEventType, + VoiceBroadcastInfoState, + VoiceBroadcastRecordingsStore, + VoiceBroadcastRecording, + hasRoomLiveVoiceBroadcast, +} from ".."; + +const startBroadcast = async ( + room: Room, + client: MatrixClient, + recordingsStore: VoiceBroadcastRecordingsStore, +): Promise => { + const { promise, resolve } = defer(); + let result: ISendEventResponse = null; + + const onRoomStateEvents = () => { + if (!result) return; + + const voiceBroadcastEvent = room.currentState.getStateEvents( + VoiceBroadcastInfoEventType, + client.getUserId(), + ); + + if (voiceBroadcastEvent?.getId() === result.event_id) { + room.off(RoomStateEvent.Events, onRoomStateEvents); + const recording = new VoiceBroadcastRecording( + voiceBroadcastEvent, + client, + ); + recordingsStore.setCurrent(recording); + recording.start(); + resolve(recording); + } + }; + + room.on(RoomStateEvent.Events, onRoomStateEvents); + + // XXX Michael W: refactor to live event + result = await client.sendStateEvent( + room.roomId, + VoiceBroadcastInfoEventType, + { + device_id: client.getDeviceId(), + state: VoiceBroadcastInfoState.Started, + chunk_length: 300, + } as VoiceBroadcastInfoEventContent, + client.getUserId(), + ); + + return promise; +}; + +const showAlreadyRecordingDialog = () => { + Modal.createDialog(InfoDialog, { + title: _t("Can't start a new voice broadcast"), + description:

{ _t("You are already recording a voice broadcast. " + + "Please end your current voice broadcast to start a new one.") }

, + hasCloseButton: true, + }); +}; + +const showInsufficientPermissionsDialog = () => { + Modal.createDialog(InfoDialog, { + title: _t("Can't start a new voice broadcast"), + description:

{ _t("You don't have the required permissions to start a voice broadcast in this room. " + + "Contact a room administrator to upgrade your permissions.") }

, + hasCloseButton: true, + }); +}; + +const showOthersAlreadyRecordingDialog = () => { + Modal.createDialog(InfoDialog, { + title: _t("Can't start a new voice broadcast"), + description:

{ _t("Someone else is already recording a voice broadcast. " + + "Wait for their voice broadcast to end to start a new one.") }

, + hasCloseButton: true, + }); +}; + +/** + * Starts a new Voice Broadcast Recording, if + * - the user has the permissions to do so in the room + * - there is no other broadcast being recorded in the room, yet + * Sends a voice_broadcast_info state event and waits for the event to actually appear in the room state. + */ +export const startNewVoiceBroadcastRecording = async ( + room: Room, + client: MatrixClient, + recordingsStore: VoiceBroadcastRecordingsStore, +): Promise => { + const currentUserId = client.getUserId(); + + if (!room.currentState.maySendStateEvent(VoiceBroadcastInfoEventType, currentUserId)) { + showInsufficientPermissionsDialog(); + return null; + } + + const { hasBroadcast, startedByUser } = hasRoomLiveVoiceBroadcast(room, currentUserId); + + if (hasBroadcast && startedByUser) { + showAlreadyRecordingDialog(); + return null; + } + + if (hasBroadcast) { + showOthersAlreadyRecordingDialog(); + return null; + } + + return startBroadcast(room, client, recordingsStore); +}; diff --git a/test/components/views/rooms/MessageComposer-test.tsx b/test/components/views/rooms/MessageComposer-test.tsx index bc0b26f745..8ebeda676a 100644 --- a/test/components/views/rooms/MessageComposer-test.tsx +++ b/test/components/views/rooms/MessageComposer-test.tsx @@ -147,7 +147,7 @@ describe("MessageComposer", () => { beforeEach(() => { SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value); - wrapper = wrapAndRender({ room, showVoiceBroadcastButton: true }); + wrapper = wrapAndRender({ room }); }); it(`should pass the prop ${prop} = ${value}`, () => { @@ -174,17 +174,6 @@ 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 472b0e7368..f41901dd7a 100644 --- a/test/components/views/rooms/MessageComposerButtons-test.tsx +++ b/test/components/views/rooms/MessageComposerButtons-test.tsx @@ -250,7 +250,6 @@ 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 96b1be95ec..1d01c4a5a5 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -72,7 +72,6 @@ describe('', () => { statusBarVisible: false, canReact: false, canSendMessages: false, - canSendVoiceBroadcasts: false, layout: Layout.Group, lowBandwidth: false, alwaysShowTimestamps: false, diff --git a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx index df2596809c..12161ae816 100644 --- a/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/WysiwygComposer-test.tsx @@ -91,7 +91,6 @@ describe('WysiwygComposer', () => { statusBarVisible: false, canReact: false, canSendMessages: false, - canSendVoiceBroadcasts: false, layout: Layout.Group, lowBandwidth: false, alwaysShowTimestamps: false, diff --git a/test/components/views/rooms/wysiwyg_composer/message-test.ts b/test/components/views/rooms/wysiwyg_composer/message-test.ts index 712b671c9f..79197a3188 100644 --- a/test/components/views/rooms/wysiwyg_composer/message-test.ts +++ b/test/components/views/rooms/wysiwyg_composer/message-test.ts @@ -123,7 +123,6 @@ describe('message', () => { statusBarVisible: false, canReact: false, canSendMessages: false, - canSendVoiceBroadcasts: false, layout: Layout.Group, lowBandwidth: false, alwaysShowTimestamps: false, diff --git a/test/voice-broadcast/utils/__snapshots__/startNewVoiceBroadcastRecording-test.ts.snap b/test/voice-broadcast/utils/__snapshots__/startNewVoiceBroadcastRecording-test.ts.snap new file mode 100644 index 0000000000..c38673e3b6 --- /dev/null +++ b/test/voice-broadcast/utils/__snapshots__/startNewVoiceBroadcastRecording-test.ts.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there already is a live broadcast of another user should show an info dialog 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + [Function], + Object { + "description":

+ Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one. +

, + "hasCloseButton": true, + "title": "Can't start a new voice broadcast", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there already is a live broadcast of the current user should show an info dialog 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + [Function], + Object { + "description":

+ You are already recording a voice broadcast. Please end your current voice broadcast to start a new one. +

, + "hasCloseButton": true, + "title": "Can't start a new voice broadcast", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; + +exports[`startNewVoiceBroadcastRecording when the current user is not allowed to send voice broadcast info state events should show an info dialog 1`] = ` +[MockFunction] { + "calls": Array [ + Array [ + [Function], + Object { + "description":

+ You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions. +

, + "hasCloseButton": true, + "title": "Can't start a new voice broadcast", + }, + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], +} +`; diff --git a/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts b/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts new file mode 100644 index 0000000000..c9fbc5f09e --- /dev/null +++ b/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts @@ -0,0 +1,144 @@ +/* +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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; + +import { + hasRoomLiveVoiceBroadcast, + VoiceBroadcastInfoEventType, + VoiceBroadcastInfoState, +} from "../../../src/voice-broadcast"; +import { mkEvent, stubClient } from "../../test-utils"; +import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; + +describe("hasRoomLiveVoiceBroadcast", () => { + const otherUserId = "@other:example.com"; + const roomId = "!room:example.com"; + let client: MatrixClient; + let room: Room; + + const addVoiceBroadcastInfoEvent = ( + state: VoiceBroadcastInfoState, + sender: string, + ) => { + room.currentState.setStateEvents([ + mkVoiceBroadcastInfoStateEvent(room.roomId, state, sender), + ]); + }; + + const itShouldReturnTrueTrue = () => { + it("should return true/true", () => { + expect(hasRoomLiveVoiceBroadcast(room, client.getUserId())).toEqual({ + hasBroadcast: true, + startedByUser: true, + }); + }); + }; + + const itShouldReturnTrueFalse = () => { + it("should return true/false", () => { + expect(hasRoomLiveVoiceBroadcast(room, client.getUserId())).toEqual({ + hasBroadcast: true, + startedByUser: false, + }); + }); + }; + + const itShouldReturnFalseFalse = () => { + it("should return false/false", () => { + expect(hasRoomLiveVoiceBroadcast(room, client.getUserId())).toEqual({ + hasBroadcast: false, + startedByUser: false, + }); + }); + }; + + beforeAll(() => { + client = stubClient(); + }); + + beforeEach(() => { + room = new Room(roomId, client, client.getUserId()); + }); + + describe("when there is no voice broadcast info at all", () => { + itShouldReturnFalseFalse(); + }); + + describe("when the »state« prop is missing", () => { + beforeEach(() => { + room.currentState.setStateEvents([ + mkEvent({ + event: true, + room: room.roomId, + user: client.getUserId(), + type: VoiceBroadcastInfoEventType, + skey: client.getUserId(), + content: {}, + }), + ]); + }); + itShouldReturnFalseFalse(); + }); + + describe("when there is a live broadcast from the current and another user", () => { + beforeEach(() => { + addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started, client.getUserId()); + addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started, otherUserId); + }); + + itShouldReturnTrueTrue(); + }); + + describe("when there are only stopped info events", () => { + beforeEach(() => { + addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped, client.getUserId()); + addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped, otherUserId); + }); + + itShouldReturnFalseFalse(); + }); + + describe.each([ + // all there are kind of live states + VoiceBroadcastInfoState.Started, + VoiceBroadcastInfoState.Paused, + VoiceBroadcastInfoState.Running, + ])("when there is a live broadcast (%s) from the current user", (state: VoiceBroadcastInfoState) => { + beforeEach(() => { + addVoiceBroadcastInfoEvent(state, client.getUserId()); + }); + + itShouldReturnTrueTrue(); + }); + + describe("when there was a live broadcast, that has been stopped", () => { + beforeEach(() => { + addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Running, client.getUserId()); + addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped, client.getUserId()); + }); + + itShouldReturnFalseFalse(); + }); + + describe("when there is a live broadcast from another user", () => { + beforeEach(() => { + addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Running, otherUserId); + }); + + itShouldReturnTrueFalse(); + }); +}); diff --git a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts index 570719539a..a320bca2eb 100644 --- a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts @@ -15,8 +15,9 @@ limitations under the License. */ import { mocked } from "jest-mock"; -import { EventType, MatrixClient, MatrixEvent, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix"; +import { EventType, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import Modal from "../../../src/Modal"; import { startNewVoiceBroadcastRecording, VoiceBroadcastInfoEventType, @@ -25,46 +26,29 @@ import { VoiceBroadcastRecording, } from "../../../src/voice-broadcast"; import { mkEvent, stubClient } from "../../test-utils"; +import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; jest.mock("../../../src/voice-broadcast/models/VoiceBroadcastRecording", () => ({ VoiceBroadcastRecording: jest.fn(), })); +jest.mock("../../../src/Modal"); + describe("startNewVoiceBroadcastRecording", () => { const roomId = "!room:example.com"; + const otherUserId = "@other:example.com"; let client: MatrixClient; let recordingsStore: VoiceBroadcastRecordingsStore; let room: Room; - let roomOnStateEventsCallbackRegistered: Promise; - let roomOnStateEventsCallbackRegisteredResolver: Function; - let roomOnStateEventsCallback: () => void; let infoEvent: MatrixEvent; let otherEvent: MatrixEvent; - let stateEvent: MatrixEvent; + let result: VoiceBroadcastRecording | null; beforeEach(() => { - roomOnStateEventsCallbackRegistered = new Promise((resolve) => { - roomOnStateEventsCallbackRegisteredResolver = resolve; - }); - - room = { - currentState: { - getStateEvents: jest.fn().mockImplementation((type, userId) => { - if (type === VoiceBroadcastInfoEventType && userId === client.getUserId()) { - return stateEvent; - } - }), - }, - on: jest.fn().mockImplementation((eventType, callback) => { - if (eventType === RoomStateEvent.Events) { - roomOnStateEventsCallback = callback; - roomOnStateEventsCallbackRegisteredResolver(); - } - }), - off: jest.fn(), - } as unknown as Room; - client = stubClient(); + room = new Room(roomId, client, client.getUserId()); + jest.spyOn(room.currentState, "maySendStateEvent"); + mocked(client.getRoom).mockImplementation((getRoomId: string) => { if (getRoomId === roomId) { return room; @@ -85,22 +69,14 @@ describe("startNewVoiceBroadcastRecording", () => { setCurrent: jest.fn(), } as unknown as VoiceBroadcastRecordingsStore; - infoEvent = mkEvent({ - event: true, - type: VoiceBroadcastInfoEventType, - content: { - device_id: client.getDeviceId(), - state: VoiceBroadcastInfoState.Started, - }, - user: client.getUserId(), - room: roomId, - }); + infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, client.getUserId()); otherEvent = mkEvent({ event: true, type: EventType.RoomMember, content: {}, user: client.getUserId(), room: roomId, + skey: "", }); mocked(VoiceBroadcastRecording).mockImplementation(( @@ -115,29 +91,96 @@ describe("startNewVoiceBroadcastRecording", () => { }); }); - it("should create a new Voice Broadcast", (done) => { - let ok = false; + afterEach(() => { + jest.clearAllMocks(); + }); - startNewVoiceBroadcastRecording(roomId, client, recordingsStore).then((recording) => { - expect(ok).toBe(true); - expect(mocked(room.off)).toHaveBeenCalledWith(RoomStateEvent.Events, roomOnStateEventsCallback); - expect(recording.infoEvent).toBe(infoEvent); - expect(recording.start).toHaveBeenCalled(); - done(); + describe("when the current user is allowed to send voice broadcast info state events", () => { + beforeEach(() => { + mocked(room.currentState.maySendStateEvent).mockReturnValue(true); }); - roomOnStateEventsCallbackRegistered.then(() => { - // no state event, yet - roomOnStateEventsCallback(); + describe("when there currently is no other broadcast", () => { + it("should create a new Voice Broadcast", async () => { + mocked(client.sendStateEvent).mockImplementation(async ( + _roomId: string, + _eventType: string, + _content: any, + _stateKey = "", + ) => { + setTimeout(() => { + // emit state events after resolving the promise + room.currentState.setStateEvents([otherEvent]); + room.currentState.setStateEvents([infoEvent]); + }, 0); + return { event_id: infoEvent.getId() }; + }); + const recording = await startNewVoiceBroadcastRecording(room, client, recordingsStore); - // other state event - stateEvent = otherEvent; - roomOnStateEventsCallback(); + expect(client.sendStateEvent).toHaveBeenCalledWith( + roomId, + VoiceBroadcastInfoEventType, + { + chunk_length: 300, + device_id: client.getDeviceId(), + state: VoiceBroadcastInfoState.Started, + }, + client.getUserId(), + ); + expect(recording.infoEvent).toBe(infoEvent); + expect(recording.start).toHaveBeenCalled(); + }); + }); - // the expected Voice Broadcast Info event - stateEvent = infoEvent; - ok = true; - roomOnStateEventsCallback(); + describe("when there already is a live broadcast of the current user", () => { + beforeEach(async () => { + room.currentState.setStateEvents([ + mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Running, client.getUserId()), + ]); + + result = await startNewVoiceBroadcastRecording(room, client, recordingsStore); + }); + + it("should not start a voice broadcast", () => { + expect(result).toBeNull(); + }); + + it("should show an info dialog", () => { + expect(Modal.createDialog).toMatchSnapshot(); + }); + }); + + describe("when there already is a live broadcast of another user", () => { + beforeEach(async () => { + room.currentState.setStateEvents([ + mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Running, otherUserId), + ]); + + result = await startNewVoiceBroadcastRecording(room, client, recordingsStore); + }); + + it("should not start a voice broadcast", () => { + expect(result).toBeNull(); + }); + + it("should show an info dialog", () => { + expect(Modal.createDialog).toMatchSnapshot(); + }); + }); + }); + + describe("when the current user is not allowed to send voice broadcast info state events", () => { + beforeEach(async () => { + mocked(room.currentState.maySendStateEvent).mockReturnValue(false); + result = await startNewVoiceBroadcastRecording(room, client, recordingsStore); + }); + + it("should not start a voice broadcast", () => { + expect(result).toBeNull(); + }); + + it("should show an info dialog", () => { + expect(Modal.createDialog).toMatchSnapshot(); }); }); }); diff --git a/test/voice-broadcast/utils/test-utils.ts b/test/voice-broadcast/utils/test-utils.ts new file mode 100644 index 0000000000..2a73877474 --- /dev/null +++ b/test/voice-broadcast/utils/test-utils.ts @@ -0,0 +1,37 @@ +/* +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 { MatrixEvent } from "matrix-js-sdk/src/matrix"; + +import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../src/voice-broadcast"; +import { mkEvent } from "../../test-utils"; + +export const mkVoiceBroadcastInfoStateEvent = ( + roomId: string, + state: VoiceBroadcastInfoState, + sender: string, +): MatrixEvent => { + return mkEvent({ + event: true, + room: roomId, + user: sender, + type: VoiceBroadcastInfoEventType, + skey: sender, + content: { + state, + }, + }); +};