diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js new file mode 100644 index 0000000000..f2ada35b24 --- /dev/null +++ b/src/UserSettingsStore.js @@ -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); + }, +}; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8ad5b44762..9273e0e03d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -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 = break; case this.PageTypes.UserSettings: - page_element = + page_element = right_panel = break; case this.PageTypes.CreateRoom: diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 59187bb69f..c8916bfd44 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -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 = ( -
- -
- -
-
- ); - 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 + switch (this.state.phase) { + case "UserSettings.LOADING": + var Loader = sdk.getComponent("elements.Spinner"); + return ( + + ); + 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 (
-
-

User Settings

-
-
-
-
- Profile Photo + + +

Profile

+ +
+
+
+
+
-
- Edit +
+
-
- -
- Edit -
-
+ {this.state.threepids.map(function(val, pidIndex) { + var id = "email-" + val.address; + return ( +
+
+ +
+
+ +
+
+ ); + })} +
-
- {this.state.threepids.map(function(val) { - return
{val.address}
; - })} -
- -
- Add email +
+ +
+ +
-
-

Global Settings

-
-
-
- Change Password -
-
- Version {this.state.clientVersion} -
-
- -
-
- +

Account

+ +
+ +
+ +
+
+ Log out +
+
+ +

Notifications

+ +
+
+
+
+ +
+
+ +
+ +

Advanced

+ +
+
+ Logged in as {this._me} +
+
+ Version {this.state.clientVersion} +
+
- ); - } + ); } }); diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 0ed443fbae..ee88f1a853 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -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 = (
- +
); } diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 10f3297b75..13959a16b9 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -73,10 +73,15 @@ module.exports = React.createClass({ var header; if (this.props.simpleHeader) { + var cancel; + if (this.props.onCancelClick) { + cancel = Close + } header =
{ this.props.simpleHeader } + { cancel }
} diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 2ae50a0cae..ee2b5ad5e1 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -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 = ; } + var uploadSection; + if (this.props.showUploadSection) { + uploadSection = ( +
+ Upload new: + + {this.state.errorText} +
+ ); + } + switch (this.state.phase) { case this.Phases.Display: case this.Phases.Error: return (
-
+
{avatarImg}
-
- Upload new: - - {this.state.errorText} -
+ {uploadSection}
); case this.Phases.Uploading: diff --git a/src/components/views/settings/ChangeDisplayName.js b/src/components/views/settings/ChangeDisplayName.js index 4af413cfbe..9410b02290 100644 --- a/src/components/views/settings/ChangeDisplayName.js +++ b/src/components/views/settings/ChangeDisplayName.js @@ -98,7 +98,9 @@ module.exports = React.createClass({ } else { var EditableText = sdk.getComponent('elements.EditableText'); return ( - + ); } } diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index a6666b7ed1..219ad10714 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -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 ( -
-
-
{this.state.errorString}
-
-
-
+
+
+
+ +
+
+ +
-
- - +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ Change Password
); @@ -119,17 +148,6 @@ module.exports = React.createClass({
); - case this.Phases.Success: - return ( -
-
- Success! -
-
- -
-
- ) } } });