Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 7ea17ae

Browse files
authored
Merge pull request #6386 from matrix-org/travis/voice-messages/download
Move download button for media to the action bar
2 parents 4a87730 + 7892539 commit 7ea17ae

22 files changed

+591
-375
lines changed

res/css/views/messages/_MessageActionBar.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,12 @@ limitations under the License.
107107
.mx_MessageActionBar_cancelButton::after {
108108
mask-image: url('$(res)/img/element-icons/trashcan.svg');
109109
}
110+
111+
.mx_MessageActionBar_downloadButton::after {
112+
mask-size: 14px;
113+
mask-image: url('$(res)/img/download.svg');
114+
}
115+
116+
.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after {
117+
background-color: transparent; // hide the download icon mask
118+
}

src/@types/common.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { JSXElementConstructor } from "react";
17+
import React, { JSXElementConstructor } from "react";
1818

1919
// Based on https://stackoverflow.com/a/53229857/3532235
2020
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>]?: never};
2121
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
2222
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
2323

2424
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
25+
export type ReactAnyComponent = React.Component | React.ExoticComponent;

src/components/views/context_menus/MessageContextMenu.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,15 @@ export function canCancel(eventStatus: EventStatus): boolean {
4343
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
4444
}
4545

46-
interface IEventTileOps {
46+
export interface IEventTileOps {
4747
isWidgetHidden(): boolean;
4848
unhideWidget(): void;
4949
}
5050

51+
export interface IOperableEventTile {
52+
getEventTileOps(): IEventTileOps;
53+
}
54+
5155
interface IProps {
5256
/* the MatrixEvent associated with the context menu */
5357
mxEvent: MatrixEvent;
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixEvent } from "matrix-js-sdk/src";
18+
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
19+
import React, { createRef } from "react";
20+
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
21+
import Spinner from "../elements/Spinner";
22+
import classNames from "classnames";
23+
import { _t } from "../../../languageHandler";
24+
import { replaceableComponent } from "../../../utils/replaceableComponent";
25+
26+
interface IProps {
27+
mxEvent: MatrixEvent;
28+
29+
// XXX: It can take a cycle or two for the MessageActionBar to have all the props/setup
30+
// required to get us a MediaEventHelper, so we use a getter function instead to prod for
31+
// one.
32+
mediaEventHelperGet: () => MediaEventHelper;
33+
}
34+
35+
interface IState {
36+
loading: boolean;
37+
blob?: Blob;
38+
}
39+
40+
@replaceableComponent("views.messages.DownloadActionButton")
41+
export default class DownloadActionButton extends React.PureComponent<IProps, IState> {
42+
private iframe: React.RefObject<HTMLIFrameElement> = createRef();
43+
44+
public constructor(props: IProps) {
45+
super(props);
46+
47+
this.state = {
48+
loading: false,
49+
};
50+
}
51+
52+
private onDownloadClick = async () => {
53+
if (this.state.loading) return;
54+
55+
this.setState({ loading: true });
56+
57+
if (this.state.blob) {
58+
// Cheat and trigger a download, again.
59+
return this.onFrameLoad();
60+
}
61+
62+
const blob = await this.props.mediaEventHelperGet().sourceBlob.value;
63+
this.setState({ blob });
64+
};
65+
66+
private onFrameLoad = () => {
67+
this.setState({ loading: false });
68+
69+
// we aren't showing the iframe, so we can send over the bare minimum styles and such.
70+
this.iframe.current.contentWindow.postMessage({
71+
imgSrc: "", // no image
72+
imgStyle: null,
73+
style: "",
74+
blob: this.state.blob,
75+
download: this.props.mediaEventHelperGet().fileName,
76+
textContent: "",
77+
auto: true, // autodownload
78+
}, '*');
79+
};
80+
81+
public render() {
82+
let spinner: JSX.Element;
83+
if (this.state.loading) {
84+
spinner = <Spinner w={18} h={18} />;
85+
}
86+
87+
const classes = classNames({
88+
'mx_MessageActionBar_maskButton': true,
89+
'mx_MessageActionBar_downloadButton': true,
90+
'mx_MessageActionBar_downloadSpinnerButton': !!spinner,
91+
});
92+
93+
return <RovingAccessibleTooltipButton
94+
className={classes}
95+
title={spinner ? _t("Downloading") : _t("Download")}
96+
onClick={this.onDownloadClick}
97+
disabled={!!spinner}
98+
>
99+
{ spinner }
100+
{ this.state.blob && <iframe
101+
src="usercontent/" // XXX: Like MFileBody, this should come from the skin
102+
ref={this.iframe}
103+
onLoad={this.onFrameLoad}
104+
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation"
105+
style={{ display: "none" }}
106+
/> }
107+
</RovingAccessibleTooltipButton>;
108+
}
109+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixEvent } from "matrix-js-sdk/src";
18+
import { TileShape } from "../rooms/EventTile";
19+
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
20+
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
21+
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
22+
23+
export interface IBodyProps {
24+
mxEvent: MatrixEvent;
25+
26+
/* a list of words to highlight */
27+
highlights: string[];
28+
29+
/* link URL for the highlights */
30+
highlightLink: string;
31+
32+
/* callback called when dynamic content in events are loaded */
33+
onHeightChanged: () => void;
34+
35+
showUrlPreview?: boolean;
36+
tileShape: TileShape;
37+
maxImageHeight?: number;
38+
replacingEventId?: string;
39+
editState?: EditorStateTransfer;
40+
onMessageAllowed: () => void; // TODO: Docs
41+
permalinkCreator: RoomPermalinkCreator;
42+
mediaEventHelper: MediaEventHelper;
43+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Copyright 2021 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
18+
19+
export interface IMediaBody {
20+
getMediaHelper(): MediaEventHelper;
21+
}

src/components/views/messages/MAudioBody.tsx

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,64 +15,58 @@ limitations under the License.
1515
*/
1616

1717
import React from "react";
18-
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
1918
import { replaceableComponent } from "../../../utils/replaceableComponent";
2019
import { Playback } from "../../../voice/Playback";
21-
import MFileBody from "./MFileBody";
2220
import InlineSpinner from '../elements/InlineSpinner';
2321
import { _t } from "../../../languageHandler";
24-
import { mediaFromContent } from "../../../customisations/Media";
25-
import { decryptFile } from "../../../utils/DecryptFile";
26-
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
2722
import AudioPlayer from "../audio_messages/AudioPlayer";
28-
29-
interface IProps {
30-
mxEvent: MatrixEvent;
31-
}
23+
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
24+
import MFileBody from "./MFileBody";
25+
import { IBodyProps } from "./IBodyProps";
3226

3327
interface IState {
3428
error?: Error;
3529
playback?: Playback;
36-
decryptedBlob?: Blob;
3730
}
3831

3932
@replaceableComponent("views.messages.MAudioBody")
40-
export default class MAudioBody extends React.PureComponent<IProps, IState> {
41-
constructor(props: IProps) {
33+
export default class MAudioBody extends React.PureComponent<IBodyProps, IState> {
34+
constructor(props: IBodyProps) {
4235
super(props);
4336

4437
this.state = {};
4538
}
4639

4740
public async componentDidMount() {
4841
let buffer: ArrayBuffer;
49-
const content: IMediaEventContent = this.props.mxEvent.getContent();
50-
const media = mediaFromContent(content);
51-
if (media.isEncrypted) {
42+
43+
try {
5244
try {
53-
const blob = await decryptFile(content.file);
45+
const blob = await this.props.mediaEventHelper.sourceBlob.value;
5446
buffer = await blob.arrayBuffer();
55-
this.setState({ decryptedBlob: blob });
5647
} catch (e) {
5748
this.setState({ error: e });
5849
console.warn("Unable to decrypt audio message", e);
5950
return; // stop processing the audio file
6051
}
61-
} else {
62-
try {
63-
buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer());
64-
} catch (e) {
65-
this.setState({ error: e });
66-
console.warn("Unable to download audio message", e);
67-
return; // stop processing the audio file
68-
}
52+
} catch (e) {
53+
this.setState({ error: e });
54+
console.warn("Unable to decrypt/download audio message", e);
55+
return; // stop processing the audio file
6956
}
7057

7158
// We should have a buffer to work with now: let's set it up
72-
const playback = new Playback(buffer);
59+
60+
// Note: we don't actually need a waveform to render an audio event, but voice messages do.
61+
const content = this.props.mxEvent.getContent<IMediaEventContent>();
62+
const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024);
63+
64+
// We should have a buffer to work with now: let's set it up
65+
const playback = new Playback(buffer, waveform);
7366
playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent);
7467
this.setState({ playback });
75-
// Note: the RecordingPlayback component will handle preparing the Playback class for us.
68+
69+
// Note: the components later on will handle preparing the Playback class for us.
7670
}
7771

7872
public componentWillUnmount() {
@@ -103,7 +97,7 @@ export default class MAudioBody extends React.PureComponent<IProps, IState> {
10397
return (
10498
<span className="mx_MAudioBody">
10599
<AudioPlayer playback={this.state.playback} mediaName={this.props.mxEvent.getContent().body} />
106-
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
100+
{ this.props.tileShape && <MFileBody {...this.props} showGenericPlaceholder={false} /> }
107101
</span>
108102
);
109103
}

0 commit comments

Comments
 (0)