Add voice broadcast pre-recoding PiP (#9548)

pull/28788/head^2
Michael Weimann 2022-11-10 09:38:48 +01:00 committed by GitHub
parent afdf289a78
commit abec724387
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 977 additions and 111 deletions

View File

@ -45,3 +45,8 @@ limitations under the License.
display: flex;
gap: $spacing-4;
}
.mx_AccessibleButton.mx_VoiceBroadcastBody_blockButton {
display: flex;
gap: $spacing-8;
}

View File

@ -54,13 +54,12 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
import { Features } from '../../../settings/Settings';
import { VoiceMessageRecording } from '../../../audio/VoiceMessageRecording';
import {
startNewVoiceBroadcastRecording,
VoiceBroadcastRecordingsStore,
} from '../../../voice-broadcast';
import { VoiceBroadcastRecordingsStore } from '../../../voice-broadcast';
import { SendWysiwygComposer, sendMessage } from './wysiwyg_composer/';
import { MatrixClientProps, withMatrixClientHOC } from '../../../contexts/MatrixClientContext';
import { htmlToPlainText } from '../../../utils/room/htmlToPlaintext';
import { setUpVoiceBroadcastPreRecording } from '../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording';
import { SdkContextClass } from '../../../contexts/SDKContext';
let instanceCount = 0;
@ -581,10 +580,11 @@ export class MessageComposer extends React.Component<IProps, IState> {
toggleButtonMenu={this.toggleButtonMenu}
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
onStartVoiceBroadcastClick={() => {
startNewVoiceBroadcastRecording(
setUpVoiceBroadcastPreRecording(
this.props.room,
MatrixClientPeg.get(),
VoiceBroadcastRecordingsStore.instance(),
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
);
this.toggleButtonMenu();
}}

View File

@ -68,6 +68,8 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
document.addEventListener("mousemove", this.onMoving);
document.addEventListener("mouseup", this.onEndMoving);
UIStore.instance.on(UI_EVENTS.Resize, this.onResize);
// correctly position the PiP
this.snap();
}
public componentWillUnmount() {

View File

@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, useState } from 'react';
import React, { createRef, useContext } from 'react';
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import { logger } from "matrix-js-sdk/src/logger";
import classNames from 'classnames';
import { Room } from "matrix-js-sdk/src/models/room";
import { Optional } from 'matrix-events-sdk';
import LegacyCallView from "./LegacyCallView";
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../../LegacyCallHandler';
@ -33,15 +34,16 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/Activ
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
import { SdkContextClass } from '../../../contexts/SDKContext';
import { SDKContext, SdkContextClass } from '../../../contexts/SDKContext';
import { CallStore } from "../../../stores/CallStore";
import {
useCurrentVoiceBroadcastPreRecording,
useCurrentVoiceBroadcastRecording,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingPip,
VoiceBroadcastRecording,
VoiceBroadcastRecordingPip,
VoiceBroadcastRecordingsStore,
VoiceBroadcastRecordingsStoreEvent,
} from '../../../voice-broadcast';
import { useTypedEventEmitter } from '../../../hooks/useEventEmitter';
const SHOW_CALL_IN_STATES = [
CallState.Connected,
@ -53,14 +55,15 @@ const SHOW_CALL_IN_STATES = [
];
interface IProps {
voiceBroadcastRecording?: VoiceBroadcastRecording;
voiceBroadcastRecording?: Optional<VoiceBroadcastRecording>;
voiceBroadcastPreRecording?: Optional<VoiceBroadcastPreRecording>;
}
interface IState {
viewedRoomId: string;
viewedRoomId?: string;
// The main call that we are displaying (ie. not including the call in the room being viewed, if any)
primaryCall: MatrixCall;
primaryCall: MatrixCall | null;
// Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms
// they belong to
@ -74,24 +77,26 @@ interface IState {
moving: boolean;
}
const getRoomAndAppForWidget = (widgetId: string, roomId: string): [Room, IApp] => {
if (!widgetId) return;
if (!roomId) return;
const getRoomAndAppForWidget = (widgetId: string, roomId: string): [Room | null, IApp | null] => {
if (!widgetId) return [null, null];
if (!roomId) return [null, null];
const room = MatrixClientPeg.get().getRoom(roomId);
const app = WidgetStore.instance.getApps(roomId).find((app) => app.id === widgetId);
return [room, app];
return [room, app || null];
};
// Splits a list of calls into one 'primary' one and a list
// (which should be a single element) of other calls.
// The primary will be the one not on hold, or an arbitrary one
// if they're all on hold)
function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall[]] {
function getPrimarySecondaryCallsForPip(roomId: Optional<string>): [MatrixCall | null, MatrixCall[]] {
if (!roomId) return [null, []];
const calls = LegacyCallHandler.instance.getAllActiveCallsForPip(roomId);
let primary: MatrixCall = null;
let primary: MatrixCall | null = null;
let secondaries: MatrixCall[] = [];
for (const call of calls) {
@ -135,8 +140,8 @@ class PipView extends React.Component<IProps, IState> {
this.state = {
moving: false,
viewedRoomId: roomId,
primaryCall: primaryCall,
viewedRoomId: roomId || undefined,
primaryCall: primaryCall || null,
secondaryCall: secondaryCalls[0],
persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
persistentRoomId: ActiveWidgetStore.instance.getPersistentRoomId(),
@ -195,7 +200,7 @@ class PipView extends React.Component<IProps, IState> {
if (oldRoom) {
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(oldRoom), this.updateCalls);
}
const newRoom = MatrixClientPeg.get()?.getRoom(newRoomId);
const newRoom = MatrixClientPeg.get()?.getRoom(newRoomId || undefined);
if (newRoom) {
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(newRoom), this.updateCalls);
}
@ -259,20 +264,27 @@ class PipView extends React.Component<IProps, IState> {
if (this.state.showWidgetInPip && widgetId && roomId) {
const [room, app] = getRoomAndAppForWidget(widgetId, roomId);
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center);
} else {
dis.dispatch({
action: 'video_fullscreen',
fullscreen: true,
});
if (room && app) {
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center);
return;
}
}
dis.dispatch({
action: 'video_fullscreen',
fullscreen: true,
});
};
private onPin = (): void => {
if (!this.state.showWidgetInPip) return;
const [room, app] = getRoomAndAppForWidget(this.state.persistentWidgetId, this.state.persistentRoomId);
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
if (room && app) {
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
}
};
private onExpand = (): void => {
@ -321,10 +333,12 @@ class PipView extends React.Component<IProps, IState> {
let pipContent;
if (this.state.primaryCall) {
// get a ref to call inside the current scope
const call = this.state.primaryCall;
pipContent = ({ onStartMoving, onResize }) =>
<LegacyCallView
onMouseDownOnHeader={onStartMoving}
call={this.state.primaryCall}
call={call}
secondaryCall={this.state.secondaryCall}
pipMode={pipMode}
onResize={onResize}
@ -361,10 +375,22 @@ class PipView extends React.Component<IProps, IState> {
</div>;
}
if (this.props.voiceBroadcastPreRecording) {
// get a ref to pre-recording inside the current scope
const preRecording = this.props.voiceBroadcastPreRecording;
pipContent = ({ onStartMoving }) => <div onMouseDown={onStartMoving}>
<VoiceBroadcastPreRecordingPip
voiceBroadcastPreRecording={preRecording}
/>
</div>;
}
if (this.props.voiceBroadcastRecording) {
// get a ref to recording inside the current scope
const recording = this.props.voiceBroadcastRecording;
pipContent = ({ onStartMoving }) => <div onMouseDown={onStartMoving}>
<VoiceBroadcastRecordingPip
recording={this.props.voiceBroadcastRecording}
recording={recording}
/>
</div>;
}
@ -385,23 +411,18 @@ class PipView extends React.Component<IProps, IState> {
}
const PipViewHOC: React.FC<IProps> = (props) => {
// TODO Michael W: extract to custom hook
const voiceBroadcastRecordingsStore = VoiceBroadcastRecordingsStore.instance();
const [voiceBroadcastRecording, setVoiceBroadcastRecording] = useState(
voiceBroadcastRecordingsStore.getCurrent(),
const sdkContext = useContext(SDKContext);
const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore;
const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(
voiceBroadcastPreRecordingStore,
);
useTypedEventEmitter(
voiceBroadcastRecordingsStore,
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
(recording: VoiceBroadcastRecording) => {
setVoiceBroadcastRecording(recording);
},
);
const voiceBroadcastRecordingsStore = sdkContext.voiceBroadcastRecordingsStore;
const { currentVoiceBroadcastRecording } = useCurrentVoiceBroadcastRecording(voiceBroadcastRecordingsStore);
return <PipView
voiceBroadcastRecording={voiceBroadcastRecording}
voiceBroadcastRecording={currentVoiceBroadcastRecording}
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
{...props}
/>;
};

View File

@ -29,6 +29,7 @@ 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";
export const SDKContext = createContext<SdkContextClass>(undefined);
SDKContext.displayName = "SDKContext";
@ -63,6 +64,8 @@ export class SdkContextClass {
protected _SpaceStore?: SpaceStoreClass;
protected _LegacyCallHandler?: LegacyCallHandler;
protected _TypingStore?: TypingStore;
protected _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore;
protected _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore;
/**
* Automatically construct stores which need to be created eagerly so they can register with
@ -141,4 +144,18 @@ export class SdkContextClass {
}
return this._TypingStore;
}
public get voiceBroadcastRecordingsStore(): VoiceBroadcastRecordingsStore {
if (!this._VoiceBroadcastRecordingsStore) {
this._VoiceBroadcastRecordingsStore = VoiceBroadcastRecordingsStore.instance();
}
return this._VoiceBroadcastRecordingsStore;
}
public get voiceBroadcastPreRecordingStore(): VoiceBroadcastPreRecordingStore {
if (!this._VoiceBroadcastPreRecordingStore) {
this._VoiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore();
}
return this._VoiceBroadcastPreRecordingStore;
}
}

View File

@ -647,6 +647,7 @@
"play voice broadcast": "play voice broadcast",
"resume voice broadcast": "resume voice broadcast",
"pause voice broadcast": "pause voice broadcast",
"Go live": "Go live",
"Live": "Live",
"Voice broadcast": "Voice broadcast",
"Cannot reach homeserver": "Cannot reach homeserver",

View File

@ -19,19 +19,25 @@ import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
import { Icon as MicrophoneIcon } from "../../../../res/img/voip/call-view/mic-on.svg";
import { _t } from "../../../languageHandler";
import RoomAvatar from "../../../components/views/avatars/RoomAvatar";
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
import { Icon as XIcon } from "../../../../res/img/element-icons/cancel-rounded.svg";
interface VoiceBroadcastHeaderProps {
live: boolean;
sender: RoomMember;
live?: boolean;
onCloseClick?: () => void;
room: Room;
sender: RoomMember;
showBroadcast?: boolean;
showClose?: boolean;
}
export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
live,
sender,
live = false,
onCloseClick = () => {},
room,
sender,
showBroadcast = false,
showClose = false,
}) => {
const broadcast = showBroadcast
? <div className="mx_VoiceBroadcastHeader_line">
@ -39,7 +45,15 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
{ _t("Voice broadcast") }
</div>
: null;
const liveBadge = live ? <LiveBadge /> : null;
const closeButton = showClose
? <AccessibleButton onClick={onCloseClick}>
<XIcon className="mx_Icon mx_Icon_16" />
</AccessibleButton>
: null;
return <div className="mx_VoiceBroadcastHeader">
<RoomAvatar room={room} width={32} height={32} />
<div className="mx_VoiceBroadcastHeader_content">
@ -53,5 +67,6 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
{ broadcast }
</div>
{ liveBadge }
{ closeButton }
</div>;
};

View File

@ -0,0 +1,48 @@
/*
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 { VoiceBroadcastHeader } from "../..";
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
import { VoiceBroadcastPreRecording } from "../../models/VoiceBroadcastPreRecording";
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
import { _t } from "../../../languageHandler";
interface Props {
voiceBroadcastPreRecording: VoiceBroadcastPreRecording;
}
export const VoiceBroadcastPreRecordingPip: React.FC<Props> = ({
voiceBroadcastPreRecording,
}) => {
return <div className="mx_VoiceBroadcastBody mx_VoiceBroadcastBody--pip">
<VoiceBroadcastHeader
onCloseClick={voiceBroadcastPreRecording.cancel}
room={voiceBroadcastPreRecording.room}
sender={voiceBroadcastPreRecording.sender}
showClose={true}
/>
<AccessibleButton
className="mx_VoiceBroadcastBody_blockButton"
kind="danger"
onClick={voiceBroadcastPreRecording.start}
>
<LiveIcon className="mx_Icon mx_Icon_16" />
{ _t("Go live") }
</AccessibleButton>
</div>;
};

View File

@ -0,0 +1,38 @@
/*
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 { useState } from "react";
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
import { VoiceBroadcastPreRecordingStore } from "../stores/VoiceBroadcastPreRecordingStore";
export const useCurrentVoiceBroadcastPreRecording = (
voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore,
) => {
const [currentVoiceBroadcastPreRecording, setCurrentVoiceBroadcastPreRecording] = useState(
voiceBroadcastPreRecordingStore.getCurrent(),
);
useTypedEventEmitter(
voiceBroadcastPreRecordingStore,
"changed",
setCurrentVoiceBroadcastPreRecording,
);
return {
currentVoiceBroadcastPreRecording,
};
};

View File

@ -0,0 +1,38 @@
/*
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 { useState } from "react";
import { VoiceBroadcastRecordingsStore, VoiceBroadcastRecordingsStoreEvent } from "..";
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
export const useCurrentVoiceBroadcastRecording = (
voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore,
) => {
const [currentVoiceBroadcastRecording, setCurrentVoiceBroadcastRecording] = useState(
voiceBroadcastRecordingsStore.getCurrent(),
);
useTypedEventEmitter(
voiceBroadcastRecordingsStore,
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
setCurrentVoiceBroadcastRecording,
);
return {
currentVoiceBroadcastRecording,
};
};

View File

@ -22,6 +22,7 @@ limitations under the License.
import { RelationType } from "matrix-js-sdk/src/matrix";
export * from "./models/VoiceBroadcastPlayback";
export * from "./models/VoiceBroadcastPreRecording";
export * from "./models/VoiceBroadcastRecording";
export * from "./audio/VoiceBroadcastRecorder";
export * from "./components/VoiceBroadcastBody";
@ -29,11 +30,16 @@ export * from "./components/atoms/LiveBadge";
export * from "./components/atoms/VoiceBroadcastControl";
export * from "./components/atoms/VoiceBroadcastHeader";
export * from "./components/molecules/VoiceBroadcastPlaybackBody";
export * from "./components/molecules/VoiceBroadcastPreRecordingPip";
export * from "./components/molecules/VoiceBroadcastRecordingBody";
export * from "./components/molecules/VoiceBroadcastRecordingPip";
export * from "./hooks/useCurrentVoiceBroadcastPreRecording";
export * from "./hooks/useCurrentVoiceBroadcastRecording";
export * from "./hooks/useVoiceBroadcastRecording";
export * from "./stores/VoiceBroadcastPlaybacksStore";
export * from "./stores/VoiceBroadcastPreRecordingStore";
export * from "./stores/VoiceBroadcastRecordingsStore";
export * from "./utils/checkVoiceBroadcastPreConditions";
export * from "./utils/getChunkLength";
export * from "./utils/hasRoomLiveVoiceBroadcast";
export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice";

View File

@ -0,0 +1,58 @@
/*
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, RoomMember } from "matrix-js-sdk/src/matrix";
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
import { IDestroyable } from "../../utils/IDestroyable";
import { VoiceBroadcastRecordingsStore } from "../stores/VoiceBroadcastRecordingsStore";
import { startNewVoiceBroadcastRecording } from "../utils/startNewVoiceBroadcastRecording";
type VoiceBroadcastPreRecordingEvent = "dismiss";
interface EventMap {
"dismiss": (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void;
}
export class VoiceBroadcastPreRecording
extends TypedEventEmitter<VoiceBroadcastPreRecordingEvent, EventMap>
implements IDestroyable {
public constructor(
public room: Room,
public sender: RoomMember,
private client: MatrixClient,
private recordingsStore: VoiceBroadcastRecordingsStore,
) {
super();
}
public start = async (): Promise<void> => {
await startNewVoiceBroadcastRecording(
this.room,
this.client,
this.recordingsStore,
);
this.emit("dismiss", this);
};
public cancel = (): void => {
this.emit("dismiss", this);
};
public destroy(): void {
this.removeAllListeners();
}
}

View File

@ -0,0 +1,70 @@
/*
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 { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
import { VoiceBroadcastPreRecording } from "..";
import { IDestroyable } from "../../utils/IDestroyable";
export type VoiceBroadcastPreRecordingEvent = "changed";
interface EventMap {
changed: (preRecording: VoiceBroadcastPreRecording | null) => void;
}
export class VoiceBroadcastPreRecordingStore
extends TypedEventEmitter<VoiceBroadcastPreRecordingEvent, EventMap>
implements IDestroyable {
private current: VoiceBroadcastPreRecording | null = null;
public setCurrent(current: VoiceBroadcastPreRecording): void {
if (this.current === current) return;
if (this.current) {
this.current.off("dismiss", this.onCancel);
}
this.current = current;
current.on("dismiss", this.onCancel);
this.emit("changed", current);
}
public clearCurrent(): void {
if (this.current === null) return;
this.current.off("dismiss", this.onCancel);
this.current = null;
this.emit("changed", null);
}
public getCurrent(): VoiceBroadcastPreRecording | null {
return this.current;
}
public destroy(): void {
this.removeAllListeners();
if (this.current) {
this.current.off("dismiss", this.onCancel);
}
}
private onCancel = (voiceBroadcastPreRecording: VoiceBroadcastPreRecording): void => {
if (this.current === voiceBroadcastPreRecording) {
this.clearCurrent();
}
};
}

View File

@ -0,0 +1,84 @@
/*
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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { hasRoomLiveVoiceBroadcast, VoiceBroadcastInfoEventType, VoiceBroadcastRecordingsStore } from "..";
import InfoDialog from "../../components/views/dialogs/InfoDialog";
import { _t } from "../../languageHandler";
import Modal from "../../Modal";
const showAlreadyRecordingDialog = () => {
Modal.createDialog(InfoDialog, {
title: _t("Can't start a new voice broadcast"),
description: <p>{ _t("You are already recording a voice broadcast. "
+ "Please end your current voice broadcast to start a new one.") }</p>,
hasCloseButton: true,
});
};
const showInsufficientPermissionsDialog = () => {
Modal.createDialog(InfoDialog, {
title: _t("Can't start a new voice broadcast"),
description: <p>{ _t("You don't have the required permissions to start a voice broadcast in this room. "
+ "Contact a room administrator to upgrade your permissions.") }</p>,
hasCloseButton: true,
});
};
const showOthersAlreadyRecordingDialog = () => {
Modal.createDialog(InfoDialog, {
title: _t("Can't start a new voice broadcast"),
description: <p>{ _t("Someone else is already recording a voice broadcast. "
+ "Wait for their voice broadcast to end to start a new one.") }</p>,
hasCloseButton: true,
});
};
export const checkVoiceBroadcastPreConditions = (
room: Room,
client: MatrixClient,
recordingsStore: VoiceBroadcastRecordingsStore,
): boolean => {
if (recordingsStore.getCurrent()) {
showAlreadyRecordingDialog();
return false;
}
const currentUserId = client.getUserId();
if (!currentUserId) return false;
if (!room.currentState.maySendStateEvent(VoiceBroadcastInfoEventType, currentUserId)) {
showInsufficientPermissionsDialog();
return false;
}
const { hasBroadcast, startedByUser } = hasRoomLiveVoiceBroadcast(room, currentUserId);
if (hasBroadcast && startedByUser) {
showAlreadyRecordingDialog();
return false;
}
if (hasBroadcast) {
showOthersAlreadyRecordingDialog();
return false;
}
return true;
};

View File

@ -0,0 +1,45 @@
/*
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 {
checkVoiceBroadcastPreConditions,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecordingsStore,
} from "..";
export const setUpVoiceBroadcastPreRecording = (
room: Room,
client: MatrixClient,
recordingsStore: VoiceBroadcastRecordingsStore,
preRecordingStore: VoiceBroadcastPreRecordingStore,
): VoiceBroadcastPreRecording | null => {
if (!checkVoiceBroadcastPreConditions(room, client, recordingsStore)) {
return null;
}
const userId = client.getUserId();
if (!userId) return null;
const sender = room.getMember(userId);
if (!sender) return null;
const preRecording = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore);
preRecordingStore.setCurrent(preRecording);
return preRecording;
};

View File

@ -14,38 +14,39 @@ 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,
getChunkLength,
} from "..";
import { checkVoiceBroadcastPreConditions } from "./checkVoiceBroadcastPreConditions";
const startBroadcast = async (
room: Room,
client: MatrixClient,
recordingsStore: VoiceBroadcastRecordingsStore,
): Promise<VoiceBroadcastRecording> => {
const { promise, resolve } = defer<VoiceBroadcastRecording>();
let result: ISendEventResponse = null;
const { promise, resolve, reject } = defer<VoiceBroadcastRecording>();
const userId = client.getUserId();
if (!userId) {
reject("unable to start voice broadcast if current user is unkonwn");
return promise;
}
let result: ISendEventResponse | null = null;
const onRoomStateEvents = () => {
if (!result) return;
const voiceBroadcastEvent = room.currentState.getStateEvents(
VoiceBroadcastInfoEventType,
client.getUserId(),
);
const voiceBroadcastEvent = room.currentState.getStateEvents(VoiceBroadcastInfoEventType, userId);
if (voiceBroadcastEvent?.getId() === result.event_id) {
room.off(RoomStateEvent.Events, onRoomStateEvents);
@ -70,39 +71,12 @@ const startBroadcast = async (
state: VoiceBroadcastInfoState.Started,
chunk_length: getChunkLength(),
} as VoiceBroadcastInfoEventContent,
client.getUserId(),
userId,
);
return promise;
};
const showAlreadyRecordingDialog = () => {
Modal.createDialog(InfoDialog, {
title: _t("Can't start a new voice broadcast"),
description: <p>{ _t("You are already recording a voice broadcast. "
+ "Please end your current voice broadcast to start a new one.") }</p>,
hasCloseButton: true,
});
};
const showInsufficientPermissionsDialog = () => {
Modal.createDialog(InfoDialog, {
title: _t("Can't start a new voice broadcast"),
description: <p>{ _t("You don't have the required permissions to start a voice broadcast in this room. "
+ "Contact a room administrator to upgrade your permissions.") }</p>,
hasCloseButton: true,
});
};
const showOthersAlreadyRecordingDialog = () => {
Modal.createDialog(InfoDialog, {
title: _t("Can't start a new voice broadcast"),
description: <p>{ _t("Someone else is already recording a voice broadcast. "
+ "Wait for their voice broadcast to end to start a new one.") }</p>,
hasCloseButton: true,
});
};
/**
* Starts a new Voice Broadcast Recording, if
* - the user has the permissions to do so in the room
@ -114,27 +88,7 @@ export const startNewVoiceBroadcastRecording = async (
client: MatrixClient,
recordingsStore: VoiceBroadcastRecordingsStore,
): Promise<VoiceBroadcastRecording | null> => {
if (recordingsStore.getCurrent()) {
showAlreadyRecordingDialog();
return null;
}
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();
if (!checkVoiceBroadcastPreConditions(room, client, recordingsStore)) {
return null;
}

View File

@ -24,6 +24,7 @@ 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";
/**
* A class which provides the same API as SdkContextClass but adds additional unsafe setters which can
@ -39,6 +40,8 @@ export class TestSdkContext extends SdkContextClass {
public _PosthogAnalytics?: PosthogAnalytics;
public _SlidingSyncManager?: SlidingSyncManager;
public _SpaceStore?: SpaceStoreClass;
public _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore;
public _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore;
constructor() {
super();

View File

@ -31,6 +31,8 @@ import {
setupAsyncStoreWithClient,
resetAsyncStoreWithClient,
wrapInMatrixClientContext,
wrapInSdkContext,
mkRoomCreateEvent,
} from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { CallStore } from "../../../../src/stores/CallStore";
@ -41,17 +43,27 @@ import DMRoomMap from "../../../../src/utils/DMRoomMap";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload";
const PipView = wrapInMatrixClientContext(UnwrappedPipView);
import { TestSdkContext } from "../../../TestSdkContext";
import {
VoiceBroadcastInfoState,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecording,
VoiceBroadcastRecordingsStore,
} from "../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
describe("PipView", () => {
useMockedCalls();
Object.defineProperty(navigator, "mediaDevices", { value: { enumerateDevices: () => [] } });
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
let sdkContext: TestSdkContext;
let client: Mocked<MatrixClient>;
let room: Room;
let alice: RoomMember;
let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore;
let voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore;
beforeEach(async () => {
stubClient();
@ -64,6 +76,9 @@ describe("PipView", () => {
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
client.getRooms.mockReturnValue([room]);
alice = mkRoomMember(room.roomId, "@alice:example.org");
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);
@ -73,6 +88,13 @@ describe("PipView", () => {
await Promise.all([CallStore.instance, WidgetMessagingStore.instance].map(
store => setupAsyncStoreWithClient(store, client),
));
sdkContext = new TestSdkContext();
voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore();
voiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore();
sdkContext.client = client;
sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore;
sdkContext._VoiceBroadcastPreRecordingStore = voiceBroadcastPreRecordingStore;
});
afterEach(async () => {
@ -82,7 +104,12 @@ describe("PipView", () => {
jest.restoreAllMocks();
});
const renderPip = () => { render(<PipView />); };
const renderPip = () => {
const PipView = wrapInMatrixClientContext(
wrapInSdkContext(UnwrappedPipView, sdkContext),
);
render(<PipView />);
};
const viewRoom = (roomId: string) =>
defaultDispatcher.dispatch<ViewRoomPayload>({
@ -172,4 +199,44 @@ describe("PipView", () => {
screen.getByRole("button", { name: /return/i });
});
});
describe("when there is a voice broadcast recording", () => {
beforeEach(() => {
const voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent(
room.roomId,
VoiceBroadcastInfoState.Started,
alice.userId,
client.getDeviceId() || "",
);
const voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client);
voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording);
renderPip();
});
it("should render the voice broadcast recording PiP", () => {
// check for the „Live“ badge
screen.getByText("Live");
});
});
describe("when there is a voice broadcast pre-recording", () => {
beforeEach(() => {
const voiceBroadcastPreRecording = new VoiceBroadcastPreRecording(
room,
alice,
client,
voiceBroadcastRecordingsStore,
);
voiceBroadcastPreRecordingStore.setCurrent(voiceBroadcastPreRecording);
renderPip();
});
it("should render the voice broadcast pre-recording PiP", () => {
// check for the „Go live“ button
screen.getByText("Go live");
});
});
});

View File

@ -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 { SdkContextClass } from "../../src/contexts/SDKContext";
import { VoiceBroadcastPreRecordingStore } from "../../src/voice-broadcast";
jest.mock("../../src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore");
describe("SdkContextClass", () => {
const sdkContext = SdkContextClass.instance;
it("instance should always return the same instance", () => {
expect(SdkContextClass.instance).toBe(sdkContext);
});
it("voiceBroadcastPreRecordingStore should always return the same VoiceBroadcastPreRecordingStore", () => {
const first = sdkContext.voiceBroadcastPreRecordingStore;
expect(first).toBeInstanceOf(VoiceBroadcastPreRecordingStore);
expect(sdkContext.voiceBroadcastPreRecordingStore).toBe(first);
});
});

View File

@ -45,6 +45,7 @@ import { getMockClientWithEventEmitter } from "../test-utils/client";
// modern fake timers and lodash.debounce are a faff
// short circuit it
jest.mock("lodash", () => ({
...jest.requireActual("lodash") as object,
debounce: jest.fn().mockImplementation(callback => callback),
}));

View File

@ -32,6 +32,7 @@ import {
IUnsigned,
IPusher,
RoomType,
KNOWN_SAFE_ROOM_VERSION,
} from 'matrix-js-sdk/src/matrix';
import { normalize } from "matrix-js-sdk/src/utils";
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
@ -223,6 +224,20 @@ type MakeEventProps = MakeEventPassThruProps & {
unsigned?: IUnsigned;
};
export const mkRoomCreateEvent = (userId: string, roomId: string): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomCreate,
content: {
creator: userId,
room_version: KNOWN_SAFE_ROOM_VERSION,
},
skey: "",
user: userId,
room: roomId,
});
};
/**
* Create an Event.
* @param {Object} opts Values for the event.
@ -567,6 +582,19 @@ export const mkSpace = (
return space;
};
export const mkRoomMemberJoinEvent = (user: string, room: string): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomMember,
content: {
membership: "join",
},
skey: user,
user,
room,
});
};
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
app_display_name: "app",
app_id: "123",

View File

@ -19,6 +19,7 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg as peg } from '../../src/MatrixClientPeg';
import MatrixClientContext from "../../src/contexts/MatrixClientContext";
import { SDKContext, SdkContextClass } from "../../src/contexts/SDKContext";
type WrapperProps<T> = { wrappedRef?: RefCallback<ComponentType<T>> } & T;
@ -39,3 +40,16 @@ export function wrapInMatrixClientContext<T>(WrappedComponent: ComponentType<T>)
}
return Wrapper;
}
export function wrapInSdkContext<T>(
WrappedComponent: ComponentType<T>,
sdkContext: SdkContextClass,
): ComponentType<WrapperProps<T>> {
return class extends React.Component<WrapperProps<T>> {
render() {
return <SDKContext.Provider value={sdkContext}>
<WrappedComponent {...this.props} />
</SDKContext.Provider>;
}
};
}

View File

@ -41,6 +41,7 @@ jest.mock('../../src/Modal', () => ({
jest.mock('../../src/settings/SettingsStore', () => ({
getValue: jest.fn(),
monitorSetting: jest.fn(),
}));
const mockPromptBeforeInviteUnknownUsers = (value: boolean) => {

View File

@ -0,0 +1,77 @@
/*
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, RoomMember } from "matrix-js-sdk/src/matrix";
import {
startNewVoiceBroadcastRecording,
VoiceBroadcastPreRecording,
VoiceBroadcastRecordingsStore,
} from "../../../src/voice-broadcast";
import { stubClient } from "../../test-utils";
jest.mock("../../../src/voice-broadcast/utils/startNewVoiceBroadcastRecording");
describe("VoiceBroadcastPreRecording", () => {
const roomId = "!room:example.com";
let client: MatrixClient;
let room: Room;
let sender: RoomMember;
let recordingsStore: VoiceBroadcastRecordingsStore;
let preRecording: VoiceBroadcastPreRecording;
let onDismiss: (voiceBroadcastPreRecording: VoiceBroadcastPreRecording) => void;
beforeAll(() => {
client = stubClient();
room = new Room(roomId, client, client.getUserId() || "");
sender = new RoomMember(roomId, client.getUserId() || "");
recordingsStore = new VoiceBroadcastRecordingsStore();
});
beforeEach(() => {
onDismiss = jest.fn();
preRecording = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore);
preRecording.on("dismiss", onDismiss);
});
describe("start", () => {
beforeEach(() => {
preRecording.start();
});
it("should start a new voice broadcast recording", () => {
expect(startNewVoiceBroadcastRecording).toHaveBeenCalledWith(
room,
client,
recordingsStore,
);
});
it("should emit a dismiss event", () => {
expect(onDismiss).toHaveBeenCalledWith(preRecording);
});
});
describe("cancel", () => {
beforeEach(() => {
preRecording.cancel();
});
it("should emit a dismiss event", () => {
expect(onDismiss).toHaveBeenCalledWith(preRecording);
});
});
});

View File

@ -0,0 +1,137 @@
/*
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, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import {
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecordingsStore,
} from "../../../src/voice-broadcast";
import { stubClient } from "../../test-utils";
jest.mock("../../../src/voice-broadcast/stores/VoiceBroadcastRecordingsStore");
describe("VoiceBroadcastPreRecordingStore", () => {
const roomId = "!room:example.com";
let client: MatrixClient;
let room: Room;
let sender: RoomMember;
let recordingsStore: VoiceBroadcastRecordingsStore;
let store: VoiceBroadcastPreRecordingStore;
let preRecording1: VoiceBroadcastPreRecording;
beforeAll(() => {
client = stubClient();
room = new Room(roomId, client, client.getUserId() || "");
sender = new RoomMember(roomId, client.getUserId() || "");
recordingsStore = new VoiceBroadcastRecordingsStore();
});
beforeEach(() => {
store = new VoiceBroadcastPreRecordingStore();
jest.spyOn(store, "emit");
jest.spyOn(store, "removeAllListeners");
preRecording1 = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore);
jest.spyOn(preRecording1, "off");
});
it("getCurrent() should return null", () => {
expect(store.getCurrent()).toBeNull();
});
it("clearCurrent() should work", () => {
store.clearCurrent();
expect(store.getCurrent()).toBeNull();
});
describe("when setting a current recording", () => {
beforeEach(() => {
store.setCurrent(preRecording1);
});
it("getCurrent() should return the recording", () => {
expect(store.getCurrent()).toBe(preRecording1);
});
it("should emit a changed event with the recording", () => {
expect(store.emit).toHaveBeenCalledWith("changed", preRecording1);
});
describe("and calling destroy()", () => {
beforeEach(() => {
store.destroy();
});
it("should remove all listeners", () => {
expect(store.removeAllListeners).toHaveBeenCalled();
});
it("should deregister from the pre-recordings", () => {
expect(preRecording1.off).toHaveBeenCalledWith("dismiss", expect.any(Function));
});
});
describe("and cancelling the pre-recording", () => {
beforeEach(() => {
preRecording1.cancel();
});
it("should clear the current recording", () => {
expect(store.getCurrent()).toBeNull();
});
it("should emit a changed event with null", () => {
expect(store.emit).toHaveBeenCalledWith("changed", null);
});
});
describe("and setting the same pre-recording again", () => {
beforeEach(() => {
mocked(store.emit).mockClear();
store.setCurrent(preRecording1);
});
it("should not emit a changed event", () => {
expect(store.emit).not.toHaveBeenCalled();
});
});
describe("and setting another pre-recording", () => {
let preRecording2: VoiceBroadcastPreRecording;
beforeEach(() => {
mocked(store.emit).mockClear();
mocked(preRecording1.off).mockClear();
preRecording2 = new VoiceBroadcastPreRecording(room, sender, client, recordingsStore);
store.setCurrent(preRecording2);
});
it("should deregister from the current pre-recording", () => {
expect(preRecording1.off).toHaveBeenCalledWith("dismiss", expect.any(Function));
});
it("getCurrent() should return the new recording", () => {
expect(store.getCurrent()).toBe(preRecording2);
});
it("should emit a changed event with the new recording", () => {
expect(store.emit).toHaveBeenCalledWith("changed", preRecording2);
});
});
});
});

View File

@ -0,0 +1,102 @@
/*
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, Room } from "matrix-js-sdk/src/matrix";
import {
checkVoiceBroadcastPreConditions,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecordingsStore,
} from "../../../src/voice-broadcast";
import { setUpVoiceBroadcastPreRecording } from "../../../src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
import { mkRoomMemberJoinEvent, stubClient } from "../../test-utils";
jest.mock("../../../src/voice-broadcast/utils/checkVoiceBroadcastPreConditions");
describe("setUpVoiceBroadcastPreRecording", () => {
const roomId = "!room:example.com";
let client: MatrixClient;
let userId: string;
let room: Room;
let preRecordingStore: VoiceBroadcastPreRecordingStore;
let recordingsStore: VoiceBroadcastRecordingsStore;
const itShouldReturnNull = () => {
it("should return null", () => {
expect(setUpVoiceBroadcastPreRecording(room, client, recordingsStore, preRecordingStore)).toBeNull();
expect(checkVoiceBroadcastPreConditions).toHaveBeenCalledWith(room, client, recordingsStore);
});
};
beforeEach(() => {
client = stubClient();
const clientUserId = client.getUserId();
if (!clientUserId) fail("empty userId");
userId = clientUserId;
room = new Room(roomId, client, userId);
preRecordingStore = new VoiceBroadcastPreRecordingStore();
recordingsStore = new VoiceBroadcastRecordingsStore();
});
describe("when the preconditions fail", () => {
beforeEach(() => {
mocked(checkVoiceBroadcastPreConditions).mockReturnValue(false);
});
itShouldReturnNull();
});
describe("when the preconditions pass", () => {
beforeEach(() => {
mocked(checkVoiceBroadcastPreConditions).mockReturnValue(true);
});
describe("and there is no user id", () => {
beforeEach(() => {
mocked(client.getUserId).mockReturnValue(null);
});
itShouldReturnNull();
});
describe("and there is no room member", () => {
beforeEach(() => {
// check test precondition
expect(room.getMember(userId)).toBeNull();
});
itShouldReturnNull();
});
describe("and there is a room member", () => {
beforeEach(() => {
room.currentState.setStateEvents([
mkRoomMemberJoinEvent(userId, roomId),
]);
});
it("should create a voice broadcast pre-recording", () => {
const result = setUpVoiceBroadcastPreRecording(room, client, recordingsStore, preRecordingStore);
expect(checkVoiceBroadcastPreConditions).toHaveBeenCalledWith(room, client, recordingsStore);
expect(result).toBeInstanceOf(VoiceBroadcastPreRecording);
});
});
});
});