Move AudioPlayer to shared components (#30386)
* feat: add `PlayPauseButton` to storybook * feat: add generic media body * feat: add seekbar component * chore: add ViewWrapper to help writing stories with vm * refactor: move `formatBytes` from `formattingUtils` into shared component * refactor: add `className` props to `Clock` * feat: add new audio player component * test(e2e): add screenshots for new shared components * feat: add AudioPlayerViewModel * feat: use new audio player in `MAudioBody` * refactor: remove old audio player * test(e2e): update existing tests * refactor: remove unused `DurationClock` * refactor: rename `SeekBar` into `LegacySeekBar`
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021-2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ReactNode } from "react";
|
||||
|
||||
import PlayPauseButton from "./PlayPauseButton";
|
||||
import { formatBytes } from "../../../utils/FormattingUtils";
|
||||
import DurationClock from "./DurationClock";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SeekBar from "./SeekBar";
|
||||
import PlaybackClock from "./PlaybackClock";
|
||||
import AudioPlayerBase from "./AudioPlayerBase";
|
||||
import { PlaybackState } from "../../../audio/Playback";
|
||||
|
||||
export default class AudioPlayer extends AudioPlayerBase {
|
||||
protected renderFileSize(): string | null {
|
||||
const bytes = this.props.playback.sizeBytes;
|
||||
if (!bytes) return null;
|
||||
|
||||
// Not translated here - we're just presenting the data which should already
|
||||
// be translated if needed.
|
||||
return `(${formatBytes(bytes)})`;
|
||||
}
|
||||
|
||||
protected renderComponent(): ReactNode {
|
||||
// tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard
|
||||
// events for accessibility
|
||||
return (
|
||||
<div className="mx_MediaBody mx_AudioPlayer_container" tabIndex={0} onKeyDown={this.onKeyDown}>
|
||||
<div className="mx_AudioPlayer_primaryContainer">
|
||||
<PlayPauseButton
|
||||
playback={this.props.playback}
|
||||
playbackPhase={this.state.playbackPhase}
|
||||
tabIndex={-1} // prevent tabbing into the button
|
||||
ref={this.playPauseRef}
|
||||
/>
|
||||
<div className="mx_AudioPlayer_mediaInfo">
|
||||
<span className="mx_AudioPlayer_mediaName">
|
||||
{this.props.mediaName || _t("timeline|m.audio|unnamed_audio")}
|
||||
</span>
|
||||
<div className="mx_AudioPlayer_byline">
|
||||
<DurationClock playback={this.props.playback} />
|
||||
{/* easiest way to introduce a gap between the components */}
|
||||
{this.renderFileSize()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_AudioPlayer_seek">
|
||||
<SeekBar
|
||||
playback={this.props.playback}
|
||||
tabIndex={-1} // prevent tabbing into the bar
|
||||
disabled={this.state.playbackPhase === PlaybackState.Decoding}
|
||||
ref={this.seekRef}
|
||||
/>
|
||||
<PlaybackClock playback={this.props.playback} defaultDisplaySeconds={0} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import type SeekBar from "./SeekBar";
|
||||
import type LegacySeekBar from "./LegacySeekBar";
|
||||
import type PlayPauseButton from "./PlayPauseButton";
|
||||
|
||||
export interface IProps {
|
||||
@@ -31,7 +31,7 @@ interface IState {
|
||||
}
|
||||
|
||||
export default abstract class AudioPlayerBase<T extends IProps = IProps> extends React.PureComponent<T, IState> {
|
||||
protected seekRef = createRef<SeekBar>();
|
||||
protected seekRef = createRef<LegacySeekBar>();
|
||||
protected playPauseRef = createRef<PlayPauseButton>();
|
||||
|
||||
public constructor(props: T) {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { Clock } from "../../../shared-components/audio/Clock";
|
||||
import { type Playback } from "../../../audio/Playback";
|
||||
|
||||
interface IProps {
|
||||
playback: Playback;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
durationSeconds: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A clock which shows a clip's maximum duration.
|
||||
*/
|
||||
export default class DurationClock extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
// we track the duration on state because we won't really know what the clip duration
|
||||
// is until the first time update, and as a PureComponent we are trying to dedupe state
|
||||
// updates as much as possible. This is just the easiest way to avoid a forceUpdate() or
|
||||
// member property to track "did we get a duration".
|
||||
durationSeconds: this.props.playback.clockInfo.durationSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate);
|
||||
}
|
||||
|
||||
private onTimeUpdate = (time: number[]): void => {
|
||||
this.setState({ durationSeconds: time[1] });
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
return <Clock seconds={this.state.durationSeconds} />;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,10 @@ interface ISeekCSS extends CSSProperties {
|
||||
|
||||
const ARROW_SKIP_SECONDS = 5; // arbitrary
|
||||
|
||||
export default class SeekBar extends React.PureComponent<IProps, IState> {
|
||||
/**
|
||||
* @deprecated Use {@link SeekBar} instead.
|
||||
*/
|
||||
export default class LegacySeekBar extends React.PureComponent<IProps, IState> {
|
||||
// We use an animation frame request to avoid overly spamming prop updates, even if we aren't
|
||||
// really using anything demanding on the CSS front.
|
||||
|
||||
@@ -11,7 +11,7 @@ import React, { type ReactNode } from "react";
|
||||
import PlayPauseButton from "./PlayPauseButton";
|
||||
import PlaybackClock from "./PlaybackClock";
|
||||
import AudioPlayerBase, { type IProps as IAudioPlayerBaseProps } from "./AudioPlayerBase";
|
||||
import SeekBar from "./SeekBar";
|
||||
import LegacySeekBar from "./LegacySeekBar";
|
||||
import PlaybackWaveform from "./PlaybackWaveform";
|
||||
import { PlaybackState } from "../../../audio/Playback";
|
||||
|
||||
@@ -49,7 +49,7 @@ export default class RecordingPlayback extends AudioPlayerBase<IProps> {
|
||||
<>
|
||||
<div className="mx_RecordingPlayback_timelineLayoutMiddle">
|
||||
<PlaybackWaveform playback={this.props.playback} />
|
||||
<SeekBar
|
||||
<LegacySeekBar
|
||||
playback={this.props.playback}
|
||||
tabIndex={0} // allow keyboard users to fall into the seek bar
|
||||
disabled={this.state.playbackPhase === PlaybackState.Decoding}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { type MediaEventContent } from "matrix-js-sdk/src/types";
|
||||
import { type Playback } from "../../../audio/Playback";
|
||||
import InlineSpinner from "../elements/InlineSpinner";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AudioPlayer from "../audio_messages/AudioPlayer";
|
||||
import MFileBody from "./MFileBody";
|
||||
import { type IBodyProps } from "./IBodyProps";
|
||||
import { PlaybackManager } from "../../../audio/PlaybackManager";
|
||||
@@ -22,10 +21,13 @@ import { isVoiceMessage } from "../../../utils/EventUtils";
|
||||
import { PlaybackQueue } from "../../../audio/PlaybackQueue";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import MediaProcessingError from "./shared/MediaProcessingError";
|
||||
import { AudioPlayerViewModel } from "../../../viewmodels/audio/AudioPlayerViewModel";
|
||||
import { AudioPlayerView } from "../../../shared-components/audio/AudioPlayerView";
|
||||
|
||||
interface IState {
|
||||
error?: boolean;
|
||||
playback?: Playback;
|
||||
audioPlayerVm?: AudioPlayerViewModel;
|
||||
}
|
||||
|
||||
export default class MAudioBody extends React.PureComponent<IBodyProps, IState> {
|
||||
@@ -61,7 +63,7 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
|
||||
// We should have a buffer to work with now: let's set it up
|
||||
const playback = PlaybackManager.instance.createPlaybackInstance(buffer, waveform);
|
||||
playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent);
|
||||
this.setState({ playback });
|
||||
this.setState({ playback, audioPlayerVm: new AudioPlayerViewModel({ playback, mediaName: content.body }) });
|
||||
|
||||
if (isVoiceMessage(this.props.mxEvent)) {
|
||||
PlaybackQueue.forRoom(this.props.mxEvent.getRoomId()!).unsortedEnqueue(this.props.mxEvent, playback);
|
||||
@@ -113,7 +115,7 @@ export default class MAudioBody extends React.PureComponent<IBodyProps, IState>
|
||||
// At this point we should have a playable state
|
||||
return (
|
||||
<span className="mx_MAudioBody">
|
||||
<AudioPlayer playback={this.state.playback} mediaName={this.props.mxEvent.getContent().body} />
|
||||
{this.state.audioPlayerVm && <AudioPlayerView vm={this.state.audioPlayerVm} />}
|
||||
{this.showFileBody && <MFileBody {...this.props} showGenericPlaceholder={false} />}
|
||||
</span>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user