diff --git a/src/SlashCommands.js b/src/SlashCommands.js index f5eaff9066..d8588170b7 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -18,6 +18,7 @@ var MatrixClientPeg = require("./MatrixClientPeg"); var MatrixTools = require("./MatrixTools"); var dis = require("./dispatcher"); var encryption = require("./encryption"); +var Tinter = require("./Tinter"); var reject = function(msg) { return { @@ -42,6 +43,10 @@ var commands = { return reject("Usage: /nick <display_name>"); }, + tint: function(room_id, args) { + Tinter.tint(args); + }, + encrypt: function(room_id, args) { if (args == "on") { var client = MatrixClientPeg.get(); diff --git a/src/Tinter.js b/src/Tinter.js new file mode 100644 index 0000000000..be81abd9c2 --- /dev/null +++ b/src/Tinter.js @@ -0,0 +1,183 @@ +/* +Copyright 2015 OpenMarket 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. +*/ + +var keyRgb = [ + "rgb(118, 207, 166)", + "rgb(234, 245, 240)", + "rgba(118, 207, 166, 0.2)", +]; + +// 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 + +var keyHex = [ + "#76CFA6", + "#EAF5F0", +]; + +var cssFixups = [ + // { + // 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 +var cssAttrs = [ + "color", + "backgroundColor", + "borderColor", +]; + +var svgFixups = [ + // { + // node: a SVG node that needs to be fixed up + // attr: name of the attribute to be clobbered, e.g. 'fill' + // index: ordinal of primary, secondary + // } +]; + +var svgAttrs = [ + "fill", + "stroke", +]; + +var cached = false; + +function calcCssFixups() { + for (var i = 0; i < document.styleSheets.length; i++) { + var ss = document.styleSheets[i]; + for (var j = 0; j < ss.cssRules.length; j++) { + var rule = ss.cssRules[j]; + for (var k = 0; k < cssAttrs.length; k++) { + var attr = cssAttrs[k]; + for (var l = 0; l < keyRgb.length; l++) { + if (rule.style && rule.style[attr] === keyRgb[l]) { + cssFixups.push({ + style: rule.style, + attr: attr, + index: l, + }); + } + } + } + } + } +} + +function calcSvgFixups() { + var svgs = document.getElementsByClassName("mx_Svg"); + for (var i = 0; i < svgs.length; i++) { + var svgDoc = svgs[i].contentDocument; + if (!svgDoc) continue; + var tags = svgDoc.getElementsByTagName("*"); + for (var j = 0; j < tags.length; j++) { + var tag = tags[j]; + for (var k = 0; k < svgAttrs.length; k++) { + var attr = svgAttrs[k]; + for (var l = 0; l < keyHex.length; l++) { + if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) { + svgFixups.push({ + node: tag, + attr: attr, + index: l, + }); + } + } + } + } + } +} + +function applyCssFixups(primaryColor, secondaryColor, tertiaryColor) { + var colors = [primaryColor, secondaryColor, tertiaryColor]; + + for (var i = 0; i < cssFixups.length; i++) { + var cssFixup = cssFixups[i]; + cssFixup.style[cssFixup.attr] = colors[cssFixup.index]; + } +} + +function applySvgFixups(primaryColor, secondaryColor, tertiaryColor) { + var colors = [primaryColor, secondaryColor, tertiaryColor]; + + for (var i = 0; i < svgFixups.length; i++) { + var svgFixup = svgFixups[i]; + svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]); + } +} + +function hexToRgb(color) { + if (color[0] === '#') color = color.slice(1); + if (color.length === 3) { + color = color[0] + color[0] + + color[1] + color[1] + + color[2] + color[2]; + } + var val = parseInt(color, 16); + var r = (val >> 16) & 255; + var g = (val >> 8) & 255; + var b = val & 255; + return [r, g, b]; +} + +function rgbToHex(rgb) { + var val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; + return '#' + (0x1000000 + val).toString(16).slice(1) +} + +module.exports = { + tint: function(primaryColor, secondaryColor, tertiaryColor) { + if (!cached) { + calcCssFixups(); + calcSvgFixups(); + cached = true; + } + + if (!secondaryColor) { + var x = 0.16; // average weighting factor calculated from vector green & light green + var rgb = hexToRgb(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 = rgbToHex(rgb); + } + + if (!tertiaryColor) { + var x = 0.19; + var rgb1 = hexToRgb(primaryColor); + var rgb2 = hexToRgb(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 = rgbToHex(rgb1); + } + + // go through manually fixing up the stylesheets. + applyCssFixups(primaryColor, secondaryColor, tertiaryColor); + + // 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. + applySvgFixups(primaryColor, secondaryColor, tertiaryColor); + } +}; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index ea055e594b..5dec8c6139 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1194,7 +1194,7 @@ module.exports = React.createClass({ <div className="mx_RoomView_tabCompleteWrapper"> <TabCompleteBar entries={this.tabComplete.peek(6)} /> <div className="mx_RoomView_tabCompleteEol" title="->|"> - <object type="image/svg+xml" data="img/eol.svg" width="22" height="16"/> + <object className="mx_Svg" type="image/svg+xml" data="img/eol.svg" width="22" height="16"/> Auto-complete </div> </div> @@ -1268,7 +1268,7 @@ module.exports = React.createClass({ if (this.state.draggingFile) { fileDropTarget = <div className="mx_RoomView_fileDropTarget"> <div className="mx_RoomView_fileDropTargetLabel" title="Drop File Here"> - <object type="image/svg+xml" data="img/upload-big.svg" width="45" height="59"/><br/> + <object className="mx_Svg" type="image/svg+xml" data="img/upload-big.svg" width="45" height="59"/><br/> Drop File Here </div> </div>; @@ -1306,7 +1306,7 @@ module.exports = React.createClass({ if (call.type === "video") { zoomButton = ( <div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title="Fill screen"> - <object type="image/svg+xml" data="img/fullscreen.svg" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }}/> + <object className="mx_Svg" type="image/svg+xml" data="img/fullscreen.svg" width="29" height="22" style={{ marginTop: 1, marginRight: 4 }}/> </div> ); @@ -1338,7 +1338,7 @@ module.exports = React.createClass({ { videoMuteButton } { zoomButton } { statusBar } - <object type="image/svg+xml" className="mx_RoomView_voipChevron" data="img/voip-chevron.svg" width="22" height="17"/> + <object className="mx_Svg" type="image/svg+xml" className="mx_RoomView_voipChevron" data="img/voip-chevron.svg" width="22" height="17"/> </div> } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index d5863cdfef..512b2eb109 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -474,11 +474,11 @@ module.exports = React.createClass({ else { callButton = <div className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title="Voice call"> - <object type="image/svg+xml" data="img/voice.svg" width="16" height="26"/> + <object className="mx_Svg" type="image/svg+xml" data="img/voice.svg" width="16" height="26"/> </div> videoCallButton = <div className="mx_MessageComposer_videocall" onClick={this.onCallClick} title="Video call"> - <object type="image/svg+xml" data="img/call.svg" width="30" height="22"/> + <object className="mx_Svg" type="image/svg+xml" data="img/call.svg" width="30" height="22"/> </div> } @@ -493,7 +493,7 @@ module.exports = React.createClass({ <textarea ref="textarea" rows="1" onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} placeholder="Type a message..." /> </div> <div className="mx_MessageComposer_upload" onClick={this.onUploadClick} title="Upload file"> - <object type="image/svg+xml" data="img/upload.svg" width="19" height="24"/> + <object className="mx_Svg" type="image/svg+xml" data="img/upload.svg" width="19" height="24"/> <input type="file" style={uploadInputStyle} ref="uploadInput" onChange={this.onUploadFileSelected} /> </div> { hangupButton } diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 7f13e8c655..6f1c884f43 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -119,7 +119,7 @@ module.exports = React.createClass({ <div className="mx_RoomHeader_nametext" title={ this.props.room.name }>{ this.props.room.name }</div> { searchStatus } <div className="mx_RoomHeader_settingsButton" title="Settings"> - <object type="image/svg+xml" data="img/settings.svg" width="12" height="12"/> + <object className="mx_Svg" type="image/svg+xml" data="img/settings.svg" width="12" height="12"/> </div> </div> if (topic) topic_el = <div className="mx_RoomHeader_topic" title={topic.getContent().topic}>{ topic.getContent().topic }</div>; @@ -136,7 +136,7 @@ module.exports = React.createClass({ if (this.props.onLeaveClick) { leave_button = <div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onLeaveClick} title="Leave room"> - <object type="image/svg+xml" data="img/leave.svg" + <object className="mx_Svg" type="image/svg+xml" data="img/leave.svg" width="26" height="20"/> </div>; } @@ -145,7 +145,7 @@ module.exports = React.createClass({ if (this.props.onForgetClick) { forget_button = <div className="mx_RoomHeader_button mx_RoomHeader_leaveButton" onClick={this.props.onForgetClick} title="Forget room"> - <object type="image/svg+xml" data="img/leave.svg" + <object className="mx_Svg" type="image/svg+xml" data="img/leave.svg" width="26" height="20"/> </div>; } @@ -167,7 +167,7 @@ module.exports = React.createClass({ { forget_button } { leave_button } <div className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title="Search"> - <object type="image/svg+xml" data="img/search.svg" width="21" height="19"/> + <object className="mx_Svg" type="image/svg+xml" data="img/search.svg" width="21" height="19"/> </div> </div> </div>