Merge pull request #33 from matrix-org/matthew/settings

WIP experiment of turning UserSettings controller into UserSettingsStore
pull/21833/head
Kegsay 2015-12-24 10:54:26 +00:00
commit b941904078
8 changed files with 388 additions and 150 deletions

67
src/UserSettingsStore.js Normal file
View File

@ -0,0 +1,67 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
var MatrixClientPeg = require("./MatrixClientPeg");
var Notifier = require("./Notifier");
/*
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
*/
module.exports = {
loadProfileInfo: function() {
var cli = MatrixClientPeg.get();
return cli.getProfileInfo(cli.credentials.userId);
},
saveDisplayName: function(newDisplayname) {
return MatrixClientPeg.get().setDisplayName(newDisplayname);
},
loadThreePids: function() {
return MatrixClientPeg.get().getThreePids();
},
saveThreePids: function(threePids) {
// TODO
},
getEnableNotifications: function() {
return Notifier.isEnabled();
},
setEnableNotifications: function(enable) {
if (!Notifier.supportsDesktopNotifications()) {
return;
}
Notifier.setEnabled(enable);
},
changePassword: function(old_password, new_password) {
var cli = MatrixClientPeg.get();
var authDict = {
type: 'm.login.password',
user: cli.credentials.userId,
password: old_password
};
return cli.setPassword(authDict, new_password);
},
};

View File

