Merge remote-tracking branch 'origin/develop' into rav/handle_received_room_key_requests

This commit is contained in:
Richard van der Hoff 2017-06-07 15:44:33 +01:00
commit 27e5098e00
71 changed files with 2580 additions and 732 deletions

View file

@ -32,6 +32,7 @@ module.exports = React.createClass({
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
width: React.PropTypes.number,
height: React.PropTypes.number,
// XXX resizeMethod not actually used.
resizeMethod: React.PropTypes.string,
defaultToInitialLetter: React.PropTypes.bool // true to add default url
},

View file

@ -16,37 +16,30 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton from '../elements/AccessibleButton';
import Unread from '../../../Unread';
import classNames from 'classnames';
import createRoom from '../../../createRoom';
export default class ChatCreateOrReuseDialog extends React.Component {
constructor(props) {
super(props);
this.onNewDMClick = this.onNewDMClick.bind(this);
this.onRoomTileClick = this.onRoomTileClick.bind(this);
this.state = {
tiles: [],
profile: {
displayName: null,
avatarUrl: null,
},
profileError: null,
};
}
onNewDMClick() {
createRoom({dmUserId: this.props.userId});
this.props.onFinished(true);
}
onRoomTileClick(roomId) {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
this.props.onFinished(true);
}
render() {
componentWillMount() {
const client = MatrixClientPeg.get();
const dmRoomMap = new DMRoomMap(client);
@ -71,40 +64,123 @@ export default class ChatCreateOrReuseDialog extends React.Component {
highlight={highlight}
isInvite={me.membership == "invite"}
onClick={this.onRoomTileClick}
/>
/>,
);
}
}
const labelClasses = classNames({
mx_MemberInfo_createRoom_label: true,
mx_RoomTile_name: true,
this.setState({
tiles: tiles,
});
const startNewChat = <AccessibleButton
className="mx_MemberInfo_createRoom"
onClick={this.onNewDMClick}
>
<div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>{_t("Start new chat")}</i></div>
</AccessibleButton>;
if (tiles.length === 0) {
this.setState({
busyProfile: true,
});
MatrixClientPeg.get().getProfileInfo(this.props.userId).done((resp) => {
const profile = {
displayName: resp.displayname,
avatarUrl: null,
};
if (resp.avatar_url) {
profile.avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(
resp.avatar_url, 48, 48, "crop",
);
}
this.setState({
busyProfile: false,
profile: profile,
});
}, (err) => {
console.error(
'Unable to get profile for user ' + this.props.userId + ':',
err,
);
this.setState({
busyProfile: false,
profileError: err,
});
});
}
}
onRoomTileClick(roomId) {
this.props.onExistingRoomSelected(roomId);
}
render() {
let title = '';
let content = null;
if (this.state.tiles.length > 0) {
// Show the existing rooms with a "+" to add a new dm
title = _t('Create a new chat or reuse an existing one');
const labelClasses = classNames({
mx_MemberInfo_createRoom_label: true,
mx_RoomTile_name: true,
});
const startNewChat = <AccessibleButton
className="mx_MemberInfo_createRoom"
onClick={this.props.onNewDMClick}
>
<div className="mx_RoomTile_avatar">
<img src="img/create-big.svg" width="26" height="26" />
</div>
<div className={labelClasses}><i>{ _t("Start new chat") }</i></div>
</AccessibleButton>;
content = <div className="mx_Dialog_content">
{ _t('You already have existing direct chats with this user:') }
<div className="mx_ChatCreateOrReuseDialog_tiles">
{ this.state.tiles }
{ startNewChat }
</div>
</div>;
} else {
// Show the avatar, name and a button to confirm that a new chat is requested
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const Spinner = sdk.getComponent('elements.Spinner');
title = _t('Start chatting');
let profile = null;
if (this.state.busyProfile) {
profile = <Spinner />;
} else if (this.state.profileError) {
profile = <div className="error">
Unable to load profile information for { this.props.userId }
</div>;
} else {
profile = <div className="mx_ChatCreateOrReuseDialog_profile">
<BaseAvatar
name={this.state.profile.displayName || this.props.userId}
url={this.state.profile.avatarUrl}
width={48} height={48}
/>
<div className="mx_ChatCreateOrReuseDialog_profile_name">
{this.state.profile.displayName || this.props.userId}
</div>
</div>;
}
content = <div>
<div className="mx_Dialog_content">
<p>
{ _t('Click on the button below to start chatting!') }
</p>
{ profile }
</div>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick}>
{ _t('Start Chatting') }
</button>
</div>
</div>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className='mx_ChatCreateOrReuseDialog'
onFinished={() => {
this.props.onFinished(false)
}}
title='Create a new chat or reuse an existing one'
onFinished={ this.props.onFinished.bind(false) }
title={title}
>
<div className="mx_Dialog_content">
You already have existing direct chats with this user:
<div className="mx_ChatCreateOrReuseDialog_tiles">
{tiles}
{startNewChat}
</div>
</div>
{ content }
</BaseDialog>
);
}
@ -112,5 +188,8 @@ export default class ChatCreateOrReuseDialog extends React.Component {
ChatCreateOrReuseDialog.propTyps = {
userId: React.PropTypes.string.isRequired,
// Called when clicking outside of the dialog
onFinished: React.PropTypes.func.isRequired,
onNewDMClick: React.PropTypes.func.isRequired,
onExistingRoomSelected: React.PropTypes.func.isRequired,
};

View file

@ -15,15 +15,12 @@ limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
import createRoom from '../../../createRoom';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import rate_limited_func from '../../../ratelimitedfunc';
import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
import q from 'q';
@ -95,16 +92,25 @@ module.exports = React.createClass({
// A Direct Message room already exists for this user, so select a
// room from a list that is similar to the one in MemberInfo panel
const ChatCreateOrReuseDialog = sdk.getComponent(
"views.dialogs.ChatCreateOrReuseDialog"
"views.dialogs.ChatCreateOrReuseDialog",
);
Modal.createDialog(ChatCreateOrReuseDialog, {
userId: userId,
onFinished: (success) => {
if (success) {
this.props.onFinished(true, inviteList[0]);
}
// else show this ChatInviteDialog again
}
this.props.onFinished(success);
},
onNewDMClick: () => {
dis.dispatch({
action: 'start_chat',
user_id: userId,
});
},
onExistingRoomSelected: (roomId) => {
dis.dispatch({
action: 'view_room',
user_id: roomId,
});
},
});
} else {
this._startChat(inviteList);
@ -270,11 +276,7 @@ module.exports = React.createClass({
_startChat: function(addrs) {
if (MatrixClientPeg.get().isGuest()) {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: _t("Please Register"),
description: _t("Guest users can't invite users. Please register."),
});
dis.dispatch({action: 'view_set_mxid'});
return;
}

View file

@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Matrix from 'matrix-js-sdk';
import React from 'react';
import sdk from '../../../index';

View file

@ -1,89 +0,0 @@
/*
Copyright 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.
*/
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
/**
* Prompt the user to set a display name.
*
* On success, `onFinished(true, newDisplayName)` is called.
*/
export default React.createClass({
displayName: 'SetDisplayNameDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
currentDisplayName: React.PropTypes.string,
},
getInitialState: function() {
if (this.props.currentDisplayName) {
return { value: this.props.currentDisplayName };
}
if (MatrixClientPeg.get().isGuest()) {
return { value : "Guest " + MatrixClientPeg.get().getUserIdLocalpart() };
}
else {
return { value : MatrixClientPeg.get().getUserIdLocalpart() };
}
},
componentDidMount: function() {
this.refs.input_value.select();
},
onValueChange: function(ev) {
this.setState({
value: ev.target.value
});
},
onFormSubmit: function(ev) {
ev.preventDefault();
this.props.onFinished(true, this.state.value);
return false;
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_SetDisplayNameDialog"
onFinished={this.props.onFinished}
title={_t("Set a Display Name")}
>
<div className="mx_Dialog_content">
{_t("Your display name is how you'll appear to others when you speak in rooms. " +
"What would you like it to be?")}
</div>
<form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content">
<input type="text" ref="input_value" value={this.state.value}
autoFocus={true} onChange={this.onValueChange} size="30"
className="mx_SetDisplayNameDialog_input"
/>
</div>
<div className="mx_Dialog_buttons">
<input className="mx_Dialog_primary" type="submit" value="Set" />
</div>
</form>
</BaseDialog>
);
},
});

View file

@ -0,0 +1,294 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations 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.
*/
import q from 'q';
import React from 'react';
import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames';
import KeyCode from '../../../KeyCode';
import { _t, _tJsx } from '../../../languageHandler';
// The amount of time to wait for further changes to the input username before
// sending a request to the server
const USERNAME_CHECK_DEBOUNCE_MS = 250;
/**
* Prompt the user to set a display name.
*
* On success, `onFinished(true, newDisplayName)` is called.
*/
export default React.createClass({
displayName: 'SetMxIdDialog',
propTypes: {
onFinished: React.PropTypes.func.isRequired,
// Called when the user requests to register with a different homeserver
onDifferentServerClicked: React.PropTypes.func.isRequired,
// Called if the user wants to switch to login instead
onLoginClick: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
// The entered username
username: '',
// Indicate ongoing work on the username
usernameBusy: false,
// Indicate error with username
usernameError: '',
// Assume the homeserver supports username checking until "M_UNRECOGNIZED"
usernameCheckSupport: true,
// Whether the auth UI is currently being used
doingUIAuth: false,
// Indicate error with auth
authError: '',
};
},
componentDidMount: function() {
this.refs.input_value.select();
this._matrixClient = MatrixClientPeg.get();
},
onValueChange: function(ev) {
this.setState({
username: ev.target.value,
usernameBusy: true,
usernameError: '',
}, () => {
if (!this.state.username || !this.state.usernameCheckSupport) {
this.setState({
usernameBusy: false,
});
return;
}
// Debounce the username check to limit number of requests sent
if (this._usernameCheckTimeout) {
clearTimeout(this._usernameCheckTimeout);
}
this._usernameCheckTimeout = setTimeout(() => {
this._doUsernameCheck().finally(() => {
this.setState({
usernameBusy: false,
});
});
}, USERNAME_CHECK_DEBOUNCE_MS);
});
},
onKeyUp: function(ev) {
if (ev.keyCode === KeyCode.ENTER) {
this.onSubmit();
}
},
onSubmit: function(ev) {
this.setState({
doingUIAuth: true,
});
},
_doUsernameCheck: function() {
// Check if username is available
return this._matrixClient.isUsernameAvailable(this.state.username).then(
(isAvailable) => {
if (isAvailable) {
this.setState({usernameError: ''});
}
},
(err) => {
// Indicate whether the homeserver supports username checking
const newState = {
usernameCheckSupport: err.errcode !== "M_UNRECOGNIZED",
};
console.error('Error whilst checking username availability: ', err);
switch (err.errcode) {
case "M_USER_IN_USE":
newState.usernameError = _t('Username not available');
break;
case "M_INVALID_USERNAME":
newState.usernameError = _t(
'Username invalid: %(errMessage)',
{ errMessage: err.message},
);
break;
case "M_UNRECOGNIZED":
// This homeserver doesn't support username checking, assume it's
// fine and rely on the error appearing in registration step.
newState.usernameError = '';
break;
case undefined:
newState.usernameError = _t('Something went wrong!');
break;
default:
newState.usernameError = _t(
'An error occurred: %(errMessage)',
{ errMessage: err.message },
);
break;
}
this.setState(newState);
},
);
},
_generatePassword: function() {
return Math.random().toString(36).slice(2);
},
_makeRegisterRequest: function(auth) {
// Not upgrading - changing mxids
const guestAccessToken = null;
if (!this._generatedPassword) {
this._generatedPassword = this._generatePassword();
}
return this._matrixClient.register(
this.state.username,
this._generatedPassword,
undefined, // session id: included in the auth dict already
auth,
{},
guestAccessToken,
);
},
_onUIAuthFinished: function(success, response) {
this.setState({
doingUIAuth: false,
});
if (!success) {
this.setState({ authError: response.message });
return;
}
// XXX Implement RTS /register here
const teamToken = null;
this.props.onFinished(true, {
userId: response.user_id,
deviceId: response.device_id,
homeserverUrl: this._matrixClient.getHomeserverUrl(),
identityServerUrl: this._matrixClient.getIdentityServerUrl(),
accessToken: response.access_token,
password: this._generatedPassword,
teamToken: teamToken,
});
},
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
const Spinner = sdk.getComponent('elements.Spinner');
let auth;
if (this.state.doingUIAuth) {
auth = <InteractiveAuth
matrixClient={this._matrixClient}
makeRequest={this._makeRegisterRequest}
onAuthFinished={this._onUIAuthFinished}
inputs={{}}
poll={true}
/>;
}
const inputClasses = classnames({
"mx_SetMxIdDialog_input": true,
"error": Boolean(this.state.usernameError),
});
let usernameIndicator = null;
let usernameBusyIndicator = null;
if (this.state.usernameBusy) {
usernameBusyIndicator = <Spinner w="24" h="24"/>;
} else {
const usernameAvailable = this.state.username &&
this.state.usernameCheckSupport && !this.state.usernameError;
const usernameIndicatorClasses = classnames({
"error": Boolean(this.state.usernameError),
"success": usernameAvailable,
});
usernameIndicator = <div className={usernameIndicatorClasses}>
{ usernameAvailable ? _t('Username available') : this.state.usernameError }
</div>;
}
let authErrorIndicator = null;
if (this.state.authError) {
authErrorIndicator = <div className="error">
{ this.state.authError }
</div>;
}
const canContinue = this.state.username &&
!this.state.usernameError &&
!this.state.usernameBusy;
return (
<BaseDialog className="mx_SetMxIdDialog"
onFinished={this.props.onFinished}
title="To get started, please pick a username!"
>
<div className="mx_Dialog_content">
<div className="mx_SetMxIdDialog_input_group">
<input type="text" ref="input_value" value={this.state.username}
autoFocus={true}
onChange={this.onValueChange}
onKeyUp={this.onKeyUp}
size="30"
className={inputClasses}
/>
{ usernameBusyIndicator }
</div>
{ usernameIndicator }
<p>
{ _tJsx(
'This will be your account name on the <span></span> ' +
'homeserver, or you can pick a <a>different server</a>.',
[
/<span><\/span>/,
/<a>(.*?)<\/a>/,
],
[
(sub) => <span>{this.props.homeserverUrl}</span>,
(sub) => <a href="#" onClick={this.props.onDifferentServerClicked}>{sub}</a>,
],
)}
</p>
<p>
{ _tJsx(
'If you already have a Matrix account you can <a>log in</a> instead.',
/<a>(.*?)<\/a>/,
[(sub) => <a href="#" onClick={this.props.onLoginClick}>{sub}</a>],
)}
</p>
{ auth }
{ authErrorIndicator }
</div>
<div className="mx_Dialog_buttons">
<input className="mx_Dialog_primary"
type="submit"
value={_t("Continue")}
onClick={this.onSubmit}
disabled={!canContinue}
/>
</div>
</BaseDialog>
);
},
});

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar';
import Resend from '../../../Resend';

View file

@ -0,0 +1,84 @@
/*
Copyright 2017 Vector Creations 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.
*/
import React from 'react';
import PropTypes from 'prop-types';
import AccessibleButton from './AccessibleButton';
import dis from '../../../dispatcher';
import sdk from '../../../index';
export default React.createClass({
displayName: 'RoleButton',
propTypes: {
size: PropTypes.string,
tooltip: PropTypes.bool,
action: PropTypes.string.isRequired,
mouseOverAction: PropTypes.string,
label: PropTypes.string.isRequired,
iconPath: PropTypes.string.isRequired,
},
getDefaultProps: function() {
return {
size: "25",
tooltip: false,
};
},
getInitialState: function() {
return {
showTooltip: false,
};
},
_onClick: function(ev) {
ev.stopPropagation();
dis.dispatch({action: this.props.action});
},
_onMouseEnter: function() {
if (this.props.tooltip) this.setState({showTooltip: true});
if (this.props.mouseOverAction) {
dis.dispatch({action: this.props.mouseOverAction});
}
},
_onMouseLeave: function() {
this.setState({showTooltip: false});
},
render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let tooltip;
if (this.state.showTooltip) {
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
tooltip = <RoomTooltip className="mx_RoleButton_tooltip" label={this.props.label} />;
}
return (
<AccessibleButton className="mx_RoleButton"
onClick={this._onClick}
onMouseEnter={this._onMouseEnter}
onMouseLeave={this._onMouseLeave}
>
<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />
{tooltip}
</AccessibleButton>
);
}
});

