mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-05 20:26:19 +02:00
Move all logic to view model and replace MFileBody with a simple wrapper
This commit is contained in:
parent
ac57fec03b
commit
e03da59ad0
@ -1,295 +1,41 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { createRef } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type MediaEventContent } from "matrix-js-sdk/src/types";
|
||||
import { FileBody as SharedFileBody, type FileInfo } from "@element-hq/web-shared-components";
|
||||
import React, { useContext, useMemo } from "react";
|
||||
import { FileBody as SharedFileBody } from "@element-hq/web-shared-components";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import Modal from "../../../Modal";
|
||||
import { mediaFromContent } from "../../../customisations/Media";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import { downloadLabelForFile, presentableTextForFile } from "../../../utils/FileUtils";
|
||||
import { type IBodyProps } from "./IBodyProps";
|
||||
import { FileDownloader } from "../../../utils/FileDownloader";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
|
||||
export let DOWNLOAD_ICON_URL: string; // cached copy of the download.svg asset for the sandboxed iframe later on
|
||||
|
||||
async function cacheDownloadIcon(): Promise<void> {
|
||||
if (DOWNLOAD_ICON_URL) return; // cached already
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const svg = await fetch(require("@vector-im/compound-design-tokens/icons/download.svg").default).then((r) =>
|
||||
r.text(),
|
||||
);
|
||||
DOWNLOAD_ICON_URL = "data:image/svg+xml;base64," + window.btoa(svg);
|
||||
}
|
||||
|
||||
// Cache the asset immediately
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
cacheDownloadIcon();
|
||||
|
||||
// User supplied content can contain scripts, we have to be careful that
|
||||
// we don't accidentally run those script within the same origin as the
|
||||
// client. Otherwise those scripts written by remote users can read
|
||||
// the access token and end-to-end keys that are in local storage.
|
||||
//
|
||||
// For attachments downloaded directly from the homeserver we can use
|
||||
// Content-Security-Policy headers to disable script execution.
|
||||
//
|
||||
// But attachments with end-to-end encryption are more difficult to handle.
|
||||
// We need to decrypt the attachment on the client and then display it.
|
||||
// To display the attachment we need to turn the decrypted bytes into a URL.
|
||||
//
|
||||
// There are two ways to turn bytes into URLs, data URL and blob URLs.
|
||||
// Data URLs aren't suitable for downloading a file because Chrome has a
|
||||
// 2MB limit on the size of URLs that can be viewed in the browser or
|
||||
// downloaded. This limit does not seem to apply when the url is used as
|
||||
// the source attribute of an image tag.
|
||||
//
|
||||
// Blob URLs are generated using window.URL.createObjectURL and unfortunately
|
||||
// for our purposes they inherit the origin of the page that created them.
|
||||
// This means that any scripts that run when the URL is viewed will be able
|
||||
// to access local storage.
|
||||
//
|
||||
// The easiest solution is to host the code that generates the blob URL on
|
||||
// a different domain to the client.
|
||||
// Another possibility is to generate the blob URL within a sandboxed iframe.
|
||||
// The downside of using a second domain is that it complicates hosting,
|
||||
// the downside of using a sandboxed iframe is that the browers are overly
|
||||
// restrictive in what you are allowed to do with the generated URL.
|
||||
|
||||
/**
|
||||
* Get the current CSS style for a DOMElement.
|
||||
* @param {HTMLElement} element The element to get the current style of.
|
||||
* @return {string} The CSS style encoded as a string.
|
||||
*/
|
||||
export function computedStyle(element: HTMLElement | null): string {
|
||||
if (!element) {
|
||||
return "";
|
||||
}
|
||||
const style = window.getComputedStyle(element, null);
|
||||
let cssText = style.cssText;
|
||||
// noinspection EqualityComparisonWithCoercionJS
|
||||
if (cssText == "") {
|
||||
// Firefox doesn't implement ".cssText" for computed styles.
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=137687
|
||||
for (const rule of style) {
|
||||
cssText += rule + ":";
|
||||
cssText += style.getPropertyValue(rule) + ";";
|
||||
}
|
||||
}
|
||||
return cssText;
|
||||
}
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import { MFileBodyViewModel } from "../../../viewmodels/messages/MFileBodyViewModel";
|
||||
|
||||
interface IProps extends IBodyProps {
|
||||
/* whether or not to show the default placeholder for the file. Defaults to true. */
|
||||
showGenericPlaceholder?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
decryptedBlob?: Blob;
|
||||
}
|
||||
/**
|
||||
* MFileBody component that wraps the shared FileBody with a ViewModel.
|
||||
* This component creates and manages the MFileBodyViewModel instance.
|
||||
*/
|
||||
export default function MFileBody(props: IProps): React.ReactElement {
|
||||
const context = useContext(RoomContext);
|
||||
|
||||
export default class MFileBody extends React.Component<IProps, IState> {
|
||||
public static contextType = RoomContext;
|
||||
declare public context: React.ContextType<typeof RoomContext>;
|
||||
|
||||
public state: IState = {};
|
||||
private iframe = createRef<HTMLIFrameElement>();
|
||||
private dummyLink = createRef<HTMLAnchorElement>();
|
||||
private userDidClick = false;
|
||||
private fileDownloader: FileDownloader = new FileDownloader(() => this.iframe.current);
|
||||
|
||||
private getContentUrl(): string | null {
|
||||
if (this.props.forExport) return null;
|
||||
const media = mediaFromContent(this.props.mxEvent.getContent());
|
||||
return media.srcHttp;
|
||||
}
|
||||
private get content(): MediaEventContent {
|
||||
return this.props.mxEvent.getContent<MediaEventContent>();
|
||||
}
|
||||
|
||||
private get fileName(): string {
|
||||
return this.props.mediaEventHelper?.fileName || _t("common|attachment");
|
||||
}
|
||||
|
||||
private get linkText(): string {
|
||||
return downloadLabelForFile(this.content, true);
|
||||
}
|
||||
|
||||
private downloadFile(fileName: string, text: string): void {
|
||||
if (!this.state.decryptedBlob) return;
|
||||
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: text,
|
||||
},
|
||||
const viewModel = useMemo(() => {
|
||||
return new MFileBodyViewModel({
|
||||
...props,
|
||||
timelineRenderingType: context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
props.mxEvent,
|
||||
props.forExport,
|
||||
props.showGenericPlaceholder,
|
||||
props.mediaEventHelper,
|
||||
context.timelineRenderingType,
|
||||
]);
|
||||
|
||||
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) {
|
||||
logger.warn("Unable to decrypt attachment: ", err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("common|error"),
|
||||
description: _t("timeline|m.file|error_decrypting"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onPlaceholderClick = async (): Promise<void> => {
|
||||
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(): React.ReactNode {
|
||||
const isEncrypted = this.props.mediaEventHelper?.media.isEncrypted;
|
||||
const contentUrl = this.getContentUrl();
|
||||
const fileType = this.content.info?.mimetype ?? "application/octet-stream";
|
||||
// defaultProps breaks types on IBodyProps, so instead define the default here.
|
||||
const showGenericPlaceholder = this.props.showGenericPlaceholder ?? true;
|
||||
|
||||
let showDownloadLink =
|
||||
!showGenericPlaceholder ||
|
||||
(this.context.timelineRenderingType !== TimelineRenderingType.Room &&
|
||||
this.context.timelineRenderingType !== TimelineRenderingType.Search &&
|
||||
this.context.timelineRenderingType !== TimelineRenderingType.Pinned);
|
||||
|
||||
if (this.context.timelineRenderingType === TimelineRenderingType.Thread) {
|
||||
showDownloadLink = false;
|
||||
}
|
||||
|
||||
const fileInfo: FileInfo = {
|
||||
filename: presentableTextForFile(this.content, _t("common|attachment"), true, true),
|
||||
tooltip: presentableTextForFile(this.content, _t("common|attachment"), true),
|
||||
mimeType: fileType,
|
||||
};
|
||||
|
||||
// Export mode
|
||||
if (this.props.forExport) {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
return (
|
||||
<SharedFileBody
|
||||
fileInfo={fileInfo}
|
||||
downloadLabel={this.linkText}
|
||||
showGenericPlaceholder={showGenericPlaceholder}
|
||||
showDownloadLink={showDownloadLink}
|
||||
forExport={true}
|
||||
exportUrl={content.file?.url || content.url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (!isEncrypted && !contentUrl) {
|
||||
return (
|
||||
<SharedFileBody
|
||||
fileInfo={fileInfo}
|
||||
downloadLabel={this.linkText}
|
||||
showGenericPlaceholder={showGenericPlaceholder}
|
||||
showDownloadLink={showDownloadLink}
|
||||
error={_t("timeline|m.file|error_invalid")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Encrypted file not yet decrypted
|
||||
if (isEncrypted && !this.state.decryptedBlob) {
|
||||
return (
|
||||
<SharedFileBody
|
||||
fileInfo={fileInfo}
|
||||
downloadLabel={this.linkText}
|
||||
showGenericPlaceholder={showGenericPlaceholder}
|
||||
showDownloadLink={showDownloadLink}
|
||||
isEncrypted={true}
|
||||
isDecrypted={false}
|
||||
onPlaceholderClick={this.onPlaceholderClick}
|
||||
onDecryptClick={this.decryptFile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Encrypted file that has been decrypted
|
||||
if (isEncrypted && this.state.decryptedBlob) {
|
||||
return (
|
||||
<SharedFileBody
|
||||
fileInfo={fileInfo}
|
||||
downloadLabel={this.linkText}
|
||||
showGenericPlaceholder={showGenericPlaceholder}
|
||||
showDownloadLink={showDownloadLink}
|
||||
isEncrypted={true}
|
||||
isDecrypted={true}
|
||||
iframeSrc="usercontent/"
|
||||
iframeRef={this.iframe}
|
||||
dummyLinkRef={this.dummyLink}
|
||||
onPlaceholderClick={this.onPlaceholderClick}
|
||||
onIframeLoad={() => this.downloadFile(this.fileName, this.linkText)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Unencrypted file
|
||||
return (
|
||||
<SharedFileBody
|
||||
fileInfo={fileInfo}
|
||||
downloadLabel={this.linkText}
|
||||
showGenericPlaceholder={showGenericPlaceholder}
|
||||
showDownloadLink={showDownloadLink}
|
||||
onPlaceholderClick={this.onPlaceholderClick}
|
||||
onDownloadClick={this.onUnencryptedDownloadClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private onUnencryptedDownloadClick = (e: React.MouseEvent): void => {
|
||||
logger.log(`Downloading ${this.content.info?.mimetype ?? "application/octet-stream"} as blob (unencrypted)`);
|
||||
|
||||
// Avoid letting the <a> do its thing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Start a fetch for the download
|
||||
// Based upon https://stackoverflow.com/a/49500465
|
||||
this.props.mediaEventHelper?.sourceBlob.value.then((blob) => {
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// We have to create an anchor to download the file
|
||||
const tempAnchor = document.createElement("a");
|
||||
tempAnchor.download = this.fileName;
|
||||
tempAnchor.href = blobUrl;
|
||||
document.body.appendChild(tempAnchor); // for firefox: https://stackoverflow.com/a/32226068
|
||||
tempAnchor.click();
|
||||
tempAnchor.remove();
|
||||
});
|
||||
};
|
||||
return <SharedFileBody vm={viewModel} />;
|
||||
}
|
||||
|
||||
44
src/utils/FileBodyUtils.ts
Normal file
44
src/utils/FileBodyUtils.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
export let DOWNLOAD_ICON_URL: string; // cached copy of the download.svg asset for the sandboxed iframe later on
|
||||
|
||||
export async function cacheDownloadIcon(): Promise<void> {
|
||||
if (DOWNLOAD_ICON_URL) return; // cached already
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const svg = await fetch(require("@vector-im/compound-design-tokens/icons/download.svg").default).then((r) =>
|
||||
r.text(),
|
||||
);
|
||||
DOWNLOAD_ICON_URL = "data:image/svg+xml;base64," + window.btoa(svg);
|
||||
}
|
||||
|
||||
// Cache the asset immediately
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
cacheDownloadIcon();
|
||||
|
||||
/**
|
||||
* Get the current CSS style for a DOMElement.
|
||||
* @param {HTMLElement} element The element to get the current style of.
|
||||
* @return {string} The CSS style encoded as a string.
|
||||
*/
|
||||
export function computedStyle(element: HTMLElement | null): string {
|
||||
if (!element) {
|
||||
return "";
|
||||
}
|
||||
const style = window.getComputedStyle(element, null);
|
||||
let cssText = style.cssText;
|
||||
// noinspection EqualityComparisonWithCoercionJS
|
||||
if (cssText == "") {
|
||||
// Firefox doesn't implement ".cssText" for computed styles.
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=137687
|
||||
for (const rule of style) {
|
||||
cssText += rule + ":";
|
||||
cssText += style.getPropertyValue(rule) + ";";
|
||||
}
|
||||
}
|
||||
return cssText;
|
||||
}
|
||||
240
src/viewmodels/messages/MFileBodyViewModel.ts
Normal file
240
src/viewmodels/messages/MFileBodyViewModel.ts
Normal file
@ -0,0 +1,240 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type MediaEventContent } from "matrix-js-sdk/src/types";
|
||||
import {
|
||||
BaseViewModel,
|
||||
type FileBodyViewSnapshot,
|
||||
type FileBodyActions,
|
||||
type FileInfo,
|
||||
} from "@element-hq/web-shared-components";
|
||||
import { createRef } from "react";
|
||||
|
||||
import { _t } from "../../languageHandler";
|
||||
import Modal from "../../Modal";
|
||||
import { mediaFromContent } from "../../customisations/Media";
|
||||
import ErrorDialog from "../../components/views/dialogs/ErrorDialog";
|
||||
import { downloadLabelForFile, presentableTextForFile } from "../../utils/FileUtils";
|
||||
import { type IBodyProps } from "../../components/views/messages/IBodyProps";
|
||||
import { FileDownloader } from "../../utils/FileDownloader";
|
||||
import { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import { computedStyle, DOWNLOAD_ICON_URL } from "../../utils/FileBodyUtils";
|
||||
|
||||
export interface MFileBodyViewModelProps extends IBodyProps {
|
||||
/* whether or not to show the default placeholder for the file. Defaults to true. */
|
||||
showGenericPlaceholder?: boolean;
|
||||
timelineRenderingType?: TimelineRenderingType;
|
||||
}
|
||||
|
||||
export class MFileBodyViewModel
|
||||
extends BaseViewModel<FileBodyViewSnapshot, MFileBodyViewModelProps>
|
||||
implements FileBodyActions
|
||||
{
|
||||
private iframe = createRef<HTMLIFrameElement>();
|
||||
private dummyLink = createRef<HTMLAnchorElement>();
|
||||
private userDidClick = false;
|
||||
private fileDownloader: FileDownloader = new FileDownloader(() => this.iframe.current);
|
||||
private decryptedBlob?: Blob;
|
||||
|
||||
public constructor(props: MFileBodyViewModelProps) {
|
||||
const snapshot = MFileBodyViewModel.createInitialSnapshot(props);
|
||||
super(props, snapshot);
|
||||
}
|
||||
|
||||
private static createInitialSnapshot(props: MFileBodyViewModelProps): FileBodyViewSnapshot {
|
||||
const content = props.mxEvent.getContent<MediaEventContent>();
|
||||
const isEncrypted = props.mediaEventHelper?.media.isEncrypted;
|
||||
const contentUrl = MFileBodyViewModel.getContentUrl(props);
|
||||
const fileType = content.info?.mimetype ?? "application/octet-stream";
|
||||
const showGenericPlaceholder = props.showGenericPlaceholder ?? true;
|
||||
|
||||
let showDownloadLink =
|
||||
!showGenericPlaceholder ||
|
||||
(props.timelineRenderingType !== TimelineRenderingType.Room &&
|
||||
props.timelineRenderingType !== TimelineRenderingType.Search &&
|
||||
props.timelineRenderingType !== TimelineRenderingType.Pinned);
|
||||
|
||||
if (props.timelineRenderingType === TimelineRenderingType.Thread) {
|
||||
showDownloadLink = false;
|
||||
}
|
||||
|
||||
const fileInfo: FileInfo = {
|
||||
filename: presentableTextForFile(content, _t("common|attachment"), true, true),
|
||||
tooltip: presentableTextForFile(content, _t("common|attachment"), true),
|
||||
mimeType: fileType,
|
||||
};
|
||||
|
||||
const downloadLabel = downloadLabelForFile(content, true);
|
||||
|
||||
// Export mode
|
||||
if (props.forExport) {
|
||||
return {
|
||||
fileInfo,
|
||||
downloadLabel,
|
||||
showGenericPlaceholder,
|
||||
showDownloadLink,
|
||||
isEncrypted: false,
|
||||
isDecrypted: false,
|
||||
forExport: true,
|
||||
exportUrl: content.file?.url || content.url,
|
||||
};
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (!isEncrypted && !contentUrl) {
|
||||
return {
|
||||
fileInfo,
|
||||
downloadLabel,
|
||||
showGenericPlaceholder,
|
||||
showDownloadLink,
|
||||
isEncrypted: false,
|
||||
isDecrypted: false,
|
||||
forExport: false,
|
||||
error: _t("timeline|m.file|error_invalid"),
|
||||
};
|
||||
}
|
||||
|
||||
// Initial state for encrypted or unencrypted files
|
||||
return {
|
||||
fileInfo,
|
||||
downloadLabel,
|
||||
showGenericPlaceholder,
|
||||
showDownloadLink,
|
||||
isEncrypted: !!isEncrypted,
|
||||
isDecrypted: false,
|
||||
forExport: false,
|
||||
};
|
||||
}
|
||||
|
||||
private static getContentUrl(props: MFileBodyViewModelProps): string | null {
|
||||
if (props.forExport) return null;
|
||||
const media = mediaFromContent(props.mxEvent.getContent());
|
||||
return media.srcHttp;
|
||||
}
|
||||
|
||||
private get content(): MediaEventContent {
|
||||
return this.props.mxEvent.getContent<MediaEventContent>();
|
||||
}
|
||||
|
||||
private get fileName(): string {
|
||||
return this.props.mediaEventHelper?.fileName || _t("common|attachment");
|
||||
}
|
||||
|
||||
private get linkText(): string {
|
||||
return downloadLabelForFile(this.content, true);
|
||||
}
|
||||
|
||||
private downloadFile(fileName: string, text: string): void {
|
||||
if (!this.decryptedBlob) return;
|
||||
this.fileDownloader.download({
|
||||
blob: this.decryptedBlob,
|
||||
name: fileName,
|
||||
autoDownload: this.userDidClick,
|
||||
opts: {
|
||||
imgSrc: DOWNLOAD_ICON_URL,
|
||||
imgStyle: null,
|
||||
style: computedStyle(this.dummyLink.current),
|
||||
textContent: text,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private decryptFile = async (): Promise<void> => {
|
||||
if (this.decryptedBlob) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.userDidClick = true;
|
||||
this.decryptedBlob = await this.props.mediaEventHelper!.sourceBlob.value;
|
||||
|
||||
// Update snapshot to reflect decrypted state
|
||||
this.snapshot.merge({
|
||||
isDecrypted: true,
|
||||
iframeSrc: "usercontent/",
|
||||
iframeRef: this.iframe,
|
||||
dummyLinkRef: this.dummyLink,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn("Unable to decrypt attachment: ", err);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("common|error"),
|
||||
description: _t("timeline|m.file|error_decrypting"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public get onPlaceholderClick(): (() => void) | undefined {
|
||||
return 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 get onDownloadClick(): ((e: React.MouseEvent) => void) | undefined {
|
||||
const current = this.snapshot.current;
|
||||
if (current.isEncrypted && !current.isDecrypted) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (e: React.MouseEvent) => {
|
||||
logger.log(
|
||||
`Downloading ${this.content.info?.mimetype ?? "application/octet-stream"} as blob (unencrypted)`,
|
||||
);
|
||||
|
||||
// Avoid letting the <a> do its thing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Start a fetch for the download
|
||||
// Based upon https://stackoverflow.com/a/49500465
|
||||
this.props.mediaEventHelper?.sourceBlob.value.then((blob) => {
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// We have to create an anchor to download the file
|
||||
const tempAnchor = document.createElement("a");
|
||||
tempAnchor.download = this.fileName;
|
||||
tempAnchor.href = blobUrl;
|
||||
document.body.appendChild(tempAnchor); // for firefox: https://stackoverflow.com/a/32226068
|
||||
tempAnchor.click();
|
||||
tempAnchor.remove();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public get onDecryptClick(): ((e: React.MouseEvent) => void) | undefined {
|
||||
const current = this.snapshot.current;
|
||||
if (!current.isEncrypted || current.isDecrypted) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await this.decryptFile();
|
||||
};
|
||||
}
|
||||
|
||||
public get onIframeLoad(): (() => void) | undefined {
|
||||
const current = this.snapshot.current;
|
||||
if (!current.isEncrypted || !current.isDecrypted) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return () => this.downloadFile(this.fileName, this.linkText);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user