From a8e15ebe60b344c572915fd2b7bc794e4287a194 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Thu, 24 Nov 2022 09:08:41 +0100 Subject: [PATCH] Add voice broadcast playback pip (#9603) --- src/components/views/voip/PipView.tsx | 23 +++- src/contexts/SDKContext.ts | 14 ++- src/stores/RoomViewStore.tsx | 35 ++++++ .../molecules/VoiceBroadcastPlaybackBody.tsx | 10 +- .../hooks/useCurrentVoiceBroadcastPlayback.ts | 44 +++++++ src/voice-broadcast/index.ts | 2 + .../stores/VoiceBroadcastPlaybacksStore.ts | 25 ++-- ...rCurrentVoiceBroadcastPlaybackIfStopped.ts | 26 ++++ ...doMaybeSetCurrentVoiceBroadcastPlayback.ts | 64 ++++++++++ .../utils/hasRoomLiveVoiceBroadcast.ts | 18 ++- test/TestSdkContext.ts | 7 +- test/components/views/voip/PipView-test.tsx | 114 ++++++++++++++++-- .../utils/hasRoomLiveVoiceBroadcast-test.ts | 31 +++-- 13 files changed, 372 insertions(+), 41 deletions(-) create mode 100644 src/voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback.ts create mode 100644 src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts create mode 100644 src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts diff --git a/src/components/views/voip/PipView.tsx b/src/components/views/voip/PipView.tsx index b5ac6a85f7..27f7798f11 100644 --- a/src/components/views/voip/PipView.tsx +++ b/src/components/views/voip/PipView.tsx @@ -39,11 +39,14 @@ import { CallStore } from "../../../stores/CallStore"; import { useCurrentVoiceBroadcastPreRecording, useCurrentVoiceBroadcastRecording, + VoiceBroadcastPlayback, + VoiceBroadcastPlaybackBody, VoiceBroadcastPreRecording, VoiceBroadcastPreRecordingPip, VoiceBroadcastRecording, VoiceBroadcastRecordingPip, } from '../../../voice-broadcast'; +import { useCurrentVoiceBroadcastPlayback } from '../../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback'; const SHOW_CALL_IN_STATES = [ CallState.Connected, @@ -57,6 +60,7 @@ const SHOW_CALL_IN_STATES = [ interface IProps { voiceBroadcastRecording?: Optional; voiceBroadcastPreRecording?: Optional; + voiceBroadcastPlayback?: Optional; } interface IState { @@ -330,6 +334,15 @@ class PipView extends React.Component { this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId }); } + private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren { + return ({ onStartMoving }) =>
+ +
; + } + private createVoiceBroadcastPreRecordingPipContent( voiceBroadcastPreRecording: VoiceBroadcastPreRecording, ): CreatePipChildren { @@ -358,6 +371,10 @@ class PipView extends React.Component { pipContent = this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording); } + if (this.props.voiceBroadcastPlayback) { + pipContent = this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback); + } + if (this.props.voiceBroadcastRecording) { pipContent = this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording); } @@ -430,9 +447,13 @@ const PipViewHOC: React.FC = (props) => { const voiceBroadcastRecordingsStore = sdkContext.voiceBroadcastRecordingsStore; const { currentVoiceBroadcastRecording } = useCurrentVoiceBroadcastRecording(voiceBroadcastRecordingsStore); + const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore; + const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore); + return ; }; diff --git a/src/contexts/SDKContext.ts b/src/contexts/SDKContext.ts index 8870cb7c1b..882f9a0b64 100644 --- a/src/contexts/SDKContext.ts +++ b/src/contexts/SDKContext.ts @@ -30,7 +30,11 @@ import TypingStore from "../stores/TypingStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore"; import WidgetStore from "../stores/WidgetStore"; -import { VoiceBroadcastPreRecordingStore, VoiceBroadcastRecordingsStore } from "../voice-broadcast"; +import { + VoiceBroadcastPlaybacksStore, + VoiceBroadcastPreRecordingStore, + VoiceBroadcastRecordingsStore, +} from "../voice-broadcast"; export const SDKContext = createContext(undefined); SDKContext.displayName = "SDKContext"; @@ -68,6 +72,7 @@ export class SdkContextClass { protected _TypingStore?: TypingStore; protected _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore; protected _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore; + protected _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore; /** * Automatically construct stores which need to be created eagerly so they can register with @@ -166,4 +171,11 @@ export class SdkContextClass { } return this._VoiceBroadcastPreRecordingStore; } + + public get voiceBroadcastPlaybacksStore(): VoiceBroadcastPlaybacksStore { + if (!this._VoiceBroadcastPlaybacksStore) { + this._VoiceBroadcastPlaybacksStore = VoiceBroadcastPlaybacksStore.instance(); + } + return this._VoiceBroadcastPlaybacksStore; + } } diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 13c1e09c76..02d8b51a6c 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -51,6 +51,11 @@ import { UPDATE_EVENT } from "./AsyncStore"; import { SdkContextClass } from "../contexts/SDKContext"; import { CallStore } from "./CallStore"; import { ThreadPayload } from "../dispatcher/payloads/ThreadPayload"; +import { + doClearCurrentVoiceBroadcastPlaybackIfStopped, + doMaybeSetCurrentVoiceBroadcastPlayback, +} from "../voice-broadcast"; +import { IRoomStateEventsActionPayload } from "../actions/MatrixActionCreators"; const NUM_JOIN_RETRY = 5; @@ -195,6 +200,28 @@ export class RoomViewStore extends EventEmitter { this.emit(UPDATE_EVENT); } + private doMaybeSetCurrentVoiceBroadcastPlayback(room: Room): void { + doMaybeSetCurrentVoiceBroadcastPlayback( + room, + this.stores.client, + this.stores.voiceBroadcastPlaybacksStore, + this.stores.voiceBroadcastRecordingsStore, + ); + } + + private onRoomStateEvents(event: MatrixEvent): void { + const roomId = event.getRoomId?.(); + + // no room or not current room + if (!roomId || roomId !== this.state.roomId) return; + + const room = this.stores.client?.getRoom(roomId); + + if (room) { + this.doMaybeSetCurrentVoiceBroadcastPlayback(room); + } + } + private onDispatch(payload): void { // eslint-disable-line @typescript-eslint/naming-convention switch (payload.action) { // view_room: @@ -219,6 +246,10 @@ export class RoomViewStore extends EventEmitter { wasContextSwitch: false, viewingCall: false, }); + doClearCurrentVoiceBroadcastPlaybackIfStopped(this.stores.voiceBroadcastPlaybacksStore); + break; + case "MatrixActions.RoomState.events": + this.onRoomStateEvents((payload as IRoomStateEventsActionPayload).event); break; case Action.ViewRoomError: this.viewRoomError(payload); @@ -395,6 +426,10 @@ export class RoomViewStore extends EventEmitter { metricsTrigger: payload.metricsTrigger as JoinRoomPayload["metricsTrigger"], }); } + + if (room) { + this.doMaybeSetCurrentVoiceBroadcastPlayback(room); + } } else if (payload.room_alias) { // Try the room alias to room ID navigation cache first to avoid // blocking room navigation on the homeserver. diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx index beb4864368..9999603c08 100644 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import React, { ReactElement } from "react"; +import classNames from "classnames"; import { VoiceBroadcastControl, @@ -36,10 +37,12 @@ import { SeekButton } from "../atoms/SeekButton"; const SEEK_TIME = 30; interface VoiceBroadcastPlaybackBodyProps { + pip?: boolean; playback: VoiceBroadcastPlayback; } export const VoiceBroadcastPlaybackBody: React.FC = ({ + pip = false, playback, }) => { const { @@ -107,8 +110,13 @@ export const VoiceBroadcastPlaybackBody: React.FC; } + const classes = classNames({ + mx_VoiceBroadcastBody: true, + ["mx_VoiceBroadcastBody--pip"]: pip, + }); + return ( -
+
{ + const [currentVoiceBroadcastPlayback, setVoiceBroadcastPlayback] = useState( + voiceBroadcastPlaybackStore.getCurrent(), + ); + + useTypedEventEmitter( + voiceBroadcastPlaybackStore, + VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, + (playback: VoiceBroadcastPlayback) => { + setVoiceBroadcastPlayback(playback); + }, + ); + + return { + currentVoiceBroadcastPlayback, + }; +}; diff --git a/src/voice-broadcast/index.ts b/src/voice-broadcast/index.ts index d2771a5b44..21e1bdd4af 100644 --- a/src/voice-broadcast/index.ts +++ b/src/voice-broadcast/index.ts @@ -40,6 +40,8 @@ export * from "./stores/VoiceBroadcastPlaybacksStore"; export * from "./stores/VoiceBroadcastPreRecordingStore"; export * from "./stores/VoiceBroadcastRecordingsStore"; export * from "./utils/checkVoiceBroadcastPreConditions"; +export * from "./utils/doClearCurrentVoiceBroadcastPlaybackIfStopped"; +export * from "./utils/doMaybeSetCurrentVoiceBroadcastPlayback"; export * from "./utils/getChunkLength"; export * from "./utils/getMaxBroadcastLength"; export * from "./utils/hasRoomLiveVoiceBroadcast"; diff --git a/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts b/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts index 03378d9492..e34a259379 100644 --- a/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts +++ b/src/voice-broadcast/stores/VoiceBroadcastPlaybacksStore.ts @@ -25,7 +25,7 @@ export enum VoiceBroadcastPlaybacksStoreEvent { } interface EventMap { - [VoiceBroadcastPlaybacksStoreEvent.CurrentChanged]: (recording: VoiceBroadcastPlayback) => void; + [VoiceBroadcastPlaybacksStoreEvent.CurrentChanged]: (recording: VoiceBroadcastPlayback | null) => void; } /** @@ -53,7 +53,14 @@ export class VoiceBroadcastPlaybacksStore this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, current); } - public getCurrent(): VoiceBroadcastPlayback { + public clearCurrent(): void { + if (this.current === null) return; + + this.current = null; + this.emit(VoiceBroadcastPlaybacksStoreEvent.CurrentChanged, null); + } + + public getCurrent(): VoiceBroadcastPlayback | null { return this.current; } @@ -80,11 +87,15 @@ export class VoiceBroadcastPlaybacksStore state: VoiceBroadcastPlaybackState, playback: VoiceBroadcastPlayback, ): void => { - if ([ - VoiceBroadcastPlaybackState.Buffering, - VoiceBroadcastPlaybackState.Playing, - ].includes(state)) { - this.pauseExcept(playback); + switch (state) { + case VoiceBroadcastPlaybackState.Buffering: + case VoiceBroadcastPlaybackState.Playing: + this.pauseExcept(playback); + this.setCurrent(playback); + break; + case VoiceBroadcastPlaybackState.Stopped: + this.clearCurrent(); + break; } }; diff --git a/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts b/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts new file mode 100644 index 0000000000..8a3bc8be9d --- /dev/null +++ b/src/voice-broadcast/utils/doClearCurrentVoiceBroadcastPlaybackIfStopped.ts @@ -0,0 +1,26 @@ +/* +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 { VoiceBroadcastPlaybacksStore, VoiceBroadcastPlaybackState } from ".."; + +export const doClearCurrentVoiceBroadcastPlaybackIfStopped = ( + voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, +) => { + if (voiceBroadcastPlaybacksStore.getCurrent()?.getState() === VoiceBroadcastPlaybackState.Stopped) { + // clear current if stopped + return; + } +}; diff --git a/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts b/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts new file mode 100644 index 0000000000..ad92a95977 --- /dev/null +++ b/src/voice-broadcast/utils/doMaybeSetCurrentVoiceBroadcastPlayback.ts @@ -0,0 +1,64 @@ +/* +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, + VoiceBroadcastPlaybacksStore, + VoiceBroadcastPlaybackState, + VoiceBroadcastRecordingsStore, +} from ".."; + +/** + * When a live voice broadcast is in the room and + * another voice broadcast is not currently being listened to or recorded + * the live broadcast in the room is set as the current broadcast to listen to. + * When there is no live broadcast in the room: clear current broadcast. + * + * @param {Room} room The room to check for a live voice broadcast + * @param {MatrixClient} client + * @param {VoiceBroadcastPlaybacksStore} voiceBroadcastPlaybacksStore + * @param {VoiceBroadcastRecordingsStore} voiceBroadcastRecordingsStore + */ +export const doMaybeSetCurrentVoiceBroadcastPlayback = ( + room: Room, + client: MatrixClient, + voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore, + voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore, +): void => { + // do not disturb the current recording + if (voiceBroadcastRecordingsStore.hasCurrent()) return; + + const currentPlayback = voiceBroadcastPlaybacksStore.getCurrent(); + + if (currentPlayback && currentPlayback.getState() !== VoiceBroadcastPlaybackState.Stopped) { + // do not disturb the current playback + return; + } + + const { infoEvent } = hasRoomLiveVoiceBroadcast(room); + + if (infoEvent) { + // live broadcast in the room + no recording + not listening yet: set the current broadcast + const voiceBroadcastPlayback = voiceBroadcastPlaybacksStore.getByInfoEvent(infoEvent, client); + voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback); + return; + } + + // no broadcast; not listening: clear current + voiceBroadcastPlaybacksStore.clearCurrent(); +}; diff --git a/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts b/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts index 577b9ed880..ae506c68ba 100644 --- a/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts +++ b/src/voice-broadcast/utils/hasRoomLiveVoiceBroadcast.ts @@ -19,36 +19,42 @@ import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from ".."; interface Result { + // whether there is a live broadcast in the room hasBroadcast: boolean; + // info event of any live broadcast in the room + infoEvent: MatrixEvent | null; + // whether the broadcast was started by the user 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 => { +export const hasRoomLiveVoiceBroadcast = (room: Room, userId?: string): Result => { let hasBroadcast = false; let startedByUser = false; + let infoEvent: MatrixEvent | null = null; const stateEvents = room.currentState.getStateEvents(VoiceBroadcastInfoEventType); - stateEvents.forEach((event: MatrixEvent) => { + stateEvents.every((event: MatrixEvent) => { const state = event.getContent()?.state; if (state && state !== VoiceBroadcastInfoState.Stopped) { hasBroadcast = true; + infoEvent = event; // state key = sender's MXID if (event.getStateKey() === userId) { + infoEvent = event; startedByUser = true; // break here, because more than true / true is not possible return false; } } + + return true; }); return { hasBroadcast, + infoEvent, startedByUser, }; }; diff --git a/test/TestSdkContext.ts b/test/TestSdkContext.ts index 7686285e23..5aad5bcfa5 100644 --- a/test/TestSdkContext.ts +++ b/test/TestSdkContext.ts @@ -24,7 +24,11 @@ import { SpaceStoreClass } from "../src/stores/spaces/SpaceStore"; import { WidgetLayoutStore } from "../src/stores/widgets/WidgetLayoutStore"; import { WidgetPermissionStore } from "../src/stores/widgets/WidgetPermissionStore"; import WidgetStore from "../src/stores/WidgetStore"; -import { VoiceBroadcastPreRecordingStore, VoiceBroadcastRecordingsStore } from "../src/voice-broadcast"; +import { + VoiceBroadcastPlaybacksStore, + VoiceBroadcastPreRecordingStore, + VoiceBroadcastRecordingsStore, +} from "../src/voice-broadcast"; /** * A class which provides the same API as SdkContextClass but adds additional unsafe setters which can @@ -42,6 +46,7 @@ export class TestSdkContext extends SdkContextClass { public _SpaceStore?: SpaceStoreClass; public _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore; public _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore; + public _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore; constructor() { super(); diff --git a/test/components/views/voip/PipView-test.tsx b/test/components/views/voip/PipView-test.tsx index 4907ca4b11..1dcc617e64 100644 --- a/test/components/views/voip/PipView-test.tsx +++ b/test/components/views/voip/PipView-test.tsx @@ -21,6 +21,7 @@ import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { Widget, ClientWidgetApi } from "matrix-widget-api"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import type { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { @@ -46,12 +47,15 @@ import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPay import { TestSdkContext } from "../../../TestSdkContext"; import { VoiceBroadcastInfoState, + VoiceBroadcastPlaybacksStore, VoiceBroadcastPreRecording, VoiceBroadcastPreRecordingStore, VoiceBroadcastRecording, VoiceBroadcastRecordingsStore, } from "../../../../src/voice-broadcast"; import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils"; +import { RoomViewStore } from "../../../../src/stores/RoomViewStore"; +import { IRoomStateEventsActionPayload } from "../../../../src/actions/MatrixActionCreators"; describe("PipView", () => { useMockedCalls(); @@ -60,9 +64,11 @@ describe("PipView", () => { let sdkContext: TestSdkContext; let client: Mocked; let room: Room; + let room2: Room; let alice: RoomMember; let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore; let voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore; + let voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore; beforeEach(async () => { stubClient(); @@ -72,17 +78,27 @@ describe("PipView", () => { room = new Room("!1:example.org", client, "@alice:example.org", { pendingEventOrdering: PendingEventOrdering.Detached, }); - client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null); - client.getRooms.mockReturnValue([room]); alice = mkRoomMember(room.roomId, "@alice:example.org"); + + room2 = new Room("!2:example.com", client, "@alice:example.org", { + pendingEventOrdering: PendingEventOrdering.Detached, + }); + client.getRoom.mockImplementation((roomId: string) => { + if (roomId === room.roomId) return room; + if (roomId === room2.roomId) return room2; + return null; + }); + client.getRooms.mockReturnValue([room, room2]); + client.reEmitter.reEmit(room, [RoomStateEvent.Events]); + room.currentState.setStateEvents([ mkRoomCreateEvent(alice.userId, room.roomId), ]); jest.spyOn(room, "getMember").mockImplementation(userId => userId === alice.userId ? alice : null); - client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null); - client.getRooms.mockReturnValue([room]); - client.reEmitter.reEmit(room, [RoomStateEvent.Events]); + room2.currentState.setStateEvents([ + mkRoomCreateEvent(alice.userId, room2.roomId), + ]); await Promise.all([CallStore.instance, WidgetMessagingStore.instance].map( store => setupAsyncStoreWithClient(store, client), @@ -91,9 +107,11 @@ describe("PipView", () => { sdkContext = new TestSdkContext(); voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore(); voiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore(); + voiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(); sdkContext.client = client; sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore; sdkContext._VoiceBroadcastPreRecordingStore = voiceBroadcastPreRecordingStore; + sdkContext._VoiceBroadcastPlaybacksStore = voiceBroadcastPlaybacksStore; }); afterEach(async () => { @@ -146,15 +164,18 @@ describe("PipView", () => { ActiveWidgetStore.instance.destroyPersistentWidget("1", room.roomId); }; - const setUpVoiceBroadcastRecording = () => { - const voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent( + const makeVoiceBroadcastInfoStateEvent = (): MatrixEvent => { + return mkVoiceBroadcastInfoStateEvent( room.roomId, VoiceBroadcastInfoState.Started, alice.userId, client.getDeviceId() || "", ); + }; - const voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client); + const setUpVoiceBroadcastRecording = () => { + const infoEvent = makeVoiceBroadcastInfoStateEvent(); + const voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client); voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording); }; @@ -168,6 +189,22 @@ describe("PipView", () => { voiceBroadcastPreRecordingStore.setCurrent(voiceBroadcastPreRecording); }; + const setUpRoomViewStore = () => { + new RoomViewStore(defaultDispatcher, sdkContext); + }; + + const startVoiceBroadcastPlayback = (room: Room): MatrixEvent => { + const infoEvent = makeVoiceBroadcastInfoStateEvent(); + room.currentState.setStateEvents([infoEvent]); + defaultDispatcher.dispatch({ + action: "MatrixActions.RoomState.events", + event: infoEvent, + state: room.currentState, + lastStateEvent: null, + }, true); + return infoEvent; + }; + it("hides if there's no content", () => { renderPip(); expect(screen.queryByRole("complementary")).toBeNull(); @@ -209,7 +246,7 @@ describe("PipView", () => { }); it("shows a persistent widget with a return button when not viewing the room", () => { - viewRoom("!2:example.org"); + viewRoom(room2.roomId); renderPip(); withWidget(() => { @@ -230,7 +267,7 @@ describe("PipView", () => { it("should render the voice broadcast recording PiP", () => { // check for the „Live“ badge - screen.getByText("Live"); + expect(screen.queryByText("Live")).toBeInTheDocument(); }); }); @@ -242,7 +279,62 @@ describe("PipView", () => { it("should render the voice broadcast pre-recording PiP", () => { // check for the „Go live“ button - screen.getByText("Go live"); + expect(screen.queryByText("Go live")).toBeInTheDocument(); + }); + }); + + describe("when viewing a room with a live voice broadcast", () => { + let startEvent: MatrixEvent | null = null; + + beforeEach(() => { + setUpRoomViewStore(); + viewRoom(room.roomId); + startEvent = startVoiceBroadcastPlayback(room); + renderPip(); + }); + + it("should render the voice broadcast playback pip", () => { + // check for the „resume voice broadcast“ button + expect(screen.queryByLabelText("play voice broadcast")).toBeInTheDocument(); + }); + + describe("and the broadcast stops", () => { + beforeEach(() => { + act(() => { + const stopEvent = mkVoiceBroadcastInfoStateEvent( + room.roomId, + VoiceBroadcastInfoState.Stopped, + alice.userId, + client.getDeviceId() || "", + startEvent, + ); + room.currentState.setStateEvents([stopEvent]); + defaultDispatcher.dispatch({ + action: "MatrixActions.RoomState.events", + event: stopEvent, + state: room.currentState, + lastStateEvent: stopEvent, + }, true); + }); + }); + + it("should not render the voice broadcast playback pip", () => { + // check for the „resume voice broadcast“ button + expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument(); + }); + }); + + describe("and leaving the room", () => { + beforeEach(() => { + act(() => { + viewRoom(room2.roomId); + }); + }); + + it("should not render the voice broadcast playback pip", () => { + // check for the „resume voice broadcast“ button + expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument(); + }); }); }); }); diff --git a/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts b/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts index a984ed5fd6..5edee8eda6 100644 --- a/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts +++ b/test/voice-broadcast/utils/hasRoomLiveVoiceBroadcast-test.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { hasRoomLiveVoiceBroadcast, @@ -29,25 +29,27 @@ describe("hasRoomLiveVoiceBroadcast", () => { const roomId = "!room:example.com"; let client: MatrixClient; let room: Room; + let expectedEvent: MatrixEvent | null = null; const addVoiceBroadcastInfoEvent = ( state: VoiceBroadcastInfoState, sender: string, - ) => { - room.currentState.setStateEvents([ - mkVoiceBroadcastInfoStateEvent( - room.roomId, - state, - sender, - "ASD123", - ), - ]); + ): MatrixEvent => { + const infoEvent = mkVoiceBroadcastInfoStateEvent( + room.roomId, + state, + sender, + "ASD123", + ); + room.currentState.setStateEvents([infoEvent]); + return infoEvent; }; const itShouldReturnTrueTrue = () => { it("should return true/true", () => { expect(hasRoomLiveVoiceBroadcast(room, client.getUserId())).toEqual({ hasBroadcast: true, + infoEvent: expectedEvent, startedByUser: true, }); }); @@ -57,6 +59,7 @@ describe("hasRoomLiveVoiceBroadcast", () => { it("should return true/false", () => { expect(hasRoomLiveVoiceBroadcast(room, client.getUserId())).toEqual({ hasBroadcast: true, + infoEvent: expectedEvent, startedByUser: false, }); }); @@ -66,6 +69,7 @@ describe("hasRoomLiveVoiceBroadcast", () => { it("should return false/false", () => { expect(hasRoomLiveVoiceBroadcast(room, client.getUserId())).toEqual({ hasBroadcast: false, + infoEvent: null, startedByUser: false, }); }); @@ -76,6 +80,7 @@ describe("hasRoomLiveVoiceBroadcast", () => { }); beforeEach(() => { + expectedEvent = null; room = new Room(roomId, client, client.getUserId()); }); @@ -101,7 +106,7 @@ describe("hasRoomLiveVoiceBroadcast", () => { describe("when there is a live broadcast from the current and another user", () => { beforeEach(() => { - addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started, client.getUserId()); + expectedEvent = addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started, client.getUserId()); addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started, otherUserId); }); @@ -124,7 +129,7 @@ describe("hasRoomLiveVoiceBroadcast", () => { VoiceBroadcastInfoState.Resumed, ])("when there is a live broadcast (%s) from the current user", (state: VoiceBroadcastInfoState) => { beforeEach(() => { - addVoiceBroadcastInfoEvent(state, client.getUserId()); + expectedEvent = addVoiceBroadcastInfoEvent(state, client.getUserId()); }); itShouldReturnTrueTrue(); @@ -141,7 +146,7 @@ describe("hasRoomLiveVoiceBroadcast", () => { describe("when there is a live broadcast from another user", () => { beforeEach(() => { - addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Resumed, otherUserId); + expectedEvent = addVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Resumed, otherUserId); }); itShouldReturnTrueFalse();