|
|
@@ -8,10 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|
|
Please see LICENSE files in the repository root for full details.
|
|
|
*/
|
|
|
|
|
|
-import React, { type JSX, createRef, type CSSProperties, useRef, useState, useMemo, useEffect } from "react";
|
|
|
+import React, { type JSX, createRef, type CSSProperties, useEffect } from "react";
|
|
|
import FocusLock from "react-focus-lock";
|
|
|
-import { type MatrixEvent, parseErrorResponse } from "matrix-js-sdk/src/matrix";
|
|
|
-import { logger } from "matrix-js-sdk/src/logger";
|
|
|
+import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
|
|
|
|
|
import { _t } from "../../../languageHandler";
|
|
|
import MemberAvatar from "../avatars/MemberAvatar";
|
|
|
@@ -31,11 +30,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
|
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
|
|
import { presentableTextForFile } from "../../../utils/FileUtils";
|
|
|
import AccessibleButton from "./AccessibleButton";
|
|
|
-import Modal from "../../../Modal";
|
|
|
-import ErrorDialog from "../dialogs/ErrorDialog";
|
|
|
-import { FileDownloader } from "../../../utils/FileDownloader";
|
|
|
-import { MediaEventHelper } from "../../../utils/MediaEventHelper.ts";
|
|
|
-import ModuleApi from "../../../modules/Api";
|
|
|
+import { useDownloadMedia } from "../../../hooks/useDownloadMedia.ts";
|
|
|
|
|
|
// Max scale to keep gaps around the image
|
|
|
const MAX_SCALE = 0.95;
|
|
|
@@ -123,6 +118,8 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|
|
private imageWrapper = createRef<HTMLDivElement>();
|
|
|
private image = createRef<HTMLImageElement>();
|
|
|
|
|
|
+ private downloadFunction?: () => Promise<void>;
|
|
|
+
|
|
|
private initX = 0;
|
|
|
private initY = 0;
|
|
|
private previousX = 0;
|
|
|
@@ -302,6 +299,13 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|
|
ev.preventDefault();
|
|
|
this.props.onFinished();
|
|
|
break;
|
|
|
+ case KeyBindingAction.Save:
|
|
|
+ ev.preventDefault();
|
|
|
+ ev.stopPropagation();
|
|
|
+ if (this.downloadFunction) {
|
|
|
+ this.downloadFunction();
|
|
|
+ }
|
|
|
+ break;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -327,6 +331,10 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
+ private onDownloadFunctionReady = (download: () => Promise<void>): void => {
|
|
|
+ this.downloadFunction = download;
|
|
|
+ };
|
|
|
+
|
|
|
private onPermalinkClicked = (ev: React.MouseEvent): void => {
|
|
|
// This allows the permalink to be opened in a new tab/window or copied as
|
|
|
// matrix.to, but also for it to enable routing within Element when clicked.
|
|
|
@@ -552,7 +560,12 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|
|
title={_t("lightbox|rotate_right")}
|
|
|
onClick={this.onRotateClockwiseClick}
|
|
|
/>
|
|
|
- <DownloadButton url={this.props.src} fileName={this.props.name} mxEvent={this.props.mxEvent} />
|
|
|
+ <DownloadButton
|
|
|
+ url={this.props.src}
|
|
|
+ fileName={this.props.name}
|
|
|
+ mxEvent={this.props.mxEvent}
|
|
|
+ onDownloadReady={this.onDownloadFunctionReady}
|
|
|
+ />
|
|
|
{contextMenuButton}
|
|
|
<AccessibleButton
|
|
|
className="mx_ImageView_button mx_ImageView_button_close"
|
|
|
@@ -585,99 +598,28 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function DownloadButton({
|
|
|
- url,
|
|
|
- fileName,
|
|
|
- mxEvent,
|
|
|
-}: {
|
|
|
+interface DownloadButtonProps {
|
|
|
url: string;
|
|
|
fileName?: string;
|
|
|
mxEvent?: MatrixEvent;
|
|
|
-}): JSX.Element | null {
|
|
|
- const downloader = useRef(new FileDownloader()).current;
|
|
|
- const [loading, setLoading] = useState(false);
|
|
|
- const [canDownload, setCanDownload] = useState<boolean>(false);
|
|
|
- const blobRef = useRef<Blob>(undefined);
|
|
|
- const mediaEventHelper = useMemo(() => (mxEvent ? new MediaEventHelper(mxEvent) : undefined), [mxEvent]);
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- if (!mxEvent) {
|
|
|
- // If we have no event, we assume this is safe to download.
|
|
|
- setCanDownload(true);
|
|
|
- return;
|
|
|
- }
|
|
|
- const hints = ModuleApi.customComponents.getHintsForMessage(mxEvent);
|
|
|
- if (hints?.allowDownloadingMedia) {
|
|
|
- // Disable downloading as soon as we know there is a hint.
|
|
|
- setCanDownload(false);
|
|
|
- hints
|
|
|
- .allowDownloadingMedia()
|
|
|
- .then((downloadable) => {
|
|
|
- setCanDownload(downloadable);
|
|
|
- })
|
|
|
- .catch((ex) => {
|
|
|
- logger.error(`Failed to check if media from ${mxEvent.getId()} could be downloaded`, ex);
|
|
|
- // Err on the side of safety.
|
|
|
- setCanDownload(false);
|
|
|
- });
|
|
|
- } else {
|
|
|
- setCanDownload(true);
|
|
|
- }
|
|
|
- }, [mxEvent]);
|
|
|
-
|
|
|
- function showError(e: unknown): void {
|
|
|
- Modal.createDialog(ErrorDialog, {
|
|
|
- title: _t("timeline|download_failed"),
|
|
|
- description: (
|
|
|
- <>
|
|
|
- <div>{_t("timeline|download_failed_description")}</div>
|
|
|
- <div>{e instanceof Error ? e.toString() : ""}</div>
|
|
|
- </>
|
|
|
- ),
|
|
|
- });
|
|
|
- setLoading(false);
|
|
|
- }
|
|
|
-
|
|
|
- const onDownloadClick = async (): Promise<void> => {
|
|
|
- try {
|
|
|
- if (loading) return;
|
|
|
- setLoading(true);
|
|
|
-
|
|
|
- if (blobRef.current) {
|
|
|
- // Cheat and trigger a download, again.
|
|
|
- return downloadBlob(blobRef.current);
|
|
|
- }
|
|
|
+ onDownloadReady?: (download: () => Promise<void>) => void;
|
|
|
+}
|
|
|
|
|
|
- const res = await fetch(url);
|
|
|
- if (!res.ok) {
|
|
|
- throw parseErrorResponse(res, await res.text());
|
|
|
- }
|
|
|
- const blob = await res.blob();
|
|
|
- blobRef.current = blob;
|
|
|
- await downloadBlob(blob);
|
|
|
- } catch (e) {
|
|
|
- showError(e);
|
|
|
- }
|
|
|
- };
|
|
|
+export const DownloadButton: React.FC<DownloadButtonProps> = ({ url, fileName, mxEvent, onDownloadReady }) => {
|
|
|
+ const { download, loading, canDownload } = useDownloadMedia(url, fileName, mxEvent);
|
|
|
|
|
|
- async function downloadBlob(blob: Blob): Promise<void> {
|
|
|
- await downloader.download({
|
|
|
- blob,
|
|
|
- name: mediaEventHelper?.fileName ?? fileName ?? _t("common|image"),
|
|
|
- });
|
|
|
- setLoading(false);
|
|
|
- }
|
|
|
+ useEffect(() => {
|
|
|
+ if (onDownloadReady) onDownloadReady(download);
|
|
|
+ }, [download, onDownloadReady]);
|
|
|
|
|
|
- if (!canDownload) {
|
|
|
- return null;
|
|
|
- }
|
|
|
+ if (!canDownload) return null;
|
|
|
|
|
|
return (
|
|
|
<AccessibleButton
|
|
|
className="mx_ImageView_button mx_ImageView_button_download"
|
|
|
title={loading ? _t("timeline|download_action_downloading") : _t("action|download")}
|
|
|
- onClick={onDownloadClick}
|
|
|
+ onClick={download}
|
|
|
disabled={loading}
|
|
|
/>
|
|
|
);
|
|
|
-}
|
|
|
+};
|