Merge pull request #6453 from matrix-org/travis/voice-messages/code-cleanup-1

Clean up voice messages code
This commit is contained in:
Travis Ralston 2021-07-23 09:09:12 -06:00 committed by GitHub
commit 2c4ab507d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 92 additions and 102 deletions

View file

@ -14,9 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Playback, PlaybackState } from "../../../voice/Playback";
import React, { createRef, ReactNode, RefObject } from "react"; import React, { createRef, ReactNode, RefObject } from "react";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlayPauseButton from "./PlayPauseButton"; import PlayPauseButton from "./PlayPauseButton";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { formatBytes } from "../../../utils/FormattingUtils"; import { formatBytes } from "../../../utils/FormattingUtils";
@ -25,47 +23,13 @@ import { Key } from "../../../Keyboard";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import SeekBar from "./SeekBar"; import SeekBar from "./SeekBar";
import PlaybackClock from "./PlaybackClock"; import PlaybackClock from "./PlaybackClock";
import AudioPlayerBase from "./AudioPlayerBase";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
mediaName: string;
}
interface IState {
playbackPhase: PlaybackState;
error?: boolean;
}
@replaceableComponent("views.audio_messages.AudioPlayer") @replaceableComponent("views.audio_messages.AudioPlayer")
export default class AudioPlayer extends React.PureComponent<IProps, IState> { export default class AudioPlayer extends AudioPlayerBase {
private playPauseRef: RefObject<PlayPauseButton> = createRef(); private playPauseRef: RefObject<PlayPauseButton> = createRef();
private seekRef: RefObject<SeekBar> = createRef(); private seekRef: RefObject<SeekBar> = createRef();
constructor(props: IProps) {
super(props);
this.state = {
playbackPhase: PlaybackState.Decoding, // default assumption
};
// We don't need to de-register: the class handles this for us internally
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
// Don't wait for the promise to complete - it will emit a progress update when it
// is done, and it's not meant to take long anyhow.
this.props.playback.prepare().catch(e => {
console.error("Error processing audio file:", e);
this.setState({ error: true });
});
}
private onPlaybackUpdate = (ev: PlaybackState) => {
this.setState({ playbackPhase: ev });
};
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
// stopPropagation() prevents the FocusComposer catch-all from triggering, // stopPropagation() prevents the FocusComposer catch-all from triggering,
// but we need to do it on key down instead of press (even though the user // but we need to do it on key down instead of press (even though the user
@ -91,10 +55,10 @@ export default class AudioPlayer extends React.PureComponent<IProps, IState> {
return `(${formatBytes(bytes)})`; return `(${formatBytes(bytes)})`;
} }
public render(): ReactNode { protected renderComponent(): ReactNode {
// tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard // tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard
// events for accessibility // events for accessibility
return <> return (
<div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}> <div className='mx_MediaBody mx_AudioPlayer_container' tabIndex={0} onKeyDown={this.onKeyDown}>
<div className='mx_AudioPlayer_primaryContainer'> <div className='mx_AudioPlayer_primaryContainer'>
<PlayPauseButton <PlayPauseButton
@ -124,7 +88,6 @@ export default class AudioPlayer extends React.PureComponent<IProps, IState> {
<PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} /> <PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} />
</div> </div>
</div> </div>
{ this.state.error && <div className="text-warning">{ _t("Error downloading audio") }</div> } );
</>;
} }
} }

View file

