Add support for module message hint allowDownloadingMedia (#30252)
* Add support for allowDownloadingMedia * Add tests. * Allow downloading when no event is associated. * fix lint * Update module API * Update lock file too * force CI
This commit is contained in:
@@ -8,9 +8,10 @@ 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 } from "react";
|
||||
import React, { type JSX, createRef, type CSSProperties, useRef, useState, useMemo, 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 { _t } from "../../../languageHandler";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
@@ -34,6 +35,7 @@ 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";
|
||||
|
||||
// Max scale to keep gaps around the image
|
||||
const MAX_SCALE = 0.95;
|
||||
@@ -591,12 +593,36 @@ function DownloadButton({
|
||||
url: string;
|
||||
fileName?: string;
|
||||
mxEvent?: MatrixEvent;
|
||||
}): JSX.Element {
|
||||
}): 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);
|
||||
});
|
||||
}
|
||||
}, [mxEvent]);
|
||||
|
||||
function showError(e: unknown): void {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("timeline|download_failed"),
|
||||
@@ -640,6 +666,10 @@ function DownloadButton({
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
if (!canDownload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_ImageView_button mx_ImageView_button_download"
|
||||
|
||||
@@ -10,6 +10,7 @@ import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import React, { type JSX } from "react";
|
||||
import classNames from "classnames";
|
||||
import { DownloadIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { type MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||
import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex";
|
||||
@@ -18,6 +19,7 @@ import { _t, _td, type TranslationKey } from "../../../languageHandler";
|
||||
import { FileDownloader } from "../../../utils/FileDownloader";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import ModuleApi from "../../../modules/Api";
|
||||
|
||||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
@@ -29,6 +31,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
canDownload: null | boolean;
|
||||
loading: boolean;
|
||||
blob?: Blob;
|
||||
tooltip: TranslationKey;
|
||||
@@ -40,9 +43,29 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const moduleHints = ModuleApi.customComponents.getHintsForMessage(props.mxEvent);
|
||||
const downloadState: Pick<IState, "canDownload"> = { canDownload: true };
|
||||
if (moduleHints?.allowDownloadingMedia) {
|
||||
downloadState.canDownload = null;
|
||||
moduleHints
|
||||
.allowDownloadingMedia()
|
||||
.then((canDownload) => {
|
||||
this.setState({
|
||||
canDownload: canDownload,
|
||||
});
|
||||
})
|
||||
.catch((ex) => {
|
||||
logger.error(`Failed to check if media from ${props.mxEvent.getId()} could be downloaded`, ex);
|
||||
this.setState({
|
||||
canDownload: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.state = {
|
||||
loading: false,
|
||||
tooltip: _td("timeline|download_action_downloading"),
|
||||
...downloadState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,6 +120,14 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS
|
||||
spinner = <Spinner w={18} h={18} />;
|
||||
}
|
||||
|
||||
if (this.state.canDownload === null) {
|
||||
spinner = <Spinner w={18} h={18} />;
|
||||
}
|
||||
|
||||
if (this.state.canDownload === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
mx_MessageActionBar_iconButton: true,
|
||||
mx_MessageActionBar_downloadButton: true,
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
CustomMessageRenderFunction,
|
||||
CustomMessageComponentProps as ModuleCustomMessageComponentProps,
|
||||
OriginalComponentProps,
|
||||
CustomMessageRenderHints,
|
||||
CustomMessageRenderHints as ModuleCustomCustomMessageRenderHints,
|
||||
MatrixEvent as ModuleMatrixEvent,
|
||||
} from "@element-hq/element-web-module-api";
|
||||
import type React from "react";
|
||||
@@ -23,13 +23,18 @@ type EventTypeOrFilter = Parameters<ICustomComponentsApi["registerMessageRendere
|
||||
type EventRenderer = {
|
||||
eventTypeOrFilter: EventTypeOrFilter;
|
||||
renderer: CustomMessageRenderFunction;
|
||||
hints: CustomMessageRenderHints;
|
||||
hints: ModuleCustomCustomMessageRenderHints;
|
||||
};
|
||||
|
||||
interface CustomMessageComponentProps extends Omit<ModuleCustomMessageComponentProps, "mxEvent"> {
|
||||
mxEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
interface CustomMessageRenderHints extends Omit<ModuleCustomCustomMessageRenderHints, "allowDownloadingMedia"> {
|
||||
// Note. This just makes it easier to use this API on Element Web as we already have the moduleized event stored.
|
||||
allowDownloadingMedia?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export class CustomComponentsApi implements ICustomComponentsApi {
|
||||
/**
|
||||
* Convert a matrix-js-sdk event into a ModuleMatrixEvent.
|
||||
@@ -63,7 +68,7 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
||||
public registerMessageRenderer(
|
||||
eventTypeOrFilter: EventTypeOrFilter,
|
||||
renderer: CustomMessageRenderFunction,
|
||||
hints: CustomMessageRenderHints = {},
|
||||
hints: ModuleCustomCustomMessageRenderHints = {},
|
||||
): void {
|
||||
this.registeredMessageRenderers.push({ eventTypeOrFilter: eventTypeOrFilter, renderer, hints });
|
||||
}
|
||||
@@ -119,7 +124,13 @@ export class CustomComponentsApi implements ICustomComponentsApi {
|
||||
const moduleEv = CustomComponentsApi.getModuleMatrixEvent(mxEvent);
|
||||
const renderer = moduleEv && this.selectRenderer(moduleEv);
|
||||
if (renderer) {
|
||||
return renderer.hints;
|
||||
return {
|
||||
...renderer.hints,
|
||||
// Convert from js-sdk style events to module events automatically.
|
||||
allowDownloadingMedia: renderer.hints.allowDownloadingMedia
|
||||
? () => renderer.hints.allowDownloadingMedia!(moduleEv)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user