Merge branch 'develop' into kegan/guest-peek-then-join

pull/21833/head
Kegan Dougal 2016-01-13 13:22:58 +00:00
commit 69cac307d9
13 changed files with 656 additions and 52 deletions

104
src/PasswordReset.js Normal file
View File

@ -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;

View File

@ -43,10 +43,26 @@ var commands = {
return reject("Usage: /nick <display_name>");
},
// 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 <primaryColor> [<secondaryColor>]");
},
encrypt: function(room_id, args) {

View File

@ -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.

View File

@ -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)
{

View File

@ -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');

View File

@ -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 (
<ForgotPassword
homeserverUrl={this.props.config.default_hs_url}
identityServerUrl={this.props.config.default_is_url}
onComplete={this.onLoginClick} />
);
} else {
return (
<Login
onLoggedIn={this.onLoggedIn}
onRegisterClick={this.onRegisterClick}
homeserverUrl={this.props.config.default_hs_url}
identityServerUrl={this.props.config.default_is_url} />
identityServerUrl={this.props.config.default_is_url}
onForgotPasswordClick={this.onForgotPasswordClick} />
);
}
}

View File

@ -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 = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '99%'}} ref={function(n) {
self.readMarkerNode = n;
}} />);
readMarkerIndex = ret.length;
ret.push(<li key="_readupto" className="mx_RoomView_myReadMarker_container">{hr}</li>);
}
// is this a continuation of the previous message?
var continuation = false;
if (prevEvent !== null) {
@ -748,13 +826,29 @@ module.exports = React.createClass({
</li>
);
if (eventId == readReceiptEventId) {
ret.push(<hr className="mx_RoomView_myReadMarker" />);
// 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 = (<hr className="mx_RoomView_myReadMarker" style={{opacity: 1, width: '85%'}} ref={function(n) {
Velocity(n, {opacity: '0', width: '10%'}, {duration: 400, easing: 'easeInSine', delay: 1000, complete: function() {
self.setState({readMarkerGhostEventId: undefined});
}});
}} />);
ret.splice(ghostIndex, 0, (
<li key="_readuptoghost" className="mx_RoomView_myReadMarker_container">{hr}</li>
));
}
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});
},

View File

@ -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 = <Spinner />
}
else if (this.state.progress === "sent_email") {
resetPasswordJsx = (
<div>
An email has been sent to {this.state.email}. Once you&#39;ve followed
the link it contains, click below.
<br />
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
value="I have verified my email address" />
</div>
);
}
else if (this.state.progress === "complete") {
resetPasswordJsx = (
<div>
<p>Your password has been reset.</p>
<p>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.</p>
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
value="Return to login screen" />
</div>
);
}
else {
resetPasswordJsx = (
<div>
To reset your password, enter the email address linked to your account:
<br />
<div>
<form onSubmit={this.onSubmitForm}>
<input className="mx_Login_field" ref="user" type="text"
value={this.state.email}
onChange={this.onInputChanged.bind(this, "email")}
placeholder="Email address" autoFocus />
<br />
<input className="mx_Login_field" ref="pass" type="password"
value={this.state.password}
onChange={this.onInputChanged.bind(this, "password")}
placeholder="New password" />
<br />
<input className="mx_Login_field" ref="pass" type="password"
value={this.state.password2}
onChange={this.onInputChanged.bind(this, "password2")}
placeholder="Confirm your new password" />
<br />
<input className="mx_Login_submit" type="submit" value="Send Reset Email" />
</form>
<ServerConfig ref="serverConfig"
withToggleButton={true}
defaultHsUrl={this.props.homeserverUrl}
defaultIsUrl={this.props.identityServerUrl}
onHsUrlChanged={this.onHsUrlChanged}
onIsUrlChanged={this.onIsUrlChanged}
delayTimeMs={0}/>
<LoginFooter />
</div>
</div>
);
}
return (
<div className="mx_Login">
<div className="mx_Login_box">
<LoginHeader />
{resetPasswordJsx}
</div>
</div>
);
}
});

View File

