diff --git a/src/PasswordReset.js b/src/PasswordReset.js new file mode 100644 index 0000000000..1029b07b70 --- /dev/null +++ b/src/PasswordReset.js @@ -0,0 +1,104 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var Matrix = require("matrix-js-sdk"); + +/** + * Allows a user to reset their password on a homeserver. + * + * This involves getting an email token from the identity server to "prove" that + * the client owns the given email address, which is then passed to the password + * API on the homeserver in question with the new password. + */ +class PasswordReset { + + /** + * Configure the endpoints for password resetting. + * @param {string} homeserverUrl The URL to the HS which has the account to reset. + * @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping. + */ + constructor(homeserverUrl, identityUrl) { + this.client = Matrix.createClient({ + baseUrl: homeserverUrl, + idBaseUrl: identityUrl + }); + this.clientSecret = generateClientSecret(); + this.identityServerDomain = identityUrl.split("://")[1]; + } + + /** + * Attempt to reset the user's password. This will trigger a side-effect of + * sending an email to the provided email address. + * @param {string} emailAddress The email address + * @param {string} newPassword The new password for the account. + * @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked(). + */ + resetPassword(emailAddress, newPassword) { + this.password = newPassword; + return this.client.requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => { + this.sessionId = res.sid; + return res; + }, function(err) { + if (err.httpStatus) { + err.message = err.message + ` (Status ${err.httpStatus})`; + } + throw err; + }); + } + + /** + * Checks if the email link has been clicked by attempting to change the password + * for the mxid linked to the email. + * @return {Promise} Resolves if the password was reset. Rejects with an object + * with a "message" property which contains a human-readable message detailing why + * the reset failed, e.g. "There is no mapped matrix user ID for the given email address". + */ + checkEmailLinkClicked() { + return this.client.setPassword({ + type: "m.login.email.identity", + threepid_creds: { + sid: this.sessionId, + client_secret: this.clientSecret, + id_server: this.identityServerDomain + } + }, this.password).catch(function(err) { + if (err.httpStatus === 401) { + err.message = "Failed to verify email address: make sure you clicked the link in the email"; + } + else if (err.httpStatus === 404) { + err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver."; + } + else if (err.httpStatus) { + err.message += ` (Status ${err.httpStatus})`; + } + throw err; + }); + } +} + +// from Angular SDK +function generateClientSecret() { + var ret = ""; + var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for (var i = 0; i < 32; i++) { + ret += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return ret; +} + +module.exports = PasswordReset; diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 363560f0c6..1dd7ecb08f 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -43,11 +43,27 @@ var commands = { return reject("Usage: /nick "); }, - // Takes an #rrggbb colourcode and retints the UI (just for debugging) + // Changes the colorscheme of your current room tint: function(room_id, args) { - Tinter.tint(args); - return success(); - }, + + if (args) { + var matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/); + if (matches) { + Tinter.tint(matches[1], matches[4]); + var colorScheme = {} + colorScheme.primary_color = matches[1]; + if (matches[4]) { + colorScheme.secondary_color = matches[4]; + } + return success( + MatrixClientPeg.get().setRoomAccountData( + room_id, "org.matrix.room.color_scheme", colorScheme + ) + ); + } + } + return reject("Usage: /tint []"); + }, encrypt: function(room_id, args) { if (args == "on") { diff --git a/src/Tinter.js b/src/Tinter.js index 7245a5825b..3e7949b65d 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -127,6 +127,11 @@ module.exports = { cached = true; } + if (!primaryColor) { + primaryColor = "#76CFA6"; // Vector green + secondaryColor = "#EAF5F0"; // Vector light green + } + if (!secondaryColor) { var x = 0.16; // average weighting factor calculated from vector green & light green var rgb = hexToRgb(primaryColor); @@ -146,6 +151,13 @@ module.exports = { tertiaryColor = rgbToHex(rgb1); } + if (colors[0] === primaryColor && + colors[1] === secondaryColor && + colors[2] === tertiaryColor) + { + return; + } + colors = [primaryColor, secondaryColor, tertiaryColor]; // go through manually fixing up the stylesheets. diff --git a/src/UserActivity.js b/src/UserActivity.js index 3048ad4454..8b136c0bcc 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -31,6 +31,11 @@ class UserActivity { start() { document.onmousemove = this._onUserActivity.bind(this); document.onkeypress = this._onUserActivity.bind(this); + // can't use document.scroll here because that's only the document + // itself being scrolled. Need to use addEventListener's useCapture. + // also this needs to be the wheel event, not scroll, as scroll is + // fired when the view scrolls down for a new message. + window.addEventListener('wheel', this._onUserActivity.bind(this), true); this.lastActivityAtTs = new Date().getTime(); this.lastDispatchAtTs = 0; } @@ -41,10 +46,11 @@ class UserActivity { stop() { document.onmousemove = undefined; document.onkeypress = undefined; + window.removeEventListener('wheel', this._onUserActivity.bind(this), true); } _onUserActivity(event) { - if (event.screenX) { + if (event.screenX && event.type == "mousemove") { if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) { diff --git a/src/component-index.js b/src/component-index.js index cef1b093a4..9fe15adfc6 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -23,6 +23,7 @@ limitations under the License. module.exports.components = {}; module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom'); +module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword'); module.exports.components['structures.login.Login'] = require('./components/structures/login/Login'); module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration'); module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration'); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 320dad09b3..e5af2a86b5 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -30,6 +30,7 @@ var Registration = require("./login/Registration"); var PostRegistration = require("./login/PostRegistration"); var Modal = require("../../Modal"); +var Tinter = require("../../Tinter"); var sdk = require('../../index'); var MatrixTools = require('../../MatrixTools'); var linkifyMatrix = require("../../linkify-matrix"); @@ -233,6 +234,13 @@ module.exports = React.createClass({ }); this.notifyNewScreen('register'); break; + case 'start_password_recovery': + if (this.state.logged_in) return; + this.replaceState({ + screen: 'forgot_password' + }); + this.notifyNewScreen('forgot_password'); + break; case 'token_login': if (this.state.logged_in) return; @@ -411,7 +419,16 @@ module.exports = React.createClass({ if (room) { var theAlias = MatrixTools.getCanonicalAliasForRoom(room); if (theAlias) presentedId = theAlias; + + var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); + var color_scheme = {}; + if (color_scheme_event) { + color_scheme = color_scheme_event.getContent(); + // XXX: we should validate the event + } + Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); } + this.notifyNewScreen('room/'+presentedId); newState.ready = true; } @@ -559,6 +576,11 @@ module.exports = React.createClass({ action: 'token_login', params: params }); + } else if (screen == 'forgot_password') { + dis.dispatch({ + action: 'start_password_recovery', + params: params + }); } else if (screen == 'new') { dis.dispatch({ action: 'view_create_room', @@ -668,6 +690,10 @@ module.exports = React.createClass({ this.showScreen("login"); }, + onForgotPasswordClick: function() { + this.showScreen("forgot_password"); + }, + onRegistered: function(credentials) { this.onLoggedIn(credentials); // do post-registration stuff @@ -706,6 +732,7 @@ module.exports = React.createClass({ var CreateRoom = sdk.getComponent('structures.CreateRoom'); var RoomDirectory = sdk.getComponent('structures.RoomDirectory'); var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar'); + var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); // needs to be before normal PageTypes as you are logged in technically if (this.state.screen == 'post_registration') { @@ -801,13 +828,21 @@ module.exports = React.createClass({ onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} /> ); + } else if (this.state.screen == 'forgot_password') { + return ( + + ); } else { return ( + identityServerUrl={this.props.config.default_is_url} + onForgotPasswordClick={this.onForgotPasswordClick} /> ); } } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 7a729a4436..ddde9c2645 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -37,9 +37,11 @@ var TabComplete = require("../../TabComplete"); var MemberEntry = require("../../TabCompleteEntries").MemberEntry; var Resend = require("../../Resend"); var dis = require("../../dispatcher"); +var Tinter = require("../../Tinter"); var PAGINATE_SIZE = 20; var INITIAL_SIZE = 20; +var SEND_READ_RECEIPT_DELAY = 2000; var DEBUG_SCROLL = false; @@ -74,7 +76,9 @@ module.exports = React.createClass({ syncState: MatrixClientPeg.get().getSyncState(), hasUnsentMessages: this._hasUnsentMessages(room), callState: null, - guestsCanJoin: false + guestsCanJoin: false, + readMarkerEventId: room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId), + readMarkerGhostEventId: undefined } }, @@ -82,6 +86,7 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.name", this.onRoomName); + MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); @@ -152,6 +157,7 @@ module.exports = React.createClass({ if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); + MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping); MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); @@ -159,6 +165,8 @@ module.exports = React.createClass({ } window.removeEventListener('resize', this.onResize); + + Tinter.tint(); // reset colourscheme }, onAction: function(payload) { @@ -272,9 +280,58 @@ module.exports = React.createClass({ } }, + updateTint: function() { + var room = MatrixClientPeg.get().getRoom(this.props.roomId); + if (!room) return; + + var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); + var color_scheme = {}; + if (color_scheme_event) { + color_scheme = color_scheme_event.getContent(); + // XXX: we should validate the event + } + Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); + }, + + onRoomAccountData: function(room, event) { + if (room.roomId == this.props.roomId) { + if (event.getType === "org.matrix.room.color_scheme") { + var color_scheme = event.getContent(); + // XXX: we should validate the event + Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); + } + } + }, + onRoomReceipt: function(receiptEvent, room) { if (room.roomId == this.props.roomId) { - this.forceUpdate(); + var readMarkerEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); + var readMarkerGhostEventId = this.state.readMarkerGhostEventId; + if (this.state.readMarkerEventId !== undefined && this.state.readMarkerEventId != readMarkerEventId) { + readMarkerGhostEventId = this.state.readMarkerEventId; + } + + + // if the event after the one referenced in the read receipt if sent by us, do nothing since + // this is a temporary period before the synthesized receipt for our own message arrives + var readMarkerGhostEventIndex; + for (var i = 0; i < room.timeline.length; ++i) { + if (room.timeline[i].getId() == readMarkerGhostEventId) { + readMarkerGhostEventIndex = i; + break; + } + } + if (readMarkerGhostEventIndex + 1 < room.timeline.length) { + var nextEvent = room.timeline[readMarkerGhostEventIndex + 1]; + if (nextEvent.sender && nextEvent.sender.userId == MatrixClientPeg.get().credentials.userId) { + readMarkerGhostEventId = undefined; + } + } + + this.setState({ + readMarkerEventId: readMarkerEventId, + readMarkerGhostEventId: readMarkerGhostEventId, + }); } }, @@ -374,6 +431,8 @@ module.exports = React.createClass({ this.scrollToBottom(); this.sendReadReceipt(); + + this.updateTint(); }, componentDidUpdate: function() { @@ -695,10 +754,10 @@ module.exports = React.createClass({ var EventTile = sdk.getComponent('rooms.EventTile'); - var prevEvent = null; // the last event we showed - var readReceiptEventId = this.state.room.getEventReadUpTo(MatrixClientPeg.get().credentials.userId); var startIdx = Math.max(0, this.state.room.timeline.length - this.state.messageCap); + var readMarkerIndex; + var ghostIndex; for (var i = startIdx; i < this.state.room.timeline.length; i++) { var mxEv = this.state.room.timeline[i]; @@ -712,6 +771,25 @@ module.exports = React.createClass({ } } + // now we've decided whether or not to show this message, + // add the read up to marker if appropriate + // doing this here means we implicitly do not show the marker + // if it's at the bottom + // NB. it would be better to decide where the read marker was going + // when the state changed rather than here in the render method, but + // this is where we decide what messages we show so it's the only + // place we know whether we're at the bottom or not. + var self = this; + var mxEvSender = mxEv.sender ? mxEv.sender.userId : null; + if (prevEvent && prevEvent.getId() == this.state.readMarkerEventId && mxEvSender != MatrixClientPeg.get().credentials.userId) { + var hr; + hr = (
); + readMarkerIndex = ret.length; + ret.push(
  • {hr}
  • ); + } + // is this a continuation of the previous message? var continuation = false; if (prevEvent !== null) { @@ -748,13 +826,29 @@ module.exports = React.createClass({ ); - if (eventId == readReceiptEventId) { - ret.push(
    ); + // A read up to marker has died and returned as a ghost! + // Lives in the dom as the ghost of the previous one while it fades away + if (eventId == this.state.readMarkerGhostEventId) { + ghostIndex = ret.length; } prevEvent = mxEv; } + // splice the read marker ghost in now that we know whether the read receipt + // is the last element or not, because we only decide as we're going along. + if (readMarkerIndex === undefined && ghostIndex && ghostIndex <= ret.length) { + var hr; + hr = (
    ); + ret.splice(ghostIndex, 0, ( +
  • {hr}
  • + )); + } + return ret; }, @@ -825,6 +919,14 @@ module.exports = React.createClass({ ); } + if (newVals.color_scheme) { + deferreds.push( + MatrixClientPeg.get().setRoomAccountData( + this.state.room.roomId, "org.matrix.room.color_scheme", newVals.color_scheme + ) + ); + } + deferreds.push( MatrixClientPeg.get().setGuestAccess(this.state.room.roomId, { allowRead: newVals.guest_read, @@ -923,11 +1025,13 @@ module.exports = React.createClass({ history_visibility: this.refs.room_settings.getHistoryVisibility(), power_levels: this.refs.room_settings.getPowerLevels(), guest_join: this.refs.room_settings.canGuestsJoin(), - guest_read: this.refs.room_settings.canGuestsRead() + guest_read: this.refs.room_settings.canGuestsRead(), + color_scheme: this.refs.room_settings.getColorScheme(), }); }, onCancelClick: function() { + this.updateTint(); this.setState({editingRoomSettings: false}); }, diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js new file mode 100644 index 0000000000..dcf6a7c28e --- /dev/null +++ b/src/components/structures/login/ForgotPassword.js @@ -0,0 +1,199 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +'use strict'; + +var React = require('react'); +var sdk = require('../../../index'); +var Modal = require("../../../Modal"); +var MatrixClientPeg = require('../../../MatrixClientPeg'); + +var PasswordReset = require("../../../PasswordReset"); + +module.exports = React.createClass({ + displayName: 'ForgotPassword', + + propTypes: { + homeserverUrl: React.PropTypes.string, + identityServerUrl: React.PropTypes.string, + onComplete: React.PropTypes.func.isRequired + }, + + getInitialState: function() { + return { + enteredHomeserverUrl: this.props.homeserverUrl, + enteredIdentityServerUrl: this.props.identityServerUrl, + progress: null + }; + }, + + submitPasswordReset: function(hsUrl, identityUrl, email, password) { + this.setState({ + progress: "sending_email" + }); + this.reset = new PasswordReset(hsUrl, identityUrl); + this.reset.resetPassword(email, password).done(() => { + this.setState({ + progress: "sent_email" + }); + }, (err) => { + this.showErrorDialog("Failed to send email: " + err.message); + this.setState({ + progress: null + }); + }) + }, + + onVerify: function(ev) { + ev.preventDefault(); + if (!this.reset) { + console.error("onVerify called before submitPasswordReset!"); + return; + } + this.reset.checkEmailLinkClicked().done((res) => { + this.setState({ progress: "complete" }); + }, (err) => { + this.showErrorDialog(err.message); + }) + }, + + onSubmitForm: function(ev) { + ev.preventDefault(); + + if (!this.state.email) { + this.showErrorDialog("The email address linked to your account must be entered."); + } + else if (!this.state.password || !this.state.password2) { + this.showErrorDialog("A new password must be entered."); + } + else if (this.state.password !== this.state.password2) { + this.showErrorDialog("New passwords must match each other."); + } + else { + this.submitPasswordReset( + this.state.enteredHomeserverUrl, this.state.enteredIdentityServerUrl, + this.state.email, this.state.password + ); + } + }, + + onInputChanged: function(stateKey, ev) { + this.setState({ + [stateKey]: ev.target.value + }); + }, + + onHsUrlChanged: function(newHsUrl) { + this.setState({ + enteredHomeserverUrl: newHsUrl + }); + }, + + onIsUrlChanged: function(newIsUrl) { + this.setState({ + enteredIdentityServerUrl: newIsUrl + }); + }, + + showErrorDialog: function(body, title) { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: title, + description: body + }); + }, + + render: function() { + var LoginHeader = sdk.getComponent("login.LoginHeader"); + var LoginFooter = sdk.getComponent("login.LoginFooter"); + var ServerConfig = sdk.getComponent("login.ServerConfig"); + var Spinner = sdk.getComponent("elements.Spinner"); + + var resetPasswordJsx; + + if (this.state.progress === "sending_email") { + resetPasswordJsx = + } + else if (this.state.progress === "sent_email") { + resetPasswordJsx = ( +
    + An email has been sent to {this.state.email}. Once you've followed + the link it contains, click below. +
    + +
    + ); + } + else if (this.state.progress === "complete") { + resetPasswordJsx = ( +
    +

    Your password has been reset.

    +

    You have been logged out of all devices and will no longer receive push notifications. + To re-enable notifications, re-log in on each device.

    + +
    + ); + } + else { + resetPasswordJsx = ( +
    + To reset your password, enter the email address linked to your account: +
    +
    +
    + +
    + +
    + +
    + +
    + + +
    +
    + ); + } + + + return ( +
    +
    + + {resetPasswordJsx} +
    +
    + ); + } +}); diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index b7d2d762a4..b853b8fd95 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -33,7 +33,9 @@ module.exports = React.createClass({displayName: 'Login', homeserverUrl: React.PropTypes.string, identityServerUrl: React.PropTypes.string, // login shouldn't know or care how registration is done. - onRegisterClick: React.PropTypes.func.isRequired + onRegisterClick: React.PropTypes.func.isRequired, + // login shouldn't care how password recovery is done. + onForgotPasswordClick: React.PropTypes.func }, getDefaultProps: function() { @@ -138,7 +140,9 @@ module.exports = React.createClass({displayName: 'Login', switch (step) { case 'm.login.password': return ( - + ); case 'm.login.cas': return ( diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 3367ac3257..a8751da1a7 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -22,7 +22,8 @@ var ReactDOM = require('react-dom'); */ module.exports = React.createClass({displayName: 'PasswordLogin', propTypes: { - onSubmit: React.PropTypes.func.isRequired // fn(username, password) + onSubmit: React.PropTypes.func.isRequired, // fn(username, password) + onForgotPasswordClick: React.PropTypes.func // fn() }, getInitialState: function() { @@ -46,6 +47,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin', }, render: function() { + var forgotPasswordJsx; + + if (this.props.onForgotPasswordClick) { + forgotPasswordJsx = ( + + Forgot your password? + + ); + } + return (
    @@ -57,6 +68,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin', value={this.state.password} onChange={this.onPasswordChanged} placeholder="Password" />
    + {forgotPasswordJsx}
    diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 8b5435e46a..cdfbc0bfc8 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -116,7 +116,7 @@ module.exports = React.createClass({ } name = -
    +
    { this.props.room.name }
    { searchStatus }
    @@ -151,7 +151,7 @@ module.exports = React.createClass({ header =
    -
    +
    { roomAvatar }
    diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 9e07385d65..c5e37521c5 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -16,8 +16,23 @@ limitations under the License. var React = require('react'); var MatrixClientPeg = require('../../../MatrixClientPeg'); +var Tinter = require('../../../Tinter'); var sdk = require('../../../index'); +var room_colors = [ + // magic room default values courtesy of Ribot + ["#76cfa6", "#eaf5f0"], + ["#81bddb", "#eaf1f4"], + ["#bd79cb", "#f3eaf5"], + ["#c65d94", "#f5eaef"], + ["#e55e5e", "#f5eaea"], + ["#eca46f", "#f5eeea"], + ["#dad658", "#f5f4ea"], + ["#80c553", "#eef5ea"], + ["#bb814e", "#eee8e3"], + ["#595959", "#ececec"], +]; + module.exports = React.createClass({ displayName: 'RoomSettings', @@ -26,8 +41,37 @@ module.exports = React.createClass({ }, getInitialState: function() { + // work out the initial color index + var room_color_index = undefined; + var color_scheme_event = this.props.room.getAccountData("org.matrix.room.color_scheme"); + if (color_scheme_event) { + var color_scheme = color_scheme_event.getContent(); + if (color_scheme.primary_color) color_scheme.primary_color = color_scheme.primary_color.toLowerCase(); + if (color_scheme.secondary_color) color_scheme.secondary_color = color_scheme.secondary_color.toLowerCase(); + // XXX: we should validate these values + for (var i = 0; i < room_colors.length; i++) { + var room_color = room_colors[i]; + if (room_color[0] === color_scheme.primary_color && + room_color[1] === color_scheme.secondary_color) + { + room_color_index = i; + break; + } + } + if (room_color_index === undefined) { + // append the unrecognised colours to our palette + room_color_index = room_colors.length; + room_colors[room_color_index] = [ color_scheme.primary_color, color_scheme.secondary_color ]; + } + } + else { + room_color_index = 0; + } + return { - power_levels_changed: false + power_levels_changed: false, + color_scheme_changed: false, + color_scheme_index: room_color_index, }; }, @@ -78,6 +122,25 @@ module.exports = React.createClass({ }); }, + getColorScheme: function() { + if (!this.state.color_scheme_changed) return undefined; + + return { + primary_color: room_colors[this.state.color_scheme_index][0], + secondary_color: room_colors[this.state.color_scheme_index][1], + }; + }, + + onColorSchemeChanged: function(index) { + // preview what the user just changed the scheme to. + Tinter.tint(room_colors[index][0], room_colors[index][1]); + + this.setState({ + color_scheme_changed: true, + color_scheme_index: index, + }); + }, + render: function() { var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); @@ -151,16 +214,80 @@ module.exports = React.createClass({ } var can_set_room_avatar = current_user_level >= room_avatar_level; + var self = this; + + var room_colors_section = +
    +

    Room Colour

    +
    + {room_colors.map(function(room_color, i) { + var selected; + if (i === self.state.color_scheme_index) { + selected = +
    + ./ +
    + } + var boundClick = self.onColorSchemeChanged.bind(self, i) + return ( +
    + { selected } +
    +
    + ); + })} +
    +
    ; + var change_avatar; if (can_set_room_avatar) { - change_avatar =
    -

    Room Icon

    - -
    ; + change_avatar = +
    +

    Room Icon

    + +
    ; } var banned = this.props.room.getMembersWithMembership("ban"); + var events_levels_section; + if (events_levels.length) { + events_levels_section = +
    +

    Event levels

    +
    + {Object.keys(events_levels).map(function(event_type, i) { + return ( +
    + + +
    + ); + })} +
    +
    ; + } + + var banned_users_section; + if (banned.length) { + banned_users_section = +
    +

    Banned users

    +
    + {banned.map(function(member, i) { + return ( +
    + {member.userId} +
    + ); + })} +
    +
    ; + } + return (