View file

@ -19,9 +19,7 @@ limitations under the License.
import React from 'react';
import classNames from 'classnames';
import sdk from "../../../index";
import Invite from "../../../Invite";
import MatrixClientPeg from "../../../MatrixClientPeg";
import Avatar from '../../../Avatar';
import { _t } from '../../../languageHandler';
// React PropType definition for an object describing

View file

@ -0,0 +1,40 @@
/*
Copyright 2017 Vector Creations 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.
*/
import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const CreateRoomButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_create_room"
mouseOverAction={props.callout ? "callout_create_room" : null}
label={ _t("Create new room") }
iconPath="img/icons-create-room.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
CreateRoomButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default CreateRoomButton;

View file

@ -0,0 +1,39 @@
/*
Copyright 2017 Vector Creations 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.
*/
import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const HomeButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_home_page"
label={ _t("Welcome page") }
iconPath="img/icons-home.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
HomeButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default HomeButton;

View file

@ -19,7 +19,6 @@ import React from 'react';
import sdk from '../../../index';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler';
import * as languageHandler from '../../../languageHandler';
function languageMatchesSearchQuery(query, language) {

View file

@ -0,0 +1,40 @@
/*
Copyright 2017 Vector Creations 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.
*/
import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const RoomDirectoryButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_room_directory"
mouseOverAction={props.callout ? "callout_room_directory" : null}
label={ _t("Room directory") }
iconPath="img/icons-directory.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
RoomDirectoryButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default RoomDirectoryButton;

View file

@ -0,0 +1,39 @@
/*
Copyright 2017 Vector Creations 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.
*/
import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const SettingsButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_user_settings"
label={ _t("Settings") }
iconPath="img/icons-settings.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
SettingsButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default SettingsButton;

View file

@ -0,0 +1,40 @@
/*
Copyright 2017 Vector Creations 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.
*/
import React from 'react';
import sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const StartChatButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_create_chat"
mouseOverAction={props.callout ? "callout_start_chat" : null}
label={ _t("Start chat") }
iconPath="img/icons-people.svg"
size={props.size}
tooltip={props.tooltip}
/>
);
};
StartChatButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default StartChatButton;

