mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-22 19:14:52 +01:00
Merge pull request #195 from vector-im/bwindels/file-downloads
File downloads
This commit is contained in:
commit
d88c3f6dea
24
assets/download-sandbox.html
Normal file
24
assets/download-sandbox.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<a id="link" href="#">Download!</a>
|
||||
<script type="text/javascript">
|
||||
var link = document.getElementById("link");
|
||||
function download(blob, filename) {
|
||||
var url = URL.createObjectURL(blob);
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
window.addEventListener("message", function(event) {
|
||||
if (event.data.type === "download") {
|
||||
download(event.data.blob, event.data.filename);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -23,6 +23,7 @@
|
||||
import {Platform} from "./src/platform/web/Platform.js";
|
||||
main(new Platform(document.body, {
|
||||
worker: "src/worker.js",
|
||||
downloadSandbox: "assets/download-sandbox.html",
|
||||
olm: {
|
||||
wasm: "lib/olm/olm.wasm",
|
||||
legacyBundle: "lib/olm/olm_legacy.js",
|
||||
|
@ -78,6 +78,10 @@ async function build({modernOnly}) {
|
||||
]));
|
||||
await assets.write(`worker.js`, await buildJsLegacy("src/platform/web/worker/main.js", ['src/platform/web/worker/polyfill.js']));
|
||||
}
|
||||
// copy over non-theme assets
|
||||
const downloadSandbox = "download-sandbox.html";
|
||||
let downloadSandboxHtml = await fs.readFile(path.join(projectDir, `assets/${downloadSandbox}`));
|
||||
await assets.write(downloadSandbox, downloadSandboxHtml);
|
||||
// creates the directories where the theme css bundles are placed in,
|
||||
// and writes to assets, so the build bundles can translate them, so do it first
|
||||
await copyThemeAssets(themes, assets);
|
||||
@ -143,6 +147,7 @@ async function buildHtml(doc, version, globalHash, modernOnly, assets) {
|
||||
});
|
||||
const pathsJSON = JSON.stringify({
|
||||
worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null,
|
||||
downloadSandbox: assets.resolve("download-sandbox.html"),
|
||||
serviceWorker: "sw.js",
|
||||
olm: {
|
||||
wasm: assets.resolve("olm.wasm"),
|
||||
@ -234,6 +239,7 @@ function isPreCached(asset) {
|
||||
asset.endsWith(".png") ||
|
||||
asset.endsWith(".css") ||
|
||||
asset.endsWith(".wasm") ||
|
||||
asset.endsWith(".html") ||
|
||||
// most environments don't need the worker
|
||||
asset.endsWith(".js") && !NON_PRECACHED_JS.includes(asset);
|
||||
}
|
||||
|
70
src/domain/session/room/timeline/tiles/FileTile.js
Normal file
70
src/domain/session/room/timeline/tiles/FileTile.js
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
Copyright 2020 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 {MessageTile} from "./MessageTile.js";
|
||||
import {formatSize} from "../../../../../utils/formatSize.js";
|
||||
|
||||
export class FileTile extends MessageTile {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this._error = null;
|
||||
this._downloading = false;
|
||||
}
|
||||
|
||||
async download() {
|
||||
if (this._downloading) {
|
||||
return;
|
||||
}
|
||||
const content = this._getContent();
|
||||
const filename = content.body;
|
||||
this._downloading = true;
|
||||
this.emitChange("label");
|
||||
let bufferHandle;
|
||||
try {
|
||||
bufferHandle = await this._mediaRepository.downloadAttachment(content);
|
||||
this.platform.offerSaveBufferHandle(bufferHandle, filename);
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
} finally {
|
||||
bufferHandle?.dispose();
|
||||
this._downloading = false;
|
||||
}
|
||||
this.emitChange("label");
|
||||
}
|
||||
|
||||
get label() {
|
||||
if (this._error) {
|
||||
return `Could not decrypt file: ${this._error.message}`;
|
||||
}
|
||||
const content = this._getContent();
|
||||
const filename = content.body;
|
||||
const size = formatSize(content.info?.size);
|
||||
if (this._downloading) {
|
||||
return this.i18n`Downloading ${filename} (${size})…`;
|
||||
} else {
|
||||
return this.i18n`Download ${filename} (${size})`;
|
||||
}
|
||||
}
|
||||
|
||||
get error() {
|
||||
return null;
|
||||
}
|
||||
|
||||
get shape() {
|
||||
return "file";
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@ export class ImageTile extends MessageTile {
|
||||
}
|
||||
|
||||
async _loadEncryptedFile(file) {
|
||||
const bufferHandle = await this._mediaRepository.downloadEncryptedFile(file);
|
||||
const bufferHandle = await this._mediaRepository.downloadEncryptedFile(file, true);
|
||||
if (this.isDisposed) {
|
||||
bufferHandle.dispose();
|
||||
return;
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
import {GapTile} from "./tiles/GapTile.js";
|
||||
import {TextTile} from "./tiles/TextTile.js";
|
||||
import {ImageTile} from "./tiles/ImageTile.js";
|
||||
import {FileTile} from "./tiles/FileTile.js";
|
||||
import {LocationTile} from "./tiles/LocationTile.js";
|
||||
import {RoomNameTile} from "./tiles/RoomNameTile.js";
|
||||
import {RoomMemberTile} from "./tiles/RoomMemberTile.js";
|
||||
@ -40,6 +41,8 @@ export function tilesCreator(baseOptions) {
|
||||
return new TextTile(options);
|
||||
case "m.image":
|
||||
return new ImageTile(options);
|
||||
case "m.file":
|
||||
return new FileTile(options);
|
||||
case "m.location":
|
||||
return new LocationTile(options);
|
||||
default:
|
||||
|
@ -52,10 +52,25 @@ export class MediaRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async downloadEncryptedFile(fileEntry) {
|
||||
async downloadEncryptedFile(fileEntry, cache = false) {
|
||||
const url = this.mxcUrl(fileEntry.url);
|
||||
const {body: encryptedBuffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache: true}).response();
|
||||
const {body: encryptedBuffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response();
|
||||
const decryptedBuffer = await decryptAttachment(this._platform.crypto, encryptedBuffer, fileEntry);
|
||||
return this._platform.createBufferHandle(decryptedBuffer, fileEntry.mimetype);
|
||||
}
|
||||
|
||||
async downloadPlaintextFile(mxcUrl, mimetype, cache = false) {
|
||||
const url = this.mxcUrl(mxcUrl);
|
||||
const {body: buffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response();
|
||||
return this._platform.createBufferHandle(buffer, mimetype);
|
||||
}
|
||||
|
||||
async downloadAttachment(content, cache = false) {
|
||||
if (content.file) {
|
||||
return this.downloadEncryptedFile(content.file, cache);
|
||||
} else {
|
||||
return this.downloadPlaintextFile(content.url, content.info?.mimetype, cache);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import {Crypto} from "./dom/Crypto.js";
|
||||
import {estimateStorageUsage} from "./dom/StorageEstimate.js";
|
||||
import {WorkerPool} from "./dom/WorkerPool.js";
|
||||
import {BufferHandle} from "./dom/BufferHandle.js";
|
||||
import {downloadInIframe} from "./dom/download.js";
|
||||
|
||||
function addScript(src) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
@ -100,7 +101,7 @@ export class Platform {
|
||||
this.request = xhrRequest;
|
||||
}
|
||||
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
|
||||
this.isIE11 = isIE11;
|
||||
this.isIE11 = isIE11;
|
||||
}
|
||||
|
||||
get updateService() {
|
||||
@ -133,4 +134,12 @@ export class Platform {
|
||||
createBufferHandle(buffer, mimetype) {
|
||||
return new BufferHandle(buffer, mimetype);
|
||||
}
|
||||
|
||||
offerSaveBufferHandle(bufferHandle, filename) {
|
||||
if (navigator.msSaveBlob) {
|
||||
navigator.msSaveBlob(bufferHandle.blob, filename);
|
||||
} else {
|
||||
downloadInIframe(this._container, this._paths.downloadSandbox, bufferHandle.blob, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
37
src/platform/web/dom/download.js
Normal file
37
src/platform/web/dom/download.js
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2020 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 async function downloadInIframe(container, iframeSrc, blob, filename) {
|
||||
let iframe = container.querySelector("iframe.downloadSandbox");
|
||||
if (!iframe) {
|
||||
iframe = document.createElement("iframe");
|
||||
iframe.setAttribute("sandbox", "allow-scripts allow-downloads allow-downloads-without-user-activation");
|
||||
iframe.setAttribute("src", iframeSrc);
|
||||
iframe.className = "downloadSandbox";
|
||||
container.appendChild(iframe);
|
||||
let detach;
|
||||
await new Promise((resolve, reject) => {
|
||||
detach = () => {
|
||||
iframe.removeEventListener("load", resolve);
|
||||
iframe.removeEventListener("error", reject);
|
||||
}
|
||||
iframe.addEventListener("load", resolve);
|
||||
iframe.addEventListener("error", reject);
|
||||
});
|
||||
detach();
|
||||
}
|
||||
iframe.contentWindow.postMessage({type: "download", blob: blob, filename: filename}, "*");
|
||||
}
|
@ -49,3 +49,7 @@ body.hydrogen {
|
||||
input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hydrogen > iframe.downloadSandbox {
|
||||
display: none;
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import {ListView} from "../../general/ListView.js";
|
||||
import {GapView} from "./timeline/GapView.js";
|
||||
import {TextMessageView} from "./timeline/TextMessageView.js";
|
||||
import {ImageView} from "./timeline/ImageView.js";
|
||||
import {FileView} from "./timeline/FileView.js";
|
||||
import {AnnouncementView} from "./timeline/AnnouncementView.js";
|
||||
|
||||
function viewClassForEntry(entry) {
|
||||
@ -28,6 +29,7 @@ function viewClassForEntry(entry) {
|
||||
case "message-status":
|
||||
return TextMessageView;
|
||||
case "image": return ImageView;
|
||||
case "file": return FileView;
|
||||
}
|
||||
}
|
||||
|
||||
|
29
src/platform/web/ui/session/room/timeline/FileView.js
Normal file
29
src/platform/web/ui/session/room/timeline/FileView.js
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
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 {TemplateView} from "../../../general/TemplateView.js";
|
||||
import {renderMessage} from "./common.js";
|
||||
|
||||
export class FileView extends TemplateView {
|
||||
render(t, vm) {
|
||||
return renderMessage(t, vm, [
|
||||
t.p([
|
||||
t.button({className: "link", onClick: () => vm.download()}, vm => vm.label),
|
||||
t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time)
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
29
src/utils/formatSize.js
Normal file
29
src/utils/formatSize.js
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2020 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 function formatSize(size, decimals = 2) {
|
||||
if (Number.isSafeInteger(size)) {
|
||||
const base = Math.min(3, Math.floor(Math.log(size) / Math.log(1024)));
|
||||
const decimalFactor = Math.pow(10, decimals);
|
||||
const formattedSize = Math.round((size / Math.pow(1024, base)) * decimalFactor) / decimalFactor;
|
||||
switch (base) {
|
||||
case 0: return `${formattedSize} bytes`;
|
||||
case 1: return `${formattedSize} KB`;
|
||||
case 2: return `${formattedSize} MB`;
|
||||
case 3: return `${formattedSize} GB`;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user