Merge branch 'develop' into kegan/translation-tamarin
This commit is contained in:
commit
c57823a31d
51 changed files with 2759 additions and 795 deletions
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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={_t('Create a new chat or reuse an existing one')}
|
||||
onFinished={ this.props.onFinished.bind(false) }
|
||||
title={title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
{_t("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,
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ import AccessibleButton from '../elements/AccessibleButton';
|
|||
import q from 'q';
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: "ChatInviteDialog",
|
||||
|
@ -40,13 +41,13 @@ module.exports = React.createClass({
|
|||
roomId: React.PropTypes.string,
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
value: "",
|
||||
focus: true
|
||||
focus: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -54,12 +55,20 @@ module.exports = React.createClass({
|
|||
return {
|
||||
error: false,
|
||||
|
||||
// List of AddressTile.InviteAddressType objects represeting
|
||||
// List of AddressTile.InviteAddressType objects representing
|
||||
// the list of addresses we're going to invite
|
||||
inviteList: [],
|
||||
|
||||
// List of AddressTile.InviteAddressType objects represeting
|
||||
// the set of autocompletion results for the current search
|
||||
// Whether a search is ongoing
|
||||
busy: false,
|
||||
// An error message generated during the user directory search
|
||||
searchError: null,
|
||||
// Whether the server supports the user_directory API
|
||||
serverSupportsUserDirectory: true,
|
||||
// The query being searched for
|
||||
query: "",
|
||||
// List of AddressTile.InviteAddressType objects representing
|
||||
// the set of auto-completion results for the current search
|
||||
// query.
|
||||
queryList: [],
|
||||
};
|
||||
|
@ -70,7 +79,6 @@ module.exports = React.createClass({
|
|||
// Set the cursor at the end of the text input
|
||||
this.refs.textinput.value = this.props.value;
|
||||
}
|
||||
this._updateUserList();
|
||||
},
|
||||
|
||||
onButtonClick: function() {
|
||||
|
@ -92,16 +100,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);
|
||||
|
@ -128,15 +145,15 @@ module.exports = React.createClass({
|
|||
} else if (e.keyCode === 38) { // up arrow
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.addressSelector.moveSelectionUp();
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionUp();
|
||||
} else if (e.keyCode === 40) { // down arrow
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.addressSelector.moveSelectionDown();
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionDown();
|
||||
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.addressSelector.chooseSelection();
|
||||
if (this.addressSelector) this.addressSelector.chooseSelection();
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
@ -159,74 +176,36 @@ module.exports = React.createClass({
|
|||
|
||||
onQueryChanged: function(ev) {
|
||||
const query = ev.target.value.toLowerCase();
|
||||
let queryList = [];
|
||||
|
||||
if (query.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.queryChangedDebouncer) {
|
||||
clearTimeout(this.queryChangedDebouncer);
|
||||
}
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@') {
|
||||
this._userList.forEach((user) => {
|
||||
if (user.userId.toLowerCase().indexOf(query) === -1 &&
|
||||
user.displayName.toLowerCase().indexOf(query) === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return objects, structure of which is defined
|
||||
// by InviteAddressType
|
||||
queryList.push({
|
||||
addressType: 'mx',
|
||||
address: user.userId,
|
||||
displayName: user.displayName,
|
||||
avatarMxc: user.avatarUrl,
|
||||
isKnown: true,
|
||||
order: user.getLastActiveTs(),
|
||||
});
|
||||
});
|
||||
|
||||
queryList = queryList.sort((a,b) => {
|
||||
return a.order < b.order;
|
||||
});
|
||||
|
||||
// If the query is a valid address, add an entry for that
|
||||
// This is important, otherwise there's no way to invite
|
||||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (addrType !== null) {
|
||||
queryList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@' && query.length >= 2) {
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
if (this.state.serverSupportsUserDirectory) {
|
||||
this._doUserDirectorySearch(query);
|
||||
} else {
|
||||
this._doLocalSearch(query);
|
||||
}
|
||||
}
|
||||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||
} else {
|
||||
this.setState({
|
||||
queryList: queryList,
|
||||
error: false,
|
||||
}, () => {
|
||||
this.addressSelector.moveSelectionTop();
|
||||
queryList: [],
|
||||
query: "",
|
||||
searchError: null,
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
|
||||
onDismissed: function(index) {
|
||||
var self = this;
|
||||
return function() {
|
||||
return () => {
|
||||
var inviteList = self.state.inviteList.slice();
|
||||
inviteList.splice(index, 1);
|
||||
self.setState({
|
||||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
};
|
||||
|
@ -245,10 +224,103 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
},
|
||||
|
||||
_doUserDirectorySearch: function(query) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
MatrixClientPeg.get().searchUserDirectory({
|
||||
term: query,
|
||||
}).then((resp) => {
|
||||
this._processResults(resp.results, query);
|
||||
}).catch((err) => {
|
||||
console.error('Error whilst searching user directory: ', err);
|
||||
this.setState({
|
||||
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||
});
|
||||
if (err.errcode === 'M_UNRECOGNIZED') {
|
||||
this.setState({
|
||||
serverSupportsUserDirectory: false,
|
||||
});
|
||||
// Do a local search immediately
|
||||
this._doLocalSearch(query);
|
||||
}
|
||||
}).done(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_doLocalSearch: function(query) {
|
||||
this.setState({
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
const results = [];
|
||||
MatrixClientPeg.get().getUsers().forEach((user) => {
|
||||
if (user.userId.toLowerCase().indexOf(query) === -1 &&
|
||||
user.displayName.toLowerCase().indexOf(query) === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Put results in the format of the new API
|
||||
results.push({
|
||||
user_id: user.userId,
|
||||
display_name: user.displayName,
|
||||
avatar_url: user.avatarUrl,
|
||||
});
|
||||
});
|
||||
this._processResults(results, query);
|
||||
},
|
||||
|
||||
_processResults: function(results, query) {
|
||||
const queryList = [];
|
||||
results.forEach((user) => {
|
||||
if (user.user_id === MatrixClientPeg.get().credentials.userId) {
|
||||
return;
|
||||
}
|
||||
// Return objects, structure of which is defined
|
||||
// by InviteAddressType
|
||||
queryList.push({
|
||||
addressType: 'mx',
|
||||
address: user.user_id,
|
||||
displayName: user.display_name,
|
||||
avatarMxc: user.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
});
|
||||
|
||||
// If the query is a valid address, add an entry for that
|
||||
// This is important, otherwise there's no way to invite
|
||||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (addrType !== null) {
|
||||
queryList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
queryList,
|
||||
error: false,
|
||||
}, () => {
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionTop();
|
||||
});
|
||||
},
|
||||
|
||||
_getDirectMessageRooms: function(addr) {
|
||||
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
|
||||
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
|
||||
|
@ -267,11 +339,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;
|
||||
}
|
||||
|
||||
|
@ -337,16 +405,6 @@ module.exports = React.createClass({
|
|||
this.props.onFinished(true, addrTexts);
|
||||
},
|
||||
|
||||
_updateUserList: function() {
|
||||
// Get all the users
|
||||
this._userList = MatrixClientPeg.get().getUsers();
|
||||
// Remove current user
|
||||
const meIx = this._userList.findIndex((u) => {
|
||||
return u.userId === MatrixClientPeg.get().credentials.userId;
|
||||
});
|
||||
this._userList.splice(meIx, 1);
|
||||
},
|
||||
|
||||
_isOnInviteList: function(uid) {
|
||||
for (let i = 0; i < this.state.inviteList.length; i++) {
|
||||
if (
|
||||
|
@ -414,6 +472,7 @@ module.exports = React.createClass({
|
|||
this.setState({
|
||||
inviteList: inviteList,
|
||||
queryList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
return inviteList;
|
||||
|
@ -449,7 +508,7 @@ module.exports = React.createClass({
|
|||
displayName: res.displayname,
|
||||
avatarMxc: res.avatar_url,
|
||||
isKnown: true,
|
||||
}]
|
||||
}],
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -481,23 +540,27 @@ module.exports = React.createClass({
|
|||
placeholder={this.props.placeholder}
|
||||
defaultValue={this.props.value}
|
||||
autoFocus={this.props.focus}>
|
||||
</textarea>
|
||||
</textarea>,
|
||||
);
|
||||
|
||||
var error;
|
||||
var addressSelector;
|
||||
let error;
|
||||
let addressSelector;
|
||||
if (this.state.error) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}</div>;
|
||||
} else if (this.state.searchError) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{this.state.searchError}</div>;
|
||||
} else if (
|
||||
this.state.query.length > 0 &&
|
||||
this.state.queryList.length === 0 &&
|
||||
!this.state.busy
|
||||
) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{_t("No results")}</div>;
|
||||
} else {
|
||||
const addressSelectorHeader = <div className="mx_ChatInviteDialog_addressSelectHeader">
|
||||
{_t("Searching known users")}
|
||||
</div>;
|
||||
addressSelector = (
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||
addressList={ this.state.queryList }
|
||||
onSelected={ this.onSelected }
|
||||
truncateAt={ TRUNCATE_QUERY_LIST }
|
||||
header={ addressSelectorHeader }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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={_t("Set")} />
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
294
src/components/views/dialogs/SetMxIdDialog.js
Normal file
294
src/components/views/dialogs/SetMxIdDialog.js
Normal 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)s',
|
||||
{ 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: %(error_string)s',
|
||||
{ error_string: 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>
|
||||
);
|
||||
},
|
||||
});
|
84
src/components/views/elements/ActionButton.js
Normal file
84
src/components/views/elements/ActionButton.js
Normal 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>
|
||||
);
|
||||
}
|
||||
});
|
40
src/components/views/elements/CreateRoomButton.js
Normal file
40
src/components/views/elements/CreateRoomButton.js
Normal 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;
|
39
src/components/views/elements/HomeButton.js
Normal file
39
src/components/views/elements/HomeButton.js
Normal 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("Home") }
|
||||
iconPath="img/icons-home.svg"
|
||||
size={props.size}
|
||||
tooltip={props.tooltip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
HomeButton.propTypes = {
|
||||
size: PropTypes.string,
|
||||
tooltip: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default HomeButton;
|
40
src/components/views/elements/RoomDirectoryButton.js
Normal file
40
src/components/views/elements/RoomDirectoryButton.js
Normal 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;
|
39
src/components/views/elements/SettingsButton.js
Normal file
39
src/components/views/elements/SettingsButton.js
Normal 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;
|
40
src/components/views/elements/StartChatButton.js
Normal file
40
src/components/views/elements/StartChatButton.js
Normal 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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -22,6 +22,8 @@ var MatrixClientPeg = require("../../../MatrixClientPeg");
|
|||
var Modal = require("../../../Modal");
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
var ROOM_COLORS = [
|
||||
// magic room default values courtesy of Ribot
|
||||
["#76cfa6", "#eaf5f0"],
|
||||
|
@ -87,11 +89,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: _t("Please Register"),
|
||||
description: _t("Saving room color settings is only available to registered users")
|
||||
});
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,20 +272,20 @@ 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");
|
||||
|
@ -277,7 +294,22 @@ module.exports = React.createClass({
|
|||
|
||||
// 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() {
|
||||
|
@ -431,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;
|
||||
|
@ -452,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 }
|
||||
|
@ -465,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 }
|
||||
|
@ -479,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 }
|
||||
|
@ -488,13 +578,13 @@ module.exports = React.createClass({
|
|||
onHeaderClick={ self.onSubListHeaderClick }
|
||||
onShowMoreRooms={ self.onShowMoreRooms } />
|
||||
|
||||
{ Object.keys(self.state.lists).map(function(tagName) {
|
||||
{ Object.keys(self.state.lists).map((tagName) => {
|
||||
if (!tagName.match(/^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||
return <RoomSubList list={ self.state.lists[tagName] }
|
||||
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 }
|
||||
|
@ -510,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 }
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue