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

WIP experiment of turning UserSettings controller into UserSettingsStore
This commit is contained in:
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");
},
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() {
var LeftPanel = sdk.getComponent('structures.LeftPanel');
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} />
break;
case this.PageTypes.UserSettings:
page_element = <UserSettings />
page_element = <UserSettings onClose={this.onUserSettingsClose} />
right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
break;
case this.PageTypes.CreateRoom:

View file

@ -17,14 +17,22 @@ var React = require('react');
var sdk = require('../../index');
var MatrixClientPeg = require("../../MatrixClientPeg");
var Modal = require('../../Modal');
var dis = require("../../dispatcher");
var q = require('q');
var version = require('../../../package.json').version;
var UserSettingsStore = require('../../UserSettingsStore');
module.exports = React.createClass({
displayName: 'UserSettings',
Phases: {
Loading: "loading",
Display: "display",
propTypes: {
onClose: React.PropTypes.func
},
getDefaultProps: function() {
return {
onClose: function() {}
};
},
getInitialState: function() {
@ -32,131 +40,227 @@ module.exports = React.createClass({
avatarUrl: null,
threePids: [],
clientVersion: version,
phase: this.Phases.Loading,
phase: "UserSettings.LOADING", // LOADING, DISPLAY
};
},
componentWillMount: function() {
var self = this;
var cli = MatrixClientPeg.get();
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); }
);
this._refreshFromServer();
},
editAvatar: function() {
var url = MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl);
var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
var avatarDialog = (
<div>
<ChangeAvatar initialAvatarUrl={url} />
<div className="mx_Dialog_buttons">
<button onClick={this.onAvatarDialogCancel}>Cancel</button>
</div>
</div>
);
this.avatarDialog = Modal.createDialogWithElement(avatarDialog);
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this._me = MatrixClientPeg.get().credentials.userId;
},
addEmail: function() {
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
editDisplayName: function() {
this.refs.displayname.edit();
_refreshFromServer: function() {
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() {
var ChangePassword = sdk.getComponent('settings.ChangePassword');
Modal.createDialog(ChangePassword);
onAction: function(payload) {
if (payload.action === "notifier_enabled") {
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) {
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() {
this.logoutModal.closeDialog();
},
onAvatarDialogCancel: function() {
this.avatarDialog.close();
onEnableNotificationsChange: function(event) {
UserSettingsStore.setEnableNotifications(event.target.checked);
},
render: function() {
var Loader = sdk.getComponent("elements.Spinner");
if (this.state.phase === this.Phases.Loading) {
return <Loader />
switch (this.state.phase) {
case "UserSettings.LOADING":
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) {
var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
var EnableNotificationsButton = sdk.getComponent('settings.EnableNotificationsButton');
return (
// can only get here if phase is UserSettings.DISPLAY
var RoomHeader = sdk.getComponent('rooms.RoomHeader');
var ChangeDisplayName = sdk.getComponent("views.settings.ChangeDisplayName");
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_User">
<h1>User Settings</h1>
<hr/>
<div className="mx_UserSettings_User_Inner">
<div className="mx_UserSettings_Avatar">
<div className="mx_UserSettings_Avatar_Text">
Profile Photo
<RoomHeader simpleHeader="Settings" />
<h2>Profile</h2>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_profileTable">
<div className="mx_UserSettings_profileTableRow">
<div className="mx_UserSettings_profileLabelCell">
<label htmlFor="displayName">Display name</label>
</div>
<div className="mx_UserSettings_Avatar_Edit" onClick={this.editAvatar}>
Edit
<div className="mx_UserSettings_profileInputCell">
<ChangeDisplayName />
</div>
</div>
<div className="mx_UserSettings_DisplayName">
<ChangeDisplayName ref="displayname" />
<div className="mx_UserSettings_DisplayName_Edit" onClick={this.editDisplayName}>
Edit
</div>
</div>
{this.state.threepids.map(function(val, pidIndex) {
var id = "email-" + val.address;
return (
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
<div className="mx_UserSettings_profileLabelCell">
<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">
{this.state.threepids.map(function(val) {
return <div key={val.address}>{val.address}</div>;
})}
</div>
<div className="mx_UserSettings_Add3pid" onClick={this.addEmail}>
Add email
<div className="mx_UserSettings_avatarPicker">
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
<div className="mx_UserSettings_avatarPicker_edit">
<label htmlFor="avatarInput">
<img src="img/upload.svg"
alt="Upload avatar" title="Upload avatar"
width="19" height="24" />
</label>
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
</div>
</div>
</div>
<div className="mx_UserSettings_Global">
<h1>Global Settings</h1>
<hr/>
<div className="mx_UserSettings_Global_Inner">
<div className="mx_UserSettings_ChangePassword" onClick={this.changePassword}>
Change Password
</div>
<div className="mx_UserSettings_ClientVersion">
Version {this.state.clientVersion}
</div>
<div className="mx_UserSettings_EnableNotifications">
<EnableNotificationsButton />
</div>
<div className="mx_UserSettings_Logout">
<button onClick={this.onLogoutClicked}>Sign Out</button>
<h2>Account</h2>
<div className="mx_UserSettings_section">
<ChangePassword
className="mx_UserSettings_accountTable"
rowClassName="mx_UserSettings_profileTableRow"
rowLabelClassName="mx_UserSettings_profileLabelCell"
rowInputClassName="mx_UserSettings_profileInputCell"
buttonClassName="mx_UserSettings_button"
onError={this.onPasswordChangeError}
onFinished={this.onPasswordChanged} />
</div>
<div className="mx_UserSettings_logout">
<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>
<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>
);
}
);
}
});

View file

@ -113,6 +113,10 @@ module.exports = React.createClass({
}
},
onBlur: function() {
this.cancelEdit();
},
render: function() {
var editable_el;
@ -125,7 +129,8 @@ module.exports = React.createClass({
} else if (this.state.phase == this.Phases.Edit) {
editable_el = (
<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>
);
}

View file

@ -73,10 +73,15 @@ module.exports = React.createClass({
var header;
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 =
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader">
{ this.props.simpleHeader }
{ cancel }
</div>
</div>
}

View file

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

View file

@ -98,7 +98,9 @@ module.exports = React.createClass({
} else {
var EditableText = sdk.getComponent('elements.EditableText');
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 MatrixClientPeg = require("../../../MatrixClientPeg");
var sdk = require("../../../index");
module.exports = React.createClass({
displayName: 'ChangePassword',
propTypes: {
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: {
Edit: "edit",
Uploading: "uploading",
Error: "error",
Success: "Success"
Error: "error"
},
getDefaultProps: function() {
return {
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() {
return {
phase: this.Phases.Edit,
errorString: ''
phase: this.Phases.Edit
}
},
@ -55,60 +72,72 @@ module.exports = React.createClass({
};
this.setState({
phase: this.Phases.Uploading,
errorString: '',
})
var d = cli.setPassword(authDict, new_password);
phase: this.Phases.Uploading
});
var self = this;
d.then(function() {
self.setState({
phase: self.Phases.Success,
errorString: '',
})
cli.setPassword(authDict, new_password).then(function() {
self.props.onFinished();
}, function(err) {
self.props.onError(err);
}).finally(function() {
self.setState({
phase: self.Phases.Error,
errorString: err.toString()
})
});
phase: self.Phases.Edit
});
}).done();
},
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;
if (new_password != confirm_password) {
this.setState({
state: this.Phases.Error,
errorString: "Passwords don't match"
});
} else if (new_password == '' || old_password == '') {
this.setState({
state: this.Phases.Error,
errorString: "Passwords can't be empty"
});
} else {
var err = this.props.onCheckPassword(
old_password, new_password, confirm_password
);
if (err) {
this.props.onError(err);
}
else {
this.changePassword(old_password, new_password);
}
},
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) {
case this.Phases.Edit:
case this.Phases.Error:
return (
<div>
<div className="mx_Dialog_content">
<div>{this.state.errorString}</div>
<div><label>Old password <input type="password" ref="old_input"/></label></div>
<div><label>New password <input type="password" ref="new_input"/></label></div>
<div><label>Confirm password <input type="password" ref="confirm_input"/></label></div>
<div className={this.props.className}>
<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>
<div className="mx_Dialog_buttons">
<button onClick={this.onClickChange}>Change Password</button>
<button onClick={this.props.onFinished}>Cancel</button>
<div className={rowClassName}>
<div className={rowLabelClassName}>
<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>
);
@ -119,17 +148,6 @@ module.exports = React.createClass({
<Loader />
</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>
)
}
}
});