diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 0c3d5b3775..7a4f0b99b0 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -93,6 +93,7 @@ class MatrixClientPeg { const opts = utils.deepCopy(this.opts); // the react sdk doesn't work without this, so don't allow opts.pendingEventOrdering = "detached"; + opts.disablePresence = true; // we do this manually try { const promise = this.matrixClient.store.startup(); diff --git a/src/Presence.js b/src/Presence.js index fab518e1cb..2652c64c96 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -56,13 +56,27 @@ class Presence { return this.state; } + /** + * Get the current status message. + * @returns {String} the status message, may be null + */ + getStatusMessage() { + return this.statusMessage; + } + /** * Set the presence state. * If the state has changed, the Home Server will be notified. * @param {string} newState the new presence state (see PRESENCE enum) + * @param {String} statusMessage an optional status message for the presence + * @param {boolean} maintain true to have this status maintained by this tracker */ - setState(newState) { - if (newState === this.state) { + setState(newState, statusMessage=null, maintain=false) { + if (this.maintain) { + // Don't update presence if we're maintaining a particular status + return; + } + if (newState === this.state && statusMessage === this.statusMessage) { return; } if (PRESENCE_STATES.indexOf(newState) === -1) { @@ -72,21 +86,37 @@ class Presence { return; } const old_state = this.state; + const old_message = this.statusMessage; this.state = newState; + this.statusMessage = statusMessage; + this.maintain = maintain; if (MatrixClientPeg.get().isGuest()) { return; // don't try to set presence when a guest; it won't work. } + const updateContent = { + presence: this.state, + status_msg: this.statusMessage ? this.statusMessage : '', + }; + const self = this; - MatrixClientPeg.get().setPresence(this.state).done(function() { + MatrixClientPeg.get().setPresence(updateContent).done(function() { console.log("Presence: %s", newState); + + // We have to dispatch because the js-sdk is unreliable at telling us about our own presence + dis.dispatch({action: "self_presence_updated", statusInfo: updateContent}); }, function(err) { console.error("Failed to set presence: %s", err); self.state = old_state; + self.statusMessage = old_message; }); } + stopMaintainingStatus() { + this.maintain = false; + } + /** * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. * @private @@ -95,7 +125,8 @@ class Presence { this.setState("unavailable"); } - _onUserActivity() { + _onUserActivity(payload) { + if (payload.action === "sync_state" || payload.action === "self_presence_updated") return; this._resetTimer(); } diff --git a/src/Roles.js b/src/Roles.js index 83d8192c67..438b6c1236 100644 --- a/src/Roles.js +++ b/src/Roles.js @@ -15,19 +15,20 @@ limitations under the License. */ import { _t } from './languageHandler'; -export function levelRoleMap() { +export function levelRoleMap(usersDefault) { return { undefined: _t('Default'), - 0: _t('User'), + 0: _t('Restricted'), + [usersDefault]: _t('Default'), 50: _t('Moderator'), 100: _t('Admin'), }; } -export function textualPowerLevel(level, userDefault) { - const LEVEL_ROLE_MAP = this.levelRoleMap(); +export function textualPowerLevel(level, usersDefault) { + const LEVEL_ROLE_MAP = this.levelRoleMap(usersDefault); if (LEVEL_ROLE_MAP[level]) { - return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); + return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`); } else { return level; } diff --git a/src/Tinter.js b/src/Tinter.js index fe4cafe744..f2a02b6e6d 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -17,23 +17,34 @@ limitations under the License. const DEBUG = 0; -// utility to turn #rrggbb into [red,green,blue] -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]; +// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue] +function colorToRgb(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]; + } + const val = parseInt(color, 16); + const r = (val >> 16) & 255; + const g = (val >> 8) & 255; + const b = val & 255; + return [r, g, b]; } - const val = parseInt(color, 16); - const r = (val >> 16) & 255; - const g = (val >> 8) & 255; - const b = val & 255; - return [r, g, b]; + else { + let 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 rgbToHex(rgb) { +function rgbToColor(rgb) { const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2]; return '#' + (0x1000000 + val).toString(16).slice(1); } @@ -45,7 +56,7 @@ class Tinter { this.keyRgb = [ "rgb(118, 207, 166)", // Vector Green "rgb(234, 245, 240)", // Vector Light Green - "rgb(211, 239, 225)", // Unused: BottomLeftMenu (20% Green overlaid on 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 @@ -59,11 +70,11 @@ class Tinter { this.keyHex = [ "#76CFA6", // Vector Green "#EAF5F0", // Vector Light Green - "#D3EFE1", // Unused: BottomLeftMenu (20% Green overlaid on Light Green) + "#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green) "#FFFFFF", // white highlights of the SVGs (for switching to dark theme) ]; - // cache of our replacement colours + // track the replacement colours actually being used // defaults to our keys. this.colors = [ this.keyHex[0], @@ -72,6 +83,15 @@ class Tinter { this.keyHex[3], ]; + // track the most current tint request inputs (which may differ from the + // end result stored in this.colors + this.currentTint = [ + undefined, + undefined, + undefined, + undefined, + ]; + this.cssFixups = [ // { theme: { // style: a style object that should be fixed up taken from a stylesheet @@ -101,6 +121,9 @@ class Tinter { // the currently loaded theme (if any) this.theme = undefined; + + // whether to force a tint (e.g. after changing theme) + this.forceTint = false; } /** @@ -122,48 +145,58 @@ class Tinter { return this.keyRgb; } - getCurrentColors() { - return this.colors; - } - tint(primaryColor, secondaryColor, tertiaryColor) { + 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 = hexToRgb(primaryColor); + 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 = rgbToHex(rgb); + secondaryColor = rgbToColor(rgb); } if (!tertiaryColor) { const x = 0.19; - const rgb1 = hexToRgb(primaryColor); - const rgb2 = hexToRgb(secondaryColor); + 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 = rgbToHex(rgb1); + tertiaryColor = rgbToColor(rgb1); } - if (this.colors[0] === primaryColor && + 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"); + if (DEBUG) console.log("Tinter.tint final: (" + primaryColor + ", " + + secondaryColor + ", " + + tertiaryColor + ")"); // go through manually fixing up the stylesheets. this.applyCssFixups(); @@ -176,6 +209,8 @@ class Tinter { } tintSvgWhite(whiteColor) { + this.currentTint[3] = whiteColor; + if (!whiteColor) { whiteColor = this.colors[3]; } @@ -202,15 +237,31 @@ class Tinter { 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'); + } else { + this.tintSvgWhite('#ffffff'); + } } calcCssFixups() { // cache our fixups if (this.cssFixups[this.theme]) return; - if (DEBUG) console.trace("calcCssFixups start for " + this.theme + " (checking " + + if (DEBUG) console.debug("calcCssFixups start for " + this.theme + " (checking " + document.styleSheets.length + " stylesheets)"); @@ -279,7 +330,15 @@ class Tinter { " fixups)"); for (let i = 0; i < this.cssFixups[this.theme].length; i++) { const cssFixup = this.cssFixups[this.theme][i]; - cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index]; + 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"); } diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index c3ad7f9cd1..3c2308e6a7 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -33,6 +33,7 @@ module.exports = { menuHeight: React.PropTypes.number, chevronOffset: React.PropTypes.number, menuColour: React.PropTypes.string, + chevronFace: React.PropTypes.string, // top, bottom, left, right }, getOrCreateContainer: function() { @@ -58,12 +59,30 @@ module.exports = { } }; - const position = { - top: props.top, - }; + const position = {}; + let chevronFace = null; + + if (props.top) { + position.top = props.top; + } else { + position.bottom = props.bottom; + } + + if (props.left) { + position.left = props.left; + chevronFace = 'left'; + } else { + position.right = props.right; + chevronFace = 'right'; + } const chevronOffset = {}; - if (props.chevronOffset) { + if (props.chevronFace) { + chevronFace = props.chevronFace; + } + if (chevronFace === 'top' || chevronFace === 'bottom') { + chevronOffset.left = props.chevronOffset; + } else { chevronOffset.top = props.chevronOffset; } @@ -74,28 +93,27 @@ module.exports = { .mx_ContextualMenu_chevron_left:after { border-right-color: ${props.menuColour}; } - .mx_ContextualMenu_chevron_right:after { border-left-color: ${props.menuColour}; } + .mx_ContextualMenu_chevron_top:after { + border-left-color: ${props.menuColour}; + } + .mx_ContextualMenu_chevron_bottom:after { + border-left-color: ${props.menuColour}; + } `; } - let chevron = null; - if (props.left) { - chevron =
; - position.left = props.left; - } else { - chevron = ; - position.right = props.right; - } - + const chevron = ; const className = 'mx_ContextualMenu_wrapper'; const menuClasses = classNames({ 'mx_ContextualMenu': true, - 'mx_ContextualMenu_left': props.left, - 'mx_ContextualMenu_right': !props.left, + 'mx_ContextualMenu_left': chevronFace === 'left', + 'mx_ContextualMenu_right': chevronFace === 'right', + 'mx_ContextualMenu_top': chevronFace === 'top', + 'mx_ContextualMenu_bottom': chevronFace === 'bottom', }); const menuStyle = {}; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 38958b473d..f50606664b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -288,7 +288,9 @@ module.exports = React.createClass({ this.handleResize(); window.addEventListener('resize', this.handleResize); - // check we have the right tint applied for this theme + // check we have the right tint applied for this theme. + // N.B. we don't call the whole of setTheme() here as we may be + // racing with the theme CSS download finishing from index.js Tinter.tint(); }, @@ -912,22 +914,44 @@ module.exports = React.createClass({ // disable all of them first, then enable the one we want. Chrome only // bothers to do an update on a true->false transition, so this ensures // that we get exactly one update, at the right time. + // + // ^ This comment was true when we used to use alternative stylesheets + // for the CSS. Nowadays we just set them all as disabled in index.html + // and enable them as needed. It might be cleaner to disable them all + // at the same time to prevent loading two themes simultaneously and + // having them interact badly... but this causes a flash of unstyled app + // which is even uglier. So we don't. - Object.values(styleElements).forEach((a) => { - a.disabled = true; - }); styleElements[theme].disabled = false; - Tinter.setTheme(theme); - const colors = Tinter.getCurrentColors(); - Tinter.tint(colors[0], colors[1]); + const switchTheme = function() { + Object.values(styleElements).forEach((a) => { + if (a == styleElements[theme]) return; + a.disabled = true; + }); + Tinter.setTheme(theme); + }; - if (theme === 'dark') { - // abuse the tinter to change all the SVG's #fff to #2d2d2d - // XXX: obviously this shouldn't be hardcoded here. - Tinter.tintSvgWhite('#2d2d2d'); - } else { - Tinter.tintSvgWhite('#ffffff'); + // turns out that Firefox preloads the CSS for link elements with + // the disabled attribute, but Chrome doesn't. + + let cssLoaded = false; + + styleElements[theme].onload = () => { + switchTheme(); + }; + + for (let i = 0; i < document.styleSheets.length; i++) { + const ss = document.styleSheets[i]; + if (ss && ss.href === styleElements[theme].href) { + cssLoaded = true; + break; + } + } + + if (cssLoaded) { + styleElements[theme].onload = undefined; + switchTheme(); } }, diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index d762a85042..bd0afcb335 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -370,6 +370,9 @@ module.exports = React.createClass({ } */ + let serverConfig; + let header; + if (!SdkConfig.get().disable_custom_urls) { serverConfig =