diff --git a/res/css/views/audio_messages/_PlaybackContainer.scss b/res/css/views/audio_messages/_PlaybackContainer.scss index c1192f188b..fd01864bba 100644 --- a/res/css/views/audio_messages/_PlaybackContainer.scss +++ b/res/css/views/audio_messages/_PlaybackContainer.scss @@ -27,9 +27,14 @@ limitations under the License. display: flex; align-items: center; + contain: content; + .mx_Waveform { .mx_Waveform_bar { background-color: $voice-record-waveform-incomplete-fg-color; + height: 100%; + /* Variable set by a JS component */ + transform: scaleY(max(0.05, var(--barHeight))); &.mx_Waveform_bar_100pct { // Small animation to remove the mechanical feel of progress diff --git a/src/components/views/audio_messages/LiveRecordingClock.tsx b/src/components/views/audio_messages/LiveRecordingClock.tsx index 41909dc201..a9dbd3c52f 100644 --- a/src/components/views/audio_messages/LiveRecordingClock.tsx +++ b/src/components/views/audio_messages/LiveRecordingClock.tsx @@ -18,6 +18,7 @@ import React from "react"; import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import Clock from "./Clock"; +import { MarkedExecution } from "../../../utils/MarkedExecution"; interface IProps { recorder: VoiceRecording; @@ -32,16 +33,31 @@ interface IState { */ @replaceableComponent("views.audio_messages.LiveRecordingClock") export default class LiveRecordingClock extends React.PureComponent { - public constructor(props) { - super(props); + private seconds = 0; + private scheduledUpdate = new MarkedExecution( + () => this.updateClock(), + () => requestAnimationFrame(() => this.scheduledUpdate.trigger()), + ); - this.state = {seconds: 0}; - this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); + constructor(props) { + super(props); + this.state = { + seconds: 0, + }; } - private onRecordingUpdate = (update: IRecordingUpdate) => { - this.setState({seconds: update.timeSeconds}); - }; + componentDidMount() { + this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => { + this.seconds = update.timeSeconds; + this.scheduledUpdate.mark(); + }); + } + + private updateClock() { + this.setState({ + seconds: this.seconds, + }); + } public render() { return ; diff --git a/src/components/views/audio_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx index 27d165e613..6e88346cae 100644 --- a/src/components/views/audio_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/audio_messages/LiveRecordingWaveform.tsx @@ -20,13 +20,14 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { arrayFastResample, arraySeed } from "../../../utils/arrays"; import { percentageOf } from "../../../utils/numbers"; import Waveform from "./Waveform"; +import { MarkedExecution } from "../../../utils/MarkedExecution"; interface IProps { recorder: VoiceRecording; } interface IState { - heights: number[]; + waveform: number[]; } /** @@ -34,27 +35,35 @@ interface IState { */ @replaceableComponent("views.audio_messages.LiveRecordingWaveform") export default class LiveRecordingWaveform extends React.PureComponent { - public constructor(props) { - super(props); - - this.state = {heights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES)}; - this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); - } - - private onRecordingUpdate = (update: IRecordingUpdate) => { - // The waveform and the downsample target are pretty close, so we should be fine to - // do this, despite the docs on arrayFastResample. - const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES); - this.setState({ - // The incoming data is between zero and one, but typically even screaming into a - // microphone won't send you over 0.6, so we artificially adjust the gain for the - // waveform. This results in a slightly more cinematic/animated waveform for the - // user. - heights: bars.map(b => percentageOf(b, 0, 0.50)), - }); + public static defaultProps = { + progress: 1, }; + private waveform: number[] = []; + private scheduledUpdate = new MarkedExecution( + () => this.updateWaveform(), + () => requestAnimationFrame(() => this.scheduledUpdate.trigger()), + ); + + constructor(props) { + super(props); + this.state = { + waveform: [], + }; + } + + componentDidMount() { + this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => { + this.waveform = update.waveform; + this.scheduledUpdate.mark(); + }); + } + + private updateWaveform() { + this.setState({ waveform: this.waveform }); + } + public render() { - return ; + return ; } } diff --git a/src/components/views/audio_messages/Waveform.tsx b/src/components/views/audio_messages/Waveform.tsx index dc7551025e..3b7a881754 100644 --- a/src/components/views/audio_messages/Waveform.tsx +++ b/src/components/views/audio_messages/Waveform.tsx @@ -17,6 +17,11 @@ limitations under the License. import React from "react"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import classNames from "classnames"; +import { CSSProperties } from "react"; + +interface WaveformCSSProperties extends CSSProperties { + '--barHeight': number; +} interface IProps { relHeights: number[]; // relative heights (0-1) @@ -40,10 +45,6 @@ export default class Waveform extends React.PureComponent { progress: 1, }; - public constructor(props) { - super(props); - } - public render() { return
{this.props.relHeights.map((h, i) => { @@ -53,7 +54,9 @@ export default class Waveform extends React.PureComponent { 'mx_Waveform_bar': true, 'mx_Waveform_bar_100pct': isCompleteBar, }); - return ; + return ; })}
; } diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index c5a196c377..4e8f258ee8 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -18,22 +18,18 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { _t } from "../../../languageHandler"; import React, {ReactNode} from "react"; import { - IRecordingUpdate, - RECORDING_PLAYBACK_SAMPLES, RecordingState, VoiceRecording, } from "../../../voice/VoiceRecording"; import {Room} from "matrix-js-sdk/src/models/room"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import classNames from "classnames"; -import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform"; +import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { arrayFastResample, arraySeed } from "../../../utils/arrays"; -import { percentageOf } from "../../../utils/numbers"; -import LiveRecordingClock from "../voice_messages/LiveRecordingClock"; +import LiveRecordingClock from "../audio_messages/LiveRecordingClock"; import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; -import RecordingPlayback from "../voice_messages/RecordingPlayback"; +import RecordingPlayback from "../audio_messages/RecordingPlayback"; import { MsgType } from "matrix-js-sdk/src/@types/event"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; @@ -46,8 +42,6 @@ interface IProps { interface IState { recorder?: VoiceRecording; recordingPhase?: RecordingState; - relHeights: number[]; - seconds: number; } /** @@ -55,58 +49,18 @@ interface IState { */ @replaceableComponent("views.rooms.VoiceRecordComposerTile") export default class VoiceRecordComposerTile extends React.PureComponent { - private waveform: number[] = []; - private seconds = 0; - private scheduledAnimationFrame = false; - public constructor(props) { super(props); this.state = { recorder: null, // no recording started by default - seconds: 0, - relHeights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES), }; } - public componentDidUpdate(prevProps, prevState) { - if (!prevState.recorder && this.state.recorder) { - this.state.recorder.liveData.onUpdate(this.onRecordingUpdate); - } - } - public async componentWillUnmount() { await VoiceRecordingStore.instance.disposeRecording(); } - private onRecordingUpdate = (update: IRecordingUpdate): void => { - this.waveform = update.waveform; - this.seconds = update.timeSeconds; - - if (this.scheduledAnimationFrame) { - return; - } - - this.scheduledAnimationFrame = true; - // The audio recorder flushes data faster than the screen refresh rate - // Using requestAnimationFrame makes sure that we only flush the data - // to react once per tick to avoid unneeded work. - requestAnimationFrame(() => { - // The waveform and the downsample target are pretty close, so we should be fine to - // do this, despite the docs on arrayFastResample. - const bars = arrayFastResample(Array.from(this.waveform), RECORDING_PLAYBACK_SAMPLES); - this.setState({ - // The incoming data is between zero and one, but typically even screaming into a - // microphone won't send you over 0.6, so we artificially adjust the gain for the - // waveform. This results in a slightly more cinematic/animated waveform for the - // user. - relHeights: bars.map(b => percentageOf(b, 0, 0.50)), - seconds: this.seconds, - }); - this.scheduledAnimationFrame = false; - }); - } - // called by composer public async send() { if (!this.state.recorder) {