mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 03:25:12 +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";
|
import {Platform} from "./src/platform/web/Platform.js";
|
||||||
main(new Platform(document.body, {
|
main(new Platform(document.body, {
|
||||||
worker: "src/worker.js",
|
worker: "src/worker.js",
|
||||||
|
downloadSandbox: "assets/download-sandbox.html",
|
||||||
olm: {
|
olm: {
|
||||||
wasm: "lib/olm/olm.wasm",
|
wasm: "lib/olm/olm.wasm",
|
||||||
legacyBundle: "lib/olm/olm_legacy.js",
|
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']));
|
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,
|
// 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
|
// and writes to assets, so the build bundles can translate them, so do it first
|
||||||
await copyThemeAssets(themes, assets);
|
await copyThemeAssets(themes, assets);
|
||||||
@ -143,6 +147,7 @@ async function buildHtml(doc, version, globalHash, modernOnly, assets) {
|
|||||||
});
|
});
|
||||||
const pathsJSON = JSON.stringify({
|
const pathsJSON = JSON.stringify({
|
||||||
worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null,
|
worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null,
|
||||||
|
downloadSandbox: assets.resolve("download-sandbox.html"),
|
||||||
serviceWorker: "sw.js",
|
serviceWorker: "sw.js",
|
||||||
olm: {
|
olm: {
|
||||||
wasm: assets.resolve("olm.wasm"),
|
wasm: assets.resolve("olm.wasm"),
|
||||||
@ -234,6 +239,7 @@ function isPreCached(asset) {
|
|||||||
asset.endsWith(".png") ||
|
asset.endsWith(".png") ||
|
||||||
asset.endsWith(".css") ||
|
asset.endsWith(".css") ||
|
||||||
asset.endsWith(".wasm") ||
|
asset.endsWith(".wasm") ||
|
||||||
|
asset.endsWith(".html") ||
|
||||||
// most environments don't need the worker
|
// most environments don't need the worker
|
||||||
asset.endsWith(".js") && !NON_PRECACHED_JS.includes(asset);
|
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) {
|
async _loadEncryptedFile(file) {
|
||||||
const bufferHandle = await this._mediaRepository.downloadEncryptedFile(file);
|
const bufferHandle = await this._mediaRepository.downloadEncryptedFile(file, true);
|
||||||
if (this.isDisposed) {
|
if (this.isDisposed) {
|
||||||
bufferHandle.dispose();
|
bufferHandle.dispose();
|
||||||
return;
|
return;
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
import {GapTile} from "./tiles/GapTile.js";
|
import {GapTile} from "./tiles/GapTile.js";
|
||||||
import {TextTile} from "./tiles/TextTile.js";
|
import {TextTile} from "./tiles/TextTile.js";
|
||||||
import {ImageTile} from "./tiles/ImageTile.js";
|
import {ImageTile} from "./tiles/ImageTile.js";
|
||||||
|
import {FileTile} from "./tiles/FileTile.js";
|
||||||
import {LocationTile} from "./tiles/LocationTile.js";
|
import {LocationTile} from "./tiles/LocationTile.js";
|
||||||
import {RoomNameTile} from "./tiles/RoomNameTile.js";
|
import {RoomNameTile} from "./tiles/RoomNameTile.js";
|
||||||
import {RoomMemberTile} from "./tiles/RoomMemberTile.js";
|
import {RoomMemberTile} from "./tiles/RoomMemberTile.js";
|
||||||
@ -40,6 +41,8 @@ export function tilesCreator(baseOptions) {
|
|||||||
return new TextTile(options);
|
return new TextTile(options);
|
||||||
case "m.image":
|
case "m.image":
|
||||||
return new ImageTile(options);
|
return new ImageTile(options);
|
||||||
|
case "m.file":
|
||||||
|
return new FileTile(options);
|
||||||
case "m.location":
|
case "m.location":
|
||||||
return new LocationTile(options);
|
return new LocationTile(options);
|
||||||
default:
|
default:
|
||||||
|
@ -52,10 +52,25 @@ export class MediaRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadEncryptedFile(fileEntry) {
|
async downloadEncryptedFile(fileEntry, cache = false) {
|
||||||
const url = this.mxcUrl(fileEntry.url);
|
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);
|
const decryptedBuffer = await decryptAttachment(this._platform.crypto, encryptedBuffer, fileEntry);
|
||||||
return this._platform.createBufferHandle(decryptedBuffer, fileEntry.mimetype);
|
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 {estimateStorageUsage} from "./dom/StorageEstimate.js";
|
||||||
import {WorkerPool} from "./dom/WorkerPool.js";
|
import {WorkerPool} from "./dom/WorkerPool.js";
|
||||||
import {BufferHandle} from "./dom/BufferHandle.js";
|
import {BufferHandle} from "./dom/BufferHandle.js";
|
||||||
|
import {downloadInIframe} from "./dom/download.js";
|
||||||
|
|
||||||
function addScript(src) {
|
function addScript(src) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
@ -133,4 +134,12 @@ export class Platform {
|
|||||||
createBufferHandle(buffer, mimetype) {
|
createBufferHandle(buffer, mimetype) {
|
||||||
return new BufferHandle(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 {
|
input::-ms-clear {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hydrogen > iframe.downloadSandbox {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@ -18,6 +18,7 @@ import {ListView} from "../../general/ListView.js";
|
|||||||
import {GapView} from "./timeline/GapView.js";
|
import {GapView} from "./timeline/GapView.js";
|
||||||
import {TextMessageView} from "./timeline/TextMessageView.js";
|
import {TextMessageView} from "./timeline/TextMessageView.js";
|
||||||
import {ImageView} from "./timeline/ImageView.js";
|
import {ImageView} from "./timeline/ImageView.js";
|
||||||
|
import {FileView} from "./timeline/FileView.js";
|
||||||
import {AnnouncementView} from "./timeline/AnnouncementView.js";
|
import {AnnouncementView} from "./timeline/AnnouncementView.js";
|
||||||
|
|
||||||
function viewClassForEntry(entry) {
|
function viewClassForEntry(entry) {
|
||||||
@ -28,6 +29,7 @@ function viewClassForEntry(entry) {
|
|||||||
case "message-status":
|
case "message-status":
|
||||||
return TextMessageView;
|
return TextMessageView;
|
||||||
case "image": return ImageView;
|
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