View file

@ -16,6 +16,7 @@ limitations under the License.
'use strict';
import { _t } from '../../../languageHandler';
import React from 'react';
module.exports = React.createClass({
@ -27,5 +28,5 @@ module.exports = React.createClass({
<a href="https://matrix.org">{_t("powered by Matrix")}</a>
</div>
);
}
},
});

View file

@ -16,7 +16,6 @@ limitations under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';

View file

@ -110,18 +110,17 @@ module.exports = React.createClass({
button: _t("Continue"),
onFinished: function(confirmed) {
if (confirmed) {
self._doSubmit();
self._doSubmit(ev);
}
},
});
}
else {
self._doSubmit();
} else {
self._doSubmit(ev);
}
}
},
_doSubmit: function() {
_doSubmit: function(ev) {
let email = this.refs.email.value.trim();
var promise = this.props.onRegisterClick({
username: this.refs.username.value.trim() || this.props.guestUsername,

View file

@ -20,7 +20,6 @@ import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import { _t } from '../../../languageHandler';

View file

@ -24,7 +24,6 @@ import { _t } from '../../../languageHandler';
import {decryptFile} from '../../../utils/DecryptFile';
import Tinter from '../../../Tinter';
import request from 'browser-request';
import q from 'q';
import Modal from '../../../Modal';

View file

@ -19,8 +19,6 @@ limitations under the License.
import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Model from '../../../Modal';
import sdk from '../../../index';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import q from 'q';
import UserSettingsStore from '../../../UserSettingsStore';

View file

@ -21,6 +21,8 @@ var Tinter = require('../../../Tinter');
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
import dis from '../../../dispatcher';
var ROOM_COLORS = [
// magic room default values courtesy of Ribot
["#76cfa6", "#eaf5f0"],
@ -86,11 +88,7 @@ module.exports = React.createClass({
}
).catch(function(err) {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Saving room color settings is only available to registered users"
});
dis.dispatch({action: 'view_set_mxid'});
}
});
}

View file

@ -4,7 +4,7 @@ import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import isEqual from 'lodash/isEqual';
import sdk from '../../../index';
import type {Completion, SelectionRange} from '../../../autocomplete/Autocompleter';
import type {Completion} from '../../../autocomplete/Autocompleter';
import Q from 'q';
import {getCompletions} from '../../../autocomplete/Autocompleter';

View file

@ -375,11 +375,7 @@ module.exports = WithMatrixClient(React.createClass({
console.log("Mod toggle success");
}, function(err) {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: _t("Please Register"),
description: _t("This action cannot be performed by a guest user. Please register to be able to do this") + ".",
});
dis.dispatch({action: 'view_set_mxid'});
} else {
console.error("Toggle moderator error:" + err);
Modal.createDialog(ErrorDialog, {

View file

@ -91,11 +91,7 @@ export default class MessageComposer extends React.Component {
onUploadClick(ev) {
if (MatrixClientPeg.get().isGuest()) {
let NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: _t('Please Register'),
description: _t('Guest users can\'t upload files. Please register to upload') + '.',
});
dis.dispatch({action: 'view_set_mxid'});
return;
}

View file

@ -28,12 +28,12 @@ import Q from 'q';
import MatrixClientPeg from '../../../MatrixClientPeg';
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
import SlashCommands from '../../../SlashCommands';
import KeyCode from '../../../KeyCode';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import KeyCode from '../../../KeyCode';
import UserSettingsStore from '../../../UserSettingsStore';
import * as RichText from '../../../RichText';
@ -45,8 +45,6 @@ import {onSendMessageFailed} from './MessageComposerInputOld';
const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
const KEY_M = 77;
const ZWS_CODE = 8203;
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
function stateToMarkdown(state) {
@ -62,7 +60,7 @@ function stateToMarkdown(state) {
export default class MessageComposerInput extends React.Component {
static getKeyBinding(e: SyntheticKeyboardEvent): string {
// C-m => Toggles between rich text and markdown modes
if (e.keyCode === KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
if (e.keyCode === KeyCode.KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
return 'toggle-mode';
}

View file

@ -29,7 +29,6 @@ var Markdown = require("../../../Markdown");
var TYPING_USER_TIMEOUT = 10000;
var TYPING_SERVER_TIMEOUT = 30000;
var MARKDOWN_ENABLED = true;
export function onSendMessageFailed(err, room) {
// XXX: temporary logging to try to diagnose
@ -77,7 +76,8 @@ export default React.createClass({
componentWillMount: function() {
this.oldScrollHeight = 0;
this.markdownEnabled = MARKDOWN_ENABLED;
this.markdownEnabled = !UserSettingsStore.getSyncedSetting('disableMarkdown', false);
var self = this;
this.sentHistory = {
// The list of typed messages. Index 0 is more recent

View file

@ -18,8 +18,6 @@ limitations under the License.
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -30,7 +31,14 @@ var Rooms = require('../../../Rooms');
import DMRoomMap from '../../../utils/DMRoomMap';
var Receipt = require('../../../utils/Receipt');
var HIDE_CONFERENCE_CHANS = true;
const HIDE_CONFERENCE_CHANS = true;
const VERBS = {
'm.favourite': 'favourite',
'im.vector.fake.direct': 'tag direct chat',
'im.vector.fake.recent': 'restore',
'm.lowpriority': 'demote',
};
module.exports = React.createClass({
displayName: 'RoomList',
@ -45,6 +53,7 @@ module.exports = React.createClass({
getInitialState: function() {
return {
isLoadingLeftRooms: false,
totalRoomCount: null,
lists: {},
incomingCall: null,
};
@ -64,8 +73,14 @@ module.exports = React.createClass({
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData);
var s = this.getRoomLists();
this.setState(s);
this.refreshRoomList();
// order of the sublists
//this.listOrder = [];
// loop count to stop a stack overflow if the user keeps waggling the
// mouse for >30s in a row, or if running under mocha
this._delayedRefreshRoomListLoopCount = 0
},
componentDidMount: function() {
@ -203,31 +218,33 @@ module.exports = React.createClass({
}, 500),
refreshRoomList: function() {
// console.log("DEBUG: Refresh room list delta=%s ms",
// (!this._lastRefreshRoomListTs ? "-" : (Date.now() - this._lastRefreshRoomListTs))
// );
// TODO: rather than bluntly regenerating and re-sorting everything
// every time we see any kind of room change from the JS SDK
// we could do incremental updates on our copy of the state
// based on the room which has actually changed. This would stop
// us re-rendering all the sublists every time anything changes anywhere
// in the state of the client.
this.setState(this.getRoomLists());
// TODO: ideally we'd calculate this once at start, and then maintain
// any changes to it incrementally, updating the appropriate sublists
// as needed.
// Alternatively we'd do something magical with Immutable.js or similar.
const lists = this.getRoomLists();
let totalRooms = 0;
for (const l of Object.values(lists)) {
totalRooms += l.length;
}
this.setState({
lists: this.getRoomLists(),
totalRoomCount: totalRooms,
});
// this._lastRefreshRoomListTs = Date.now();
},
getRoomLists: function() {
var self = this;
var s = { lists: {} };
const lists = {};
s.lists["im.vector.fake.invite"] = [];
s.lists["m.favourite"] = [];
s.lists["im.vector.fake.recent"] = [];
s.lists["im.vector.fake.direct"] = [];
s.lists["m.lowpriority"] = [];
s.lists["im.vector.fake.archived"] = [];
lists["im.vector.fake.invite"] = [];
lists["m.favourite"] = [];
lists["im.vector.fake.recent"] = [];
lists["im.vector.fake.direct"] = [];
lists["m.lowpriority"] = [];
lists["im.vector.fake.archived"] = [];
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
@ -241,7 +258,7 @@ module.exports = React.createClass({
// ", prevMembership = " + me.events.member.getPrevContent().membership);
if (me.membership == "invite") {
s.lists["im.vector.fake.invite"].push(room);
lists["im.vector.fake.invite"].push(room);
}
else if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, me, self.props.ConferenceHandler)) {
// skip past this room & don't put it in any lists
@ -255,66 +272,44 @@ module.exports = React.createClass({
if (tagNames.length) {
for (var i = 0; i < tagNames.length; i++) {
var tagName = tagNames[i];
s.lists[tagName] = s.lists[tagName] || [];
s.lists[tagNames[i]].push(room);
lists[tagName] = lists[tagName] || [];
lists[tagName].push(room);
}
}
else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
s.lists["im.vector.fake.direct"].push(room);
lists["im.vector.fake.direct"].push(room);
}
else {
s.lists["im.vector.fake.recent"].push(room);
lists["im.vector.fake.recent"].push(room);
}
}
else if (me.membership === "leave") {
s.lists["im.vector.fake.archived"].push(room);
lists["im.vector.fake.archived"].push(room);
}
else {
console.error("unrecognised membership: " + me.membership + " - this should never happen");
}
});
if (s.lists["im.vector.fake.direct"].length == 0 &&
MatrixClientPeg.get().getAccountData('m.direct') === undefined &&
!MatrixClientPeg.get().isGuest())
{
// scan through the 'recents' list for any rooms which look like DM rooms
// and make them DM rooms
const oldRecents = s.lists["im.vector.fake.recent"];
s.lists["im.vector.fake.recent"] = [];
for (const room of oldRecents) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && Rooms.looksLikeDirectMessageRoom(room, me)) {
s.lists["im.vector.fake.direct"].push(room);
} else {
s.lists["im.vector.fake.recent"].push(room);
}
}
// save these new guessed DM rooms into the account data
const newMDirectEvent = {};
for (const room of s.lists["im.vector.fake.direct"]) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
const otherPerson = Rooms.getOnlyOtherMember(room, me);
if (!otherPerson) continue;
const roomList = newMDirectEvent[otherPerson.userId] || [];
roomList.push(room.roomId);
newMDirectEvent[otherPerson.userId] = roomList;
}
// if this fails, fine, we'll just do the same thing next time we get the room lists
MatrixClientPeg.get().setAccountData('m.direct', newMDirectEvent).done();
}
//console.log("calculated new roomLists; im.vector.fake.recent = " + s.lists["im.vector.fake.recent"]);
// we actually apply the sorting to this when receiving the prop in RoomSubLists.
return s;
// we'll need this when we get to iterating through lists programatically - e.g. ctrl-shift-up/down
/*
this.listOrder = [
"im.vector.fake.invite",
"m.favourite",
"im.vector.fake.recent",
"im.vector.fake.direct",
Object.keys(otherTagNames).filter(tagName=>{
return (!tagName.match(/^m\.(favourite|lowpriority)$/));
}).sort(),
"m.lowpriority",
"im.vector.fake.archived"
];
*/
return lists;
},
_getScrollNode: function() {
@ -468,6 +463,62 @@ module.exports = React.createClass({
this.refs.gemscroll.forceUpdate();
},
_getEmptyContent: function(section) {
const RoomDropTarget = sdk.getComponent('rooms.RoomDropTarget');
if (this.props.collapsed) {
return <RoomDropTarget label="" />;
}
const StartChatButton = sdk.getComponent('elements.StartChatButton');
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
switch (section) {
case 'im.vector.fake.direct':
return <div className="mx_RoomList_emptySubListTip">
Press
<StartChatButton size="16" callout={true}/>
to start a chat with someone
</div>;
case 'im.vector.fake.recent':
return <div className="mx_RoomList_emptySubListTip">
You're not in any rooms yet! Press
<CreateRoomButton size="16" callout={true}/>
to make a room or
<RoomDirectoryButton size="16" callout={true}/>
to browse the directory
</div>;
}
// We don't want to display drop targets if there are no room tiles to drag'n'drop
if (this.state.totalRoomCount === 0) {
return null;
}
const labelText = 'Drop here to ' + (VERBS[section] || 'tag ' + section);
return <RoomDropTarget label={labelText} />;
},
_getHeaderItems: function(section) {
const StartChatButton = sdk.getComponent('elements.StartChatButton');
const RoomDirectoryButton = sdk.getComponent('elements.RoomDirectoryButton');
const CreateRoomButton = sdk.getComponent('elements.CreateRoomButton');
switch (section) {
case 'im.vector.fake.direct':
return <span className="mx_RoomList_headerButtons">
<StartChatButton size="16" />
</span>;
case 'im.vector.fake.recent':
return <span className="mx_RoomList_headerButtons">
<RoomDirectoryButton size="16" />
<CreateRoomButton size="16" />
</span>;
}
},
render: function() {
var RoomSubList = sdk.getComponent('structures.RoomSubList');
var self = this;
@ -489,7 +540,7 @@ module.exports = React.createClass({
<RoomSubList list={ self.state.lists['m.favourite'] }
label={ _t('Favourites') }
tagName="m.favourite"
verb={ _t('to favourite') }
emptyContent={this._getEmptyContent('m.favourite')}
editable={ true }
order="manual"
selectedRoom={ self.props.selectedRoom }
@ -502,7 +553,8 @@ module.exports = React.createClass({
<RoomSubList list={ self.state.lists['im.vector.fake.direct'] }
label={ _t('People') }
tagName="im.vector.fake.direct"
verb={ _t('to tag direct chat') }
emptyContent={this._getEmptyContent('im.vector.fake.direct')}
headerItems={this._getHeaderItems('im.vector.fake.direct')}
editable={ true }
order="recent"
selectedRoom={ self.props.selectedRoom }
@ -516,7 +568,8 @@ module.exports = React.createClass({
<RoomSubList list={ self.state.lists['im.vector.fake.recent'] }
label={ _t('Rooms') }
editable={ true }
verb={ _t('to restore') }
emptyContent={this._getEmptyContent('im.vector.fake.recent')}
headerItems={this._getHeaderItems('im.vector.fake.recent')}
order="recent"
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
@ -531,7 +584,7 @@ module.exports = React.createClass({
key={ tagName }
label={ tagName }
tagName={ tagName }
verb={ _t('to tag as %(tagName)s', {tagName: tagName}) }
emptyContent={this._getEmptyContent(tagName)}
editable={ true }
order="manual"
selectedRoom={ self.props.selectedRoom }
@ -547,7 +600,7 @@ module.exports = React.createClass({
<RoomSubList list={ self.state.lists['m.lowpriority'] }
label={ _t('Low priority') }
tagName="m.lowpriority"
verb={ _t('to demote') }
emptyContent={this._getEmptyContent('m.lowpriority')}
editable={ true }
order="recent"
selectedRoom={ self.props.selectedRoom }

View file

@ -23,6 +23,8 @@ var sdk = require("../../../index");
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
import sessionStore from '../../../stores/SessionStore';
module.exports = React.createClass({
displayName: 'ChangePassword',
propTypes: {
@ -32,7 +34,10 @@ module.exports = React.createClass({
rowClassName: React.PropTypes.string,
rowLabelClassName: React.PropTypes.string,
rowInputClassName: React.PropTypes.string,
buttonClassName: React.PropTypes.string
buttonClassName: React.PropTypes.string,
confirm: React.PropTypes.bool,
// Whether to autoFocus the new password input
autoFocusNewPasswordInput: React.PropTypes.bool,
},
Phases: {
@ -55,20 +60,48 @@ module.exports = React.createClass({
error: _t("Passwords can't be empty")
};
}
}
},
confirm: true,
};
},
getInitialState: function() {
return {
phase: this.Phases.Edit
phase: this.Phases.Edit,
cachedPassword: null,
};
},
changePassword: function(old_password, new_password) {
var cli = MatrixClientPeg.get();
componentWillMount: function() {
this._sessionStore = sessionStore;
this._sessionStoreToken = this._sessionStore.addListener(
this._setStateFromSessionStore,
);
var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
this._setStateFromSessionStore();
},
componentWillUnmount: function() {
if (this._sessionStoreToken) {
this._sessionStoreToken.remove();
}
},
_setStateFromSessionStore: function() {
this.setState({
cachedPassword: this._sessionStore.getCachedPassword(),
});
},
changePassword: function(oldPassword, newPassword) {
const cli = MatrixClientPeg.get();
if (!this.props.confirm) {
this._changePassword(cli, oldPassword, newPassword);
return;
}
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: _t("Warning!"),
description:
@ -89,31 +122,34 @@ module.exports = React.createClass({
],
onFinished: (confirmed) => {
if (confirmed) {
var authDict = {
type: 'm.login.password',
user: cli.credentials.userId,
password: old_password
};
this.setState({
phase: this.Phases.Uploading
});
var self = this;
cli.setPassword(authDict, new_password).then(function() {
self.props.onFinished();
}, function(err) {
self.props.onError(err);
}).finally(function() {
self.setState({
phase: self.Phases.Edit
});
}).done();
this._changePassword(cli, oldPassword, newPassword);
}
},
});
},
_changePassword: function(cli, oldPassword, newPassword) {
const authDict = {
type: 'm.login.password',
user: cli.credentials.userId,
password: oldPassword,
};
this.setState({
phase: this.Phases.Uploading,
});
cli.setPassword(authDict, newPassword).then(() => {
this.props.onFinished();
}, (err) => {
this.props.onError(err);
}).finally(() => {
this.setState({
phase: this.Phases.Edit,
});
}).done();
},
_onExportE2eKeysClicked: function() {
Modal.createDialogAsync(
(cb) => {
@ -124,47 +160,53 @@ module.exports = React.createClass({
matrixClient: MatrixClientPeg.get(),
}
);
},
},
onClickChange: function() {
var old_password = this.refs.old_input.value;
var new_password = this.refs.new_input.value;
var confirm_password = this.refs.confirm_input.value;
var err = this.props.onCheckPassword(
old_password, new_password, confirm_password
const oldPassword = this.state.cachedPassword || this.refs.old_input.value;
const newPassword = this.refs.new_input.value;
const confirmPassword = this.refs.confirm_input.value;
const err = this.props.onCheckPassword(
oldPassword, newPassword, confirmPassword,
);
if (err) {
this.props.onError(err);
}
else {
this.changePassword(old_password, new_password);
} else {
this.changePassword(oldPassword, newPassword);
}
},
render: function() {
var rowClassName = this.props.rowClassName;
var rowLabelClassName = this.props.rowLabelClassName;
var rowInputClassName = this.props.rowInputClassName;
var buttonClassName = this.props.buttonClassName;
const rowClassName = this.props.rowClassName;
const rowLabelClassName = this.props.rowLabelClassName;
const rowInputClassName = this.props.rowInputClassName;
const buttonClassName = this.props.buttonClassName;
let currentPassword = null;
if (!this.state.cachedPassword) {
currentPassword = <div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="passwordold">Current password</label>
</div>
<div className={rowInputClassName}>
<input id="passwordold" type="password" ref="old_input" />
</div>
</div>;
}
switch (this.state.phase) {
case this.Phases.Edit:
const passwordLabel = this.state.cachedPassword ?
_t('Password') : _t('New Password');
return (
<div className={this.props.className}>
{ currentPassword }
<div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="passwordold">{ _t('Current password') }</label>
<label htmlFor="password1">{ passwordLabel }</label>
</div>
<div className={rowInputClassName}>
<input id="passwordold" type="password" ref="old_input" />
</div>
</div>
<div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="password1">{ _t('New password') }</label>
</div>
<div className={rowInputClassName}>
<input id="password1" type="password" ref="new_input" />
<input id="password1" type="password" ref="new_input" autoFocus={this.props.autoFocusNewPasswordInput} />
</div>
</div>
<div className={rowClassName}>
@ -176,7 +218,8 @@ module.exports = React.createClass({
</div>
</div>
<AccessibleButton className={buttonClassName}
onClick={this.onClickChange}>
onClick={this.onClickChange}
element="button">
{ _t('Change Password') }
</AccessibleButton>
</div>