Add voice broadcast pre-recoding PiP (#9548)

This commit is contained in:
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

@ -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;
}