mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 03:36:07 +03:00
Update visual style and sandbox properly
This commit is contained in:
parent
623f2e7613
commit
b57fff5739
7 changed files with 140 additions and 26 deletions
|
@ -107,3 +107,12 @@ limitations under the License.
|
|||
.mx_MessageActionBar_cancelButton::after {
|
||||
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_downloadButton::after {
|
||||
mask-size: 16px;
|
||||
mask-image: url('$(res)/img/download.svg');
|
||||
}
|
||||
|
||||
.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after {
|
||||
background-color: transparent; // hide the download icon mask
|
||||
}
|
||||
|
|
110
src/components/views/messages/DownloadActionButton.tsx
Normal file
110
src/components/views/messages/DownloadActionButton.tsx
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "matrix-js-sdk/src";
|
||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||
import React, { createRef } from "react";
|
||||
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import classNames from "classnames";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { DOWNLOAD_ICON_URL } from "./MFileBody";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
loading: boolean;
|
||||
blob?: Blob;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.DownloadActionButton")
|
||||
export class DownloadActionButton extends React.PureComponent<IProps, IState> {
|
||||
private iframe: React.RefObject<HTMLIFrameElement> = createRef();
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
private onDownloadClick = async () => {
|
||||
if (this.state.loading) return;
|
||||
|
||||
this.setState({ loading: true });
|
||||
|
||||
if (this.state.blob) {
|
||||
// Cheat and trigger a download, again.
|
||||
return this.onFrameLoad();
|
||||
}
|
||||
|
||||
const blob = await this.props.mediaEventHelperGet().sourceBlob.value;
|
||||
this.setState({ blob });
|
||||
};
|
||||
|
||||
private onFrameLoad = () => {
|
||||
this.setState({ loading: false });
|
||||
|
||||
// we aren't showing the iframe, so we can send over the bare minimum styles and such.
|
||||
this.iframe.current.contentWindow.postMessage({
|
||||
imgSrc: DOWNLOAD_ICON_URL,
|
||||
imgStyle: null,
|
||||
style: "",
|
||||
blob: this.state.blob,
|
||||
download: this.props.mediaEventHelperGet().fileName,
|
||||
textContent: "",
|
||||
auto: true, // autodownload
|
||||
}, '*');
|
||||
};
|
||||
|
||||
public render() {
|
||||
let spinner: JSX.Element;
|
||||
if (this.state.loading) {
|
||||
spinner = <Spinner w={18} h={18} />;
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
'mx_MessageActionBar_maskButton': true,
|
||||
'mx_MessageActionBar_downloadButton': true,
|
||||
'mx_MessageActionBar_downloadSpinnerButton': !!spinner,
|
||||
});
|
||||
|
||||
return <RovingAccessibleTooltipButton
|
||||
className={classes}
|
||||
title={_t("Download")}
|
||||
onClick={this.onDownloadClick}
|
||||
disabled={!!spinner}
|
||||
>
|
||||
{ spinner }
|
||||
{ this.state.blob && <iframe
|
||||
src={"usercontent/" /* XXX: Like MFileBody, this should come from the skin */}
|
||||
ref={this.iframe}
|
||||
onLoad={this.onFrameLoad}
|
||||
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation"
|
||||
style={{ display: "none" }}
|
||||
/> }
|
||||
</RovingAccessibleTooltipButton>;
|
||||
}
|
||||
}
|
|
@ -27,12 +27,12 @@ import { IContent } from "matrix-js-sdk/src";
|
|||
import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent";
|
||||
import { IBodyProps } from "./IBodyProps";
|
||||
|
||||
let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on
|
||||
export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on
|
||||
|
||||
async function cacheDownloadIcon() {
|
||||
if (downloadIconUrl) return; // cached already
|
||||
if (DOWNLOAD_ICON_URL) return; // cached already
|
||||
const svg = await fetch(require("../../../../res/img/download.svg")).then(r => r.text());
|
||||
downloadIconUrl = "data:image/svg+xml;base64," + window.btoa(svg);
|
||||
DOWNLOAD_ICON_URL = "data:image/svg+xml;base64," + window.btoa(svg);
|
||||
}
|
||||
|
||||
// Cache the asset immediately
|
||||
|
@ -74,7 +74,7 @@ cacheDownloadIcon();
|
|||
* @param {HTMLElement} element The element to get the current style of.
|
||||
* @return {string} The CSS style encoded as a string.
|
||||
*/
|
||||
function computedStyle(element) {
|
||||
export function computedStyle(element: HTMLElement) {
|
||||
if (!element) {
|
||||
return "";
|
||||
}
|
||||
|
@ -217,7 +217,7 @@ export default class MFileBody extends React.Component<IProps, IState> {
|
|||
// When the iframe loads we tell it to render a download link
|
||||
const onIframeLoad = (ev) => {
|
||||
ev.target.contentWindow.postMessage({
|
||||
imgSrc: downloadIconUrl,
|
||||
imgSrc: DOWNLOAD_ICON_URL,
|
||||
imgStyle: null, // it handles this internally for us. Useful if a downstream changes the icon.
|
||||
style: computedStyle(this.dummyLink.current),
|
||||
blob: this.state.decryptedBlob,
|
||||
|
|
|
@ -33,6 +33,7 @@ import { canCancel } from "../context_menus/MessageContextMenu";
|
|||
import Resend from "../../../Resend";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||
import { DownloadActionButton } from "./DownloadActionButton";
|
||||
|
||||
const OptionsButton = ({ mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange }) => {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
@ -176,21 +177,6 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
});
|
||||
};
|
||||
|
||||
onDownloadClick = async (ev) => {
|
||||
if (!this.props.getTile || !this.props.getTile().getMediaHelper) {
|
||||
console.warn("Action bar triggered a download but the event tile is missing a media helper");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Maybe just call into MFileBody and render it as null
|
||||
const src = this.props.getTile().getMediaHelper();
|
||||
const a = document.createElement("a");
|
||||
a.href = await src.sourceUrl.value;
|
||||
a.download = "todo.png";
|
||||
a.target = "_blank";
|
||||
a.click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs a given fn on the set of possible events to test. The first event
|
||||
* that passes the checkFn will have fn executed on it. Both functions take
|
||||
|
@ -286,11 +272,9 @@ export default class MessageActionBar extends React.PureComponent {
|
|||
|
||||
// XXX: Assuming that the underlying tile will be a media event if it is eligible media.
|
||||
if (MediaEventHelper.isEligible(this.props.mxEvent)) {
|
||||
toolbarOpts.splice(0, 0, <RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_downloadButton"
|
||||
title={_t("Download")}
|
||||
onClick={this.onDownloadClick}
|
||||
disabled={false}
|
||||
toolbarOpts.splice(0, 0, <DownloadActionButton
|
||||
mxEvent={this.props.mxEvent}
|
||||
mediaEventHelperGet={() => this.props.getTile?.().getMediaHelper?.()}
|
||||
key="download"
|
||||
/>);
|
||||
}
|
||||
|
|
|
@ -1870,6 +1870,7 @@
|
|||
"Saturday": "Saturday",
|
||||
"Today": "Today",
|
||||
"Yesterday": "Yesterday",
|
||||
"Download": "Download",
|
||||
"View Source": "View Source",
|
||||
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.",
|
||||
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.",
|
||||
|
@ -2002,7 +2003,6 @@
|
|||
"Zoom in": "Zoom in",
|
||||
"Rotate Left": "Rotate Left",
|
||||
"Rotate Right": "Rotate Right",
|
||||
"Download": "Download",
|
||||
"Information": "Information",
|
||||
"Language Dropdown": "Language Dropdown",
|
||||
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
let hasCalled = false;
|
||||
function remoteRender(event) {
|
||||
const data = event.data;
|
||||
|
||||
// If we're handling secondary calls, start from scratch
|
||||
if (hasCalled) {
|
||||
document.body.replaceWith(document.createElement("BODY"));
|
||||
}
|
||||
hasCalled = true;
|
||||
|
||||
const img = document.createElement("span"); // we'll mask it as an image
|
||||
img.id = "img";
|
||||
|
||||
|
|
|
@ -40,6 +40,10 @@ export class MediaEventHelper implements IDestroyable {
|
|||
this.media = mediaFromContent(this.event.getContent());
|
||||
}
|
||||
|
||||
public get fileName(): string {
|
||||
return this.event.getContent<IMediaEventContent>().body || "download";
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (this.media.isEncrypted) {
|
||||
if (this.sourceUrl.present) URL.revokeObjectURL(this.sourceUrl.cachedValue);
|
||||
|
|
Loading…
Reference in a new issue