diff --git a/src/components/views/messages/MAudioBody.js b/src/components/views/messages/MAudioBody.js
deleted file mode 100644
index 0d5e449fc0..0000000000
--- a/src/components/views/messages/MAudioBody.js
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- Copyright 2016 OpenMarket Ltd
-
- 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 React from 'react';
-import MFileBody from './MFileBody';
-
-import { decryptFile } from '../../../utils/DecryptFile';
-import { _t } from '../../../languageHandler';
-import InlineSpinner from '../elements/InlineSpinner';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-import {mediaFromContent} from "../../../customisations/Media";
-
-@replaceableComponent("views.messages.MAudioBody")
-export default class MAudioBody extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- playing: false,
- decryptedUrl: null,
- decryptedBlob: null,
- error: null,
- };
- }
- onPlayToggle() {
- this.setState({
- playing: !this.state.playing,
- });
- }
-
- _getContentUrl() {
- const media = mediaFromContent(this.props.mxEvent.getContent());
- if (media.isEncrypted) {
- return this.state.decryptedUrl;
- } else {
- return media.srcHttp;
- }
- }
-
- componentDidMount() {
- const content = this.props.mxEvent.getContent();
- if (content.file !== undefined && this.state.decryptedUrl === null) {
- let decryptedBlob;
- decryptFile(content.file).then(function(blob) {
- decryptedBlob = blob;
- return URL.createObjectURL(decryptedBlob);
- }).then((url) => {
- this.setState({
- decryptedUrl: url,
- decryptedBlob: decryptedBlob,
- });
- }, (err) => {
- console.warn("Unable to decrypt attachment: ", err);
- this.setState({
- error: err,
- });
- });
- }
- }
-
- componentWillUnmount() {
- if (this.state.decryptedUrl) {
- URL.revokeObjectURL(this.state.decryptedUrl);
- }
- }
-
- render() {
- const content = this.props.mxEvent.getContent();
-
- if (this.state.error !== null) {
- return (
-
-
- { _t("Error decrypting audio") }
-
- );
- }
-
- if (content.file !== undefined && this.state.decryptedUrl === null) {
- // Need to decrypt the attachment
- // The attachment is decrypted in componentDidMount.
- // For now add an img tag with a 16x16 spinner.
- // Not sure how tall the audio player is so not sure how tall it should actually be.
- return (
-
-
-
- );
- }
-
- const contentUrl = this._getContentUrl();
-
- return (
-
-
-
-
- );
- }
-}
diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx
new file mode 100644
index 0000000000..9e77bc0893
--- /dev/null
+++ b/src/components/views/messages/MAudioBody.tsx
@@ -0,0 +1,111 @@
+/*
+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 React from "react";
+import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+import {replaceableComponent} from "../../../utils/replaceableComponent";
+import {Playback} from "../../../voice/Playback";
+import MFileBody from "./MFileBody";
+import InlineSpinner from '../elements/InlineSpinner';
+import {_t} from "../../../languageHandler";
+import {mediaFromContent} from "../../../customisations/Media";
+import {decryptFile} from "../../../utils/DecryptFile";
+import RecordingPlayback from "../voice_messages/RecordingPlayback";
+import {IMediaEventContent} from "../../../customisations/models/IMediaEventContent";
+
+interface IProps {
+ mxEvent: MatrixEvent;
+}
+
+interface IState {
+ error?: Error;
+ playback?: Playback;
+ decryptedBlob?: Blob;
+}
+
+@replaceableComponent("views.messages.MAudioBody")
+export default class MAudioBody extends React.PureComponent {
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {};
+ }
+
+ public async componentDidMount() {
+ let buffer: ArrayBuffer;
+ const content: IMediaEventContent = this.props.mxEvent.getContent();
+ const media = mediaFromContent(content);
+ if (media.isEncrypted) {
+ try {
+ const blob = await decryptFile(content.file);
+ buffer = await blob.arrayBuffer();
+ this.setState({decryptedBlob: blob});
+ } catch (e) {
+ this.setState({error: e});
+ console.warn("Unable to decrypt voice message", e);
+ return; // stop processing the audio file
+ }
+ } else {
+ try {
+ buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer());
+ } catch (e) {
+ this.setState({error: e});
+ console.warn("Unable to download voice message", e);
+ return; // stop processing the audio file
+ }
+ }
+
+ const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024);
+
+ // We should have a buffer to work with now: let's set it up
+ const playback = new Playback(buffer, waveform);
+ this.setState({ playback });
+ // Note: the RecordingPlayback component will handle preparing the Playback class for us.
+ }
+
+ public componentWillUnmount() {
+ this.state.playback?.destroy();
+ }
+
+ public render() {
+ if (this.state.error) {
+ // TODO: @@TR: Verify error state
+ return (
+
+
+ { _t("Error processing voice message") }
+
+ );
+ }
+
+ if (!this.state.playback) {
+ // TODO: @@TR: Verify loading/decrypting state
+ return (
+
+
+
+ );
+ }
+
+ // At this point we should have a playable state
+ return (
+
+
+
+
+ )
+ }
+}