@ -628,6 +628,22 @@ module.exports = React.createClass({
this.showScreen("settings"); this.showScreen("settings");
}, },
onUserSettingsClose: function() {
// XXX: use browser history instead to find the previous room?
if (this.state.currentRoom) {
dis.dispatch({
action: 'view_room',
room_id: this.state.currentRoom,
});
}
else {
dis.dispatch({
action: 'view_indexed_room',
roomIndex: 0,
});
}
},
render: function() { render: function() {
var LeftPanel = sdk.getComponent('structures.LeftPanel'); var LeftPanel = sdk.getComponent('structures.LeftPanel');
var RoomView = sdk.getComponent('structures.RoomView'); var RoomView = sdk.getComponent('structures.RoomView');
@ -660,7 +676,7 @@ module.exports = React.createClass({
right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} /> right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} />
break; break;
case this.PageTypes.UserSettings: case this.PageTypes.UserSettings:
page_element = <UserSettings /> page_element = <UserSettings onClose={this.onUserSettingsClose} />
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/> right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
break; break;
case this.PageTypes.CreateRoom: case this.PageTypes.CreateRoom:

View File

@ -17,14 +17,22 @@ var React = require('react');
var sdk = require('../../index'); var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg"); var MatrixClientPeg = require("../../MatrixClientPeg");
var Modal = require('../../Modal'); var Modal = require('../../Modal');
var dis = require("../../dispatcher");
var q = require('q'); var q = require('q');
var version = require('../../../package.json').version; var version = require('../../../package.json').version;
var UserSettingsStore = require('../../UserSettingsStore');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'UserSettings', displayName: 'UserSettings',
Phases: {
Loading: "loading", propTypes: {
Display: "display", onClose: React.PropTypes.func
},
getDefaultProps: function() {
return {
onClose: function() {}
};
}, },
getInitialState: function() { getInitialState: function() {
@ -32,131 +40,227 @@ module.exports = React.createClass({
avatarUrl: null, avatarUrl: null,
threePids: [], threePids: [],
clientVersion: version, clientVersion: version,
phase: this.Phases.Loading, phase: "UserSettings.LOADING", // LOADING, DISPLAY
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
var self = this; var self = this;
var cli = MatrixClientPeg.get(); this._refreshFromServer();
var profile_d = cli.getProfileInfo(cli.credentials.userId);
var threepid_d = cli.getThreePids();
q.all([profile_d, threepid_d]).then(
function(resps) {
self.setState({
avatarUrl: resps[0].avatar_url,
threepids: resps[1].threepids,
phase: self.Phases.Display,
});
},
function(err) { console.err(err); }
);
}, },
editAvatar: function() { componentDidMount: function() {
var url = MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl); this.dispatcherRef = dis.register(this.onAction);
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); this._me = MatrixClientPeg.get().credentials.userId;
var avatarDialog = (
<div>
<ChangeAvatar initialAvatarUrl={url} />
<div className="mx_Dialog_buttons">
<button onClick={this.onAvatarDialogCancel}>Cancel</button>
</div>
</div>
);
this.avatarDialog = Modal.createDialogWithElement(avatarDialog);
}, },
addEmail: function() { componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
}, },
editDisplayName: function() { _refreshFromServer: function() {
this.refs.displayname.edit(); var self = this;
q.all([
UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids()
]).done(function(resps) {
self.setState({
avatarUrl: resps[0].avatar_url,
threepids: resps[1].threepids,
phase: "UserSettings.DISPLAY",
});
}, function(error) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Can't load user settings",
description: error.toString()
});
});
}, },
changePassword: function() { onAction: function(payload) {
var ChangePassword = sdk.getComponent('settings.ChangePassword'); if (payload.action === "notifier_enabled") {
Modal.createDialog(ChangePassword); this.forceUpdate();
}
},
onAvatarSelected: function(ev) {
var self = this;
var changeAvatar = this.refs.changeAvatar;
if (!changeAvatar) {
console.error("No ChangeAvatar found to upload image to!");
return;
}
changeAvatar.onFileSelected(ev).done(function() {
// dunno if the avatar changed, re-check it.
self._refreshFromServer();
}, function(err) {
var errMsg = (typeof err === "string") ? err : (err.error || "");
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "Failed to set avatar. " + errMsg
});
});
}, },
onLogoutClicked: function(ev) { onLogoutClicked: function(ev) {
var LogoutPrompt = sdk.getComponent('dialogs.LogoutPrompt'); var LogoutPrompt = sdk.getComponent('dialogs.LogoutPrompt');
this.logoutModal = Modal.createDialog(LogoutPrompt, {onCancel: this.onLogoutPromptCancel}); this.logoutModal = Modal.createDialog(
LogoutPrompt, {onCancel: this.onLogoutPromptCancel}
);
},
onPasswordChangeError: function(err) {
var errMsg = err.error || "";
if (err.httpStatus === 403) {
errMsg = "Failed to change password. Is your password correct?";
}
else if (err.httpStatus) {
errMsg += ` (HTTP status ${err.httpStatus})`;
}
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Error",
description: errMsg
});
},
onPasswordChanged: function() {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Success",
description: `Your password was successfully changed. You will not
receive push notifications on other devices until you
log back in to them.`
});
}, },
onLogoutPromptCancel: function() { onLogoutPromptCancel: function() {
this.logoutModal.closeDialog(); this.logoutModal.closeDialog();
}, },
onAvatarDialogCancel: function() { onEnableNotificationsChange: function(event) {
this.avatarDialog.close(); UserSettingsStore.setEnableNotifications(event.target.checked);
}, },
render: function() { render: function() {
var Loader = sdk.getComponent("elements.Spinner"); switch (this.state.phase) {
if (this.state.phase === this.Phases.Loading) { case "UserSettings.LOADING":
return <Loader /> var Loader = sdk.getComponent("elements.Spinner");
return (
<Loader />
);
case "UserSettings.DISPLAY":
break; // quit the switch to return the common state
default:
throw new Error("Unknown state.phase => " + this.state.phase);
} }
else if (this.state.phase === this.Phases.Display) { // can only get here if phase is UserSettings.DISPLAY
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName'); var RoomHeader = sdk.getComponent('rooms.RoomHeader');
var EnableNotificationsButton = sdk.getComponent('settings.EnableNotificationsButton'); var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
return ( var ChangePassword = sdk.getComponent("views.settings.ChangePassword");
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var avatarUrl = (
this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null
);
return (
<div className="mx_UserSettings"> <div className="mx_UserSettings">
<div className="mx_UserSettings_User"> <RoomHeader simpleHeader="Settings" />
<h1>User Settings</h1>
<hr/> <h2>Profile</h2>
<div className="mx_UserSettings_User_Inner">
<div className="mx_UserSettings_Avatar"> <div className="mx_UserSettings_section">
<div className="mx_UserSettings_Avatar_Text"> <div className="mx_UserSettings_profileTable">
Profile Photo <div className="mx_UserSettings_profileTableRow">
<div className="mx_UserSettings_profileLabelCell">
<label htmlFor="displayName">Display name</label>
</div> </div>
<div className="mx_UserSettings_Avatar_Edit" onClick={this.editAvatar}> <div className="mx_UserSettings_profileInputCell">
Edit <ChangeDisplayName />
</div> </div>
</div> </div>
<div className="mx_UserSettings_DisplayName"> {this.state.threepids.map(function(val, pidIndex) {
<ChangeDisplayName ref="displayname" /> var id = "email-" + val.address;
<div className="mx_UserSettings_DisplayName_Edit" onClick={this.editDisplayName}> return (
Edit <div className="mx_UserSettings_profileTableRow" key={pidIndex}>
</div> <div className="mx_UserSettings_profileLabelCell">
</div> <label htmlFor={id}>Email</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<input key={val.address} id={id} value={val.address} disabled />
</div>
</div>
);
})}
</div>
<div className="mx_UserSettings_3pids"> <div className="mx_UserSettings_avatarPicker">
{this.state.threepids.map(function(val) { <ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
return <div key={val.address}>{val.address}</div>; showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
})} <div className="mx_UserSettings_avatarPicker_edit">
</div> <label htmlFor="avatarInput">
<img src="img/upload.svg"
<div className="mx_UserSettings_Add3pid" onClick={this.addEmail}> alt="Upload avatar" title="Upload avatar"
Add email width="19" height="24" />
</label>
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
</div> </div>
</div> </div>
</div> </div>
<div className="mx_UserSettings_Global"> <h2>Account</h2>
<h1>Global Settings</h1>
<hr/> <div className="mx_UserSettings_section">
<div className="mx_UserSettings_Global_Inner"> <ChangePassword
<div className="mx_UserSettings_ChangePassword" onClick={this.changePassword}> className="mx_UserSettings_accountTable"
Change Password rowClassName="mx_UserSettings_profileTableRow"
</div> rowLabelClassName="mx_UserSettings_profileLabelCell"
<div className="mx_UserSettings_ClientVersion"> rowInputClassName="mx_UserSettings_profileInputCell"
Version {this.state.clientVersion} buttonClassName="mx_UserSettings_button"
</div> onError={this.onPasswordChangeError}
<div className="mx_UserSettings_EnableNotifications"> onFinished={this.onPasswordChanged} />
<EnableNotificationsButton /> </div>
</div>
<div className="mx_UserSettings_Logout"> <div className="mx_UserSettings_logout">
<button onClick={this.onLogoutClicked}>Sign Out</button> <div className="mx_UserSettings_button" onClick={this.onLogoutClicked}>
Log out
</div>
</div>
<h2>Notifications</h2>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_notifTable">
<div className="mx_UserSettings_notifTableRow">
<div className="mx_UserSettings_notifInputCell">
<input id="enableNotifications"
ref="enableNotifications"
type="checkbox"
checked={ UserSettingsStore.getEnableNotifications() }
onChange={ this.onEnableNotificationsChange } />
</div>
<div className="mx_UserSettings_notifLabelCell">
<label htmlFor="enableNotifications">
Enable desktop notifications
</label>
</div>
</div> </div>
</div> </div>
</div> </div>
<h2>Advanced</h2>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_advanced">
Logged in as {this._me}
</div>
<div className="mx_UserSettings_advanced">
Version {this.state.clientVersion}
</div>
</div>
</div> </div>
); );
}
} }
}); });

