From 6ccc825f0d833c0e0c6f92ccce1e8bd50b0c2f81 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Wed, 16 Nov 2016 14:16:51 +0000 Subject: [PATCH] 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 --- src/Tinter.js | 29 +++++++--- src/components/views/elements/TintableSvg.js | 9 ++++ src/components/views/messages/MFileBody.js | 56 +++++++++++++++++++- 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/Tinter.js b/src/Tinter.js index 336fb90fa2..534a1d810b 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -153,7 +153,25 @@ function rgbToHex(rgb) { return '#' + (0x1000000 + val).toString(16).slice(1) } +// List of functions to call when the tint changes. +const tintables = []; + 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) { if (!cached) { @@ -201,12 +219,9 @@ module.exports = { // tell all the SVGs to go fix themselves up // we don't do this as a dispatch otherwise it will visually lag - var TintableSvg = sdk.getComponent("elements.TintableSvg"); - if (TintableSvg.mounts) { - Object.keys(TintableSvg.mounts).forEach((id) => { - TintableSvg.mounts[id].tint(); - }); - } + tintables.forEach(function(tintable) { + tintable(); + }); }, // 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]); } if (DEBUG) console.log("applySvgFixups end"); - }, + } }; diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js index e8be5f3415..0157131506 100644 --- a/src/components/views/elements/TintableSvg.js +++ b/src/components/views/elements/TintableSvg.js @@ -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; diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 3f29915561..60b0653f1f 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -21,7 +21,42 @@ import filesize from 'filesize'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; 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({ displayName: 'MFileBody', @@ -70,6 +105,12 @@ module.exports = React.createClass({ }, 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(); if (content.file !== undefined && this.state.decryptedUrl === null) { 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() { const content = this.props.mxEvent.getContent(); const text = this.presentableTextForFile(content); - var TintableSvg = sdk.getComponent("elements.TintableSvg"); if (content.file !== undefined && this.state.decryptedUrl === null) { // Need to decrypt the attachment @@ -155,7 +207,7 @@ module.exports = React.createClass({
- + Download {text}