Fix tinting for download icon

As shown in https://github.com/vector-im/element-web/issues/16546
This commit is contained in:
Travis Ralston 2021-03-09 12:54:20 -07:00
parent af9f17219b
commit a7debdd946
3 changed files with 42 additions and 87 deletions

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,6 +16,19 @@ limitations under the License.
.mx_MFileBody_download { .mx_MFileBody_download {
color: $accent-color; color: $accent-color;
.mx_MFileBody_download_icon {
// 12px instead of 14px to better match surrounding font size
width: 12px;
height: 12px;
mask-size: 12px;
mask-position: center;
mask-repeat: no-repeat;
mask-image: url("$(res)/img/download.svg");
background-color: $accent-color;
display: inline-block;
}
} }
.mx_MFileBody_download a { .mx_MFileBody_download a {

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016, 2018, 2021 The Matrix.org Foundation C.I.C.
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -18,52 +17,24 @@ limitations under the License.
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import filesize from 'filesize'; import filesize from 'filesize';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {decryptFile} from '../../../utils/DecryptFile'; import {decryptFile} from '../../../utils/DecryptFile';
import Tinter from '../../../Tinter';
import request from 'browser-request';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromContent} from "../../../customisations/Media"; import {mediaFromContent} from "../../../customisations/Media";
import ErrorDialog from "../dialogs/ErrorDialog";
let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on
// A cached tinted copy of require("../../../../res/img/download.svg") async function cacheDownloadIcon() {
let tintedDownloadImageURL; if (downloadIconUrl) return; // cached already
// Track a list of mounted MFileBody instances so that we can update const svg = await fetch(require("../../../../res/img/download.svg")).then(r => r.text());
// the require("../../../../res/img/download.svg") when the tint changes. downloadIconUrl = "data:image/svg+xml;base64," + window.btoa(svg);
let nextMountId = 0;
const mounts = {};
/**
* Updates the tinted copy of require("../../../../res/img/download.svg") when the tint changes.
*/
function updateTintedDownloadImage() {
// Download the svg as an XML document.
// We could cache the XML response here, but since the tint rarely changes
// it's probably not worth it.
// Also note that we can't use fetch here because fetch doesn't support
// file URLs, which the download image will be if we're running from
// the filesystem (like in an Electron wrapper).
request({uri: require("../../../../res/img/download.svg")}, (err, response, body) => {
if (err) return;
const svg = new DOMParser().parseFromString(body, "image/svg+xml");
// Apply the fixups to the XML.
const fixups = Tinter.calcSvgFixups([{contentDocument: svg}]);
Tinter.applySvgFixups(fixups);
// Encoded the fixed up SVG as a data URL.
const svgString = new XMLSerializer().serializeToString(svg);
tintedDownloadImageURL = "data:image/svg+xml;base64," + window.btoa(svgString);
// Notify each mounted MFileBody that the URL has changed.
Object.keys(mounts).forEach(function(id) {
mounts[id].tint();
});
});
} }
Tinter.registerTintable(updateTintedDownloadImage); // Cache the asset immediately
cacheDownloadIcon();
// User supplied content can contain scripts, we have to be careful that // User supplied content can contain scripts, we have to be careful that
// we don't accidentally run those script within the same origin as the // we don't accidentally run those script within the same origin as the
@ -106,6 +77,7 @@ function computedStyle(element) {
} }
const style = window.getComputedStyle(element, null); const style = window.getComputedStyle(element, null);
let cssText = style.cssText; let cssText = style.cssText;
// noinspection EqualityComparisonWithCoercionJS
if (cssText == "") { if (cssText == "") {
// Firefox doesn't implement ".cssText" for computed styles. // Firefox doesn't implement ".cssText" for computed styles.
// https://bugzilla.mozilla.org/show_bug.cgi?id=137687 // https://bugzilla.mozilla.org/show_bug.cgi?id=137687
@ -145,7 +117,6 @@ export default class MFileBody extends React.Component {
this._iframe = createRef(); this._iframe = createRef();
this._dummyLink = createRef(); this._dummyLink = createRef();
this._downloadImage = createRef();
} }
/** /**
@ -182,48 +153,18 @@ export default class MFileBody extends React.Component {
return media.srcHttp; return media.srcHttp;
} }
componentDidMount() {
// Add this to the list of mounted components to receive notifications
// when the tint changes.
this.id = nextMountId++;
mounts[this.id] = this;
this.tint();
}
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) { if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) {
this.props.onHeightChanged(); this.props.onHeightChanged();
} }
} }
componentWillUnmount() {
// Remove this from the list of mounted components
delete mounts[this.id];
}
tint = () => {
// Update our tinted copy of require("../../../../res/img/download.svg")
if (this._downloadImage.current) {
this._downloadImage.current.src = tintedDownloadImageURL;
}
if (this._iframe.current) {
// If the attachment is encrypted then the download image
// will be inside the iframe so we wont be able to update
// it directly.
this._iframe.current.contentWindow.postMessage({
imgSrc: tintedDownloadImageURL,
style: computedStyle(this._dummyLink.current),
}, "*");
}
};
render() { render() {
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
const text = this.presentableTextForFile(content); const text = this.presentableTextForFile(content);
const isEncrypted = content.file !== undefined; const isEncrypted = content.file !== undefined;
const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment"); const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment");
const contentUrl = this._getContentUrl(); const contentUrl = this._getContentUrl();
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const fileSize = content.info ? content.info.size : null; const fileSize = content.info ? content.info.size : null;
const fileType = content.info ? content.info.mimetype : "application/octet-stream"; const fileType = content.info ? content.info.mimetype : "application/octet-stream";
@ -280,7 +221,7 @@ export default class MFileBody extends React.Component {
// When the iframe loads we tell it to render a download link // When the iframe loads we tell it to render a download link
const onIframeLoad = (ev) => { const onIframeLoad = (ev) => {
ev.target.contentWindow.postMessage({ ev.target.contentWindow.postMessage({
imgSrc: tintedDownloadImageURL, imgSrc: downloadIconUrl,
style: computedStyle(this._dummyLink.current), style: computedStyle(this._dummyLink.current),
blob: this.state.decryptedBlob, blob: this.state.decryptedBlob,
// Set a download attribute for encrypted files so that the file // Set a download attribute for encrypted files so that the file
@ -384,7 +325,7 @@ export default class MFileBody extends React.Component {
{placeholder} {placeholder}
<div className="mx_MFileBody_download"> <div className="mx_MFileBody_download">
<a {...downloadProps}> <a {...downloadProps}>
<img src={tintedDownloadImageURL} width="12" height="14" ref={this._downloadImage} /> <span className="mx_MFileBody_download_icon" />
{ _t("Download %(text)s", { text: text }) } { _t("Download %(text)s", { text: text }) }
</a> </a>
</div> </div>

View file

@ -1,10 +1,8 @@
function remoteRender(event) { function remoteRender(event) {
const data = event.data; const data = event.data;
const img = document.createElement("img"); const img = document.createElement("span"); // we'll mask it as an image
img.id = "img"; img.id = "img";
img.src = data.imgSrc;
img.style = data.imgStyle;
const a = document.createElement("a"); const a = document.createElement("a");
a.id = "a"; a.id = "a";
@ -16,6 +14,21 @@ function remoteRender(event) {
a.appendChild(img); a.appendChild(img);
a.appendChild(document.createTextNode(data.textContent)); a.appendChild(document.createTextNode(data.textContent));
// Apply image style after so we can steal the anchor's colour.
// Style copied from a rendered version of mx_MFileBody_download_icon
img.style = "" +
"width: 12px; height: 12px;" +
"-webkit-mask-size: 12px;" +
"mask-size: 12px;" +
"-webkit-mask-position: center;" +
"mask-position: center;" +
"-webkit-mask-repeat: no-repeat;" +
"mask-repeat: no-repeat;" +
`-webkit-mask-image: url('${data.imgSrc}');` +
`mask-image: url('${data.imgSrc}');` +
`background-color: ${a.style.color};` +
"display: inline-block;";
const body = document.body; const body = document.body;
// Don't display scrollbars if the link takes more than one line to display. // Don't display scrollbars if the link takes more than one line to display.
body.style = "margin: 0px; overflow: hidden"; body.style = "margin: 0px; overflow: hidden";
@ -26,20 +39,8 @@ function remoteRender(event) {
} }
} }
function remoteSetTint(event) {
const data = event.data;
const img = document.getElementById("img");
img.src = data.imgSrc;
img.style = data.imgStyle;
const a = document.getElementById("a");
a.style = data.style;
}
window.onmessage = function(e) { window.onmessage = function(e) {
if (e.origin === window.location.origin) { if (e.origin === window.location.origin) {
if (e.data.blob) remoteRender(e); if (e.data.blob) remoteRender(e);
else remoteSetTint(e);
} }
}; };