@ -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 (
<PasswordLogin onSubmit={this.onPasswordLogin} />
<PasswordLogin
onSubmit={this.onPasswordLogin}
onForgotPasswordClick={this.props.onForgotPasswordClick} />
);
case 'm.login.cas':
return (

View File

@ -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 = (
<a className="mx_Login_forgot" onClick={this.props.onForgotPasswordClick} href="#">
Forgot your password?
</a>
);
}
return (
<div>
<form onSubmit={this.onSubmitForm}>
@ -57,6 +68,7 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
value={this.state.password} onChange={this.onPasswordChanged}
placeholder="Password" />
<br />
{forgotPasswordJsx}
<input className="mx_Login_submit" type="submit" value="Log in" />
</form>
</div>

View File

@ -116,7 +116,7 @@ module.exports = React.createClass({
}
name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<div className="mx_RoomHeader_name">
<div className="mx_RoomHeader_nametext" title={ this.props.room.name }>{ this.props.room.name }</div>
{ searchStatus }
<div className="mx_RoomHeader_settingsButton" title="Settings">
@ -151,7 +151,7 @@ module.exports = React.createClass({
header =
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow">
<div className="mx_RoomHeader_leftRow" onClick={this.props.onSettingsClick}>
<div className="mx_RoomHeader_avatar">
{ roomAvatar }
</div>

View File

@ -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,9 +214,38 @@ module.exports = React.createClass({
}
var can_set_room_avatar = current_user_level >= room_avatar_level;
var self = this;
var room_colors_section =
<div>
<h3>Room Colour</h3>
<div className="mx_RoomSettings_roomColors">
{room_colors.map(function(room_color, i) {
var selected;
if (i === self.state.color_scheme_index) {
selected =
<div className="mx_RoomSettings_roomColor_selected">
<img src="img/tick.svg" width="17" height="14" alt="./"/>
</div>
}
var boundClick = self.onColorSchemeChanged.bind(self, i)
return (
<div className="mx_RoomSettings_roomColor"
key={ "room_color_" + i }
style={{ backgroundColor: room_color[1] }}
onClick={ boundClick }>
{ selected }
<div className="mx_RoomSettings_roomColorPrimary" style={{ backgroundColor: room_color[0] }}></div>
</div>
);
})}
</div>
</div>;
var change_avatar;
if (can_set_room_avatar) {
change_avatar = <div>
change_avatar =
<div>
<h3>Room Icon</h3>
<ChangeAvatar room={this.props.room} />
</div>;
@ -161,6 +253,41 @@ module.exports = React.createClass({
var banned = this.props.room.getMembersWithMembership("ban");
var events_levels_section;
if (events_levels.length) {
events_levels_section =
<div>
<h3>Event levels</h3>
<div className="mx_RoomSettings_eventLevels mx_RoomSettings_settings">
{Object.keys(events_levels).map(function(event_type, i) {
return (
<div key={event_type}>
<label htmlFor={"mx_RoomSettings_event_"+i}>{event_type}</label>
<input type="text" defaultValue={events_levels[event_type]} size="3" id={"mx_RoomSettings_event_"+i} disabled/>
</div>
);
})}
</div>
</div>;
}
var banned_users_section;
if (banned.length) {
banned_users_section =
<div>
<h3>Banned users</h3>
<div className="mx_RoomSettings_banned">
{banned.map(function(member, i) {
return (
<div key={i}>
{member.userId}
</div>
);
})}
</div>
</div>;
}
return (
<div className="mx_RoomSettings">
<textarea className="mx_RoomSettings_description" placeholder="Topic" defaultValue={topic} ref="topic"/> <br/>
@ -174,10 +301,13 @@ module.exports = React.createClass({
<input type="checkbox" ref="guests_join" defaultChecked={guest_access === "can_join"}/>
Allow guests to join this room
</label> <br/>
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label> <br/>
<label className="mx_RoomSettings_encrypt"><input type="checkbox" /> Encrypt room</label>
{ room_colors_section }
<h3>Power levels</h3>
<div className="mx_RoomSettings_power_levels mx_RoomSettings_settings">
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
<div>
<label htmlFor="mx_RoomSettings_ban_level">Ban level</label>
<input type="text" defaultValue={ban_level} size="3" ref="ban" id="mx_RoomSettings_ban_level"
@ -217,7 +347,7 @@ module.exports = React.createClass({
</div>
<h3>User levels</h3>
<div className="mx_RoomSettings_user_levels mx_RoomSettings_settings">
<div className="mx_RoomSettings_userLevels mx_RoomSettings_settings">
{Object.keys(user_levels).map(function(user, i) {
return (
<div key={user}>
@ -228,29 +358,9 @@ module.exports = React.createClass({
})}
</div>
<h3>Event levels</h3>
<div className="mx_RoomSettings_event_lvels mx_RoomSettings_settings">
{Object.keys(events_levels).map(function(event_type, i) {
return (
<div key={event_type}>
<label htmlFor={"mx_RoomSettings_event_"+i}>{event_type}</label>
<input type="text" defaultValue={events_levels[event_type]} size="3" id={"mx_RoomSettings_event_"+i} disabled/>
</div>
);
})}
</div>
<h3>Banned users</h3>
<div className="mx_RoomSettings_banned">
{banned.map(function(member, i) {
return (
<div key={i}>
{member.userId}
</div>
);
})}
</div>
{change_avatar}
{ events_levels_section }
{ banned_users_section }
{ change_avatar }
</div>
);
}

View File

@ -111,13 +111,14 @@ module.exports = React.createClass({
// Having just set an avatar we just display that since it will take a little
// time to propagate through to the RoomAvatar.
if (this.props.room && !this.avatarSet) {
avatarImg = <RoomAvatar room={this.props.room} width='320' height='240' resizeMethod='scale' />;
avatarImg = <RoomAvatar room={this.props.room} width='240' height='240' resizeMethod='crop' />;
} else {
var style = {
maxWidth: 320,
maxWidth: 240,
maxHeight: 240,
objectFit: 'cover',
};
avatarImg = <img src={this.state.avatarUrl} style={style} />;
avatarImg = <img className="mx_RoomAvatar" src={this.state.avatarUrl} style={style} />;
}
var uploadSection;