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

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