@ -0,0 +1,70 @@
/*
Copyright 2021 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 { Playback, PlaybackState } from "../../../audio/Playback";
import { TileShape } from "../rooms/EventTile";
import React, { ReactNode } from "react";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { _t } from "../../../languageHandler";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
mediaName?: string;
tileShape?: TileShape;
}
interface IState {
playbackPhase: PlaybackState;
error?: boolean;
}
@replaceableComponent("views.audio_messages.AudioPlayerBase")
export default abstract class AudioPlayerBase extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
playbackPhase: PlaybackState.Decoding, // default assumption
};
// We don't need to de-register: the class handles this for us internally
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
// Don't wait for the promise to complete - it will emit a progress update when it
// is done, and it's not meant to take long anyhow.
this.props.playback.prepare().catch(e => {
console.error("Error processing audio file:", e);
this.setState({ error: true });
});
}
private onPlaybackUpdate = (ev: PlaybackState) => {
this.setState({ playbackPhase: ev });
};
protected abstract renderComponent(): ReactNode;
public render(): ReactNode {
return <>
{ this.renderComponent() }
{ this.state.error && <div className="text-warning">{ _t("Error downloading audio") }</div> }
</>;
}
}

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock"; import Clock from "./Clock";
import { Playback } from "../../../voice/Playback"; import { Playback } from "../../../audio/Playback";
interface IProps { interface IProps {
playback: Playback; playback: Playback;

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording"; import { IRecordingUpdate, VoiceRecording } from "../../../audio/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock"; import Clock from "./Clock";
import { MarkedExecution } from "../../../utils/MarkedExecution"; import { MarkedExecution } from "../../../utils/MarkedExecution";

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../voice/VoiceRecording"; import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arrayFastResample } from "../../../utils/arrays"; import { arrayFastResample } from "../../../utils/arrays";
import { percentageOf } from "../../../utils/numbers"; import { percentageOf } from "../../../utils/numbers";

View file

@ -18,7 +18,7 @@ import React, { ReactNode } from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { Playback, PlaybackState } from "../../../voice/Playback"; import { Playback, PlaybackState } from "../../../audio/Playback";
import classNames from "classnames"; import classNames from "classnames";
// omitted props are handled by render function // omitted props are handled by render function

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from "react"; import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock"; import Clock from "./Clock";
import { Playback, PlaybackState } from "../../../voice/Playback"; import { Playback, PlaybackState } from "../../../audio/Playback";
import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore";
interface IProps { interface IProps {

View file

@ -18,7 +18,7 @@ import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { arraySeed, arrayTrimFill } from "../../../utils/arrays"; import { arraySeed, arrayTrimFill } from "../../../utils/arrays";
import Waveform from "./Waveform"; import Waveform from "./Waveform";
import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../voice/Playback"; import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/Playback";
import { percentageOf } from "../../../utils/numbers"; import { percentageOf } from "../../../utils/numbers";
interface IProps { interface IProps {

View file

@ -14,68 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Playback, PlaybackState } from "../../../voice/Playback";
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlayPauseButton from "./PlayPauseButton"; import PlayPauseButton from "./PlayPauseButton";
import PlaybackClock from "./PlaybackClock"; import PlaybackClock from "./PlaybackClock";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { TileShape } from "../rooms/EventTile"; import { TileShape } from "../rooms/EventTile";
import PlaybackWaveform from "./PlaybackWaveform"; import PlaybackWaveform from "./PlaybackWaveform";
import { _t } from "../../../languageHandler"; import AudioPlayerBase from "./AudioPlayerBase";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
// an all-new component instead.
playback: Playback;
tileShape?: TileShape;
}
interface IState {
playbackPhase: PlaybackState;
error?: boolean;
}
@replaceableComponent("views.audio_messages.RecordingPlayback") @replaceableComponent("views.audio_messages.RecordingPlayback")
export default class RecordingPlayback extends React.PureComponent<IProps, IState> { export default class RecordingPlayback extends AudioPlayerBase {
constructor(props: IProps) {
super(props);
this.state = {
playbackPhase: PlaybackState.Decoding, // default assumption
};
// We don't need to de-register: the class handles this for us internally
this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
// Don't wait for the promise to complete - it will emit a progress update when it
// is done, and it's not meant to take long anyhow.
this.props.playback.prepare().catch(e => {
console.error("Error processing audio file:", e);
this.setState({ error: true });
});
}
private get isWaveformable(): boolean { private get isWaveformable(): boolean {
return this.props.tileShape !== TileShape.Notif return this.props.tileShape !== TileShape.Notif
&& this.props.tileShape !== TileShape.FileGrid && this.props.tileShape !== TileShape.FileGrid
&& this.props.tileShape !== TileShape.Pinned; && this.props.tileShape !== TileShape.Pinned;
} }
private onPlaybackUpdate = (ev: PlaybackState) => { protected renderComponent(): ReactNode {
this.setState({ playbackPhase: ev });
};
public render(): ReactNode {
const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : ''; const shapeClass = !this.isWaveformable ? 'mx_VoiceMessagePrimaryContainer_noWaveform' : '';
return <> return (
<div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}> <div className={'mx_MediaBody mx_VoiceMessagePrimaryContainer ' + shapeClass}>
<PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} /> <PlayPauseButton playback={this.props.playback} playbackPhase={this.state.playbackPhase} />
<PlaybackClock playback={this.props.playback} /> <PlaybackClock playback={this.props.playback} />
{ this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> } { this.isWaveformable && <PlaybackWaveform playback={this.props.playback} /> }
</div> </div>
{ this.state.error && <div className="text-warning">{ _t("Error downloading audio") }</div> } );
</>;
} }
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Playback, PlaybackState } from "../../../voice/Playback"; import { Playback, PlaybackState } from "../../../audio/Playback";
import React, { ChangeEvent, CSSProperties, ReactNode } from "react"; import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { MarkedExecution } from "../../../utils/MarkedExecution"; import { MarkedExecution } from "../../../utils/MarkedExecution";

View file

@ -16,14 +16,14 @@ limitations under the License.
import React from "react"; import React from "react";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { Playback } from "../../../voice/Playback"; import { Playback } from "../../../audio/Playback";
import InlineSpinner from '../elements/InlineSpinner'; import InlineSpinner from '../elements/InlineSpinner';
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AudioPlayer from "../audio_messages/AudioPlayer"; import AudioPlayer from "../audio_messages/AudioPlayer";
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
import MFileBody from "./MFileBody"; import MFileBody from "./MFileBody";
import { IBodyProps } from "./IBodyProps"; import { IBodyProps } from "./IBodyProps";
import { PlaybackManager } from "../../../voice/PlaybackManager"; import { PlaybackManager } from "../../../audio/PlaybackManager";
interface IState { interface IState {
error?: Error; error?: Error;
@ -76,7 +76,6 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
public render() { public render() {
if (this.state.error) { if (this.state.error) {
// TODO: @@TR: Verify error state
return ( return (
<span className="mx_MAudioBody"> <span className="mx_MAudioBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" /> <img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
@ -86,7 +85,6 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
} }
if (!this.state.playback) { if (!this.state.playback) {
// TODO: @@TR: Verify loading/decrypting state
return ( return (
<span className="mx_MAudioBody"> <span className="mx_MAudioBody">
<InlineSpinner /> <InlineSpinner />

View file

@ -27,7 +27,6 @@ export default class MVoiceMessageBody extends MAudioBody {
// A voice message is an audio file but rendered in a special way. // A voice message is an audio file but rendered in a special way.
public render() { public render() {
if (this.state.error) { if (this.state.error) {
// TODO: @@TR: Verify error state
return ( return (
<span className="mx_MVoiceMessageBody"> <span className="mx_MVoiceMessageBody">
<img src={require("../../../../res/img/warning.svg")} width="16" height="16" /> <img src={require("../../../../res/img/warning.svg")} width="16" height="16" />
@ -37,7 +36,6 @@ export default class MVoiceMessageBody extends MAudioBody {
} }
if (!this.state.playback) { if (!this.state.playback) {
// TODO: @@TR: Verify loading/decrypting state
return ( return (
<span className="mx_MVoiceMessageBody"> <span className="mx_MVoiceMessageBody">
<InlineSpinner /> <InlineSpinner />

View file

@ -35,7 +35,7 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import VoiceRecordComposerTile from "./VoiceRecordComposerTile"; import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore"; import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
import { RecordingState } from "../../../voice/VoiceRecording"; import { RecordingState } from "../../../audio/VoiceRecording";
import Tooltip, { Alignment } from "../elements/Tooltip"; import Tooltip, { Alignment } from "../elements/Tooltip";
import ResizeNotifier from "../../../utils/ResizeNotifier"; import ResizeNotifier from "../../../utils/ResizeNotifier";
import { E2EStatus } from '../../../utils/ShieldUtils'; import { E2EStatus } from '../../../utils/ShieldUtils';

View file

@ -20,7 +20,7 @@ import React, { ReactNode } from "react";
import { import {
RecordingState, RecordingState,
VoiceRecording, VoiceRecording,
} from "../../../voice/VoiceRecording"; } from "../../../audio/VoiceRecording";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import classNames from "classnames"; import classNames from "classnames";
@ -189,7 +189,6 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
if (!this.state.recorder) return null; // no recorder means we're not recording: no waveform if (!this.state.recorder) return null; // no recorder means we're not recording: no waveform
if (this.state.recordingPhase !== RecordingState.Started) { if (this.state.recordingPhase !== RecordingState.Started) {
// TODO: @@ TR: Should we disable this during upload? What does a failed upload look like?
return <RecordingPlayback playback={this.state.recorder.getPlayback()} />; return <RecordingPlayback playback={this.state.recorder.getPlayback()} />;
} }

View file

@ -17,7 +17,7 @@ limitations under the License.
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 "../voice/VoiceRecording"; import { VoiceRecording } from "../audio/VoiceRecording";
interface IState { interface IState {
recording?: VoiceRecording; recording?: VoiceRecording;