From 5f59ce182e6deb7f12830e351cb3a41f0c8816d6 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Fri, 21 Oct 2022 09:30:02 +0200 Subject: [PATCH] Resume voice broadcast on load (#9475) --- src/components/structures/MatrixChat.tsx | 5 + src/voice-broadcast/index.ts | 3 + .../models/VoiceBroadcastRecording.ts | 18 ++- .../utils/VoiceBroadcastResumer.ts | 56 ++++++++ ...RoomLiveVoiceBroadcastFromUserAndDevice.ts | 37 +++++ .../utils/resumeVoiceBroadcastInRoom.ts | 34 +++++ .../components/VoiceBroadcastBody-test.tsx | 8 +- .../VoiceBroadcastRecordingBody-test.tsx | 3 +- .../VoiceBroadcastRecordingPip-test.tsx | 14 +- .../VoiceBroadcastRecordingsStore-test.ts | 14 +- .../utils/VoiceBroadcastResumer-test.ts | 111 +++++++++++++++ ...iveVoiceBroadcastFromUserAndDevice-test.ts | 127 ++++++++++++++++++ .../utils/hasRoomLiveVoiceBroadcast-test.ts | 7 +- .../utils/resumeVoiceBroadcastInRoom-test.ts | 110 +++++++++++++++ .../startNewVoiceBroadcastRecording-test.ts | 21 ++- test/voice-broadcast/utils/test-utils.ts | 8 +- 16 files changed, 554 insertions(+), 22 deletions(-) create mode 100644 src/voice-broadcast/utils/VoiceBroadcastResumer.ts create mode 100644 src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts create mode 100644 src/voice-broadcast/utils/resumeVoiceBroadcastInRoom.ts create mode 100644 test/voice-broadcast/utils/VoiceBroadcastResumer-test.ts create mode 100644 test/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice-test.ts create mode 100644 test/voice-broadcast/utils/resumeVoiceBroadcastInRoom-test.ts diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 06a73ff605..0e3fc304e3 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -139,6 +139,7 @@ import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig'; import { isLocalRoom } from '../../utils/localRoom/isLocalRoom'; import { SdkContextClass, SDKContext } from '../../contexts/SDKContext'; import { viewUserDeviceSettings } from '../../actions/handlers/viewUserDeviceSettings'; +import { VoiceBroadcastResumer } from '../../voice-broadcast'; // legacy export export { default as Views } from "../../Views"; @@ -234,6 +235,7 @@ export default class MatrixChat extends React.PureComponent { private focusComposer: boolean; private subTitleStatus: string; private prevWindowWidth: number; + private voiceBroadcastResumer: VoiceBroadcastResumer; private readonly loggedInView: React.RefObject; private readonly dispatcherRef: string; @@ -433,6 +435,7 @@ export default class MatrixChat extends React.PureComponent { window.removeEventListener("resize", this.onWindowResized); if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer); + if (this.voiceBroadcastResumer) this.voiceBroadcastResumer.destroy(); } private onWindowResized = (): void => { @@ -1618,6 +1621,8 @@ export default class MatrixChat extends React.PureComponent { }); } }); + + this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli); } /** diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index 164a59c5fb..39149c0a78 100644 --- a/src/voice-broadcast/index.ts +++ b/src/voice-broadcast/index.ts @@ -36,9 +36,12 @@ export * from "./stores/VoiceBroadcastPlaybacksStore"; export * from "./stores/VoiceBroadcastRecordingsStore"; export * from "./utils/getChunkLength"; export * from "./utils/hasRoomLiveVoiceBroadcast"; +export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice"; +export * from "./utils/resumeVoiceBroadcastInRoom"; export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile"; export * from "./utils/shouldDisplayAsVoiceBroadcastTile"; export * from "./utils/startNewVoiceBroadcastRecording"; +export * from "./utils/VoiceBroadcastResumer"; export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info"; export const VoiceBroadcastChunkEventType = "io.element.voice_broadcast_chunk"; diff --git a/src/voice-broadcast/models/VoiceBroadcastRecording.ts b/src/voice-broadcast/models/VoiceBroadcastRecording.ts index fccd9d5778..28cdd72301 100644 --- a/src/voice-broadcast/models/VoiceBroadcastRecording.ts +++ b/src/voice-broadcast/models/VoiceBroadcastRecording.ts @@ -52,9 +52,23 @@ export class VoiceBroadcastRecording public constructor( public readonly infoEvent: MatrixEvent, private client: MatrixClient, + initialState?: VoiceBroadcastInfoState, ) { super(); + if (initialState) { + this.state = initialState; + } else { + this.setInitialStateFromInfoEvent(); + } + + // TODO Michael W: listen for state updates + // + this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); + this.dispatcherRef = dis.register(this.onAction); + } + + private setInitialStateFromInfoEvent(): void { const room = this.client.getRoom(this.infoEvent.getRoomId()); const relations = room?.getUnfilteredTimelineSet()?.relations?.getChildEventsForEvent( this.infoEvent.getId(), @@ -65,10 +79,6 @@ export class VoiceBroadcastRecording this.state = !relatedEvents?.find((event: MatrixEvent) => { return event.getContent()?.state === VoiceBroadcastInfoState.Stopped; }) ? VoiceBroadcastInfoState.Started : VoiceBroadcastInfoState.Stopped; - // TODO Michael W: add listening for updates - - this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction); - this.dispatcherRef = dis.register(this.onAction); } public async start(): Promise { diff --git a/src/voice-broadcast/utils/VoiceBroadcastResumer.ts b/src/voice-broadcast/utils/VoiceBroadcastResumer.ts new file mode 100644 index 0000000000..c8b3407451 --- /dev/null +++ b/src/voice-broadcast/utils/VoiceBroadcastResumer.ts @@ -0,0 +1,56 @@ +/* +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 { ClientEvent, MatrixClient, Room } from "matrix-js-sdk/src/matrix"; + +import { IDestroyable } from "../../utils/IDestroyable"; +import { findRoomLiveVoiceBroadcastFromUserAndDevice } from "./findRoomLiveVoiceBroadcastFromUserAndDevice"; +import { resumeVoiceBroadcastInRoom } from "./resumeVoiceBroadcastInRoom"; + +export class VoiceBroadcastResumer implements IDestroyable { + private seenRooms = new Set(); + private userId: string; + private deviceId: string; + + public constructor( + private client: MatrixClient, + ) { + this.client.on(ClientEvent.Room, this.onRoom); + this.userId = this.client.getUserId(); + this.deviceId = this.client.getDeviceId(); + } + + private onRoom = (room: Room): void => { + if (this.seenRooms.has(room.roomId)) return; + + this.seenRooms.add(room.roomId); + + const infoEvent = findRoomLiveVoiceBroadcastFromUserAndDevice( + room, + this.userId, + this.deviceId, + ); + + if (infoEvent) { + resumeVoiceBroadcastInRoom(infoEvent, room, this.client); + } + }; + + destroy(): void { + this.client.off(ClientEvent.Room, this.onRoom); + this.seenRooms = new Set(); + } +} diff --git a/src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts b/src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.ts new file mode 100644 index 0000000000..61d54a7660 --- /dev/null +++ b/src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice.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, Room } from "matrix-js-sdk/src/matrix"; + +import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; + +export const findRoomLiveVoiceBroadcastFromUserAndDevice = ( + room: Room, + userId: string, + deviceId: string, +): MatrixEvent | null => { + const stateEvent = room.currentState.getStateEvents(VoiceBroadcastInfoEventType, userId); + + // no broadcast from that user + if (!stateEvent) return null; + + const content = stateEvent.getContent() || {}; + + // stopped broadcast + if (content.state === VoiceBroadcastInfoState.Stopped) return null; + + return content.device_id === deviceId ? stateEvent : null; +}; diff --git a/src/voice-broadcast/utils/resumeVoiceBroadcastInRoom.ts b/src/voice-broadcast/utils/resumeVoiceBroadcastInRoom.ts new file mode 100644 index 0000000000..f365fce226 --- /dev/null +++ b/src/voice-broadcast/utils/resumeVoiceBroadcastInRoom.ts @@ -0,0 +1,34 @@ +/* +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, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; + +import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from ".."; +import { VoiceBroadcastRecordingsStore } from "../stores/VoiceBroadcastRecordingsStore"; + +export const resumeVoiceBroadcastInRoom = (latestInfoEvent: MatrixEvent, room: Room, client: MatrixClient) => { + // voice broadcasts are based on their started event, try to find it + const infoEvent = latestInfoEvent.getContent()?.state === VoiceBroadcastInfoState.Started + ? latestInfoEvent + : room.findEventById(latestInfoEvent.getRelation()?.event_id); + + if (!infoEvent) { + return; + } + + const recording = new VoiceBroadcastRecording(infoEvent, client, VoiceBroadcastInfoState.Paused); + VoiceBroadcastRecordingsStore.instance().setCurrent(recording); +}; diff --git a/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx b/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx index 99cfb69444..ff47ab4c20 100644 --- a/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx +++ b/test/voice-broadcast/components/VoiceBroadcastBody-test.tsx @@ -67,11 +67,17 @@ describe("VoiceBroadcastBody", () => { if (getRoomId === roomId) return room; }); - infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, client.getUserId()); + infoEvent = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Started, + client.getUserId(), + client.getDeviceId(), + ); stoppedEvent = mkVoiceBroadcastInfoStateEvent( roomId, VoiceBroadcastInfoState.Stopped, client.getUserId(), + client.getDeviceId(), infoEvent, ); room.addEventsToTimeline([infoEvent], true, room.getLiveTimeline()); diff --git a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx index fdcab9bca2..25e1f7c215 100644 --- a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx +++ b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody-test.tsx @@ -20,6 +20,7 @@ import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { VoiceBroadcastInfoEventType, + VoiceBroadcastInfoState, VoiceBroadcastRecording, VoiceBroadcastRecordingBody, } from "../../../../src/voice-broadcast"; @@ -49,7 +50,7 @@ describe("VoiceBroadcastRecordingBody", () => { room: roomId, user: userId, }); - recording = new VoiceBroadcastRecording(infoEvent, client); + recording = new VoiceBroadcastRecording(infoEvent, client, VoiceBroadcastInfoState.Running); }); describe("when rendering a live broadcast", () => { diff --git a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx index 8dd6c0a495..4cd85d37ef 100644 --- a/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx +++ b/test/voice-broadcast/components/molecules/VoiceBroadcastRecordingPip-test.tsx @@ -47,13 +47,13 @@ describe("VoiceBroadcastRecordingPip", () => { let renderResult: RenderResult; const renderPip = (state: VoiceBroadcastInfoState) => { - infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, state, client.getUserId()); - recording = new VoiceBroadcastRecording(infoEvent, client); - - if (state === VoiceBroadcastInfoState.Paused) { - recording.pause(); - } - + infoEvent = mkVoiceBroadcastInfoStateEvent( + roomId, + state, + client.getUserId(), + client.getDeviceId(), + ); + recording = new VoiceBroadcastRecording(infoEvent, client, state); renderResult = render(); }; diff --git a/test/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts b/test/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts index 3edb74592e..a18f6c55db 100644 --- a/test/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts +++ b/test/voice-broadcast/stores/VoiceBroadcastRecordingsStore-test.ts @@ -45,8 +45,18 @@ describe("VoiceBroadcastRecordingsStore", () => { return room; } }); - infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, client.getUserId()); - otherInfoEvent = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, client.getUserId()); + infoEvent = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Started, + client.getUserId(), + client.getDeviceId(), + ); + otherInfoEvent = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Started, + client.getUserId(), + client.getDeviceId(), + ); recording = new VoiceBroadcastRecording(infoEvent, client); otherRecording = new VoiceBroadcastRecording(otherInfoEvent, client); recordings = new VoiceBroadcastRecordingsStore(); diff --git a/test/voice-broadcast/utils/VoiceBroadcastResumer-test.ts b/test/voice-broadcast/utils/VoiceBroadcastResumer-test.ts new file mode 100644 index 0000000000..4583590342 --- /dev/null +++ b/test/voice-broadcast/utils/VoiceBroadcastResumer-test.ts @@ -0,0 +1,111 @@ +/* +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 { mocked } from "jest-mock"; +import { ClientEvent, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; + +import { + findRoomLiveVoiceBroadcastFromUserAndDevice, + resumeVoiceBroadcastInRoom, + VoiceBroadcastInfoState, + VoiceBroadcastResumer, +} from "../../../src/voice-broadcast"; +import { stubClient } from "../../test-utils"; +import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; + +jest.mock("../../../src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice"); +jest.mock("../../../src/voice-broadcast/utils/resumeVoiceBroadcastInRoom"); + +describe("VoiceBroadcastResumer", () => { + const roomId = "!room:example.com"; + let client: MatrixClient; + let room: Room; + let resumer: VoiceBroadcastResumer; + let infoEvent: MatrixEvent; + + beforeEach(() => { + client = stubClient(); + jest.spyOn(client, "off"); + room = new Room(roomId, client, client.getUserId()); + mocked(client.getRoom).mockImplementation((getRoomId: string) => { + if (getRoomId === roomId) return room; + }); + resumer = new VoiceBroadcastResumer(client); + infoEvent = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Started, + client.getUserId(), + client.getDeviceId(), + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("when there is no info event", () => { + beforeEach(() => { + client.emit(ClientEvent.Room, room); + }); + + it("should not resume a broadcast", () => { + expect(resumeVoiceBroadcastInRoom).not.toHaveBeenCalled(); + }); + }); + + describe("when there is an info event", () => { + beforeEach(() => { + mocked(findRoomLiveVoiceBroadcastFromUserAndDevice).mockImplementation(( + findRoom: Room, + userId: string, + deviceId: string, + ) => { + if (findRoom === room && userId === client.getUserId() && deviceId === client.getDeviceId()) { + return infoEvent; + } + }); + client.emit(ClientEvent.Room, room); + }); + + it("should resume a broadcast", () => { + expect(resumeVoiceBroadcastInRoom).toHaveBeenCalledWith( + infoEvent, + room, + client, + ); + }); + + describe("and emitting a room event again", () => { + beforeEach(() => { + client.emit(ClientEvent.Room, room); + }); + + it("should not resume the broadcast again", () => { + expect(resumeVoiceBroadcastInRoom).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe("when calling destroy", () => { + beforeEach(() => { + resumer.destroy(); + }); + + it("should deregister from the client", () => { + expect(client.off).toHaveBeenCalledWith(ClientEvent.Room, expect.any(Function)); + }); + }); +}); diff --git a/test/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice-test.ts b/test/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice-test.ts new file mode 100644 index 0000000000..0fa04963ae --- /dev/null +++ b/test/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice-test.ts @@ -0,0 +1,127 @@ +/* +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 { mocked } from "jest-mock"; +import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; + +import { + findRoomLiveVoiceBroadcastFromUserAndDevice, + VoiceBroadcastInfoEventType, + VoiceBroadcastInfoState, +} from "../../../src/voice-broadcast"; +import { mkEvent, stubClient } from "../../test-utils"; +import { mkVoiceBroadcastInfoStateEvent } from "./test-utils"; + +describe("findRoomLiveVoiceBroadcastFromUserAndDevice", () => { + const roomId = "!room:example.com"; + let client: MatrixClient; + let room: Room; + + const itShouldReturnNull = () => { + it("should return null", () => { + expect(findRoomLiveVoiceBroadcastFromUserAndDevice( + room, + client.getUserId(), + client.getDeviceId(), + )).toBeNull(); + }); + }; + + beforeAll(() => { + client = stubClient(); + room = new Room(roomId, client, client.getUserId()); + jest.spyOn(room.currentState, "getStateEvents"); + mocked(client.getRoom).mockImplementation((getRoomId: string) => { + if (getRoomId === roomId) return room; + }); + }); + + describe("when there is no info event", () => { + itShouldReturnNull(); + }); + + describe("when there is an info event without content", () => { + beforeEach(() => { + room.currentState.setStateEvents([ + mkEvent({ + event: true, + type: VoiceBroadcastInfoEventType, + room: roomId, + user: client.getUserId(), + content: {}, + }), + ]); + }); + + itShouldReturnNull(); + }); + + describe("when there is a stopped info event", () => { + beforeEach(() => { + room.currentState.setStateEvents([ + mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Stopped, + client.getUserId(), + client.getDeviceId(), + ), + ]); + }); + + itShouldReturnNull(); + }); + + describe("when there is a started info event from another device", () => { + beforeEach(() => { + const event = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Stopped, + client.getUserId(), + "JKL123", + ); + room.currentState.setStateEvents([event]); + }); + + itShouldReturnNull(); + }); + + describe("when there is a started info event", () => { + let event: MatrixEvent; + + beforeEach(() => { + event = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Started, + client.getUserId(), + client.getDeviceId(), + ); + room.currentState.setStateEvents([event]); + }); + + it("should return this event", () => { + expect(room.currentState.getStateEvents).toHaveBeenCalledWith( + VoiceBroadcastInfoEventType, + client.getUserId(), + ); + + expect(findRoomLiveVoiceBroadcastFromUserAndDevice( + room, + client.getUserId(), + client.getDeviceId(), + )).toBe(event); + }); + }); +}); diff --git a/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts b/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts index c9fbc5f09e..40b50ec883 100644 --- a/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts +++ b/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts @@ -35,7 +35,12 @@ describe("hasRoomLiveVoiceBroadcast", () => { sender: string, ) => { room.currentState.setStateEvents([ - mkVoiceBroadcastInfoStateEvent(room.roomId, state, sender), + mkVoiceBroadcastInfoStateEvent( + room.roomId, + state, + sender, + "ASD123", + ), ]); }; diff --git a/test/voice-broadcast/utils/resumeVoiceBroadcastInRoom-test.ts b/test/voice-broadcast/utils/resumeVoiceBroadcastInRoom-test.ts new file mode 100644 index 0000000000..8d435fb62e --- /dev/null +++ b/test/voice-broadcast/utils/resumeVoiceBroadcastInRoom-test.ts @@ -0,0 +1,110 @@ +/* +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 { mocked } from "jest-mock"; +import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; + +import { + resumeVoiceBroadcastInRoom, + VoiceBroadcastInfoState, + VoiceBroadcastRecording, + VoiceBroadcastRecordingsStore, +} from "../../../src/voice-broadcast"; +import { stubClient } from "../../test-utils"; +import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils"; + +const mockRecording = jest.fn(); + +jest.mock("../../../src/voice-broadcast/models/VoiceBroadcastRecording", () => ({ + ...jest.requireActual("../../../src/voice-broadcast/models/VoiceBroadcastRecording") as object, + VoiceBroadcastRecording: jest.fn().mockImplementation(() => mockRecording), +})); + +describe("resumeVoiceBroadcastInRoom", () => { + let client: MatrixClient; + const roomId = "!room:example.com"; + let room: Room; + let startedInfoEvent: MatrixEvent; + let stoppedInfoEvent: MatrixEvent; + + const itShouldStartAPausedRecording = () => { + it("should start a paused recording", () => { + expect(VoiceBroadcastRecording).toHaveBeenCalledWith( + startedInfoEvent, + client, + VoiceBroadcastInfoState.Paused, + ); + expect(VoiceBroadcastRecordingsStore.instance().setCurrent).toHaveBeenCalledWith(mockRecording); + }); + }; + + beforeEach(() => { + client = stubClient(); + room = new Room(roomId, client, client.getUserId()); + jest.spyOn(room, "findEventById"); + jest.spyOn(VoiceBroadcastRecordingsStore.instance(), "setCurrent").mockImplementation(); + + startedInfoEvent = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Started, + client.getUserId(), + client.getDeviceId(), + ); + + stoppedInfoEvent = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Stopped, + client.getUserId(), + client.getDeviceId(), + startedInfoEvent, + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("when called with a stopped info event", () => { + describe("and there is a related event", () => { + beforeEach(() => { + mocked(room.findEventById).mockReturnValue(startedInfoEvent); + resumeVoiceBroadcastInRoom(stoppedInfoEvent, room, client); + }); + + itShouldStartAPausedRecording(); + }); + + describe("and there is no related event", () => { + beforeEach(() => { + mocked(room.findEventById).mockReturnValue(null); + resumeVoiceBroadcastInRoom(stoppedInfoEvent, room, client); + }); + + it("should not start a broadcast", () => { + expect(VoiceBroadcastRecording).not.toHaveBeenCalled(); + expect(VoiceBroadcastRecordingsStore.instance().setCurrent).not.toHaveBeenCalled(); + }); + }); + }); + + describe("when called with a started info event", () => { + beforeEach(() => { + resumeVoiceBroadcastInRoom(startedInfoEvent, room, client); + }); + + itShouldStartAPausedRecording(); + }); +}); diff --git a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts index a0fac9ebd6..b19ea3c691 100644 --- a/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/utils/startNewVoiceBroadcastRecording-test.ts @@ -70,7 +70,12 @@ describe("startNewVoiceBroadcastRecording", () => { getCurrent: jest.fn(), } as unknown as VoiceBroadcastRecordingsStore; - infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, client.getUserId()); + infoEvent = mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Started, + client.getUserId(), + client.getDeviceId(), + ); otherEvent = mkEvent({ event: true, type: EventType.RoomMember, @@ -154,7 +159,12 @@ describe("startNewVoiceBroadcastRecording", () => { describe("when there already is a live broadcast of the current user in the room", () => { beforeEach(async () => { room.currentState.setStateEvents([ - mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Running, client.getUserId()), + mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Running, + client.getUserId(), + client.getDeviceId(), + ), ]); result = await startNewVoiceBroadcastRecording(room, client, recordingsStore); @@ -172,7 +182,12 @@ describe("startNewVoiceBroadcastRecording", () => { describe("when there already is a live broadcast of another user", () => { beforeEach(async () => { room.currentState.setStateEvents([ - mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Running, otherUserId), + mkVoiceBroadcastInfoStateEvent( + roomId, + VoiceBroadcastInfoState.Running, + otherUserId, + "ASD123", + ), ]); result = await startNewVoiceBroadcastRecording(room, client, recordingsStore); diff --git a/test/voice-broadcast/utils/test-utils.ts b/test/voice-broadcast/utils/test-utils.ts index fc8635fc4c..09a2ba0ed3 100644 --- a/test/voice-broadcast/utils/test-utils.ts +++ b/test/voice-broadcast/utils/test-utils.ts @@ -22,7 +22,8 @@ import { mkEvent } from "../../test-utils"; export const mkVoiceBroadcastInfoStateEvent = ( roomId: string, state: VoiceBroadcastInfoState, - sender: string, + senderId: string, + senderDeviceId: string, startedInfoEvent?: MatrixEvent, ): MatrixEvent => { const relationContent = {}; @@ -37,11 +38,12 @@ export const mkVoiceBroadcastInfoStateEvent = ( return mkEvent({ event: true, room: roomId, - user: sender, + user: senderId, type: VoiceBroadcastInfoEventType, - skey: sender, + skey: senderId, content: { state, + device_id: senderDeviceId, ...relationContent, }, });