Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.
49 changes: 45 additions & 4 deletions res/css/views/audio_messages/_PlaybackContainer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,57 @@ limitations under the License.
padding-left: 8px; // isolate from recording circle / play control
}

// For timeline-rendered playback, mirror the values for where the clock is in
// the waveform version.
.mx_SeekBar {
.mx_RecordingPlayback_timelineLayoutMiddle {
margin-left: 8px;
margin-right: 6px;
position: relative;
display: inline-block;
flex: 1;
height: 30px; // same height as mx_Waveform, needed for automatic vertical centering

.mx_Waveform {
position: absolute;
left: 0;
top: 0;
}

.mx_SeekBar {
position: absolute;
left: 0;
height: 30px;
top: -2px; // visually vertically centered

// Hide the hairline progress bar since we're at 100% height. Need to have distinct rules
// because CSS is weird.
background: none;
&::before {
background: none;
}
&::-moz-range-progress {
background: none;
}

// Make the thumb easier to see. Like the SeekBar original styles, these need to be
// distinct. We make it transparent so it doesn't show up on the UI, but also larger
// so it's easier to grab by mouse users in some browsers. Most browsers let the user
// move and drag the thumb regardless of hitting the thumb, however.
&::-webkit-slider-thumb {
width: 10px;
height: 10px;
background-color: transparent;
}
&::-moz-range-thumb {
width: 10px;
height: 10px;
background-color: transparent;
}
}

// For timeline-rendered playback, the clock is on the other side of the waveform.
& + .mx_Clock {
text-align: right;

// Take the padding off the clock because it's accounted for in the seek bar
// Take the padding off the clock because it's accounted for by the `timelineLayoutMiddle`
padding: 0;
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/components/views/audio_messages/Clock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import React, { HTMLProps } from "react";

import { formatSeconds } from "../../../DateUtils";

export interface IProps extends Pick<HTMLProps<HTMLSpanElement>, "aria-live"> {
interface IProps extends Pick<HTMLProps<HTMLSpanElement>, "aria-live" | "role"> {
seconds: number;
}

Expand All @@ -31,14 +31,14 @@ export default class Clock extends React.Component<IProps> {
super(props);
}

shouldComponentUpdate(nextProps: Readonly<IProps>): boolean {
public shouldComponentUpdate(nextProps: Readonly<IProps>): boolean {
const currentFloor = Math.floor(this.props.seconds);
const nextFloor = Math.floor(nextProps.seconds);
return currentFloor !== nextFloor;
}

public render() {
return <span aria-live={this.props["aria-live"]} className='mx_Clock'>
return <span aria-live={this.props["aria-live"]} role={this.props.role} className='mx_Clock'>
{ formatSeconds(this.props.seconds) }
</span>;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/audio_messages/PlaybackClock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export default class PlaybackClock extends React.PureComponent<IProps, IState> {
}
return <Clock
seconds={seconds}
aria-live={this.state.playbackPhase === PlaybackState.Playing ? "off" : undefined}
role="timer"
/>;
}
}
47 changes: 35 additions & 12 deletions src/components/views/audio_messages/RecordingPlayback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,45 +22,68 @@ import AudioPlayerBase, { IProps as IAudioPlayerBaseProps } from "./AudioPlayerB
import SeekBar from "./SeekBar";
import PlaybackWaveform from "./PlaybackWaveform";

interface IProps extends IAudioPlayerBaseProps {
export enum PlaybackLayout {
/**
* Clock on the left side of a waveform, without seek bar.
*/
Composer,

/**
* When true, use a waveform instead of a seek bar
* Clock on the right side of a waveform, with an added seek bar.
*/
withWaveform?: boolean;
Timeline,
}

interface IProps extends IAudioPlayerBaseProps {
layout?: PlaybackLayout; // Defaults to Timeline layout
}

export default class RecordingPlayback extends AudioPlayerBase<IProps> {
// This component is rendered in two ways: the composer and timeline. They have different
// rendering properties (specifically the difference of a waveform or not).

private renderWaveformLook(): ReactNode {
private renderComposerLook(): ReactNode {
return <>
<PlaybackClock playback={this.props.playback} />
<PlaybackWaveform playback={this.props.playback} />
</>;
}

private renderSeekableLook(): ReactNode {
private renderTimelineLook(): ReactNode {
return <>
<SeekBar
playback={this.props.playback}
tabIndex={-1} // prevent tabbing into the bar
playbackPhase={this.state.playbackPhase}
ref={this.seekRef}
/>
<div className="mx_RecordingPlayback_timelineLayoutMiddle">
<PlaybackWaveform playback={this.props.playback} />
<SeekBar
playback={this.props.playback}
tabIndex={0} // allow keyboard users to fall into the seek bar
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the default value ftr, both for inputs and for SeekBar's defaultProps

playbackPhase={this.state.playbackPhase}
ref={this.seekRef}
/>
</div>
<PlaybackClock playback={this.props.playback} />
</>;
}

protected renderComponent(): ReactNode {
let body: ReactNode;
switch (this.props.layout) {
case PlaybackLayout.Composer:
body = this.renderComposerLook();
break;
case PlaybackLayout.Timeline: // default is timeline, fall through.
default:
body = this.renderTimelineLook();
break;
}

return (
<div className="mx_MediaBody mx_VoiceMessagePrimaryContainer" onKeyDown={this.onKeyDown}>
<PlayPauseButton
playback={this.props.playback}
playbackPhase={this.state.playbackPhase}
ref={this.playPauseRef}
/>
{ this.props.withWaveform ? this.renderWaveformLook() : this.renderSeekableLook() }
{ body }
</div>
);
}
Expand Down
7 changes: 5 additions & 2 deletions src/components/views/rooms/VoiceRecordComposerTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform";
import LiveRecordingClock from "../audio_messages/LiveRecordingClock";
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import RecordingPlayback from "../audio_messages/RecordingPlayback";
import RecordingPlayback, { PlaybackLayout } from "../audio_messages/RecordingPlayback";
import Modal from "../../../Modal";
import ErrorDialog from "../dialogs/ErrorDialog";
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler";
Expand Down Expand Up @@ -231,7 +231,10 @@ 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.recordingPhase !== RecordingState.Started) {
return <RecordingPlayback playback={this.state.recorder.getPlayback()} withWaveform={true} />;
return <RecordingPlayback
playback={this.state.recorder.getPlayback()}
layout={PlaybackLayout.Composer}
/>;
}

// only other UI is the recording-in-progress UI
Expand Down
38 changes: 27 additions & 11 deletions test/components/views/audio_messages/RecordingPlayback-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ import { mocked } from 'jest-mock';
import { logger } from 'matrix-js-sdk/src/logger';
import { act } from 'react-dom/test-utils';

import RecordingPlayback from '../../../../src/components/views/audio_messages/RecordingPlayback';
import RecordingPlayback, { PlaybackLayout } from '../../../../src/components/views/audio_messages/RecordingPlayback';
import { Playback } from '../../../../src/audio/Playback';
import RoomContext, { TimelineRenderingType } from '../../../../src/contexts/RoomContext';
import { createAudioContext } from '../../../../src/audio/compat';
import { findByTestId, flushPromises } from '../../../test-utils';
import PlaybackWaveform from '../../../../src/components/views/audio_messages/PlaybackWaveform';
import SeekBar from "../../../../src/components/views/audio_messages/SeekBar";
import PlaybackClock from "../../../../src/components/views/audio_messages/PlaybackClock";

jest.mock('../../../../src/audio/compat', () => ({
createAudioContext: jest.fn(),
Expand Down Expand Up @@ -128,19 +129,34 @@ describe('<RecordingPlayback />', () => {
expect(playback.toggle).toHaveBeenCalled();
});

it('should render a seek bar by default', () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback });
describe('Composer Layout', () => {
it('should have a waveform, no seek bar, and clock', () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback, layout: PlaybackLayout.Composer });

expect(component.find(PlaybackWaveform).length).toBeFalsy();
expect(component.find(SeekBar).length).toBeTruthy();
expect(component.find(PlaybackClock).length).toBeTruthy();
expect(component.find(PlaybackWaveform).length).toBeTruthy();
expect(component.find(SeekBar).length).toBeFalsy();
});
});

it('should render a waveform when requested', () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback, withWaveform: true });
describe('Timeline Layout', () => {
it('should have a waveform, a seek bar, and clock', () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback, layout: PlaybackLayout.Timeline });

expect(component.find(PlaybackWaveform).length).toBeTruthy();
expect(component.find(SeekBar).length).toBeFalsy();
expect(component.find(PlaybackClock).length).toBeTruthy();
expect(component.find(PlaybackWaveform).length).toBeTruthy();
expect(component.find(SeekBar).length).toBeTruthy();
});

it('should be the default', () => {
const playback = new Playback(new ArrayBuffer(8));
const component = getComponent({ playback }); // no layout set for test

expect(component.find(PlaybackClock).length).toBeTruthy();
expect(component.find(PlaybackWaveform).length).toBeTruthy();
expect(component.find(SeekBar).length).toBeTruthy();
});
});
});