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.
This commit is contained in:
Travis Ralston 2021-07-29 15:36:50 -06:00
parent 94af6db201
commit fb89b45c06
4 changed files with 196 additions and 67 deletions

104
src/utils/FileDownloader.ts Normal file
View file

@ -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,
}, '*');
}
}