Merge branch 'develop' into midhun/fix-spotlight-1
This commit is contained in:
@@ -8,9 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { createRef, CSSProperties } from "react";
|
||||
import React, { createRef, CSSProperties, useRef, useState } from "react";
|
||||
import FocusLock from "react-focus-lock";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { MatrixEvent, parseErrorResponse } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
@@ -30,6 +30,9 @@ 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";
|
||||
|
||||
// Max scale to keep gaps around the image
|
||||
const MAX_SCALE = 0.95;
|
||||
@@ -309,15 +312,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||
this.setZoomAndRotation(cur + 90);
|
||||
};
|
||||
|
||||
private onDownloadClick = (): void => {
|
||||
const a = document.createElement("a");
|
||||
a.href = this.props.src;
|
||||
if (this.props.name) a.download = this.props.name;
|
||||
a.target = "_blank";
|
||||
a.rel = "noreferrer noopener";
|
||||
a.click();
|
||||
};
|
||||
|
||||
private onOpenContextMenu = (): void => {
|
||||
this.setState({
|
||||
contextMenuDisplayed: true,
|
||||
@@ -555,11 +549,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||
title={_t("lightbox|rotate_right")}
|
||||
onClick={this.onRotateClockwiseClick}
|
||||
/>
|
||||
<AccessibleButton
|
||||
className="mx_ImageView_button mx_ImageView_button_download"
|
||||
title={_t("action|download")}
|
||||
onClick={this.onDownloadClick}
|
||||
/>
|
||||
<DownloadButton url={this.props.src} fileName={this.props.name} />
|
||||
{contextMenuButton}
|
||||
<AccessibleButton
|
||||
className="mx_ImageView_button mx_ImageView_button_close"
|
||||
@@ -591,3 +581,61 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function DownloadButton({ url, fileName }: { url: string; fileName?: string }): JSX.Element {
|
||||
const downloader = useRef(new FileDownloader()).current;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const blobRef = useRef<Blob>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
async function downloadBlob(blob: Blob): Promise<void> {
|
||||
await downloader.download({
|
||||
blob,
|
||||
name: fileName ?? _t("common|image"),
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_ImageView_button mx_ImageView_button_download"
|
||||
title={loading ? _t("timeline|download_action_downloading") : _t("action|download")}
|
||||
onClick={onDownloadClick}
|
||||
disabled={loading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { MutableRefObject, ReactNode, StrictMode } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
|
||||
@@ -24,7 +24,7 @@ export const getPersistKey = (appId: string): string => "widget_" + appId;
|
||||
// We contain all persisted elements within a master container to allow them all to be within the same
|
||||
// CSS stacking context, and thus be able to control their z-indexes relative to each other.
|
||||
function getOrCreateMasterContainer(): HTMLDivElement {
|
||||
let container = getContainer("mx_PersistedElement_container");
|
||||
let container = document.getElementById("mx_PersistedElement_container") as HTMLDivElement;
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = "mx_PersistedElement_container";
|
||||
@@ -34,18 +34,10 @@ function getOrCreateMasterContainer(): HTMLDivElement {
|
||||
return container;
|
||||
}
|
||||
|
||||
function getContainer(containerId: string): HTMLDivElement {
|
||||
return document.getElementById(containerId) as HTMLDivElement;
|
||||
}
|
||||
|
||||
function getOrCreateContainer(containerId: string): HTMLDivElement {
|
||||
let container = getContainer(containerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = containerId;
|
||||
getOrCreateMasterContainer().appendChild(container);
|
||||
}
|
||||
const container = document.createElement("div");
|
||||
container.id = containerId;
|
||||
getOrCreateMasterContainer().appendChild(container);
|
||||
|
||||
return container;
|
||||
}
|
||||
@@ -83,6 +75,8 @@ export default class PersistedElement extends React.Component<IProps> {
|
||||
private childContainer?: HTMLDivElement;
|
||||
private child?: HTMLDivElement;
|
||||
|
||||
private static rootMap: Record<string, [root: Root, container: Element]> = {};
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
@@ -99,14 +93,16 @@ export default class PersistedElement extends React.Component<IProps> {
|
||||
* @param {string} persistKey Key used to uniquely identify this PersistedElement
|
||||
*/
|
||||
public static destroyElement(persistKey: string): void {
|
||||
const container = getContainer("mx_persistedElement_" + persistKey);
|
||||
if (container) {
|
||||
container.remove();
|
||||
const pair = PersistedElement.rootMap[persistKey];
|
||||
if (pair) {
|
||||
pair[0].unmount();
|
||||
pair[1].remove();
|
||||
}
|
||||
delete PersistedElement.rootMap[persistKey];
|
||||
}
|
||||
|
||||
public static isMounted(persistKey: string): boolean {
|
||||
return Boolean(getContainer("mx_persistedElement_" + persistKey));
|
||||
return Boolean(PersistedElement.rootMap[persistKey]);
|
||||
}
|
||||
|
||||
private collectChildContainer = (ref: HTMLDivElement): void => {
|
||||
@@ -179,7 +175,14 @@ export default class PersistedElement extends React.Component<IProps> {
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
ReactDOM.render(content, getOrCreateContainer("mx_persistedElement_" + this.props.persistKey));
|
||||
let rootPair = PersistedElement.rootMap[this.props.persistKey];
|
||||
if (!rootPair) {
|
||||
const container = getOrCreateContainer("mx_persistedElement_" + this.props.persistKey);
|
||||
const root = createRoot(container);
|
||||
rootPair = [root, container];
|
||||
PersistedElement.rootMap[this.props.persistKey] = rootPair;
|
||||
}
|
||||
rootPair[0].render(content);
|
||||
}
|
||||
|
||||
private updateChildVisibility(child?: HTMLDivElement, visible = false): void {
|
||||
|
||||
Reference in New Issue
Block a user