View File

@ -113,6 +113,10 @@ module.exports = React.createClass({
} }
}, },
onBlur: function() {
this.cancelEdit();
},
render: function() { render: function() {
var editable_el; var editable_el;
@ -125,7 +129,8 @@ module.exports = React.createClass({
} else if (this.state.phase == this.Phases.Edit) { } else if (this.state.phase == this.Phases.Edit) {
editable_el = ( editable_el = (
<div> <div>
<input type="text" defaultValue={this.state.value} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onFinish} placeholder={this.props.placeHolder} autoFocus/> <input type="text" defaultValue={this.state.value}
onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur} placeholder={this.props.placeHolder} autoFocus/>
</div> </div>
); );
} }

View File

@ -73,10 +73,15 @@ module.exports = React.createClass({
var header; var header;
if (this.props.simpleHeader) { if (this.props.simpleHeader) {
var cancel;
if (this.props.onCancelClick) {
cancel = <img className="mx_RoomHeader_simpleHeaderCancel" src="img/cancel-black.png" onClick={ this.props.onCancelClick } alt="Close" width="18" height="18"/>
}
header = header =
<div className="mx_RoomHeader_wrapper"> <div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader"> <div className="mx_RoomHeader_simpleHeader">
{ this.props.simpleHeader } { this.props.simpleHeader }
{ cancel }
</div> </div>
</div> </div>
} }

View File

@ -23,6 +23,9 @@ module.exports = React.createClass({
propTypes: { propTypes: {
initialAvatarUrl: React.PropTypes.string, initialAvatarUrl: React.PropTypes.string,
room: React.PropTypes.object, room: React.PropTypes.object,
// if false, you need to call changeAvatar.onFileSelected yourself.
showUploadSection: React.PropTypes.bool,
className: React.PropTypes.string
}, },
Phases: { Phases: {
@ -31,6 +34,13 @@ module.exports = React.createClass({
Error: "error", Error: "error",
}, },
getDefaultProps: function() {
return {
showUploadSection: true,
className: "mx_Dialog_content" // FIXME - shouldn't be this by default
};
},
getInitialState: function() { getInitialState: function() {
return { return {
avatarUrl: this.props.initialAvatarUrl, avatarUrl: this.props.initialAvatarUrl,
@ -55,7 +65,7 @@ module.exports = React.createClass({
phase: this.Phases.Uploading phase: this.Phases.Uploading
}); });
var self = this; var self = this;
MatrixClientPeg.get().uploadContent(file).then(function(url) { var httpPromise = MatrixClientPeg.get().uploadContent(file).then(function(url) {
newUrl = url; newUrl = url;
if (self.props.room) { if (self.props.room) {
return MatrixClientPeg.get().sendStateEvent( return MatrixClientPeg.get().sendStateEvent(
@ -67,7 +77,9 @@ module.exports = React.createClass({
} else { } else {
return MatrixClientPeg.get().setAvatarUrl(url); return MatrixClientPeg.get().setAvatarUrl(url);
} }
}).done(function() { });
httpPromise.done(function() {
self.setState({ self.setState({
phase: self.Phases.Display, phase: self.Phases.Display,
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl) avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(newUrl)
@ -78,11 +90,13 @@ module.exports = React.createClass({
}); });
self.onError(error); self.onError(error);
}); });
return httpPromise;
}, },
onFileSelected: function(ev) { onFileSelected: function(ev) {
this.avatarSet = true; this.avatarSet = true;
this.setAvatarFromFile(ev.target.files[0]); return this.setAvatarFromFile(ev.target.files[0]);
}, },
onError: function(error) { onError: function(error) {
@ -106,19 +120,26 @@ module.exports = React.createClass({
avatarImg = <img src={this.state.avatarUrl} style={style} />; avatarImg = <img src={this.state.avatarUrl} style={style} />;
} }
var uploadSection;
if (this.props.showUploadSection) {
uploadSection = (
<div className={this.props.className}>
Upload new:
<input type="file" onChange={this.onFileSelected}/>
{this.state.errorText}
</div>
);
}
switch (this.state.phase) { switch (this.state.phase) {
case this.Phases.Display: case this.Phases.Display:
case this.Phases.Error: case this.Phases.Error:
return ( return (
<div> <div>
<div className="mx_Dialog_content"> <div className={this.props.className}>
{avatarImg} {avatarImg}
</div> </div>
<div className="mx_Dialog_content"> {uploadSection}
Upload new:
<input type="file" onChange={this.onFileSelected}/>
{this.state.errorText}
</div>
</div> </div>
); );
case this.Phases.Uploading: case this.Phases.Uploading:

View File

@ -98,7 +98,9 @@ module.exports = React.createClass({
} else { } else {
var EditableText = sdk.getComponent('elements.EditableText'); var EditableText = sdk.getComponent('elements.EditableText');
return ( return (
<EditableText ref="displayname_edit" initialValue={this.state.displayName} label="Click to set display name." onValueChanged={this.onValueChanged}/> <EditableText ref="displayname_edit" initialValue={this.state.displayName}
label="Click to set display name."
onValueChanged={this.onValueChanged} />
); );
} }
} }

View File

@ -18,30 +18,47 @@ limitations under the License.
var React = require('react'); var React = require('react');
var MatrixClientPeg = require("../../../MatrixClientPeg"); var MatrixClientPeg = require("../../../MatrixClientPeg");
var sdk = require("../../../index");
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ChangePassword', displayName: 'ChangePassword',
propTypes: { propTypes: {
onFinished: React.PropTypes.func, onFinished: React.PropTypes.func,
onError: React.PropTypes.func,
onCheckPassword: React.PropTypes.func,
rowClassName: React.PropTypes.string,
rowLabelClassName: React.PropTypes.string,
rowInputClassName: React.PropTypes.string,
buttonClassName: React.PropTypes.string
}, },
Phases: { Phases: {
Edit: "edit", Edit: "edit",
Uploading: "uploading", Uploading: "uploading",
Error: "error", Error: "error"
Success: "Success"
}, },
getDefaultProps: function() { getDefaultProps: function() {
return { return {
onFinished: function() {}, onFinished: function() {},
onError: function() {},
onCheckPassword: function(oldPass, newPass, confirmPass) {
if (newPass !== confirmPass) {
return {
error: "New passwords don't match."
};
} else if (!newPass || newPass.length === 0) {
return {
error: "Passwords can't be empty"
};
}
}
}; };
}, },
getInitialState: function() { getInitialState: function() {
return { return {
phase: this.Phases.Edit, phase: this.Phases.Edit
errorString: ''
} }
}, },
@ -55,60 +72,72 @@ module.exports = React.createClass({
}; };
this.setState({ this.setState({
phase: this.Phases.Uploading, phase: this.Phases.Uploading
errorString: '', });
})
var d = cli.setPassword(authDict, new_password);
var self = this; var self = this;
d.then(function() { cli.setPassword(authDict, new_password).then(function() {
self.setState({ self.props.onFinished();
phase: self.Phases.Success,
errorString: '',
})
}, function(err) { }, function(err) {
self.props.onError(err);
}).finally(function() {
self.setState({ self.setState({
phase: self.Phases.Error, phase: self.Phases.Edit
errorString: err.toString() });
}) }).done();
});
}, },
onClickChange: function() { onClickChange: function() {
var old_password = this.refs.old_input.value; var old_password = this.refs.old_input.value;
var new_password = this.refs.new_input.value; var new_password = this.refs.new_input.value;
var confirm_password = this.refs.confirm_input.value; var confirm_password = this.refs.confirm_input.value;
if (new_password != confirm_password) { var err = this.props.onCheckPassword(
this.setState({ old_password, new_password, confirm_password
state: this.Phases.Error, );
errorString: "Passwords don't match" if (err) {
}); this.props.onError(err);
} else if (new_password == '' || old_password == '') { }
this.setState({ else {
state: this.Phases.Error,
errorString: "Passwords can't be empty"
});
} else {
this.changePassword(old_password, new_password); this.changePassword(old_password, new_password);
} }
}, },
render: function() { render: function() {
var rowClassName = this.props.rowClassName;
var rowLabelClassName = this.props.rowLabelClassName;
var rowInputClassName = this.props.rowInputClassName
var buttonClassName = this.props.buttonClassName;
switch (this.state.phase) { switch (this.state.phase) {
case this.Phases.Edit: case this.Phases.Edit:
case this.Phases.Error:
return ( return (
<div> <div className={this.props.className}>
<div className="mx_Dialog_content"> <div className={rowClassName}>
<div>{this.state.errorString}</div> <div className={rowLabelClassName}>
<div><label>Old password <input type="password" ref="old_input"/></label></div> <label htmlFor="passwordold">Current password</label>
<div><label>New password <input type="password" ref="new_input"/></label></div> </div>
<div><label>Confirm password <input type="password" ref="confirm_input"/></label></div> <div className={rowInputClassName}>
<input id="passwordold" type="password" ref="old_input" />
</div>
</div> </div>
<div className="mx_Dialog_buttons"> <div className={rowClassName}>
<button onClick={this.onClickChange}>Change Password</button> <div className={rowLabelClassName}>
<button onClick={this.props.onFinished}>Cancel</button> <label htmlFor="password1">New password</label>
</div>
<div className={rowInputClassName}>
<input id="password1" type="password" ref="new_input" />
</div>
</div>
<div className={rowClassName}>
<div className={rowLabelClassName}>
<label htmlFor="password2">Confirm password</label>
</div>
<div className={rowInputClassName}>
<input id="password2" type="password" ref="confirm_input" />
</div>
</div>
<div className={buttonClassName} onClick={this.onClickChange}>
Change Password
</div> </div>
</div> </div>
); );
@ -119,17 +148,6 @@ module.exports = React.createClass({
<Loader /> <Loader />
</div> </div>
); );
case this.Phases.Success:
return (
<div>
<div className="mx_Dialog_content">
Success!
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onFinished}>Ok</button>
</div>
</div>
)
} }
} }
}); });