From b0a04c9f814e7f68a02cdddc88a62c422a730193 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Apr 2021 20:00:16 -0600 Subject: [PATCH 1/3] Rename VoiceRecorder -> VoiceRecording to better match expected function --- src/@types/global.d.ts | 4 ++-- src/components/views/rooms/VoiceRecordComposerTile.tsx | 4 ++-- src/components/views/voice_messages/LiveRecordingClock.tsx | 4 ++-- src/components/views/voice_messages/LiveRecordingWaveform.tsx | 4 ++-- src/voice/{VoiceRecorder.ts => VoiceRecording.ts} | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) rename src/voice/{VoiceRecorder.ts => VoiceRecording.ts} (99%) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 051e5cc429..ee0963e537 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -39,7 +39,7 @@ import {ModalWidgetStore} from "../stores/ModalWidgetStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import VoipUserMapper from "../VoipUserMapper"; import {SpaceStoreClass} from "../stores/SpaceStore"; -import {VoiceRecorder} from "../voice/VoiceRecorder"; +import {VoiceRecording} from "../voice/VoiceRecording"; declare global { interface Window { @@ -71,7 +71,7 @@ declare global { mxModalWidgetStore: ModalWidgetStore; mxVoipUserMapper: VoipUserMapper; mxSpaceStore: SpaceStoreClass; - mxVoiceRecorder: typeof VoiceRecorder; + mxVoiceRecorder: typeof VoiceRecording; } interface Document { diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index b4999ac0df..e83aa8b994 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -17,7 +17,7 @@ limitations under the License. import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import {_t} from "../../../languageHandler"; import React from "react"; -import {VoiceRecorder} from "../../../voice/VoiceRecorder"; +import {VoiceRecording} from "../../../voice/VoiceRecording"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import classNames from "classnames"; @@ -31,7 +31,7 @@ interface IProps { } interface IState { - recorder?: VoiceRecorder; + recorder?: VoiceRecording; } /** diff --git a/src/components/views/voice_messages/LiveRecordingClock.tsx b/src/components/views/voice_messages/LiveRecordingClock.tsx index 00316d196a..5e9006c6ab 100644 --- a/src/components/views/voice_messages/LiveRecordingClock.tsx +++ b/src/components/views/voice_messages/LiveRecordingClock.tsx @@ -15,12 +15,12 @@ limitations under the License. */ import React from "react"; -import {IRecordingUpdate, VoiceRecorder} from "../../../voice/VoiceRecorder"; +import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import Clock from "./Clock"; interface IProps { - recorder: VoiceRecorder; + recorder: VoiceRecording; } interface IState { diff --git a/src/components/views/voice_messages/LiveRecordingWaveform.tsx b/src/components/views/voice_messages/LiveRecordingWaveform.tsx index e7cab4a5cb..c1f5e97fff 100644 --- a/src/components/views/voice_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/voice_messages/LiveRecordingWaveform.tsx @@ -15,14 +15,14 @@ limitations under the License. */ import React from "react"; -import {IRecordingUpdate, VoiceRecorder} from "../../../voice/VoiceRecorder"; +import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {arrayFastResample, arraySeed} from "../../../utils/arrays"; import {percentageOf} from "../../../utils/numbers"; import Waveform from "./Waveform"; interface IProps { - recorder: VoiceRecorder; + recorder: VoiceRecording; } interface IState { diff --git a/src/voice/VoiceRecorder.ts b/src/voice/VoiceRecording.ts similarity index 99% rename from src/voice/VoiceRecorder.ts rename to src/voice/VoiceRecording.ts index 077990ac17..77c182fc54 100644 --- a/src/voice/VoiceRecorder.ts +++ b/src/voice/VoiceRecording.ts @@ -30,7 +30,7 @@ export interface IRecordingUpdate { timeSeconds: number; // float } -export class VoiceRecorder { +export class VoiceRecording { private recorder: Recorder; private recorderContext: AudioContext; private recorderSource: MediaStreamAudioSourceNode; @@ -209,4 +209,4 @@ export class VoiceRecorder { } } -window.mxVoiceRecorder = VoiceRecorder; +window.mxVoiceRecorder = VoiceRecording; From 3cafed478cc78ca1157877b1cf1ac860529bb4b2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Apr 2021 20:11:34 -0600 Subject: [PATCH 2/3] Run voice recording updates through a dedicated store --- src/components/views/rooms/MessageComposer.js | 11 +-- .../views/rooms/VoiceRecordComposerTile.tsx | 7 +- src/stores/VoiceRecordingStore.ts | 82 +++++++++++++++++++ 3 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 src/stores/VoiceRecordingStore.ts diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index b7078766fb..283b11a437 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -34,6 +34,7 @@ import {UPDATE_EVENT} from "../../../stores/AsyncStore"; import ActiveWidgetStore from "../../../stores/ActiveWidgetStore"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import VoiceRecordComposerTile from "./VoiceRecordComposerTile"; +import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; function ComposerAvatar(props) { const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar'); @@ -180,6 +181,7 @@ export default class MessageComposer extends React.Component { this.renderPlaceholderText = this.renderPlaceholderText.bind(this); WidgetStore.instance.on(UPDATE_EVENT, this._onWidgetUpdate); ActiveWidgetStore.on('update', this._onActiveWidgetUpdate); + VoiceRecordingStore.instance.on(UPDATE_EVENT, this._onVoiceStoreUpdate); this._dispatcherRef = null; this.state = { @@ -240,6 +242,7 @@ export default class MessageComposer extends React.Component { } WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate); ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate); + VoiceRecordingStore.instance.off(UPDATE_EVENT, this._onVoiceStoreUpdate); dis.unregister(this.dispatcherRef); } @@ -327,8 +330,8 @@ export default class MessageComposer extends React.Component { }); } - onVoiceUpdate = (haveRecording: boolean) => { - this.setState({haveRecording}); + _onVoiceStoreUpdate = () => { + this.setState({haveRecording: !!VoiceRecordingStore.instance.activeRecording}); }; render() { @@ -352,7 +355,6 @@ export default class MessageComposer extends React.Component { permalinkCreator={this.props.permalinkCreator} replyToEvent={this.props.replyToEvent} onChange={this.onChange} - // TODO: @@ TravisR - Disabling the composer doesn't work disabled={this.state.haveRecording} />, ); @@ -373,8 +375,7 @@ export default class MessageComposer extends React.Component { if (SettingsStore.getValue("feature_voice_messages")) { controls.push(); + room={this.props.room} />); } if (!this.state.isComposerEmpty || this.state.haveRecording) { diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index e83aa8b994..1210a44958 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -24,10 +24,10 @@ import classNames from "classnames"; import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import LiveRecordingClock from "../voice_messages/LiveRecordingClock"; +import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; interface IProps { room: Room; - onRecording: (haveRecording: boolean) => void; } interface IState { @@ -57,13 +57,12 @@ export default class VoiceRecordComposerTile extends React.PureComponent { + private static internalInstance: VoiceRecordingStore; + + public constructor() { + super(defaultDispatcher, {}); + } + + /** + * Gets the active recording instance, if any. + */ + public get activeRecording(): VoiceRecording | null { + return this.state.recording; + } + + public static get instance(): VoiceRecordingStore { + if (!VoiceRecordingStore.internalInstance) { + VoiceRecordingStore.internalInstance = new VoiceRecordingStore(); + } + return VoiceRecordingStore.internalInstance; + } + + protected async onAction(payload: ActionPayload): Promise { + // Nothing to do, but we're required to override the function + return; + } + + /** + * 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. + * @returns {VoiceRecording} The recording. + */ + public startRecording(): VoiceRecording { + if (!this.matrixClient) throw new Error("Cannot start a recording without a MatrixClient"); + if (this.state.recording) 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({recording}); + + return recording; + } + + /** + * Disposes of the current recording, no matter the state of it. + * @returns {Promise} Resolves when complete. + */ + public disposeRecording(): Promise { + if (this.state.recording) { + // Stop for good measure, but completely async because we're not concerned with this + // passing or failing. + this.state.recording.stop().catch(e => console.error("Error stopping recording", e)); + } + return this.updateState({recording: null}); + } +} From fedb5b9f63083ab7511d15be01a441f524868610 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Apr 2021 20:12:10 -0600 Subject: [PATCH 3/3] Fix disabled state of the composer --- res/css/views/rooms/_BasicMessageComposer.scss | 2 +- src/components/views/rooms/BasicMessageComposer.tsx | 9 ++++++--- src/components/views/rooms/SendMessageComposer.js | 4 ++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index 4f58c08617..e1ba468204 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -68,8 +68,8 @@ limitations under the License. } &.mx_BasicMessageComposer_input_disabled { + // Ignore all user input to avoid accidentally triggering the composer pointer-events: none; - cursor: not-allowed; } } diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 9d9e3a1ba0..e83f066bd0 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -140,7 +140,12 @@ export default class BasicMessageEditor extends React.Component } public componentDidUpdate(prevProps: IProps) { - if (this.props.placeholder !== prevProps.placeholder && this.props.placeholder) { + // We need to re-check the placeholder when the enabled state changes because it causes the + // placeholder element to remount, which gets rid of the `::before` class. Re-evaluating the + // placeholder means we get a proper `::before` with the placeholder. + const enabledChange = this.props.disabled !== prevProps.disabled; + const placeholderChanged = this.props.placeholder !== prevProps.placeholder; + if (this.props.placeholder && (placeholderChanged || enabledChange)) { const {isEmpty} = this.props.model; if (isEmpty) { this.showPlaceholder(); @@ -670,8 +675,6 @@ export default class BasicMessageEditor extends React.Component }); const classes = classNames("mx_BasicMessageComposer_input", { "mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar, - - // TODO: @@ TravisR: This doesn't work properly. The composer resets in a strange way. "mx_BasicMessageComposer_input_disabled": this.props.disabled, }); diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 75bc943146..0d3a174766 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -477,6 +477,10 @@ export default class SendMessageComposer extends React.Component { } onAction = (payload) => { + // don't let the user into the composer if it is disabled - all of these branches lead + // to the cursor being in the composer + if (this.props.disabled) return; + switch (payload.action) { case 'reply_to_event': case Action.FocusComposer: