From bde2f757aa1072f7874b943245c58e3799225850 Mon Sep 17 00:00:00 2001 From: Travis Ralston <travisr@matrix.org> Date: Thu, 29 Jul 2021 15:36:50 -0600 Subject: [PATCH 1/7] Unify and improve download interactions With help from Palid. This does two major things: 1. Makes the tile part of a file body clickable to trigger a download. 2. Refactors a lot of the recyclable code out of the DownloadActionButton in favour of a utility. It's not a perfect refactoring, but it sets the stage for future work in the area (if needed). The refactoring still has a heavy reliance on being supplied an iframe, but simplifies the DownloadActionButton and a hair of the MFileBody download code. In future, we'd probably want to make the iframe completely managed by the FileDownloader rather than have it only sometimes manage a hidden iframe. --- res/css/views/messages/_MFileBody.scss | 4 +- .../views/messages/DownloadActionButton.tsx | 34 ++--- src/components/views/messages/MFileBody.tsx | 121 +++++++++++------- src/utils/FileDownloader.ts | 104 +++++++++++++++ 4 files changed, 196 insertions(+), 67 deletions(-) create mode 100644 src/utils/FileDownloader.ts diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index 403f671673..d941a8132f 100644 --- a/res/css/views/messages/_MFileBody.scss +++ b/res/css/views/messages/_MFileBody.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016, 2021 The Matrix.org Foundation C.I.C. +Copyright 2015 - 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. @@ -60,6 +60,8 @@ limitations under the License. } .mx_MFileBody_info { + cursor: pointer; + .mx_MFileBody_info_icon { background-color: $message-body-panel-icon-bg-color; border-radius: 20px; diff --git a/src/components/views/messages/DownloadActionButton.tsx b/src/components/views/messages/DownloadActionButton.tsx index 2bdae04eda..262783a867 100644 --- a/src/components/views/messages/DownloadActionButton.tsx +++ b/src/components/views/messages/DownloadActionButton.tsx @@ -16,12 +16,13 @@ limitations under the License. import { MatrixEvent } from "matrix-js-sdk/src"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; -import React, { createRef } from "react"; +import React from "react"; import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; import Spinner from "../elements/Spinner"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { FileDownloader } from "../../../utils/FileDownloader"; interface IProps { mxEvent: MatrixEvent; @@ -39,7 +40,7 @@ interface IState { @replaceableComponent("views.messages.DownloadActionButton") export default class DownloadActionButton extends React.PureComponent<IProps, IState> { - private iframe: React.RefObject<HTMLIFrameElement> = createRef(); + private downloader = new FileDownloader(); public constructor(props: IProps) { super(props); @@ -56,27 +57,21 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS if (this.state.blob) { // Cheat and trigger a download, again. - return this.onFrameLoad(); + return this.doDownload(); } const blob = await this.props.mediaEventHelperGet().sourceBlob.value; this.setState({ blob }); + await this.doDownload(); }; - private onFrameLoad = () => { - this.setState({ loading: false }); - - // we aren't showing the iframe, so we can send over the bare minimum styles and such. - this.iframe.current.contentWindow.postMessage({ - imgSrc: "", // no image - imgStyle: null, - style: "", + private async doDownload() { + await this.downloader.download({ blob: this.state.blob, - download: this.props.mediaEventHelperGet().fileName, - textContent: "", - auto: true, // autodownload - }, '*'); - }; + name: this.props.mediaEventHelperGet().fileName, + }); + this.setState({ loading: false }); + } public render() { let spinner: JSX.Element; @@ -97,13 +92,6 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS disabled={!!spinner} > { spinner } - { this.state.blob && <iframe - src="usercontent/" // XXX: Like MFileBody, this should come from the skin - ref={this.iframe} - onLoad={this.onFrameLoad} - sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" - style={{ display: "none" }} - /> } </RovingAccessibleTooltipButton>; } } diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 116fd8bd43..4de4c4c804 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -26,6 +26,7 @@ import { TileShape } from "../rooms/EventTile"; import { presentableTextForFile } from "../../../utils/FileUtils"; import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import { IBodyProps } from "./IBodyProps"; +import { FileDownloader } from "../../../utils/FileDownloader"; export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on @@ -111,6 +112,7 @@ export default class MFileBody extends React.Component<IProps, IState> { private iframe: React.RefObject<HTMLIFrameElement> = createRef(); private dummyLink: React.RefObject<HTMLAnchorElement> = createRef(); private userDidClick = false; + private fileDownloader: FileDownloader = new FileDownloader(() => this.iframe.current) public constructor(props: IProps) { super(props); @@ -118,6 +120,32 @@ export default class MFileBody extends React.Component<IProps, IState> { this.state = {}; } + private get content(): IMediaEventContent { + return this.props.mxEvent.getContent<IMediaEventContent>(); + } + + private get fileName(): string { + return this.content.body && this.content.body.length > 0 ? this.content.body : _t("Attachment"); + } + + private get linkText(): string { + return presentableTextForFile(this.content); + } + + private downloadFile(fileName: string, text: string) { + this.fileDownloader.download({ + blob: this.state.decryptedBlob, + name: fileName, + autoDownload: this.userDidClick, + opts: { + imgSrc: DOWNLOAD_ICON_URL, + imgStyle: null, + style: computedStyle(this.dummyLink.current), + textContent: _t("Download %(text)s", { text }) + }, + }); + } + private getContentUrl(): string { const media = mediaFromContent(this.props.mxEvent.getContent()); return media.srcHttp; @@ -129,24 +157,55 @@ export default class MFileBody extends React.Component<IProps, IState> { } } + private decryptFile = async (): Promise<void> => { + if (this.state.decryptedBlob) { + return; + } + try { + this.userDidClick = true; + this.setState({ + decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, + }); + } catch (err) { + console.warn("Unable to decrypt attachment: ", err); + Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, { + title: _t("Error"), + description: _t("Error decrypting attachment"), + }); + } + }; + + private onPlaceholderClick = async () => { + const mediaHelper = this.props.mediaEventHelper; + if (mediaHelper.media.isEncrypted) { + await this.decryptFile(); + this.downloadFile(this.fileName, this.linkText); + } else { + // As a button we're missing the `download` attribute for styling reasons, so + // download with the file downloader. + this.fileDownloader.download({ + blob: await mediaHelper.sourceBlob.value, + name: this.fileName, + }); + } + }; + public render() { const content = this.props.mxEvent.getContent<IMediaEventContent>(); - const text = presentableTextForFile(content); const isEncrypted = this.props.mediaEventHelper.media.isEncrypted; - const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment"); const contentUrl = this.getContentUrl(); const fileSize = content.info ? content.info.size : null; const fileType = content.info ? content.info.mimetype : "application/octet-stream"; - let placeholder = null; + let placeholder: React.ReactNode = null; if (this.props.showGenericPlaceholder) { placeholder = ( - <div className="mx_MediaBody mx_MFileBody_info"> + <AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}> <span className="mx_MFileBody_info_icon" /> <span className="mx_MFileBody_info_filename"> { presentableTextForFile(content, _t("Attachment"), false) } </span> - </div> + </AccessibleButton> ); } @@ -157,20 +216,6 @@ export default class MFileBody extends React.Component<IProps, IState> { // Need to decrypt the attachment // Wait for the user to click on the link before downloading // and decrypting the attachment. - const decrypt = async () => { - try { - this.userDidClick = true; - this.setState({ - decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, - }); - } catch (err) { - console.warn("Unable to decrypt attachment: ", err); - Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, { - title: _t("Error"), - description: _t("Error decrypting attachment"), - }); - } - }; // This button should actually Download because usercontent/ will try to click itself // but it is not guaranteed between various browsers' settings. @@ -178,31 +223,14 @@ export default class MFileBody extends React.Component<IProps, IState> { <span className="mx_MFileBody"> { placeholder } { showDownloadLink && <div className="mx_MFileBody_download"> - <AccessibleButton onClick={decrypt}> - { _t("Decrypt %(text)s", { text: text }) } + <AccessibleButton onClick={this.decryptFile}> + { _t("Decrypt %(text)s", { text: this.linkText }) } </AccessibleButton> </div> } </span> ); } - // When the iframe loads we tell it to render a download link - const onIframeLoad = (ev) => { - ev.target.contentWindow.postMessage({ - imgSrc: DOWNLOAD_ICON_URL, - imgStyle: null, // it handles this internally for us. Useful if a downstream changes the icon. - style: computedStyle(this.dummyLink.current), - blob: this.state.decryptedBlob, - // Set a download attribute for encrypted files so that the file - // will have the correct name when the user tries to download it. - // We can't provide a Content-Disposition header like we would for HTTP. - download: fileName, - textContent: _t("Download %(text)s", { text: text }), - // only auto-download if a user triggered this iframe explicitly - auto: this.userDidClick, - }, "*"); - }; - const url = "usercontent/"; // XXX: this path should probably be passed from the skin // If the attachment is encrypted then put the link inside an iframe. @@ -218,9 +246,16 @@ export default class MFileBody extends React.Component<IProps, IState> { */ } <a ref={this.dummyLink} /> </div> + { /* + TODO: Move iframe (and dummy link) into FileDownloader. + We currently have it set up this way because of styles applied to the iframe + itself which cannot be easily handled/overridden by the FileDownloader. In + future, the download link may disappear entirely at which point it could also + be suitable to just remove this bit of code. + */ } <iframe src={url} - onLoad={onIframeLoad} + onLoad={() => this.downloadFile(this.fileName, this.linkText)} ref={this.iframe} sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" /> </div> } @@ -259,7 +294,7 @@ export default class MFileBody extends React.Component<IProps, IState> { // We have to create an anchor to download the file const tempAnchor = document.createElement('a'); - tempAnchor.download = fileName; + tempAnchor.download = this.fileName; tempAnchor.href = blobUrl; document.body.appendChild(tempAnchor); // for firefox: https://stackoverflow.com/a/32226068 tempAnchor.click(); @@ -268,7 +303,7 @@ export default class MFileBody extends React.Component<IProps, IState> { }; } else { // Else we are hoping the browser will do the right thing - downloadProps["download"] = fileName; + downloadProps["download"] = this.fileName; } return ( @@ -277,7 +312,7 @@ export default class MFileBody extends React.Component<IProps, IState> { { showDownloadLink && <div className="mx_MFileBody_download"> <a {...downloadProps}> <span className="mx_MFileBody_download_icon" /> - { _t("Download %(text)s", { text: text }) } + { _t("Download %(text)s", { text: this.linkText }) } </a> { this.props.tileShape === TileShape.FileGrid && <div className="mx_MImageBody_size"> { content.info && content.info.size ? filesize(content.info.size) : "" } @@ -286,7 +321,7 @@ export default class MFileBody extends React.Component<IProps, IState> { </span> ); } else { - const extra = text ? (': ' + text) : ''; + const extra = this.linkText ? (': ' + this.linkText) : ''; return <span className="mx_MFileBody"> { placeholder } { _t("Invalid file%(extra)s", { extra: extra }) } diff --git a/src/utils/FileDownloader.ts b/src/utils/FileDownloader.ts new file mode 100644 index 0000000000..0f329e6ada --- /dev/null +++ b/src/utils/FileDownloader.ts @@ -0,0 +1,104 @@ +/* +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. +*/ + +export type getIframeFn = () => HTMLIFrameElement; + +export const DEFAULT_STYLES = { + imgSrc: "", + imgStyle: null, // css props + style: "", + textContent: "", +} + +type DownloadOptions = { + blob: Blob, + name: string, + autoDownload?: boolean, + opts?: typeof DEFAULT_STYLES +} + +// set up the iframe as a singleton so we don't have to figure out destruction of it down the line. +let managedIframe: HTMLIFrameElement; +let onLoadPromise: Promise<void>; +function getManagedIframe(): { iframe: HTMLIFrameElement, onLoadPromise: Promise<void> } { + if (managedIframe) return { iframe: managedIframe, onLoadPromise }; + + managedIframe = document.createElement("iframe"); + + // Need to append the iframe in order for the browser to load it. + document.body.appendChild(managedIframe); + + // Dev note: the reassignment warnings are entirely incorrect here. + + // @ts-ignore + // noinspection JSConstantReassignment + managedIframe.style = { display: "none" }; + // @ts-ignore + // noinspection JSConstantReassignment + managedIframe.sandbox = "allow-scripts allow-downloads allow-downloads-without-user-activation"; + + onLoadPromise = new Promise(resolve => { + managedIframe.onload = () => { + resolve(); + }; + managedIframe.src = "usercontent/"; // XXX: Should come from the skin + }); + + return { iframe: managedIframe, onLoadPromise }; +} + +// TODO: If we decide to keep the download link behaviour, we should bring the style management into here. + +/** + * Helper to handle safe file downloads. This operates off an iframe for reasons described + * by the blob helpers. By default, this will use a hidden iframe to manage the download + * through a user content wrapper, but can be given an iframe reference if the caller needs + * additional control over the styling/position of the iframe itself. + */ +export class FileDownloader { + private onLoadPromise: Promise<void>; + + /** + * Creates a new file downloader + * @param iframeFn Function to get a pre-configured iframe. Set to null to have the downloader + * use a generic, hidden, iframe. + */ + constructor(private iframeFn: getIframeFn = null) { + } + + private get iframe(): HTMLIFrameElement { + const iframe = this.iframeFn?.(); + if (!iframe) { + const managed = getManagedIframe(); + this.onLoadPromise = managed.onLoadPromise; + return managed.iframe; + } + this.onLoadPromise = null; + return iframe; + } + + public async download({blob, name, autoDownload = true, opts = DEFAULT_STYLES}: DownloadOptions) { + const iframe = this.iframe; // get the iframe first just in case we need to await onload + console.log("@@", {blob, name, autoDownload, opts, iframe, m: iframe === managedIframe, p: this.onLoadPromise}); + if (this.onLoadPromise) await this.onLoadPromise; + iframe.contentWindow.postMessage({ + ...opts, + blob: blob, + download: name, + auto: autoDownload, + }, '*'); + } +} From 790696a4bbac5cf90925491e7b9f3e765fc329e6 Mon Sep 17 00:00:00 2001 From: Travis Ralston <travisr@matrix.org> Date: Thu, 29 Jul 2021 15:37:09 -0600 Subject: [PATCH 2/7] Change "Downloading" tooltip to "Decrypting" Fixes https://github.com/vector-im/element-web/issues/18239 --- src/components/views/messages/DownloadActionButton.tsx | 2 +- src/i18n/strings/en_EN.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/DownloadActionButton.tsx b/src/components/views/messages/DownloadActionButton.tsx index 262783a867..6dc48b0936 100644 --- a/src/components/views/messages/DownloadActionButton.tsx +++ b/src/components/views/messages/DownloadActionButton.tsx @@ -87,7 +87,7 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS return <RovingAccessibleTooltipButton className={classes} - title={spinner ? _t("Downloading") : _t("Download")} + title={spinner ? _t("Decrypting") : _t("Download")} onClick={this.onDownloadClick} disabled={!!spinner} > diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1093f478bb..a1d2ee053a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1882,7 +1882,7 @@ "Saturday": "Saturday", "Today": "Today", "Yesterday": "Yesterday", - "Downloading": "Downloading", + "Decrypting": "Decrypting", "Download": "Download", "View Source": "View Source", "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.", @@ -1897,9 +1897,9 @@ "Retry": "Retry", "Reply": "Reply", "Message Actions": "Message Actions", + "Download %(text)s": "Download %(text)s", "Error decrypting attachment": "Error decrypting attachment", "Decrypt %(text)s": "Decrypt %(text)s", - "Download %(text)s": "Download %(text)s", "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", From 15f6c6300adb81b6dd205aea5a9eefc870f4f2fb Mon Sep 17 00:00:00 2001 From: Travis Ralston <travisr@matrix.org> Date: Thu, 29 Jul 2021 15:49:23 -0600 Subject: [PATCH 3/7] Improve general style per design direction --- src/components/views/messages/MFileBody.tsx | 10 +++++++--- src/utils/FileUtils.ts | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 4de4c4c804..ccf5fe6745 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -27,6 +27,7 @@ import { presentableTextForFile } from "../../../utils/FileUtils"; import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import { IBodyProps } from "./IBodyProps"; import { FileDownloader } from "../../../utils/FileDownloader"; +import TextWithTooltip from "../elements/TextWithTooltip"; export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on @@ -202,9 +203,12 @@ export default class MFileBody extends React.Component<IProps, IState> { placeholder = ( <AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}> <span className="mx_MFileBody_info_icon" /> - <span className="mx_MFileBody_info_filename"> - { presentableTextForFile(content, _t("Attachment"), false) } - </span> + <TextWithTooltip + className="mx_MFileBody_info_filename" + tooltip={presentableTextForFile(content, _t("Attachment"), false)} + > + { presentableTextForFile(content, _t("Attachment"), true, true) } + </TextWithTooltip> </AccessibleButton> ); } diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index 355fa2135c..c83f2ed417 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -26,12 +26,14 @@ import { _t } from '../languageHandler'; * @param {IMediaEventContent} content The "content" key of the matrix event. * @param {string} fallbackText The fallback text * @param {boolean} withSize Whether to include size information. Default true. + * @param {boolean} shortened Ensure the extension of the file name is visible. Default false. * @return {string} the human readable link text for the attachment. */ export function presentableTextForFile( content: IMediaEventContent, fallbackText = _t("Attachment"), withSize = true, + shortened = false, ): string { let text = fallbackText; if (content.body && content.body.length > 0) { @@ -40,6 +42,21 @@ export function presentableTextForFile( text = content.body; } + // We shorten to 15 characters somewhat arbitrarily, and assume most files + // will have a 3 character (plus full stop) extension. The goal is to knock + // the label down to 15-25 characters, not perfect accuracy. + if (shortened && text.length > 19) { + const parts = text.split('.'); + let fileName = parts.slice(0, parts.length - 1).join('.').substring(0, 15); + const extension = parts[parts.length - 1]; + + // Trim off any full stops from the file name to avoid a case where we + // add an ellipsis that looks really funky. + fileName = fileName.replace(/\.*$/g, ''); + + text = `${fileName}...${extension}`; + } + if (content.info && content.info.size && withSize) { // If we know the size of the file then add it as human readable // string to the end of the link text so that the user knows how From ac1014ae3f046530d0b01dad2f091eb7c2670320 Mon Sep 17 00:00:00 2001 From: Travis Ralston <travisr@matrix.org> Date: Thu, 29 Jul 2021 15:55:45 -0600 Subject: [PATCH 4/7] Fix file name not ellipsizing Turns out the tooltip component doesn't copy over class names. --- src/components/views/messages/MFileBody.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index ccf5fe6745..239d3b1316 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -203,11 +203,10 @@ export default class MFileBody extends React.Component<IProps, IState> { placeholder = ( <AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}> <span className="mx_MFileBody_info_icon" /> - <TextWithTooltip - className="mx_MFileBody_info_filename" - tooltip={presentableTextForFile(content, _t("Attachment"), false)} - > - { presentableTextForFile(content, _t("Attachment"), true, true) } + <TextWithTooltip tooltip={presentableTextForFile(content, _t("Attachment"), false)}> + <span className="mx_MFileBody_info_filename"> + { presentableTextForFile(content, _t("Attachment"), true, true) } + </span> </TextWithTooltip> </AccessibleButton> ); From b1090a35b5c111fe7181cbb1071bc23b8caf16fd Mon Sep 17 00:00:00 2001 From: Travis Ralston <travisr@matrix.org> Date: Thu, 29 Jul 2021 15:57:54 -0600 Subject: [PATCH 5/7] Use new getter for content in MFileBody --- src/components/views/messages/MFileBody.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 239d3b1316..ecc60bc3e3 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -192,20 +192,19 @@ export default class MFileBody extends React.Component<IProps, IState> { }; public render() { - const content = this.props.mxEvent.getContent<IMediaEventContent>(); const isEncrypted = this.props.mediaEventHelper.media.isEncrypted; const contentUrl = this.getContentUrl(); - const fileSize = content.info ? content.info.size : null; - const fileType = content.info ? content.info.mimetype : "application/octet-stream"; + const fileSize = this.content.info ? this.content.info.size : null; + const fileType = this.content.info ? this.content.info.mimetype : "application/octet-stream"; let placeholder: React.ReactNode = null; if (this.props.showGenericPlaceholder) { placeholder = ( <AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}> <span className="mx_MFileBody_info_icon" /> - <TextWithTooltip tooltip={presentableTextForFile(content, _t("Attachment"), false)}> + <TextWithTooltip tooltip={presentableTextForFile(this.content, _t("Attachment"), false)}> <span className="mx_MFileBody_info_filename"> - { presentableTextForFile(content, _t("Attachment"), true, true) } + { presentableTextForFile(this.content, _t("Attachment"), true, true) } </span> </TextWithTooltip> </AccessibleButton> @@ -318,7 +317,7 @@ export default class MFileBody extends React.Component<IProps, IState> { { _t("Download %(text)s", { text: this.linkText }) } </a> { this.props.tileShape === TileShape.FileGrid && <div className="mx_MImageBody_size"> - { content.info && content.info.size ? filesize(content.info.size) : "" } + { this.content.info && this.content.info.size ? filesize(this.content.info.size) : "" } </div> } </div> } </span> From a8ec8f4c4b27a0efb61ddc872dcdb84529ea3a08 Mon Sep 17 00:00:00 2001 From: Travis Ralston <travisr@matrix.org> Date: Thu, 29 Jul 2021 16:01:26 -0600 Subject: [PATCH 6/7] Appease the linter --- src/components/views/messages/MFileBody.tsx | 4 ++-- src/utils/FileDownloader.ts | 17 ++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index ecc60bc3e3..13449ff6d9 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -113,7 +113,7 @@ export default class MFileBody extends React.Component<IProps, IState> { private iframe: React.RefObject<HTMLIFrameElement> = createRef(); private dummyLink: React.RefObject<HTMLAnchorElement> = createRef(); private userDidClick = false; - private fileDownloader: FileDownloader = new FileDownloader(() => this.iframe.current) + private fileDownloader: FileDownloader = new FileDownloader(() => this.iframe.current); public constructor(props: IProps) { super(props); @@ -142,7 +142,7 @@ export default class MFileBody extends React.Component<IProps, IState> { imgSrc: DOWNLOAD_ICON_URL, imgStyle: null, style: computedStyle(this.dummyLink.current), - textContent: _t("Download %(text)s", { text }) + textContent: _t("Download %(text)s", { text }), }, }); } diff --git a/src/utils/FileDownloader.ts b/src/utils/FileDownloader.ts index 0f329e6ada..a22ff506de 100644 --- a/src/utils/FileDownloader.ts +++ b/src/utils/FileDownloader.ts @@ -14,21 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -export type getIframeFn = () => HTMLIFrameElement; +export type getIframeFn = () => HTMLIFrameElement; // eslint-disable-line @typescript-eslint/naming-convention export const DEFAULT_STYLES = { imgSrc: "", imgStyle: null, // css props style: "", textContent: "", -} +}; type DownloadOptions = { - blob: Blob, - name: string, - autoDownload?: boolean, - opts?: typeof DEFAULT_STYLES -} + blob: Blob; + name: string; + autoDownload?: boolean; + opts?: typeof DEFAULT_STYLES; +}; // set up the iframe as a singleton so we don't have to figure out destruction of it down the line. let managedIframe: HTMLIFrameElement; @@ -90,9 +90,8 @@ export class FileDownloader { return iframe; } - public async download({blob, name, autoDownload = true, opts = DEFAULT_STYLES}: DownloadOptions) { + public async download({ blob, name, autoDownload = true, opts = DEFAULT_STYLES }: DownloadOptions) { const iframe = this.iframe; // get the iframe first just in case we need to await onload - console.log("@@", {blob, name, autoDownload, opts, iframe, m: iframe === managedIframe, p: this.onLoadPromise}); if (this.onLoadPromise) await this.onLoadPromise; iframe.contentWindow.postMessage({ ...opts, From c5c58a5e91bec4e72a32d13fc2d74496e5711480 Mon Sep 17 00:00:00 2001 From: Travis Ralston <travisr@matrix.org> Date: Fri, 30 Jul 2021 08:56:55 -0600 Subject: [PATCH 7/7] Add file size to tooltip --- src/components/views/messages/MFileBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 13449ff6d9..13fc4b01e7 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -202,7 +202,7 @@ export default class MFileBody extends React.Component<IProps, IState> { placeholder = ( <AccessibleButton className="mx_MediaBody mx_MFileBody_info" onClick={this.onPlaceholderClick}> <span className="mx_MFileBody_info_icon" /> - <TextWithTooltip tooltip={presentableTextForFile(this.content, _t("Attachment"), false)}> + <TextWithTooltip tooltip={presentableTextForFile(this.content, _t("Attachment"), true)}> <span className="mx_MFileBody_info_filename"> { presentableTextForFile(this.content, _t("Attachment"), true, true) } </span>