459 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			459 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
| /*
 | |
| Copyright 2015 OpenMarket Ltd
 | |
| Copyright 2017 New Vector Ltd
 | |
| 
 | |
| 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.
 | |
| */
 | |
| 
 | |
| const DEBUG = 0;
 | |
| 
 | |
| // utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
 | |
| function colorToRgb(color) {
 | |
|     if (!color) {
 | |
|         return [0, 0, 0];
 | |
|     }
 | |
| 
 | |
|     if (color[0] === '#') {
 | |
|         color = color.slice(1);
 | |
|         if (color.length === 3) {
 | |
|             color = color[0] + color[0] +
 | |
|                     color[1] + color[1] +
 | |
|                     color[2] + color[2];
 | |
|         }
 | |
|         const val = parseInt(color, 16);
 | |
|         const r = (val >> 16) & 255;
 | |
|         const g = (val >> 8) & 255;
 | |
|         const b = val & 255;
 | |
|         return [r, g, b];
 | |
|     } else {
 | |
|         const match = color.match(/rgb\((.*?),(.*?),(.*?)\)/);
 | |
|         if (match) {
 | |
|             return [
 | |
|                 parseInt(match[1]),
 | |
|                 parseInt(match[2]),
 | |
|                 parseInt(match[3]),
 | |
|             ];
 | |
|         }
 | |
|     }
 | |
|     return [0, 0, 0];
 | |
| }
 | |
| 
 | |
| // utility to turn [red,green,blue] into #rrggbb
 | |
| function rgbToColor(rgb) {
 | |
|     const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
 | |
|     return '#' + (0x1000000 + val).toString(16).slice(1);
 | |
| }
 | |
| 
 | |
