Cache the tinted SVGs for MFileBody as data URLs (#559)

* Use a list of callbacks for things that need tinting.

Rather than gutwrenching the internals of TintableSVG inside the Tinter.

* Share a data: url for the tinted download svg in MFileBody

* Check image exists before tinting

* Add comments

* Use fetch+DomParser rather than XMLHttpRequest

* Remove comment about XMLHttpRequest
This commit is contained in:
Mark Haines 2016-11-16 14:16:51 +00:00 committed by GitHub
parent 218ced0276
commit 6ccc825f0d
3 changed files with 85 additions and 9 deletions

View File

@ -153,7 +153,25 @@ function rgbToHex(rgb) {
return '#' + (0x1000000 + val).toString(16).slice(1) return '#' + (0x1000000 + val).toString(16).slice(1)
} }
// List of functions to call when the tint changes.
const tintables = [];
module.exports = { module.exports = {
/**
* Register a callback to fire when the tint changes.
* This is used to rewrite the tintable SVGs with the new tint.
*
* It's not possible to unregister a tintable callback. So this can only be
* used to register a static callback. If a set of tintables will change
* over time then the best bet is to register a single callback for the
* entire set.
*
* @param {Function} tintable Function to call when the tint changes.
*/
registerTintable : function(tintable) {
tintables.push(tintable);
},
tint: function(primaryColor, secondaryColor, tertiaryColor) { tint: function(primaryColor, secondaryColor, tertiaryColor) {
if (!cached) { if (!cached) {
@ -201,12 +219,9 @@ module.exports = {
// tell all the SVGs to go fix themselves up // tell all the SVGs to go fix themselves up
// we don't do this as a dispatch otherwise it will visually lag // we don't do this as a dispatch otherwise it will visually lag
var TintableSvg = sdk.getComponent("elements.TintableSvg"); tintables.forEach(function(tintable) {
if (TintableSvg.mounts) { tintable();
Object.keys(TintableSvg.mounts).forEach((id) => { });
TintableSvg.mounts[id].tint();
});
}
}, },
// XXX: we could just move this all into TintableSvg, but as it's so similar // XXX: we could just move this all into TintableSvg, but as it's so similar
@ -265,5 +280,5 @@ module.exports = {
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]); svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]);
} }
if (DEBUG) console.log("applySvgFixups end"); if (DEBUG) console.log("applySvgFixups end");
}, }
}; };

View File

@ -74,4 +74,13 @@ var TintableSvg = React.createClass({
} }
}); });
// Register with the Tinter so that we will be told if the tint changes
Tinter.registerTintable(function() {
if (TintableSvg.mounts) {
Object.keys(TintableSvg.mounts).forEach((id) => {
TintableSvg.mounts[id].tint();
});
}
});
module.exports = TintableSvg; module.exports = TintableSvg;

View File

@ -21,7 +21,42 @@ import filesize from 'filesize';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import {decryptFile} from '../../../utils/DecryptFile'; import {decryptFile} from '../../../utils/DecryptFile';
import Tinter from '../../../Tinter';
import 'isomorphic-fetch';
import q from 'q';
// A cached tinted copy of "img/download.svg"
var tintedDownloadImageURL;
// Track a list of mounted MFileBody instances so that we can update
// the "img/download.svg" when the tint changes.
var nextMountId = 0;
const mounts = {};
/**
* Updates the tinted copy of "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.
q(fetch("img/download.svg")).then(function(response) {
return response.text();
}).then(function(svgText) {
const svg = new DOMParser().parseFromString(svgText, "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();
});
}).done();
}
Tinter.registerTintable(updateTintedDownloadImage);
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MFileBody', displayName: 'MFileBody',
@ -70,6 +105,12 @@ module.exports = React.createClass({
}, },
componentDidMount: function() { componentDidMount: function() {
// Add this to the list of mounted components to receive notifications
// when the tint changes.
this.id = nextMountId++;
mounts[this.id] = this;
this.tint();
// Check whether we need to decrypt the file content.
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) { if (content.file !== undefined && this.state.decryptedUrl === null) {
decryptFile(content.file).done((url) => { decryptFile(content.file).done((url) => {
@ -84,12 +125,23 @@ module.exports = React.createClass({
} }
}, },
componentWillUnmount: function() {
// Remove this from the list of mounted components
delete mounts[this.id];
},
tint: function() {
// Update our tinted copy of "img/download.svg"
if (this.refs.downloadImage) {
this.refs.downloadImage.src = tintedDownloadImageURL;
}
},
render: function() { render: function() {
const content = this.props.mxEvent.getContent(); const content = this.props.mxEvent.getContent();
const text = this.presentableTextForFile(content); const text = this.presentableTextForFile(content);
var TintableSvg = sdk.getComponent("elements.TintableSvg");
if (content.file !== undefined && this.state.decryptedUrl === null) { if (content.file !== undefined && this.state.decryptedUrl === null) {
// Need to decrypt the attachment // Need to decrypt the attachment
@ -155,7 +207,7 @@ module.exports = React.createClass({
<span className="mx_MFileBody"> <span className="mx_MFileBody">
<div className="mx_MImageBody_download"> <div className="mx_MImageBody_download">
<a href={contentUrl} target="_blank" rel="noopener" download={downloadAttr}> <a href={contentUrl} target="_blank" rel="noopener" download={downloadAttr}>
<TintableSvg src="img/download.svg" width="12" height="14"/> <img src={tintedDownloadImageURL} width="12" height="14" ref="downloadImage"/>
Download {text} Download {text}
</a> </a>
</div> </div>