Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Fix voice messages with multiple composers #9208

Merged
merged 11 commits into from
Sep 5, 2022
16 changes: 13 additions & 3 deletions src/components/views/rooms/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import classNames from 'classnames';
import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { EventType } from 'matrix-js-sdk/src/@types/event';
import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event';
import { Optional } from "matrix-events-sdk";
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';

Expand Down Expand Up @@ -308,7 +308,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
};

private updateRecordingState() {
this.voiceRecording = VoiceRecordingStore.instance.getActiveRecording(this.props.room.roomId);
this.voiceRecording =
this.props.relation?.rel_type === ("io.element.thread")
|| this.props.relation?.rel_type === RelationType.Thread ?
VoiceRecordingStore.instance.getActiveRecording(this.props.room.roomId+this.props.relation.event_id)
:
VoiceRecordingStore.instance.getActiveRecording(this.props.room.roomId);
if (this.voiceRecording) {
// If the recording has already started, it's probably a cached one.
if (this.voiceRecording.hasRecording && !this.voiceRecording.isRecording) {
Expand All @@ -323,7 +328,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {

private onRecordingStarted = () => {
// update the recording instance, just in case
this.voiceRecording = VoiceRecordingStore.instance.getActiveRecording(this.props.room.roomId);
this.voiceRecording =
this.props.relation?.rel_type === ("io.element.thread")
|| this.props.relation?.rel_type === RelationType.Thread ?
VoiceRecordingStore.instance.getActiveRecording(this.props.room.roomId+this.props.relation.event_id)
:
VoiceRecordingStore.instance.getActiveRecording(this.props.room.roomId);
this.setState({
haveRecording: !!this.voiceRecording,
});
Expand Down
23 changes: 15 additions & 8 deletions src/components/views/rooms/VoiceRecordComposerTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ limitations under the License.

import React, { ReactNode } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import { MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
import { logger } from "matrix-js-sdk/src/logger";
import { Optional } from "matrix-events-sdk";
import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
Expand Down Expand Up @@ -64,17 +64,25 @@ interface IState {
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
private voiceRecordingId: string;

public constructor(props: IProps) {
super(props);

this.state = {
recorder: null, // no recording started by default
};

this.voiceRecordingId =
this.props.relation?.rel_type === "io.element.thread"
|| this.props.relation?.rel_type === RelationType.Thread ?
this.props.room.roomId + this.props.relation.event_id
:
this.props.room.roomId;
}

public componentDidMount() {
const recorder = VoiceRecordingStore.instance.getActiveRecording(this.props.room.roomId);
const recorder = VoiceRecordingStore.instance.getActiveRecording(this.voiceRecordingId);
if (recorder) {
if (recorder.isRecording || !recorder.hasRecording) {
logger.warn("Cached recording hasn't ended yet and might cause issues");
Expand All @@ -87,7 +95,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
public async componentWillUnmount() {
// Stop recording, but keep the recording memory (don't dispose it). This is to let the user
// 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();

// Clean up our listeners by binding a falsy recorder
Expand All @@ -106,7 +114,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,

let upload: IUpload;
try {
upload = await this.state.recorder.upload(this.props.room.roomId);
upload = await this.state.recorder.upload(this.voiceRecordingId);
} catch (e) {
logger.error("Error uploading voice message:", e);

Expand Down Expand Up @@ -179,7 +187,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
}

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)
this.setState({ recorder: null, recordingPhase: null, didUploadFail: false });
Expand Down Expand Up @@ -232,8 +240,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
try {
// stop any noises which might be happening
PlaybackManager.instance.pauseAllExcept(null);

const recorder = VoiceRecordingStore.instance.startRecording(this.props.room.roomId);
const recorder = VoiceRecordingStore.instance.startRecording(this.voiceRecordingId);
await recorder.start();

this.bindNewRecorder(recorder);
Expand All @@ -244,7 +251,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
accessError();

// 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);
}
};

Expand Down
28 changes: 14 additions & 14 deletions src/stores/VoiceRecordingStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { ActionPayload } from "../dispatcher/payloads";
import { VoiceRecording } from "../audio/VoiceRecording";

interface IState {
[roomId: string]: Optional<VoiceRecording>;
[voiceRecordingId: string]: Optional<VoiceRecording>;
}

export class VoiceRecordingStore extends AsyncStoreWithClient<IState> {
Expand All @@ -46,46 +46,46 @@ export class VoiceRecordingStore extends AsyncStoreWithClient<IState> {

/**
* 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 optionnaly the thread ID if in one) to get the recording in.
* @returns {Optional<VoiceRecording>} The recording, if any.
*/
public getActiveRecording(roomId: string): Optional<VoiceRecording> {
return this.state[roomId];
public getActiveRecording(voiceRecordingId: string): Optional<VoiceRecording> {
return this.state[voiceRecordingId];
}

/**
* 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
* can be seen via the VoiceRecording class.
* @param {string} roomId The room ID to start recording in.
* @param {string} voiceRecordingId The room ID (with optionnaly the thread ID if in one) to start recording in.
* @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 (!roomId) throw new Error("Recording must be associated with a room");
if (this.state[roomId]) throw new Error("A recording is already in progress");
if (!voiceRecordingId) throw new Error("Recording must be associated with a room");
if (this.state[voiceRecordingId]) throw new Error("A recording is already in progress");

const recording = new VoiceRecording(this.matrixClient);

// noinspection JSIgnoredPromiseFromCall - we can safely run this async
this.updateState({ ...this.state, [roomId]: recording });
this.updateState({ ...this.state, [voiceRecordingId]: recording });

return recording;
}

/**
* 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 optionnaly the thread ID if in one) to dispose of the recording in.
* @returns {Promise<void>} Resolves when complete.
*/
public disposeRecording(roomId: string): Promise<void> {
if (this.state[roomId]) {
this.state[roomId].destroy(); // stops internally
public disposeRecording(voiceRecordingId: string): Promise<void> {
if (this.state[voiceRecordingId]) {
this.state[voiceRecordingId].destroy(); // stops internally
}

const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[roomId]: _toDelete,
[voiceRecordingId]: _toDelete,
...newState
} = this.state;
// unexpectedly AsyncStore.updateState merges state
Expand Down