| class Tinter {
 | |
|     constructor() {
 | |
|         // The default colour keys to be replaced as referred to in CSS
 | |
|         // (should be overridden by .mx_theme_accentColor and .mx_theme_secondaryAccentColor)
 | |
|         this.keyRgb = [
 | |
|             "rgb(118, 207, 166)", // Vector Green
 | |
|             "rgb(234, 245, 240)", // Vector Light Green
 | |
|             "rgb(211, 239, 225)", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
 | |
|         ];
 | |
| 
 | |
|         // Some algebra workings for calculating the tint % of Vector Green & Light Green
 | |
|         // x * 118 + (1 - x) * 255 = 234
 | |
|         // x * 118 + 255 - 255 * x = 234
 | |
|         // x * 118 - x * 255 = 234 - 255
 | |
|         // (255 - 118) x = 255 - 234
 | |
|         // x = (255 - 234) / (255 - 118) = 0.16
 | |
| 
 | |
|         // The colour keys to be replaced as referred to in SVGs
 | |
|         this.keyHex = [
 | |
|             "#76CFA6", // Vector Green
 | |
|             "#EAF5F0", // Vector Light Green
 | |
|             "#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
 | |
|             "#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
 | |
|             "#000000", // black lowlights of the SVGs (for switching to dark theme)
 | |
|         ];
 | |
| 
 | |
|         // track the replacement colours actually being used
 | |
|         // defaults to our keys.
 | |
|         this.colors = [
 | |
|             this.keyHex[0],
 | |
|             this.keyHex[1],
 | |
|             this.keyHex[2],
 | |
|             this.keyHex[3],
 | |
|             this.keyHex[4],
 | |
|         ];
 | |
| 
 | |
|         // track the most current tint request inputs (which may differ from the
 | |
|         // end result stored in this.colors
 | |
|         this.currentTint = [
 | |
|             undefined,
 | |
|             undefined,
 | |
|             undefined,
 | |
|             undefined,
 | |
|             undefined,
 | |
|         ];
 | |
| 
 | |
|         this.cssFixups = [
 | |
|             // { theme: {
 | |
|             //      style: a style object that should be fixed up taken from a stylesheet
 | |
|             //      attr: name of the attribute to be clobbered, e.g. 'color'
 | |
|             //      index: ordinal of primary, secondary or tertiary
 | |
|             //   },
 | |
|             // }
 | |
|         ];
 | |
| 
 | |
|         // CSS attributes to be fixed up
 | |
|         this.cssAttrs = [
 | |
|             "color",
 | |
|             "backgroundColor",
 | |
|             "borderColor",
 | |
|             "borderTopColor",
 | |
|             "borderBottomColor",
 | |
|             "borderLeftColor",
 | |
|         ];
 | |
| 
 | |
|         this.svgAttrs = [
 | |
|             "fill",
 | |
|             "stroke",
 | |
|         ];
 | |
| 
 | |
|         // List of functions to call when the tint changes.
 | |
|         this.tintables = [];
 | |
| 
 | |
|         // the currently loaded theme (if any)
 | |
|         this.theme = undefined;
 | |
| 
 | |
|         // whether to force a tint (e.g. after changing theme)
 | |
|         this.forceTint = false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 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.
 | |
|      *
 | |
|      * To ensure the tintable work happens at least once, it is also called as
 | |
|      * part of registration.
 | |
|      *
 | |
|      * @param {Function} tintable Function to call when the tint changes.
 | |
|      */
 | |
|     registerTintable(tintable) {
 | |
|         this.tintables.push(tintable);
 | |
|         tintable();
 | |
|     }
 | |
| 
 | |
|     getKeyRgb() {
 | |
|         return this.keyRgb;
 | |
|     }
 | |
| 
 | |
|     tint(primaryColor, secondaryColor, tertiaryColor) {
 | |
|         return;
 | |
|         // eslint-disable-next-line no-unreachable
 | |
|         this.currentTint[0] = primaryColor;
 | |
|         this.currentTint[1] = secondaryColor;
 | |
|         this.currentTint[2] = tertiaryColor;
 | |
| 
 | |
|         this.calcCssFixups();
 | |
| 
 | |
|         if (DEBUG) {
 | |
|             console.log("Tinter.tint(" + primaryColor + ", " +
 | |
|                 secondaryColor + ", " +
 | |
|                 tertiaryColor + ")");
 | |
|         }
 | |
| 
 | |
|         if (!primaryColor) {
 | |
|             primaryColor = this.keyRgb[0];
 | |
|             secondaryColor = this.keyRgb[1];
 | |
|             tertiaryColor = this.keyRgb[2];
 | |
|         }
 | |
| 
 | |
|         if (!secondaryColor) {
 | |
|             const x = 0.16; // average weighting factor calculated from vector green & light green
 | |
|             const rgb = colorToRgb(primaryColor);
 | |
|             rgb[0] = x * rgb[0] + (1 - x) * 255;
 | |
|             rgb[1] = x * rgb[1] + (1 - x) * 255;
 | |
|             rgb[2] = x * rgb[2] + (1 - x) * 255;
 | |
|             secondaryColor = rgbToColor(rgb);
 | |
|         }
 | |
| 
 | |
|         if (!tertiaryColor) {
 | |
|             const x = 0.19;
 | |
|             const rgb1 = colorToRgb(primaryColor);
 | |
|             const rgb2 = colorToRgb(secondaryColor);
 | |
|             rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
 | |
|             rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
 | |
|             rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
 | |
|             tertiaryColor = rgbToColor(rgb1);
 | |
|         }
 | |
| 
 | |
|         if (this.forceTint == false &&
 | |
|             this.colors[0] === primaryColor &&
 | |
|             this.colors[1] === secondaryColor &&
 | |
|             this.colors[2] === tertiaryColor) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         this.forceTint = false;
 | |
| 
 | |
|         this.colors[0] = primaryColor;
 | |
|         this.colors[1] = secondaryColor;
 | |
|         this.colors[2] = tertiaryColor;
 | |
| 
 | |
|         if (DEBUG) {
 | |
|             console.log("Tinter.tint final: (" + primaryColor + ", " +
 | |
|                 secondaryColor + ", " +
 | |
|                 tertiaryColor + ")");
 | |
|         }
 | |
| 
 | |
|         // go through manually fixing up the stylesheets.
 | |
|         this.applyCssFixups();
 | |
| 
 | |
|         // tell all the SVGs to go fix themselves up
 | |
|         // we don't do this as a dispatch otherwise it will visually lag
 | |
|         this.tintables.forEach(function(tintable) {
 | |
|             tintable();
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     tintSvgWhite(whiteColor) {
 | |
|         this.currentTint[3] = whiteColor;
 | |
| 
 | |
|         if (!whiteColor) {
 | |
|             whiteColor = this.colors[3];
 | |
|         }
 | |
|         if (this.colors[3] === whiteColor) {
 | |
|             return;
 | |
|         }
 | |
|         this.colors[3] = whiteColor;
 | |
|         this.tintables.forEach(function(tintable) {
 | |
|             tintable();
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     tintSvgBlack(blackColor) {
 | |
|         this.currentTint[4] = blackColor;
 | |
| 
 | |
|         if (!blackColor) {
 | |
|             blackColor = this.colors[4];
 | |
|         }
 | |
|         if (this.colors[4] === blackColor) {
 | |
|             return;
 | |
|         }
 | |
|         this.colors[4] = blackColor;
 | |
|         this.tintables.forEach(function(tintable) {
 | |
|             tintable();
 | |
|         });
 | |
|     }
 | |
| 
 | |
| 
 | |
|     setTheme(theme) {
 | |
|         this.theme = theme;
 | |
| 
 | |
|         // update keyRgb from the current theme CSS itself, if it defines it
 | |
|         if (document.getElementById('mx_theme_accentColor')) {
 | |
|             this.keyRgb[0] = window.getComputedStyle(
 | |
|                 document.getElementById('mx_theme_accentColor')).color;
 | |
|         }
 | |
|         if (document.getElementById('mx_theme_secondaryAccentColor')) {
 | |
|             this.keyRgb[1] = window.getComputedStyle(
 | |
|                 document.getElementById('mx_theme_secondaryAccentColor')).color;
 | |
|         }
 | |
|         if (document.getElementById('mx_theme_tertiaryAccentColor')) {
 | |
|             this.keyRgb[2] = window.getComputedStyle(
 | |
|                 document.getElementById('mx_theme_tertiaryAccentColor')).color;
 | |
|         }
 | |
| 
 | |
|         this.calcCssFixups();
 | |
|         this.forceTint = true;
 | |
| 
 | |
|         this.tint(this.currentTint[0], this.currentTint[1], this.currentTint[2]);
 | |
| 
 | |
|         if (theme === 'dark') {
 | |
|             // abuse the tinter to change all the SVG's #fff to #2d2d2d
 | |
|             // XXX: obviously this shouldn't be hardcoded here.
 | |
|             this.tintSvgWhite('#2d2d2d');
 | |
|             this.tintSvgBlack('#dddddd');
 | |
|         } else {
 | |
|             this.tintSvgWhite('#ffffff');
 | |
|             this.tintSvgBlack('#000000');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     calcCssFixups() {
 | |
|         // cache our fixups
 | |
|         if (this.cssFixups[this.theme]) return;
 | |
| 
 | |
|         if (DEBUG) {
 | |
|             console.debug("calcCssFixups start for " + this.theme + " (checking " +
 | |
|                 document.styleSheets.length +
 | |
|                 " stylesheets)");
 | |
|         }
 | |
| 
 | |
|         this.cssFixups[this.theme] = [];
 | |
| 
 | |
|         for (let i = 0; i < document.styleSheets.length; i++) {
 | |
|             const ss = document.styleSheets[i];
 | |
|             try {
 | |
|                 if (!ss) continue; // well done safari >:(
 | |
|                 // Chromium apparently sometimes returns null here; unsure why.
 | |
|                 // see $14534907369972FRXBx:matrix.org in HQ
 | |
|                 // ...ah, it's because there's a third party extension like
 | |
|                 // privacybadger inserting its own stylesheet in there with a
 | |
|                 // resource:// URI or something which results in a XSS error.
 | |
|                 // See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
 | |
|                 // ...except some browsers apparently return stylesheets without
 | |
|                 // hrefs, which we have no choice but ignore right now
 | |
| 
 | |
|                 // XXX seriously? we are hardcoding the name of vector's CSS file in
 | |
|                 // here?
 | |
|                 //
 | |
|                 // Why do we need to limit it to vector's CSS file anyway - if there
 | |
|                 // are other CSS files affecting the doc don't we want to apply the
 | |
|                 // same transformations to them?
 | |
|                 //
 | |
|                 // Iterating through the CSS looking for matches to hack on feels
 | |
|                 // pretty horrible anyway. And what if the application skin doesn't use
 | |
|                 // Vector Green as its primary color?
 | |
|                 // --richvdh
 | |
| 
 | |
|                 // Yes, tinting assumes that you are using the Element skin for now.
 | |
|                 // The right solution will be to move the CSS over to react-sdk.
 | |
|                 // And yes, the default assets for the base skin might as well use
 | |
|                 // Vector Green as any other colour.
 | |
|                 // --matthew
 | |
| 
 | |
|                 // stylesheets we don't have permission to access (eg. ones from extensions) have a null
 | |
|                 // href and will throw exceptions if we try to access their rules.
 | |
|                 if (!ss.href || !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
 | |
|                 if (ss.disabled) continue;
 | |
|                 if (!ss.cssRules) continue;
 | |
| 
 | |
|                 if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
 | |
| 
 | |
|                 for (let j = 0; j < ss.cssRules.length; j++) {
 | |
|                     const rule = ss.cssRules[j];
 | |
|                     if (!rule.style) continue;
 | |
|                     if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
 | |
|                     for (let k = 0; k < this.cssAttrs.length; k++) {
 | |
|                         const attr = this.cssAttrs[k];
 | |
|                         for (let l = 0; l < this.keyRgb.length; l++) {
 | |
|                             if (rule.style[attr] === this.keyRgb[l]) {
 | |
|                                 this.cssFixups[this.theme].push({
 | |
|                                     style: rule.style,
 | |
|                                     attr: attr,
 | |
|                                     index: l,
 | |
|                                 });
 | |
|                             }
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|             } catch (e) {
 | |
|                 // Catch any random exceptions that happen here: all sorts of things can go
 | |
|                 // wrong with this (nulls, SecurityErrors) and mostly it's for other
 | |
|                 // stylesheets that we don't want to proces anyway. We should not propagate an
 | |
|                 // exception out since this will cause the app to fail to start.
 | |
|                 console.log("Failed to calculate CSS fixups for a stylesheet: " + ss.href, e);
 | |
|             }
 | |
|         }
 | |
|         if (DEBUG) {
 | |
|             console.log("calcCssFixups end (" +
 | |
|                 this.cssFixups[this.theme].length +
 | |
|                 " fixups)");
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     applyCssFixups() {
 | |
|         if (DEBUG) {
 | |
|             console.log("applyCssFixups start (" +
 | |
|                 this.cssFixups[this.theme].length +
 | |
|                 " fixups)");
 | |
|         }
 | |
|         for (let i = 0; i < this.cssFixups[this.theme].length; i++) {
 | |
|             const cssFixup = this.cssFixups[this.theme][i];
 | |
|             try {
 | |
|                 cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index];
 | |
|             } catch (e) {
 | |
|                 // Firefox Quantum explodes if you manually edit the CSS in the
 | |
|                 // inspector and then try to do a tint, as apparently all the
 | |
|                 // fixups are then stale.
 | |
|                 console.error("Failed to apply cssFixup in Tinter! ", e.name);
 | |
|             }
 | |
|         }
 | |
|         if (DEBUG) console.log("applyCssFixups end");
 | |
|     }
 | |
| 
 | |
|     // XXX: we could just move this all into TintableSvg, but as it's so similar
 | |
|     // to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
 | |
|     // keeping it here for now.
 | |
|     calcSvgFixups(svgs) {
 | |
|         // go through manually fixing up SVG colours.
 | |
|         // we could do this by stylesheets, but keeping the stylesheets
 | |
|         // updated would be a PITA, so just brute-force search for the
 | |
|         // key colour; cache the element and apply.
 | |
| 
 | |
|         if (DEBUG) console.log("calcSvgFixups start for " + svgs);
 | |
|         const fixups = [];
 | |
|         for (let i = 0; i < svgs.length; i++) {
 | |
|             let svgDoc;
 | |
|             try {
 | |
|                 svgDoc = svgs[i].contentDocument;
 | |
|             } catch (e) {
 | |
|                 let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
 | |
|                 if (e.message) {
 | |
|                     msg += e.message;
 | |
|                 }
 | |
|                 if (e.stack) {
 | |
|                     msg += ' | stack: ' + e.stack;
 | |
|                 }
 | |
|                 console.error(msg);
 | |
|             }
 | |
|             if (!svgDoc) continue;
 | |
|             const tags = svgDoc.getElementsByTagName("*");
 | |
|             for (let j = 0; j < tags.length; j++) {
 | |
|                 const tag = tags[j];
 | |
|                 for (let k = 0; k < this.svgAttrs.length; k++) {
 | |
|                     const attr = this.svgAttrs[k];
 | |
|                     for (let l = 0; l < this.keyHex.length; l++) {
 | |
|                         if (tag.getAttribute(attr) &&
 | |
|                             tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
 | |
|                             fixups.push({
 | |
|                                 node: tag,
 | |
|                                 attr: attr,
 | |
|                                 index: l,
 | |
|                             });
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         if (DEBUG) console.log("calcSvgFixups end");
 | |
| 
 | |
|         return fixups;
 | |
|     }
 | |
| 
 | |
|     applySvgFixups(fixups) {
 | |
|         if (DEBUG) console.log("applySvgFixups start for " + fixups);
 | |
|         for (let i = 0; i < fixups.length; i++) {
 | |
|             const svgFixup = fixups[i];
 | |
|             svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
 | |
|         }
 | |
|         if (DEBUG) console.log("applySvgFixups end");
 | |
|     }
 | |
| }
 | |
| 
 | |
| if (global.singletonTinter === undefined) {
 | |
|     global.singletonTinter = new Tinter();
 | |
| }
 | |
| export default global.singletonTinter;
 |