/* Copyright 2024 New Vector Ltd. Copyright 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 { 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"; import Spinner from "../elements/Spinner"; 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; // XXX: It can take a cycle or two for the MessageActionBar to have all the props/setup // required to get us a MediaEventHelper, so we use a getter function instead to prod for // one. mediaEventHelperGet: () => MediaEventHelper | undefined; } interface IState { canDownload: null | boolean; loading: boolean; blob?: Blob; tooltip: TranslationKey; } export default class DownloadActionButton extends React.PureComponent { private downloader = new FileDownloader(); public constructor(props: IProps) { super(props); const moduleHints = ModuleApi.customComponents.getHintsForMessage(props.mxEvent); const downloadState: Pick = { 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, }; } private onDownloadClick = async (): Promise => { try { await this.doDownload(); } catch (e) { Modal.createDialog(ErrorDialog, { title: _t("timeline|download_failed"), description: ( <>
{_t("timeline|download_failed_description")}
{e instanceof Error ? e.toString() : ""}
), }); this.setState({ loading: false }); } }; private async doDownload(): Promise { const mediaEventHelper = this.props.mediaEventHelperGet(); if (this.state.loading || !mediaEventHelper) return; if (mediaEventHelper.media.isEncrypted) { this.setState({ tooltip: _td("timeline|download_action_decrypting") }); } this.setState({ loading: true }); if (this.state.blob) { // Cheat and trigger a download, again. return this.downloadBlob(this.state.blob); } const blob = await mediaEventHelper.sourceBlob.value; this.setState({ blob }); await this.downloadBlob(blob); } private async downloadBlob(blob: Blob): Promise { await this.downloader.download({ blob, name: this.props.mediaEventHelperGet()!.fileName, }); this.setState({ loading: false }); } public render(): React.ReactNode { let spinner: JSX.Element | undefined; if (this.state.loading) { spinner = ; } if (this.state.canDownload === null) { spinner = ; } if (this.state.canDownload === false) { return null; } const classes = classNames({ mx_MessageActionBar_iconButton: true, mx_MessageActionBar_downloadButton: true, mx_MessageActionBar_downloadSpinnerButton: !!spinner, }); return ( {spinner} ); } }