From 6cd07731c4b35bcaae70d026d3890f7b36a6e87e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 14:37:47 -0600 Subject: [PATCH 01/23] Add MemberPresenceAvatar and control presence ourselves Includes rudimentary support for custom statuses and user-controlled status. Some minor tweaks have also been made to better control how we advertise our presence. Signed-off-by: Travis Ralston --- src/MatrixClientPeg.js | 1 + src/Presence.js | 39 ++++- .../views/avatars/MemberPresenceAvatar.js | 135 ++++++++++++++++++ src/components/views/rooms/MessageComposer.js | 4 +- 4 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 src/components/views/avatars/MemberPresenceAvatar.js 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/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js new file mode 100644 index 0000000000..e90f2e3e62 --- /dev/null +++ b/src/components/views/avatars/MemberPresenceAvatar.js @@ -0,0 +1,135 @@ +/* + Copyright 2017 Travis Ralston + + 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. + */ + +'use strict'; + +import React from "react"; +import * as sdk from "../../../index"; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import AccessibleButton from '../elements/AccessibleButton'; +import Presence from "../../../Presence"; +import dispatcher from "../../../dispatcher"; + +module.exports = React.createClass({ + displayName: 'MemberPresenceAvatar', + + propTypes: { + member: React.PropTypes.object.isRequired, + width: React.PropTypes.number, + height: React.PropTypes.number, + resizeMethod: React.PropTypes.string, + }, + + getDefaultProps: function() { + return { + width: 40, + height: 40, + resizeMethod: 'crop', + }; + }, + + getInitialState: function() { + const presenceState = this.props.member.user.presence; + return { + status: presenceState, + }; + }, + + componentWillMount: function() { + MatrixClientPeg.get().on("User.presence", this.onUserPresence); + this.dispatcherRef = dispatcher.register(this.onAction); + }, + + componentWillUnmount: function() { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("User.presence", this.onUserPresence); + } + dispatcher.unregister(this.dispatcherRef); + }, + + onAction: function(payload) { + if (payload.action !== "self_presence_updated") return; + if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) return; + this.setState({ + status: payload.statusInfo.presence, + message: payload.statusInfo.status_msg, + }); + }, + + onUserPresence: function(event, user) { + if (user.userId !== MatrixClientPeg.get().getUserId()) return; + this.setState({ + status: user.presence, + message: user.presenceStatusMsg, + }); + }, + + onClick: function() { + if (Presence.getState() === "online") { + Presence.setState("unavailable", "This is a message", true); + } else { + Presence.stopMaintainingStatus(); + } + console.log("CLICK"); + + const presenceState = this.props.member.user.presence; + const presenceLastActiveAgo = this.props.member.user.lastActiveAgo; + const presenceLastTs = this.props.member.user.lastPresenceTs; + const presenceCurrentlyActive = this.props.member.user.currentlyActive; + const presenceMessage = this.props.member.user.presenceStatusMsg; + + console.log({ + presenceState, + presenceLastActiveAgo, + presenceLastTs, + presenceCurrentlyActive, + presenceMessage, + }); + }, + + render: function() { + const MemberAvatar = sdk.getComponent("avatars.MemberAvatar"); + + let onClickFn = null; + if (this.props.member.userId === MatrixClientPeg.get().getUserId()) { + onClickFn = this.onClick; + } + + const avatarNode = ( + + ); + const statusNode = ( + + ); + + let avatar = ( +
+ {avatarNode} + {statusNode} +
+ ); + if (onClickFn) { + avatar = ( + + {avatarNode} + {statusNode} + + ); + } + return avatar; + }, +}); diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 8e27520d89..d06cd76bdb 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -238,7 +238,7 @@ export default class MessageComposer extends React.Component { render() { const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); const uploadInputStyle = {display: 'none'}; - const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + const MemberPresenceAvatar = sdk.getComponent('avatars.MemberPresenceAvatar'); const TintableSvg = sdk.getComponent("elements.TintableSvg"); const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput"); @@ -246,7 +246,7 @@ export default class MessageComposer extends React.Component { controls.push(
- +
, ); From 0b20681f6a2963a260f4c2031123a5b3975995d4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 19:13:46 -0600 Subject: [PATCH 02/23] Put presence management behind a labs setting Signed-off-by: Travis Ralston --- src/UserSettingsStore.js | 4 ++++ src/components/views/avatars/MemberPresenceAvatar.js | 9 ++++++++- src/i18n/strings/en_EN.json | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index b274e6a594..cb4d184eff 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -34,6 +34,10 @@ const FEATURES = [ id: 'feature_pinning', name: _td("Message Pinning"), }, + { + id: 'feature_presence_management', + name: _td("Presence Management"), + }, ]; export default { diff --git a/src/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js index e90f2e3e62..486688250e 100644 --- a/src/components/views/avatars/MemberPresenceAvatar.js +++ b/src/components/views/avatars/MemberPresenceAvatar.js @@ -22,6 +22,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import AccessibleButton from '../elements/AccessibleButton'; import Presence from "../../../Presence"; import dispatcher from "../../../dispatcher"; +import UserSettingsStore from "../../../UserSettingsStore"; module.exports = React.createClass({ displayName: 'MemberPresenceAvatar', @@ -112,10 +113,16 @@ module.exports = React.createClass({ ); - const statusNode = ( + let statusNode = ( ); + // LABS: Disable presence management functions for now + if (!UserSettingsStore.isFeatureEnabled("feature_presence_management")) { + statusNode = null; + onClickFn = null; + } + let avatar = (
{avatarNode} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index df236636a2..1a9e59e3cd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -620,6 +620,7 @@ "Cancel": "Cancel", "or": "or", "Message Pinning": "Message Pinning", + "Presence Management": "Presence Management", "Active call": "Active call", "Monday": "Monday", "Tuesday": "Tuesday", From 788e16a716dcef588f5d73cfc799df3dd91972c4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 20:23:50 -0600 Subject: [PATCH 03/23] Linting Signed-off-by: Travis Ralston --- src/components/views/avatars/MemberPresenceAvatar.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js index 486688250e..8005dcd405 100644 --- a/src/components/views/avatars/MemberPresenceAvatar.js +++ b/src/components/views/avatars/MemberPresenceAvatar.js @@ -111,10 +111,10 @@ module.exports = React.createClass({ const avatarNode = ( + resizeMethod={this.props.resizeMethod} /> ); let statusNode = ( - + ); // LABS: Disable presence management functions for now @@ -125,15 +125,15 @@ module.exports = React.createClass({ let avatar = (
- {avatarNode} - {statusNode} + { avatarNode } + { statusNode }
); if (onClickFn) { avatar = ( - {avatarNode} - {statusNode} + { avatarNode } + { statusNode } ); } From 03800b747608873f242446a330ba2387db2f871d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 21:43:47 -0600 Subject: [PATCH 04/23] Support more positioning options on context menus Signed-off-by: Travis Ralston --- src/components/structures/ContextualMenu.js | 50 ++++++++++++++------- 1 file changed, 34 insertions(+), 16 deletions(-) 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 = {}; From c4837172821bed0be652abd64663c46c031f60c0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 21:44:07 -0600 Subject: [PATCH 05/23] Make onClick be a context menu for presence Signed-off-by: Travis Ralston --- .../views/avatars/MemberPresenceAvatar.js | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js index 8005dcd405..de7c28154f 100644 --- a/src/components/views/avatars/MemberPresenceAvatar.js +++ b/src/components/views/avatars/MemberPresenceAvatar.js @@ -23,6 +23,7 @@ import AccessibleButton from '../elements/AccessibleButton'; import Presence from "../../../Presence"; import dispatcher from "../../../dispatcher"; import UserSettingsStore from "../../../UserSettingsStore"; +import * as ContextualMenu from "../../structures/ContextualMenu"; module.exports = React.createClass({ displayName: 'MemberPresenceAvatar', @@ -44,8 +45,10 @@ module.exports = React.createClass({ getInitialState: function() { const presenceState = this.props.member.user.presence; + const presenceMessage = this.props.member.user.presenceStatusMsg; return { status: presenceState, + message: presenceMessage, }; }, @@ -78,27 +81,38 @@ module.exports = React.createClass({ }); }, - onClick: function() { - if (Presence.getState() === "online") { - Presence.setState("unavailable", "This is a message", true); - } else { - Presence.stopMaintainingStatus(); - } - console.log("CLICK"); + onStatusChange: function(newStatus) { + console.log(this.state); + console.log(newStatus); + }, - const presenceState = this.props.member.user.presence; - const presenceLastActiveAgo = this.props.member.user.lastActiveAgo; - const presenceLastTs = this.props.member.user.lastPresenceTs; - const presenceCurrentlyActive = this.props.member.user.currentlyActive; - const presenceMessage = this.props.member.user.presenceStatusMsg; + onClick: function(e) { + const PresenceContextMenu = sdk.getComponent('context_menus.PresenceContextMenu'); + const elementRect = e.target.getBoundingClientRect(); - console.log({ - presenceState, - presenceLastActiveAgo, - presenceLastTs, - presenceCurrentlyActive, - presenceMessage, + // The window X and Y offsets are to adjust position when zoomed in to page + const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3; + const chevronOffset = 12; + let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; + y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron + + const self = this; + ContextualMenu.createMenu(PresenceContextMenu, { + chevronOffset: chevronOffset, + chevronFace: 'bottom', + left: x, + top: y, + menuWidth: 300, + currentStatus: this.state.status, + onChange: this.onStatusChange, }); + + e.stopPropagation(); + // const presenceState = this.props.member.user.presence; + // const presenceLastActiveAgo = this.props.member.user.lastActiveAgo; + // const presenceLastTs = this.props.member.user.lastPresenceTs; + // const presenceCurrentlyActive = this.props.member.user.currentlyActive; + // const presenceMessage = this.props.member.user.presenceStatusMsg; }, render: function() { From 7307bc412f794e06f4035c9210e6394f9158ce17 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 14 Oct 2017 23:16:12 -0600 Subject: [PATCH 06/23] Respond to updates from presence context menu Signed-off-by: Travis Ralston --- src/components/views/avatars/MemberPresenceAvatar.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js index de7c28154f..19342f3492 100644 --- a/src/components/views/avatars/MemberPresenceAvatar.js +++ b/src/components/views/avatars/MemberPresenceAvatar.js @@ -82,8 +82,10 @@ module.exports = React.createClass({ }, onStatusChange: function(newStatus) { - console.log(this.state); - console.log(newStatus); + Presence.stopMaintainingStatus(); + if (newStatus === "online") { + Presence.setState(newStatus); + } else Presence.setState(newStatus, null, true); }, onClick: function(e) { @@ -96,13 +98,12 @@ module.exports = React.createClass({ let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset; y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron - const self = this; ContextualMenu.createMenu(PresenceContextMenu, { chevronOffset: chevronOffset, chevronFace: 'bottom', left: x, top: y, - menuWidth: 300, + menuWidth: 125, currentStatus: this.state.status, onChange: this.onStatusChange, }); From 5c9970950bde6581fae7623fd8b656b47e81f2ba Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Oct 2017 11:21:12 -0600 Subject: [PATCH 07/23] Undo i18n change to make merge easier Signed-off-by: Travis Ralston --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1a9e59e3cd..df236636a2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -620,7 +620,6 @@ "Cancel": "Cancel", "or": "or", "Message Pinning": "Message Pinning", - "Presence Management": "Presence Management", "Active call": "Active call", "Monday": "Monday", "Tuesday": "Tuesday", From 24000ee45689d397f35249ec8c66418e10b5741b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Oct 2017 11:23:04 -0600 Subject: [PATCH 08/23] Re-add i18n for labs Signed-off-by: Travis Ralston --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fbff51299a..a33560a052 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -152,6 +152,7 @@ "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", "Communities": "Communities", "Message Pinning": "Message Pinning", + "Presence Management": "Presence Management", "%(displayName)s is typing": "%(displayName)s is typing", "%(names)s and one other are typing": "%(names)s and one other are typing", "%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing", From 88010fa26cf658fa9445d0a0c003a9a902266129 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 13 Nov 2017 11:57:34 +0000 Subject: [PATCH 09/23] Determine whether power level is custom once Roles have been determined Instead of potentially inspecting an empty {} before mounting. This fixes an issue where "Custom of N" would appear on the first mount of MemberInfo - part of https://github.com/vector-im/riot-web/issues/5107#issuecomment-331882294 --- .../views/elements/PowerSelector.js | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index a0aaa12ff1..50893850c1 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -20,9 +20,6 @@ import React from 'react'; import * as Roles from '../../../Roles'; import { _t } from '../../../languageHandler'; -let LEVEL_ROLE_MAP = {}; -const reverseRoles = {}; - module.exports = React.createClass({ displayName: 'PowerSelector', @@ -43,15 +40,23 @@ module.exports = React.createClass({ getInitialState: function() { return { - custom: (LEVEL_ROLE_MAP[this.props.value] === undefined), + levelRoleMap: {}, + reverseRoles: {}, }; }, componentWillMount: function() { - LEVEL_ROLE_MAP = Roles.levelRoleMap(); - Object.keys(LEVEL_ROLE_MAP).forEach(function(key) { - reverseRoles[LEVEL_ROLE_MAP[key]] = key; - }); + // This needs to be done now because levelRoleMap has translated strings + const levelRoleMap = Roles.levelRoleMap(); + const reverseRoles = {}; + Object.keys(levelRoleMap).forEach(function(key) { + reverseRoles[levelRoleMap[key]] = key; + }); + this.setState({ + levelRoleMap, + reverseRoles, + custom: levelRoleMap[this.props.value] === undefined, + }); }, onSelectChange: function(event) { @@ -74,7 +79,7 @@ module.exports = React.createClass({ getValue: function() { let value; if (this.refs.select) { - value = reverseRoles[this.refs.select.value]; + value = this.state.reverseRoles[this.refs.select.value]; if (this.refs.custom) { if (value === undefined) value = parseInt( this.refs.custom.value ); } @@ -98,17 +103,17 @@ module.exports = React.createClass({ if (this.state.custom) { selectValue = "Custom"; } else { - selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom"; + selectValue = this.state.levelRoleMap[this.props.value] || "Custom"; } let select; if (this.props.disabled) { select = { selectValue }; } else { - // Each level must have a definition in LEVEL_ROLE_MAP + // Each level must have a definition in this.state.levelRoleMap const levels = [0, 50, 100]; let options = levels.map((level) => { return { - value: LEVEL_ROLE_MAP[level], + value: this.state.levelRoleMap[level], // Give a userDefault (users_default in the power event) of 0 but // because level !== undefined, this should never be used. text: Roles.textualPowerLevel(level, 0), From 52af7a7659635665734c2278acb5d0f921bae1d4 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Mon, 13 Nov 2017 12:08:53 +0000 Subject: [PATCH 10/23] Attempt to clarify the affect that the users_default has on power levels This modifies displayed power levels such that: - If users_default is !== 0: - the power level 0 is displayed as "Restricted (0)" - the power level users_default is displayed as "Default ({users_default})" - Otherwise: - the power level 0 is displayed as "Default (0)" When changing users_default, to say, 10, when the textual powers are rendered again, they will take users_default into account. So those previously at 10 and which would have previously have been rendered "Custom of 10" will now read "Default (10)". Conversely, those that were "Default (0)" will now read "Restricted (0)". --- src/Roles.js | 11 ++-- .../views/elements/PowerSelector.js | 52 ++++++++++++++++--- src/components/views/rooms/MemberInfo.js | 21 ++++---- src/components/views/rooms/RoomSettings.js | 16 +++--- src/i18n/strings/en_EN.json | 4 +- 5 files changed, 72 insertions(+), 32 deletions(-) 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/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index 50893850c1..6a31259494 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -25,6 +25,11 @@ module.exports = React.createClass({ propTypes: { value: React.PropTypes.number.isRequired, + // The maximum value that can be set with the power selector + maxValue: React.PropTypes.number.isRequired, + + // Default user power level for the room + usersDefault: React.PropTypes.number.isRequired, // if true, the ; + input = ; } customPicker = of { input }; } @@ -110,13 +149,10 @@ module.exports = React.createClass({ select = { selectValue }; } else { // Each level must have a definition in this.state.levelRoleMap - const levels = [0, 50, 100]; - let options = levels.map((level) => { + let options = this.state.options.map((level) => { return { value: this.state.levelRoleMap[level], - // Give a userDefault (users_default in the power event) of 0 but - // because level !== undefined, this should never be used. - text: Roles.textualPowerLevel(level, 0), + text: Roles.textualPowerLevel(level, this.props.usersDefault), }; }); options.push({ value: "Custom", text: _t("Custom level") }); diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index c043b3714d..4d875ea24a 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -494,7 +494,6 @@ module.exports = withMatrixClient(React.createClass({ const defaultPerms = { can: {}, muted: false, - modifyLevel: false, }; const room = this.props.matrixClient.getRoom(member.roomId); if (!room) return defaultPerms; @@ -516,13 +515,15 @@ module.exports = withMatrixClient(React.createClass({ }, _calculateCanPermissions: function(me, them, powerLevels) { + const isMe = me.userId === them.userId; const can = { kick: false, ban: false, mute: false, modifyLevel: false, + modifyLevelMax: 0, }; - const canAffectUser = them.powerLevel < me.powerLevel; + const canAffectUser = them.powerLevel < me.powerLevel || isMe; if (!canAffectUser) { //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); return can; @@ -531,16 +532,13 @@ module.exports = withMatrixClient(React.createClass({ (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || powerLevels.state_default ); - const levelToSend = ( - (powerLevels.events ? powerLevels.events["m.room.message"] : null) || - powerLevels.events_default - ); can.kick = me.powerLevel >= powerLevels.kick; can.ban = me.powerLevel >= powerLevels.ban; can.mute = me.powerLevel >= editPowerLevel; - can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend; - can.modifyLevel = me.powerLevel > them.powerLevel && me.powerLevel >= editPowerLevel; + can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel); + can.modifyLevelMax = me.powerLevel; + return can; }, @@ -832,8 +830,11 @@ module.exports = withMatrixClient(React.createClass({ presenceCurrentlyActive = this.props.member.user.currentlyActive; } - let roomMemberDetails = null; + const room = this.props.matrixClient.getRoom(this.props.member.roomId); + const poweLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null; + const powerLevelUsersDefault = poweLevelEvent.getContent().users_default; + let roomMemberDetails = null; if (this.props.member.roomId) { // is in room const PowerSelector = sdk.getComponent('elements.PowerSelector'); const PresenceLabel = sdk.getComponent('rooms.PresenceLabel'); @@ -842,7 +843,9 @@ module.exports = withMatrixClient(React.createClass({ { _t("Level:") }
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index c7e839ab40..40a3209362 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -910,31 +910,31 @@ module.exports = React.createClass({
{ _t('The default role for new room members is') } - +
{ _t('To send messages, you must be a') } - +
{ _t('To invite users into the room, you must be a') } - +
{ _t('To configure the room, you must be a') } - +
{ _t('To kick users, you must be a') } - +
{ _t('To ban users, you must be a') } - +
{ _t('To remove other users\' messages, you must be a') } - +
{ Object.keys(events_levels).map(function(event_type, i) { @@ -944,7 +944,7 @@ module.exports = React.createClass({ return (
{ label } -
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4052d098c1..ae74da0a18 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -63,7 +63,7 @@ "This email address was not found": "This email address was not found", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.", "Default": "Default", - "User": "User", + "Restricted": "Restricted", "Moderator": "Moderator", "Admin": "Admin", "Start a chat": "Start a chat", @@ -150,7 +150,6 @@ "%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", - "Communities": "Communities", "Message Pinning": "Message Pinning", "%(displayName)s is typing": "%(displayName)s is typing", "%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing", @@ -524,6 +523,7 @@ "Unverify": "Unverify", "Verify...": "Verify...", "No results": "No results", + "Communities": "Communities", "Home": "Home", "Integrations Error": "Integrations Error", "Could not connect to the integration server": "Could not connect to the integration server", From d2ef6bffa88c954b222f467af67a09f2df00f19d Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 14 Nov 2017 12:02:37 +0000 Subject: [PATCH 11/23] Remove reverseRoles This variable seemed redundant in hindsight, it seemed better to remove it than to worry about where it went in the component. --- .../views/elements/PowerSelector.js | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index 50893850c1..8dd848db00 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -41,27 +41,21 @@ module.exports = React.createClass({ getInitialState: function() { return { levelRoleMap: {}, - reverseRoles: {}, }; }, componentWillMount: function() { // This needs to be done now because levelRoleMap has translated strings const levelRoleMap = Roles.levelRoleMap(); - const reverseRoles = {}; - Object.keys(levelRoleMap).forEach(function(key) { - reverseRoles[levelRoleMap[key]] = key; - }); this.setState({ levelRoleMap, - reverseRoles, custom: levelRoleMap[this.props.value] === undefined, }); }, onSelectChange: function(event) { - this.setState({ custom: event.target.value === "Custom" }); - if (event.target.value !== "Custom") { + this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" }); + if (event.target.value !== "SELECT_VALUE_CUSTOM") { this.props.onChange(this.getValue()); } }, @@ -79,7 +73,7 @@ module.exports = React.createClass({ getValue: function() { let value; if (this.refs.select) { - value = this.state.reverseRoles[this.refs.select.value]; + value = this.refs.select.value; if (this.refs.custom) { if (value === undefined) value = parseInt( this.refs.custom.value ); } @@ -101,25 +95,26 @@ module.exports = React.createClass({ let selectValue; if (this.state.custom) { - selectValue = "Custom"; + selectValue = "SELECT_VALUE_CUSTOM"; } else { - selectValue = this.state.levelRoleMap[this.props.value] || "Custom"; + selectValue = this.state.levelRoleMap[selectValue] ? + this.props.value : "SELECT_VALUE_CUSTOM"; } let select; if (this.props.disabled) { - select = { selectValue }; + select = { this.state.levelRoleMap[selectValue] }; } else { // Each level must have a definition in this.state.levelRoleMap const levels = [0, 50, 100]; let options = levels.map((level) => { return { - value: this.state.levelRoleMap[level], + value: level, // Give a userDefault (users_default in the power event) of 0 but // because level !== undefined, this should never be used. text: Roles.textualPowerLevel(level, 0), }; }); - options.push({ value: "Custom", text: _t("Custom level") }); + options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") }); options = options.map((op) => { return ; }); From 3fa1bece0a8ad73ab91c297a17e6babdc4e7b6c6 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 14 Nov 2017 13:06:54 +0000 Subject: [PATCH 12/23] Simplify further Also fix not-i18n-friendly "of" to be "=". --- .../views/elements/PowerSelector.js | 38 ++++++------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index 2d90711739..d5c167fac9 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -59,14 +59,14 @@ module.exports = React.createClass({ }, componentWillMount: function() { - this._initStateFromProps(this.props, true); + this._initStateFromProps(this.props); }, componentWillReceiveProps: function(newProps) { this._initStateFromProps(newProps); }, - _initStateFromProps: function(newProps, initial) { + _initStateFromProps: function(newProps) { // This needs to be done now because levelRoleMap has translated strings const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault); const options = Object.keys(levelRoleMap).filter((l) => { @@ -76,69 +76,53 @@ module.exports = React.createClass({ this.setState({ levelRoleMap, options, + custom: levelRoleMap[newProps.value] === undefined, }); - - if (initial) { - this.setState({ - custom: levelRoleMap[newProps.value] === undefined, - }); - } }, onSelectChange: function(event) { this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" }); if (event.target.value !== "SELECT_VALUE_CUSTOM") { - this.props.onChange(this.getValue()); + this.props.onChange(event.target.value); } }, onCustomBlur: function(event) { - this.props.onChange(this.getValue()); + this.props.onChange(parseInt(this.refs.custom.value)); }, onCustomKeyDown: function(event) { if (event.key == "Enter") { - this.props.onChange(this.getValue()); + this.props.onChange(parseInt(this.refs.custom.value)); } }, - getValue: function() { - if (this.refs.custom) { - return parseInt(this.refs.custom.value); - } - if (this.refs.select) { - return this.refs.select.value; - } - return undefined; - }, - render: function() { let customPicker; if (this.state.custom) { - let input; if (this.props.disabled) { - input = { _t( + customPicker = { _t( "Custom of %(powerLevel)s", { powerLevel: this.props.value }, ) }; } else { - input = = ; + /> + ; } - customPicker = of { input }; } let selectValue; if (this.state.custom) { selectValue = "SELECT_VALUE_CUSTOM"; } else { - selectValue = this.state.levelRoleMap[selectValue] ? + selectValue = this.state.levelRoleMap[this.props.value] ? this.props.value : "SELECT_VALUE_CUSTOM"; } let select; From b58514f42730cd40fe4d5c3781384cb8f9af9223 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 14 Nov 2017 14:27:27 +0000 Subject: [PATCH 13/23] Stop FF quantum exploding on CSS edits --- src/Tinter.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Tinter.js b/src/Tinter.js index fe4cafe744..be894f003b 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -279,7 +279,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"); } From 8c218557dfe198217a5dfa8a9083da5b1eac234a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 14 Nov 2017 15:17:50 +0000 Subject: [PATCH 14/23] fix disable_custom_urls --- src/components/structures/login/Login.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 33ea57029b..821af9c0fe 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 = ; } - let serverConfig; - let header; - // FIXME: remove status.im theme tweaks const theme = UserSettingsStore.getTheme(); if (theme !== "status") { From 805796e4e0d292cddb4016caf17094a5773dc97f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 14 Nov 2017 15:27:28 +0000 Subject: [PATCH 15/23] force the tinter to refresh when we change theme --- src/Tinter.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Tinter.js b/src/Tinter.js index be894f003b..1b2416762f 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -101,6 +101,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; } /** @@ -153,12 +156,15 @@ class Tinter { tertiaryColor = rgbToHex(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; @@ -204,6 +210,7 @@ class Tinter { } this.calcCssFixups(); + this.forceTint = true; } calcCssFixups() { From dcfbe93409f297f34cb055c18982302c250906a4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 14 Nov 2017 15:28:34 +0000 Subject: [PATCH 16/23] fix race when loading CSS fixes https://github.com/vector-im/riot-web/issues/5590 --- src/components/structures/MatrixChat.js | 35 ++++++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 268654103d..90c84afa7b 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(); }, @@ -909,15 +911,34 @@ 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]); + let switchTheme = function() { + const colors = Tinter.getCurrentColors(); + Object.values(styleElements).forEach((a) => { + if (a == styleElements[theme]) return; + a.disabled = true; + }); + Tinter.setTheme(theme); + Tinter.tint(colors[0], colors[1]); + }; + + if (styleElements[theme].complete) { + switchTheme(); + } + else { + styleElements[theme].onload = () => { + switchTheme(); + }; + } if (theme === 'dark') { // abuse the tinter to change all the SVG's #fff to #2d2d2d From 8d6e3dd27d35d79d2a111f3f388bcfcb775e42bd Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 14 Nov 2017 15:37:03 +0000 Subject: [PATCH 17/23] fix lint --- src/components/structures/MatrixChat.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 90c84afa7b..f2df1df851 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -921,7 +921,7 @@ module.exports = React.createClass({ styleElements[theme].disabled = false; - let switchTheme = function() { + const switchTheme = function() { const colors = Tinter.getCurrentColors(); Object.values(styleElements).forEach((a) => { if (a == styleElements[theme]) return; @@ -933,9 +933,8 @@ module.exports = React.createClass({ if (styleElements[theme].complete) { switchTheme(); - } - else { - styleElements[theme].onload = () => { + } else { + styleElements[theme].onload = () => { switchTheme(); }; } From 1fd7ac30e444226dcfc9cdc6cab37616c38af633 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 14 Nov 2017 16:04:11 +0000 Subject: [PATCH 18/23] turns out i made up the .complete property on link elements --- src/Tinter.js | 2 +- src/components/structures/MatrixChat.js | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Tinter.js b/src/Tinter.js index 1b2416762f..c926ada591 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -217,7 +217,7 @@ class Tinter { // 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)"); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index f2df1df851..26eed952d8 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -931,7 +931,17 @@ module.exports = React.createClass({ Tinter.tint(colors[0], colors[1]); }; - if (styleElements[theme].complete) { + let cssLoaded = false; + + 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) { switchTheme(); } else { styleElements[theme].onload = () => { From 7c98558b6ab3b9ae51b282874335049840ddcad8 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 14 Nov 2017 16:07:48 +0000 Subject: [PATCH 19/23] fix neglible race when loading CSS --- src/components/structures/MatrixChat.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 26eed952d8..f287ff27af 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -931,8 +931,15 @@ module.exports = React.createClass({ Tinter.tint(colors[0], colors[1]); }; + // 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) { @@ -942,11 +949,8 @@ module.exports = React.createClass({ } if (cssLoaded) { + styleElements[theme].onload = undefined; switchTheme(); - } else { - styleElements[theme].onload = () => { - switchTheme(); - }; } if (theme === 'dark') { From e87940f63cf2460a0bccfcdf7b31fff5b40a7755 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 14 Nov 2017 19:53:32 +0000 Subject: [PATCH 20/23] Make app tile title stronger --- src/components/views/elements/AppTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index b2fb1805ba..025bf44027 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -381,7 +381,7 @@ export default React.createClass({ return (
- { this.formatAppTileName() } + { this.formatAppTileName() } { /* Edit widget */ } { showEditButton && Date: Tue, 14 Nov 2017 19:53:59 +0000 Subject: [PATCH 21/23] Make edit icon green by default --- src/components/views/elements/AppTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 025bf44027..aa781c2d62 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -385,8 +385,8 @@ export default React.createClass({ { /* Edit widget */ } { showEditButton && {_t('Edit')} Date: Wed, 15 Nov 2017 01:01:17 +0000 Subject: [PATCH 22/23] fix new tinter problems correctly handle skinned tertiaryColors (turns out they're used for the RoomSublist divider bars) handle the fact that some room accountData apparently has ended up with rgb() colors in it... --- src/Tinter.js | 65 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/src/Tinter.js b/src/Tinter.js index c926ada591..11dea0170f 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,7 +70,7 @@ 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) ]; @@ -132,28 +143,33 @@ class Tinter { tint(primaryColor, secondaryColor, 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.forceTint == false && @@ -169,7 +185,9 @@ class Tinter { 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(); @@ -208,6 +226,11 @@ 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; From 546b062d824a5c5e52a75543bb771a6fdc3810a4 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 15 Nov 2017 01:45:51 +0000 Subject: [PATCH 23/23] automatically and correctly retint when changing theme --- src/Tinter.js | 31 +++++++++++++++++++++---- src/components/structures/MatrixChat.js | 10 -------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/Tinter.js b/src/Tinter.js index 11dea0170f..f2a02b6e6d 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -74,7 +74,7 @@ class Tinter { "#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], @@ -83,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 @@ -136,11 +145,11 @@ 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 + ", " + @@ -200,6 +209,8 @@ class Tinter { } tintSvgWhite(whiteColor) { + this.currentTint[3] = whiteColor; + if (!whiteColor) { whiteColor = this.colors[3]; } @@ -234,6 +245,16 @@ class Tinter { 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() { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index f287ff27af..7847e8e8dc 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -922,13 +922,11 @@ module.exports = React.createClass({ styleElements[theme].disabled = false; const switchTheme = function() { - const colors = Tinter.getCurrentColors(); Object.values(styleElements).forEach((a) => { if (a == styleElements[theme]) return; a.disabled = true; }); Tinter.setTheme(theme); - Tinter.tint(colors[0], colors[1]); }; // turns out that Firefox preloads the CSS for link elements with @@ -952,14 +950,6 @@ module.exports = React.createClass({ styleElements[theme].onload = undefined; switchTheme(); } - - 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'); - } }, /**