Fix voice messages with multiple composers (#9208)
* Allow having multiple voice messages in composers Co-authored-by: grimhilt <grimhilt@users.noreply.github.com> Co-authored-by: Janne Mareike Koschinski <janne@kuschku.de>
This commit is contained in:
parent
85f92308f9
commit
6d03cb35b7
3 changed files with 39 additions and 24 deletions
|
@ -308,7 +308,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateRecordingState() {
|
private updateRecordingState() {
|
||||||
this.voiceRecording = VoiceRecordingStore.instance.getActiveRecording(this.props.room.roomId);
|
const voiceRecordingId = VoiceRecordingStore.getVoiceRecordingId(this.props.room, this.props.relation);
|
||||||
|
this.voiceRecording = VoiceRecordingStore.instance.getActiveRecording(voiceRecordingId);
|
||||||
if (this.voiceRecording) {
|
if (this.voiceRecording) {
|
||||||
// If the recording has already started, it's probably a cached one.
|
// If the recording has already started, it's probably a cached one.
|
||||||
if (this.voiceRecording.hasRecording && !this.voiceRecording.isRecording) {
|
if (this.voiceRecording.hasRecording && !this.voiceRecording.isRecording) {
|
||||||
|
@ -323,7 +324,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private onRecordingStarted = () => {
|
private onRecordingStarted = () => {
|
||||||
// update the recording instance, just in case
|
// update the recording instance, just in case
|
||||||
this.voiceRecording = VoiceRecordingStore.instance.getActiveRecording(this.props.room.roomId);
|
const voiceRecordingId = VoiceRecordingStore.getVoiceRecordingId(this.props.room, this.props.relation);
|
||||||
|
this.voiceRecording = VoiceRecordingStore.instance.getActiveRecording(voiceRecordingId);
|
||||||
this.setState({
|
this.setState({
|
||||||
haveRecording: !!this.voiceRecording,
|
haveRecording: !!this.voiceRecording,
|
||||||
});
|
});
|
||||||
|
|
|
@ -64,6 +64,7 @@ interface IState {
|
||||||
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
|
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
|
||||||
static contextType = RoomContext;
|
static contextType = RoomContext;
|
||||||
public context!: React.ContextType<typeof RoomContext>;
|
public context!: React.ContextType<typeof RoomContext>;
|
||||||
|
private voiceRecordingId: string;
|
||||||
|
|
||||||
public constructor(props: IProps) {
|
public constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -71,10 +72,12 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
this.state = {
|
this.state = {
|
||||||
recorder: null, // no recording started by default
|
recorder: null, // no recording started by default
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.voiceRecordingId = VoiceRecordingStore.getVoiceRecordingId(this.props.room, this.props.relation);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
const recorder = VoiceRecordingStore.instance.getActiveRecording(this.props.room.roomId);
|
const recorder = VoiceRecordingStore.instance.getActiveRecording(this.voiceRecordingId);
|
||||||
if (recorder) {
|
if (recorder) {
|
||||||
if (recorder.isRecording || !recorder.hasRecording) {
|
if (recorder.isRecording || !recorder.hasRecording) {
|
||||||
logger.warn("Cached recording hasn't ended yet and might cause issues");
|
logger.warn("Cached recording hasn't ended yet and might cause issues");
|
||||||
|
@ -87,7 +90,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
public async componentWillUnmount() {
|
public async componentWillUnmount() {
|
||||||
// Stop recording, but keep the recording memory (don't dispose it). This is to let the user
|
// Stop recording, but keep the recording memory (don't dispose it). This is to let the user
|
||||||
// come back and finish working with it.
|
// come back and finish working with it.
|
||||||
const recording = VoiceRecordingStore.instance.getActiveRecording(this.props.room.roomId);
|
const recording = VoiceRecordingStore.instance.getActiveRecording(this.voiceRecordingId);
|
||||||
await recording?.stop();
|
await recording?.stop();
|
||||||
|
|
||||||
// Clean up our listeners by binding a falsy recorder
|
// Clean up our listeners by binding a falsy recorder
|
||||||
|
@ -106,7 +109,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
|
|
||||||
let upload: IUpload;
|
let upload: IUpload;
|
||||||
try {
|
try {
|
||||||
upload = await this.state.recorder.upload(this.props.room.roomId);
|
upload = await this.state.recorder.upload(this.voiceRecordingId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error uploading voice message:", e);
|
logger.error("Error uploading voice message:", e);
|
||||||
|
|
||||||
|
@ -179,7 +182,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
private async disposeRecording() {
|
private async disposeRecording() {
|
||||||
await VoiceRecordingStore.instance.disposeRecording(this.props.room.roomId);
|
await VoiceRecordingStore.instance.disposeRecording(this.voiceRecordingId);
|
||||||
|
|
||||||
// Reset back to no recording, which means no phase (ie: restart component entirely)
|
// Reset back to no recording, which means no phase (ie: restart component entirely)
|
||||||
this.setState({ recorder: null, recordingPhase: null, didUploadFail: false });
|
this.setState({ recorder: null, recordingPhase: null, didUploadFail: false });
|
||||||
|
@ -232,8 +235,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
try {
|
try {
|
||||||
// stop any noises which might be happening
|
// stop any noises which might be happening
|
||||||
PlaybackManager.instance.pauseAllExcept(null);
|
PlaybackManager.instance.pauseAllExcept(null);
|
||||||
|
const recorder = VoiceRecordingStore.instance.startRecording(this.voiceRecordingId);
|
||||||
const recorder = VoiceRecordingStore.instance.startRecording(this.props.room.roomId);
|
|
||||||
await recorder.start();
|
await recorder.start();
|
||||||
|
|
||||||
this.bindNewRecorder(recorder);
|
this.bindNewRecorder(recorder);
|
||||||
|
@ -244,7 +246,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
accessError();
|
accessError();
|
||||||
|
|
||||||
// noinspection ES6MissingAwait - if this goes wrong we don't want it to affect the call stack
|
// noinspection ES6MissingAwait - if this goes wrong we don't want it to affect the call stack
|
||||||
VoiceRecordingStore.instance.disposeRecording(this.props.room.roomId);
|
VoiceRecordingStore.instance.disposeRecording(this.voiceRecordingId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -15,14 +15,19 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Optional } from "matrix-events-sdk";
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { RelationType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { IEventRelation } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
|
||||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||||
import { ActionPayload } from "../dispatcher/payloads";
|
import { ActionPayload } from "../dispatcher/payloads";
|
||||||
import { VoiceRecording } from "../audio/VoiceRecording";
|
import { VoiceRecording } from "../audio/VoiceRecording";
|
||||||
|
|
||||||
|
const SEPARATOR = "|";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
[roomId: string]: Optional<VoiceRecording>;
|
[voiceRecordingId: string]: Optional<VoiceRecording>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VoiceRecordingStore extends AsyncStoreWithClient<IState> {
|
export class VoiceRecordingStore extends AsyncStoreWithClient<IState> {
|
||||||
|
@ -45,48 +50,54 @@ export class VoiceRecordingStore extends AsyncStoreWithClient<IState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getVoiceRecordingId(room: Room, relation?: IEventRelation): string {
|
||||||
|
if (relation?.rel_type === "io.element.thread" || relation?.rel_type === RelationType.Thread) {
|
||||||
|
return room.roomId + SEPARATOR + relation.event_id;
|
||||||
|
} else {
|
||||||
|
return room.roomId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the active recording instance, if any.
|
* Gets the active recording instance, if any.
|
||||||
* @param {string} roomId The room ID to get the recording in.
|
* @param {string} voiceRecordingId The room ID (with optionally the thread ID if in one) to get the recording in.
|
||||||
* @returns {Optional<VoiceRecording>} The recording, if any.
|
* @returns {Optional<VoiceRecording>} The recording, if any.
|
||||||
*/
|
*/
|
||||||
public getActiveRecording(roomId: string): Optional<VoiceRecording> {
|
public getActiveRecording(voiceRecordingId: string): Optional<VoiceRecording> {
|
||||||
return this.state[roomId];
|
return this.state[voiceRecordingId];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a new recording if one isn't already in progress. Note that this simply
|
* Starts a new recording if one isn't already in progress. Note that this simply
|
||||||
* creates a recording instance - whether or not recording is actively in progress
|
* creates a recording instance - whether or not recording is actively in progress
|
||||||
* can be seen via the VoiceRecording class.
|
* can be seen via the VoiceRecording class.
|
||||||
* @param {string} roomId The room ID to start recording in.
|
* @param {string} voiceRecordingId The room ID (with optionally the thread ID if in one) to start recording in.
|
||||||
* @returns {VoiceRecording} The recording.
|
* @returns {VoiceRecording} The recording.
|
||||||
*/
|
*/
|
||||||
public startRecording(roomId: string): VoiceRecording {
|
public startRecording(voiceRecordingId: string): VoiceRecording {
|
||||||
if (!this.matrixClient) throw new Error("Cannot start a recording without a MatrixClient");
|
if (!this.matrixClient) throw new Error("Cannot start a recording without a MatrixClient");
|
||||||
if (!roomId) throw new Error("Recording must be associated with a room");
|
if (!voiceRecordingId) throw new Error("Recording must be associated with a room");
|
||||||
if (this.state[roomId]) throw new Error("A recording is already in progress");
|
if (this.state[voiceRecordingId]) throw new Error("A recording is already in progress");
|
||||||
|
|
||||||
const recording = new VoiceRecording(this.matrixClient);
|
const recording = new VoiceRecording(this.matrixClient);
|
||||||
|
|
||||||
// noinspection JSIgnoredPromiseFromCall - we can safely run this async
|
// noinspection JSIgnoredPromiseFromCall - we can safely run this async
|
||||||
this.updateState({ ...this.state, [roomId]: recording });
|
this.updateState({ ...this.state, [voiceRecordingId]: recording });
|
||||||
|
|
||||||
return recording;
|
return recording;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes of the current recording, no matter the state of it.
|
* Disposes of the current recording, no matter the state of it.
|
||||||
* @param {string} roomId The room ID to dispose of the recording in.
|
* @param {string} voiceRecordingId The room ID (with optionally the thread ID if in one) to dispose of the recording in.
|
||||||
* @returns {Promise<void>} Resolves when complete.
|
* @returns {Promise<void>} Resolves when complete.
|
||||||
*/
|
*/
|
||||||
public disposeRecording(roomId: string): Promise<void> {
|
public disposeRecording(voiceRecordingId: string): Promise<void> {
|
||||||
if (this.state[roomId]) {
|
this.state[voiceRecordingId]?.destroy(); // stops internally
|
||||||
this.state[roomId].destroy(); // stops internally
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
[roomId]: _toDelete,
|
[voiceRecordingId]: _toDelete,
|
||||||
...newState
|
...newState
|
||||||
} = this.state;
|
} = this.state;
|
||||||
// unexpectedly AsyncStore.updateState merges state
|
// unexpectedly AsyncStore.updateState merges state
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue