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 = ; } - let serverConfig; - let header; - // FIXME: remove status.im theme tweaks const theme = SettingsStore.getValue("theme"); if (theme !== "status") { diff --git a/src/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js new file mode 100644 index 0000000000..19342f3492 --- /dev/null +++ b/src/components/views/avatars/MemberPresenceAvatar.js @@ -0,0 +1,157 @@ +/* + 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"; +import UserSettingsStore from "../../../UserSettingsStore"; +import * as ContextualMenu from "../../structures/ContextualMenu"; + +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; + const presenceMessage = this.props.member.user.presenceStatusMsg; + return { + status: presenceState, + message: presenceMessage, + }; + }, + + 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, + }); + }, + + onStatusChange: function(newStatus) { + Presence.stopMaintainingStatus(); + if (newStatus === "online") { + Presence.setState(newStatus); + } else Presence.setState(newStatus, null, true); + }, + + onClick: function(e) { + const PresenceContextMenu = sdk.getComponent('context_menus.PresenceContextMenu'); + const elementRect = e.target.getBoundingClientRect(); + + // 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 + + ContextualMenu.createMenu(PresenceContextMenu, { + chevronOffset: chevronOffset, + chevronFace: 'bottom', + left: x, + top: y, + menuWidth: 125, + 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() { + const MemberAvatar = sdk.getComponent("avatars.MemberAvatar"); + + let onClickFn = null; + if (this.props.member.userId === MatrixClientPeg.get().getUserId()) { + onClickFn = this.onClick; + } + + const avatarNode = ( + + ); + let statusNode = ( + + ); + + // LABS: Disable presence management functions for now + if (!UserSettingsStore.isFeatureEnabled("feature_presence_management")) { + statusNode = null; + onClickFn = null; + } + + let avatar = ( +
+ { avatarNode } + { statusNode } +
+ ); + if (onClickFn) { + avatar = ( + + { avatarNode } + { statusNode } + + ); + } + return avatar; + }, +}); diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index b2fb1805ba..aa781c2d62 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -381,12 +381,12 @@ export default React.createClass({ return (
- { this.formatAppTileName() } + { this.formatAppTileName() } { /* Edit widget */ } { showEditButton && {_t('Edit')} should be a 'controlled' form element and updated by React // to reflect the current value, rather than left freeform. @@ -43,78 +45,98 @@ module.exports = React.createClass({ getInitialState: function() { return { - custom: (LEVEL_ROLE_MAP[this.props.value] === undefined), + levelRoleMap: {}, + // List of power levels to show in the drop-down + options: [], + }; + }, + + getDefaultProps: function() { + return { + maxValue: Infinity, + usersDefault: 0, }; }, componentWillMount: function() { - LEVEL_ROLE_MAP = Roles.levelRoleMap(); - Object.keys(LEVEL_ROLE_MAP).forEach(function(key) { - reverseRoles[LEVEL_ROLE_MAP[key]] = key; - }); + this._initStateFromProps(this.props); + }, + + componentWillReceiveProps: function(newProps) { + this._initStateFromProps(newProps); + }, + + _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) => { + return l === undefined || l <= newProps.maxValue; + }); + + this.setState({ + levelRoleMap, + options, + custom: levelRoleMap[newProps.value] === undefined, + }); }, onSelectChange: function(event) { - this.setState({ custom: event.target.value === "Custom" }); - if (event.target.value !== "Custom") { - this.props.onChange(this.getValue()); + this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" }); + if (event.target.value !== "SELECT_VALUE_CUSTOM") { + 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() { - let value; - if (this.refs.select) { - value = reverseRoles[this.refs.select.value]; - if (this.refs.custom) { - if (value === undefined) value = parseInt( this.refs.custom.value ); - } - } - return value; - }, - render: function() { let customPicker; if (this.state.custom) { - let input; if (this.props.disabled) { - input = { this.props.value }; + customPicker = { _t( + "Custom of %(powerLevel)s", + { powerLevel: this.props.value }, + ) }; } else { - input = ; + customPicker = = + ; } - customPicker = of { input }; } let selectValue; if (this.state.custom) { - selectValue = "Custom"; + selectValue = "SELECT_VALUE_CUSTOM"; } else { - selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom"; + selectValue = this.state.levelRoleMap[this.props.value] ? + 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 LEVEL_ROLE_MAP - const levels = [0, 50, 100]; - let options = levels.map((level) => { + // Each level must have a definition in this.state.levelRoleMap + let options = this.state.options.map((level) => { return { - value: LEVEL_ROLE_MAP[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), + value: level, + text: Roles.textualPowerLevel(level, this.props.usersDefault), }; }); - options.push({ value: "Custom", text: _t("Custom level") }); + options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") }); options = options.map((op) => { return ; }); 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/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 585900e2c0..2ac7075189 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(
- +
, ); diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 6a689fb47f..8ba18ee96e 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -901,31 +901,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) { @@ -935,7 +935,7 @@ module.exports = React.createClass({ return (
{ label } -
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a9371c3126..51f23c0968 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,8 +150,8 @@ "%(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", + "Presence Management": "Presence Management", "%(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", "%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing", @@ -524,6 +524,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", @@ -580,6 +581,7 @@ "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", + "Custom of %(powerLevel)s": "Custom of %(powerLevel)s", "Custom level": "Custom level", "Room directory": "Room directory", "Start chat": "Start chat",