From 0630a9c4489d3c1dd4a079c1be2c73a75b77b6bc Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Sun, 16 Oct 2022 13:35:25 +0200 Subject: [PATCH] End voice broadcast recording on any call (#9425) --- src/stores/widgets/StopGapWidget.ts | 5 +++ .../models/VoiceBroadcastRecording.ts | 14 ++++++- test/stores/widgets/StopGapWidget-test.ts | 42 +++++++++++++++++++ .../models/VoiceBroadcastRecording-test.ts | 21 ++++++++-- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index bbf166150c..91a262fdca 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -65,6 +65,7 @@ import { arrayFastClone } from "../../utils/arrays"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import Modal from "../../Modal"; import ErrorDialog from "../../components/views/dialogs/ErrorDialog"; +import { VoiceBroadcastRecordingsStore } from "../../voice-broadcast"; // TODO: Destroy all of this code @@ -280,6 +281,10 @@ export class StopGapWidget extends EventEmitter { }); this.messaging.on("capabilitiesNotified", () => this.emit("capabilitiesNotified")); this.messaging.on(`action:${WidgetApiFromWidgetAction.OpenModalWidget}`, this.onOpenModal); + this.messaging.on(`action:${ElementWidgetActions.JoinCall}`, () => { + // stop voice broadcast recording when any widget sends a "join" + VoiceBroadcastRecordingsStore.instance().getCurrent()?.stop(); + }); // Always attach a handler for ViewRoom, but permission check it internally this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent) => { diff --git a/src/voice-broadcast/models/VoiceBroadcastRecording.ts b/src/voice-broadcast/models/VoiceBroadcastRecording.ts index 74b6f2e128..96b62a670f 100644 --- a/src/voice-broadcast/models/VoiceBroadcastRecording.ts +++ b/src/voice-broadcast/models/VoiceBroadcastRecording.ts @@ -30,6 +30,8 @@ import { uploadFile } from "../../ContentMessages"; import { IEncryptedFile } from "../../customisations/models/IMediaEventContent"; import { createVoiceMessageContent } from "../../utils/createVoiceMessageContent"; import { IDestroyable } from "../../utils/IDestroyable"; +import dis from "../../dispatcher/dispatcher"; +import { ActionPayload } from "../../dispatcher/payloads"; export enum VoiceBroadcastRecordingEvent { StateChanged = "liveness_changed", @@ -45,6 +47,7 @@ export class VoiceBroadcastRecording private state: VoiceBroadcastInfoState; private recorder: VoiceBroadcastRecorder; private sequence = 1; + private dispatcherRef: string; public constructor( public readonly infoEvent: MatrixEvent, @@ -62,8 +65,9 @@ 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.dispatcherRef = dis.register(this.onAction); } public async start(): Promise { @@ -96,8 +100,16 @@ export class VoiceBroadcastRecording } this.removeAllListeners(); + dis.unregister(this.dispatcherRef); } + private onAction = (payload: ActionPayload) => { + if (payload.action !== "call_state") return; + + // stop on any call action + this.stop(); + }; + private setState(state: VoiceBroadcastInfoState): void { this.state = state; this.emit(VoiceBroadcastRecordingEvent.StateChanged, this.state); diff --git a/test/stores/widgets/StopGapWidget-test.ts b/test/stores/widgets/StopGapWidget-test.ts index 9f5ca03280..133f830f3d 100644 --- a/test/stores/widgets/StopGapWidget-test.ts +++ b/test/stores/widgets/StopGapWidget-test.ts @@ -15,12 +15,19 @@ limitations under the License. */ import { mocked, MockedObject } from "jest-mock"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client"; import { ClientWidgetApi } from "matrix-widget-api"; import { stubClient, mkRoom, mkEvent } from "../../test-utils"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import { StopGapWidget } from "../../../src/stores/widgets/StopGapWidget"; +import { ElementWidgetActions } from "../../../src/stores/widgets/ElementWidgetActions"; +import { + VoiceBroadcastInfoEventType, + VoiceBroadcastRecording, + VoiceBroadcastRecordingsStore, +} from "../../../src/voice-broadcast"; jest.mock("matrix-widget-api/lib/ClientWidgetApi"); @@ -68,4 +75,39 @@ describe("StopGapWidget", () => { await Promise.resolve(); // flush promises expect(messaging.feedToDevice).toHaveBeenCalledWith(event.getEffectiveEvent(), false); }); + + describe("when there is a voice broadcast recording", () => { + let voiceBroadcastInfoEvent: MatrixEvent; + let voiceBroadcastRecording: VoiceBroadcastRecording; + + beforeEach(() => { + voiceBroadcastInfoEvent = mkEvent({ + event: true, + room: client.getRoom("x").roomId, + user: client.getUserId(), + type: VoiceBroadcastInfoEventType, + content: {}, + }); + voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client); + jest.spyOn(voiceBroadcastRecording, "stop"); + jest.spyOn(VoiceBroadcastRecordingsStore.instance(), "getCurrent").mockReturnValue(voiceBroadcastRecording); + }); + + describe(`and receiving a action:${ElementWidgetActions.JoinCall} message`, () => { + beforeEach(async () => { + messaging.on.mock.calls.find( + ([event, listener]) => { + if (event === `action:${ElementWidgetActions.JoinCall}`) { + listener(); + return true; + } + }, + ); + }); + + it("should stop the current voice broadcast recording", () => { + expect(voiceBroadcastRecording.stop).toHaveBeenCalled(); + }); + }); + }); }); diff --git a/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts b/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts index 5fca34e035..25a325aba7 100644 --- a/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts +++ b/test/voice-broadcast/models/VoiceBroadcastRecording-test.ts @@ -41,6 +41,7 @@ import { VoiceBroadcastRecordingEvent, } from "../../../src/voice-broadcast"; import { mkEvent, mkStubRoom, stubClient } from "../../test-utils"; +import dis from "../../../src/dispatcher/dispatcher"; jest.mock("../../../src/voice-broadcast/audio/VoiceBroadcastRecorder", () => ({ ...jest.requireActual("../../../src/voice-broadcast/audio/VoiceBroadcastRecorder") as object, @@ -83,6 +84,12 @@ describe("VoiceBroadcastRecording", () => { jest.spyOn(voiceBroadcastRecording, "removeAllListeners"); }; + const itShouldBeInState = (state: VoiceBroadcastInfoState) => { + it(`should be in state stopped ${state}`, () => { + expect(voiceBroadcastRecording.getState()).toBe(state); + }); + }; + beforeEach(() => { client = stubClient(); room = mkStubRoom(roomId, "Test Room", client); @@ -191,9 +198,7 @@ describe("VoiceBroadcastRecording", () => { ); }); - it("should be in state stopped", () => { - expect(voiceBroadcastRecording.getState()).toBe(VoiceBroadcastInfoState.Stopped); - }); + itShouldBeInState(VoiceBroadcastInfoState.Stopped); it("should emit a stopped state changed event", () => { expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Stopped); @@ -209,6 +214,16 @@ describe("VoiceBroadcastRecording", () => { expect(voiceBroadcastRecorder.start).toHaveBeenCalled(); }); + describe("and receiving a call action", () => { + beforeEach(() => { + dis.dispatch({ + action: "call_state", + }, true); + }); + + itShouldBeInState(VoiceBroadcastInfoState.Stopped); + }); + describe("and a chunk has been recorded", () => { beforeEach(async () => { await onChunkRecorded({