From e9132a873b870dea6f360e807ebe74b7365814d2 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 17 Nov 2015 02:13:42 +0000 Subject: [PATCH 01/13] experiment with turning the UserSettings controller into a UserSettingsStore logic class --- src/UserSettingsStore.js | 80 +++++++++++++++++++++++ src/controllers/organisms/UserSettings.js | 54 --------------- 2 files changed, 80 insertions(+), 54 deletions(-) create mode 100644 src/UserSettingsStore.js delete mode 100644 src/controllers/organisms/UserSettings.js diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js new file mode 100644 index 0000000000..50ee03a433 --- /dev/null +++ b/src/UserSettingsStore.js @@ -0,0 +1,80 @@ +/* +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 sdk = require('./index'); + +// XXX: should we be doing something here to use this as a singleton rather than +// class methods? + +module.exports = { + + // we add these wrappers to the js-sdk here in case we want to do react-specific + // dispatches or similar in future, and to give us a place to clearly separate + // business logic specific from the 'thin' react component and parent wiring component + // which actually handles the UI. + // XXX: I'm not convinced this abstraction is worth it though. + + 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) { + + }, + + getEnableNotifications: function() { + var Notifier = sdk.getComponent('organisms.Notifier'); + return Notifier.isEnabled(); + }, + + setEnableNotifications: function(enable) { + var Notifier = sdk.getComponent('organisms.Notifier'); + var self = this; + 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 + }; + + this.setState({ + phase: this.Phases.Uploading, + errorString: '', + }) + + return cli.setPassword(authDict, new_password); + }, +} diff --git a/src/controllers/organisms/UserSettings.js b/src/controllers/organisms/UserSettings.js deleted file mode 100644 index d7bb8d391a..0000000000 --- a/src/controllers/organisms/UserSettings.js +++ /dev/null @@ -1,54 +0,0 @@ -/* -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. -*/ - -var MatrixClientPeg = require("../../MatrixClientPeg"); -var q = require('q'); -var version = require('../../../package.json').version; - -module.exports = { - Phases: { - Loading: "loading", - Display: "display", - }, - - getInitialState: function() { - return { - avatarUrl: null, - threePids: [], - clientVersion: version, - phase: this.Phases.Loading, - }; - }, - - 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); } - ); - } -} From e2ae2dd199babc8a50d1d15178ae7c8582b26f09 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 18 Dec 2015 00:37:56 +0000 Subject: [PATCH 02/13] merge stuff from vector --- src/components/structures/MatrixChat.js | 2 +- src/components/structures/UserSettings.js | 268 ++++++++++++++++------ src/components/views/rooms/RoomHeader.js | 5 + 3 files changed, 205 insertions(+), 70 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 8ad5b44762..7148848af1 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -660,7 +660,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..aa46a7770c 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -17,19 +17,24 @@ var React = require('react'); var sdk = require('../../index'); var MatrixClientPeg = require("../../MatrixClientPeg"); var Modal = require('../../Modal'); +var dis = require('matrix-react-sdk/lib/dispatcher') var q = require('q'); var version = require('../../../package.json').version; +var UserSettingsStore = require('matrix-react-sdk/lib/UserSettingsStore'); module.exports = React.createClass({ displayName: 'UserSettings', + Phases: { Loading: "loading", + Saving: "saving", Display: "display", }, getInitialState: function() { return { avatarUrl: null, + displayName: null, threePids: [], clientVersion: version, phase: this.Phases.Loading, @@ -38,23 +43,113 @@ module.exports = React.createClass({ componentWillMount: function() { var self = this; - var cli = MatrixClientPeg.get(); - var profile_d = cli.getProfileInfo(cli.credentials.userId); - var threepid_d = cli.getThreePids(); + var profilePromise = UserSettingsStore.loadProfileInfo(); + var threepidPromise = UserSettingsStore.loadThreePids(); - q.all([profile_d, threepid_d]).then( + q.all([profilePromise, threepidPromise]).then( function(resps) { self.setState({ avatarUrl: resps[0].avatar_url, + displayName: resps[0].displayname, threepids: resps[1].threepids, phase: self.Phases.Display, }); + + // keep a copy of the original state in order to track changes + self.setState({ + originalState: self.state + }); }, - function(err) { console.err(err); } + function(error) { + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Can't load user settings", + description: error.toString() + }); + } ); }, + componentDidMount: function() { + this.dispatcherRef = dis.register(this.onAction); + }, + + componentWillUnmount: function() { + dis.unregister(this.dispatcherRef); + }, + + onSaveClicked: function(ev) { + var self = this; + var savePromises = []; + + // XXX: this is managed in ChangeAvatar.js, although could be moved out here in order + // to allow for the change to be staged alongside the rest of the form. + // + // if (this.state.originalState.avatarUrl !== this.state.avatarUrl) { + // savePromises.push( UserSettingsStore.saveAvatarUrl(this.state.avatarUrl) ); + // } + + if (this.state.originalState.displayName !== this.state.displayName) { + savePromises.push( UserSettingsStore.saveDisplayName(this.state.displayName) ); + } + + if (this.state.originalState.threepids.length !== this.state.threepids.length || + this.state.originalState.threepids.every(function(element, index) { + return element === this.state.threepids[index]; + })) + { + savePromises.push( UserSettingsStore.saveThreePids(this.state.threepids) ); + } + + self.setState({ + phase: self.Phases.Saving, + }); + + q.all(savePromises).then( + function(resps) { + self.setState({ + phase: self.Phases.Display, + }); + self.onClose(); + }, + function(error) { + self.setState({ + phase: self.Phases.Display, + }); + var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Can't save user settings", + description: error.toString() + }); + } + ); + }, + + onClose: function(ev) { + // XXX: use browser history instead to find the previous room? + if (this.props.roomId) { + dis.dispatch({ + action: 'view_room', + room_id: this.props.roomId, + }); + } + else { + dis.dispatch({ + action: 'view_indexed_room', + roomIndex: 0, + }); + } + }, + + onAction: function(payload) { + if (payload.action === "notifier_enabled") { + this.setState({ + enableNotifications : UserSettingsStore.getEnableNotifications() + }); + } + }, + editAvatar: function() { var url = MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl); var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); @@ -69,17 +164,8 @@ module.exports = React.createClass({ this.avatarDialog = Modal.createDialogWithElement(avatarDialog); }, - addEmail: function() { - - }, - - editDisplayName: function() { - this.refs.displayname.edit(); - }, - - changePassword: function() { - var ChangePassword = sdk.getComponent('settings.ChangePassword'); - Modal.createDialog(ChangePassword); + onAvatarDialogCancel: function() { + this.avatarDialog.close(); }, onLogoutClicked: function(ev) { @@ -91,72 +177,116 @@ module.exports = React.createClass({ this.logoutModal.closeDialog(); }, - onAvatarDialogCancel: function() { - this.avatarDialog.close(); + onDisplayNameChange: function(event) { + this.setState({ displayName: event.target.value }); + }, + + onEnableNotificationsChange: function(event) { + // don't bother waiting for Save to be clicked, as that'd be silly + UserSettingsStore.setEnableNotifications( this.refs.enableNotifications.value ); + this.setState({ + enableNotifications : UserSettingsStore.getEnableNotifications() + }); }, render: function() { - var Loader = sdk.getComponent("elements.Spinner"); - if (this.state.phase === this.Phases.Loading) { - return - } - else if (this.state.phase === this.Phases.Display) { - var ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName'); - var EnableNotificationsButton = sdk.getComponent('settings.EnableNotificationsButton'); - return ( -
-
-

User Settings

-
-
-
-
- Profile Photo -
-
- Edit + var Loader = sdk.getComponent("atoms.Spinner"); + var saving; + switch (this.state.phase) { + case this.Phases.Loading: + return + case this.Phases.Saving: + saving = + case this.Phases.Display: + var RoomHeader = sdk.getComponent('molecules.RoomHeader'); + return ( +
+ + +

Profile

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

Notifications

+ +
+
+
+
+ +
+
+ +
+
-
- {this.state.threepids.map(function(val) { - return
{val.address}
; - })} +

Advanced

+ +
+
+ Version {this.state.clientVersion} +
-
- Add email +
+
{ saving }
+
Save and close
-
- -
-

Global Settings

-
-
-
- Change Password -
-
- Version {this.state.clientVersion} -
-
- -
-
- -
-
-
-
- ); + ); } } }); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 068dff85d6..bc5c70ce08 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 }
} From 4baf9d55899754164801b9fb95168ce6a3de5066 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 18 Dec 2015 00:40:01 +0000 Subject: [PATCH 03/13] Fix paths --- src/components/structures/UserSettings.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index aa46a7770c..65b3db1de9 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -17,10 +17,10 @@ var React = require('react'); var sdk = require('../../index'); var MatrixClientPeg = require("../../MatrixClientPeg"); var Modal = require('../../Modal'); -var dis = require('matrix-react-sdk/lib/dispatcher') +var dis = require("../../dispatcher"); var q = require('q'); var version = require('../../../package.json').version; -var UserSettingsStore = require('matrix-react-sdk/lib/UserSettingsStore'); +var UserSettingsStore = require('../../UserSettingsStore'); module.exports = React.createClass({ displayName: 'UserSettings', From 08ffadc2c4921cfb4af9810494acf2ccdf2ba533 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 18 Dec 2015 14:04:39 +0000 Subject: [PATCH 04/13] unbreak --- src/components/structures/UserSettings.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 65b3db1de9..9a62149023 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -62,7 +62,7 @@ module.exports = React.createClass({ }); }, function(error) { - var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Can't load user settings", description: error.toString() @@ -117,7 +117,7 @@ module.exports = React.createClass({ self.setState({ phase: self.Phases.Display, }); - var ErrorDialog = sdk.getComponent("organisms.ErrorDialog"); + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Can't save user settings", description: error.toString() @@ -190,7 +190,7 @@ module.exports = React.createClass({ }, render: function() { - var Loader = sdk.getComponent("atoms.Spinner"); + var Loader = sdk.getComponent("elements.Spinner"); var saving; switch (this.state.phase) { case this.Phases.Loading: @@ -198,7 +198,7 @@ module.exports = React.createClass({ case this.Phases.Saving: saving = case this.Phases.Display: - var RoomHeader = sdk.getComponent('molecules.RoomHeader'); + var RoomHeader = sdk.getComponent('rooms.RoomHeader'); return (
From 1af5018597742465ed3703759f52af44271126cb Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 23 Dec 2015 11:47:56 +0000 Subject: [PATCH 05/13] General code cleanup / tweaks / fixes - Swap Phases enum to be using string literals - Swap roomId prop on UserSettings for a more sane onUserSettingsClose and make MatrixChat responsible for swapping the room. - s/then/done/ when terminating Promise chains to avoid subtle errors. - Rejig render() of UserSettings so we don't need to indent quite so much. --- src/UserSettingsStore.js | 21 +- src/components/structures/MatrixChat.js | 18 +- src/components/structures/UserSettings.js | 297 +++++++++++----------- 3 files changed, 170 insertions(+), 166 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 50ee03a433..bac85ab325 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -17,19 +17,10 @@ limitations under the License. 'use strict'; var MatrixClientPeg = require("./MatrixClientPeg"); -var sdk = require('./index'); - -// XXX: should we be doing something here to use this as a singleton rather than -// class methods? +var Notifier = require("./Notifier"); module.exports = { - // we add these wrappers to the js-sdk here in case we want to do react-specific - // dispatches or similar in future, and to give us a place to clearly separate - // business logic specific from the 'thin' react component and parent wiring component - // which actually handles the UI. - // XXX: I'm not convinced this abstraction is worth it though. - loadProfileInfo: function() { var cli = MatrixClientPeg.get(); return cli.getProfileInfo(cli.credentials.userId); @@ -48,13 +39,10 @@ module.exports = { }, getEnableNotifications: function() { - var Notifier = sdk.getComponent('organisms.Notifier'); return Notifier.isEnabled(); }, setEnableNotifications: function(enable) { - var Notifier = sdk.getComponent('organisms.Notifier'); - var self = this; if (!Notifier.supportsDesktopNotifications()) { return; } @@ -70,11 +58,6 @@ module.exports = { password: old_password }; - this.setState({ - phase: this.Phases.Uploading, - errorString: '', - }) - return cli.setPassword(authDict, new_password); }, -} +}; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7148848af1..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 9a62149023..1401bee337 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -25,10 +25,14 @@ var UserSettingsStore = require('../../UserSettingsStore'); module.exports = React.createClass({ displayName: 'UserSettings', - Phases: { - Loading: "loading", - Saving: "saving", - Display: "display", + propTypes: { + onClose: React.PropTypes.func + }, + + getDefaultProps: function() { + return { + onClose: function() {} + }; }, getInitialState: function() { @@ -37,38 +41,34 @@ module.exports = React.createClass({ displayName: null, threePids: [], clientVersion: version, - phase: this.Phases.Loading, + phase: "UserSettings.LOADING", // LOADING, DISPLAY, SAVING }; }, componentWillMount: function() { var self = this; - var profilePromise = UserSettingsStore.loadProfileInfo(); - var threepidPromise = UserSettingsStore.loadThreePids(); + q.all([ + UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids() + ]).done(function(resps) { + self.setState({ + avatarUrl: resps[0].avatar_url, + displayName: resps[0].displayname, + threepids: resps[1].threepids, + phase: "UserSettings.DISPLAY", + }); - q.all([profilePromise, threepidPromise]).then( - function(resps) { - self.setState({ - avatarUrl: resps[0].avatar_url, - displayName: resps[0].displayname, - threepids: resps[1].threepids, - phase: self.Phases.Display, - }); - - // keep a copy of the original state in order to track changes - self.setState({ - originalState: self.state - }); - }, - function(error) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: "Can't load user settings", - description: error.toString() - }); - } - ); + // keep a copy of the original state in order to track changes + self.setState({ + originalState: self.state + }); + }, function(error) { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Can't load user settings", + description: error.toString() + }); + }); }, componentDidMount: function() { @@ -95,51 +95,33 @@ module.exports = React.createClass({ } if (this.state.originalState.threepids.length !== this.state.threepids.length || - this.state.originalState.threepids.every(function(element, index) { + this.state.originalState.threepids.every((element, index) => { return element === this.state.threepids[index]; - })) - { - savePromises.push( UserSettingsStore.saveThreePids(this.state.threepids) ); + })) { + savePromises.push( + UserSettingsStore.saveThreePids(this.state.threepids) + ); } self.setState({ - phase: self.Phases.Saving, + phase: "UserSettings.SAVING", }); - q.all(savePromises).then( - function(resps) { - self.setState({ - phase: self.Phases.Display, - }); - self.onClose(); - }, - function(error) { - self.setState({ - phase: self.Phases.Display, - }); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: "Can't save user settings", - description: error.toString() - }); - } - ); - }, - - onClose: function(ev) { - // XXX: use browser history instead to find the previous room? - if (this.props.roomId) { - dis.dispatch({ - action: 'view_room', - room_id: this.props.roomId, + q.all(savePromises).done(function(resps) { + self.setState({ + phase: "UserSettings.DISPLAY", }); - } - else { - dis.dispatch({ - action: 'view_indexed_room', - roomIndex: 0, + self.props.onClose(); + }, function(error) { + self.setState({ + phase: "UserSettings.DISPLAY", }); - } + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Can't save user settings", + description: error.toString() + }); + }); }, onAction: function(payload) { @@ -170,7 +152,9 @@ module.exports = React.createClass({ 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} + ); }, onLogoutPromptCancel: function() { @@ -193,100 +177,121 @@ module.exports = React.createClass({ var Loader = sdk.getComponent("elements.Spinner"); var saving; switch (this.state.phase) { - case this.Phases.Loading: + case "UserSettings.LOADING": return - case this.Phases.Saving: + case "UserSettings.SAVING": saving = - case this.Phases.Display: - var RoomHeader = sdk.getComponent('rooms.RoomHeader'); - return ( -
- + // intentional fall through + case "UserSettings.DISPLAY": + break; // quit the switch to return the common state + default: + throw new Error("Unknown state.phase => " + this.state.phase); + } + // can only get here if phase is UserSettings.DISPLAY + var RoomHeader = sdk.getComponent('rooms.RoomHeader'); + return ( +
+ -

Profile

+

Profile

-
-
-
-
- -
-
- -
-
- - {this.state.threepids.map(function(val) { - var id = "email-" + val.address; - return ( -
-
- -
-
- -
-
- ); - })} - -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
- -
-
+
+
+
+
+ +
+
+
-
-
Log out
-
- -

Notifications

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

Advanced

+
-
-
- Version {this.state.clientVersion} -
-
- -
-
{ saving }
-
Save and close
+
+
- ); - } +
+ +
+
+ Log out +
+
+ +

Notifications

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

Advanced

+ +
+
+ Version {this.state.clientVersion} +
+
+ +
+
{ saving }
+
+ Save and close +
+
+
+ ); } }); From e657b40a7ee1154b9fb1926c9380c3132448a1a9 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 23 Dec 2015 14:14:25 +0000 Subject: [PATCH 06/13] Use ChangeDisplayName for implict display name saving on enter --- src/components/structures/UserSettings.js | 15 ++------------- .../views/settings/ChangeDisplayName.js | 4 +++- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 1401bee337..e2f183d92a 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -21,6 +21,7 @@ var dis = require("../../dispatcher"); var q = require('q'); var version = require('../../../package.json').version; var UserSettingsStore = require('../../UserSettingsStore'); +var ChangeDisplayName = require("../views/settings/ChangeDisplayName"); module.exports = React.createClass({ displayName: 'UserSettings', @@ -38,7 +39,6 @@ module.exports = React.createClass({ getInitialState: function() { return { avatarUrl: null, - displayName: null, threePids: [], clientVersion: version, phase: "UserSettings.LOADING", // LOADING, DISPLAY, SAVING @@ -53,7 +53,6 @@ module.exports = React.createClass({ ]).done(function(resps) { self.setState({ avatarUrl: resps[0].avatar_url, - displayName: resps[0].displayname, threepids: resps[1].threepids, phase: "UserSettings.DISPLAY", }); @@ -90,10 +89,6 @@ module.exports = React.createClass({ // savePromises.push( UserSettingsStore.saveAvatarUrl(this.state.avatarUrl) ); // } - if (this.state.originalState.displayName !== this.state.displayName) { - savePromises.push( UserSettingsStore.saveDisplayName(this.state.displayName) ); - } - if (this.state.originalState.threepids.length !== this.state.threepids.length || this.state.originalState.threepids.every((element, index) => { return element === this.state.threepids[index]; @@ -161,10 +156,6 @@ module.exports = React.createClass({ this.logoutModal.closeDialog(); }, - onDisplayNameChange: function(event) { - this.setState({ displayName: event.target.value }); - }, - onEnableNotificationsChange: function(event) { // don't bother waiting for Save to be clicked, as that'd be silly UserSettingsStore.setEnableNotifications( this.refs.enableNotifications.value ); @@ -202,9 +193,7 @@ module.exports = React.createClass({
- +
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 ( - + ); } } From 72b8cf1be20e13188f98dc3b4c0d931efdd0e90e Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 23 Dec 2015 15:38:28 +0000 Subject: [PATCH 07/13] Refactor ChangePassword to get it working. Add 'Account' section because trying to make ChangePassword divs part of the same table as the display name is nigh impossible. Feels okay though --- src/components/structures/UserSettings.js | 64 ++++++---- .../views/settings/ChangePassword.js | 113 +++++++++++------- 2 files changed, 109 insertions(+), 68 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index e2f183d92a..23b28e0a0a 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -21,7 +21,6 @@ var dis = require("../../dispatcher"); var q = require('q'); var version = require('../../../package.json').version; var UserSettingsStore = require('../../UserSettingsStore'); -var ChangeDisplayName = require("../views/settings/ChangeDisplayName"); module.exports = React.createClass({ displayName: 'UserSettings', @@ -152,6 +151,31 @@ module.exports = React.createClass({ ); }, + 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(); }, @@ -180,6 +204,9 @@ module.exports = React.createClass({ } // 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"); + return (
@@ -210,27 +237,7 @@ module.exports = React.createClass({
); })} - -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
+
+

Account

+ +
+ +
+
Log out diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index a6666b7ed1..2f097387e9 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -18,23 +18,41 @@ 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" + }; + } + } }; }, @@ -57,58 +75,72 @@ module.exports = React.createClass({ this.setState({ phase: this.Phases.Uploading, errorString: '', - }) - - var d = cli.setPassword(authDict, new_password); + }); 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, + errorString: "" + }); + }).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 +151,6 @@ module.exports = React.createClass({
); - case this.Phases.Success: - return ( -
-
- Success! -
-
- -
-
- ) } } }); From 19bd39b06641fe5b24683e626f46e889f92d3f0f Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 23 Dec 2015 16:02:18 +0000 Subject: [PATCH 08/13] More random tweaks - Make onBlur reset the EditText to show that it hasn't submitted it. - Add the user ID of the logged in user to Advanced. - Remove remnants of the Save/Cancel buttons. --- src/components/structures/UserSettings.js | 30 ++++++++----------- src/components/views/elements/EditableText.js | 7 ++++- .../views/settings/ChangePassword.js | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 23b28e0a0a..b135ea747d 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -40,7 +40,7 @@ module.exports = React.createClass({ avatarUrl: null, threePids: [], clientVersion: version, - phase: "UserSettings.LOADING", // LOADING, DISPLAY, SAVING + phase: "UserSettings.LOADING", // LOADING, DISPLAY }; }, @@ -71,13 +71,15 @@ module.exports = React.createClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); + this._me = MatrixClientPeg.get().credentials.userId; }, componentWillUnmount: function() { dis.unregister(this.dispatcherRef); }, - onSaveClicked: function(ev) { +/* + onSaveClicked: function(ev) { // XXX unused var self = this; var savePromises = []; @@ -116,7 +118,7 @@ module.exports = React.createClass({ description: error.toString() }); }); - }, + }, */ onAction: function(payload) { if (payload.action === "notifier_enabled") { @@ -189,14 +191,12 @@ module.exports = React.createClass({ }, render: function() { - var Loader = sdk.getComponent("elements.Spinner"); - var saving; switch (this.state.phase) { case "UserSettings.LOADING": - return - case "UserSettings.SAVING": - saving = - // intentional fall through + var Loader = sdk.getComponent("elements.Spinner"); + return ( + + ); case "UserSettings.DISPLAY": break; // quit the switch to return the common state default: @@ -209,7 +209,7 @@ module.exports = React.createClass({ return (
- +

Profile

@@ -290,14 +290,10 @@ module.exports = React.createClass({
- Version {this.state.clientVersion} + Logged in as {this._me}
-
- -
-
{ saving }
-
- Save and close +
+ 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/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 2f097387e9..a77b20d3d8 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -117,7 +117,7 @@ module.exports = React.createClass({
- +
From a279dce0270b5299e8b694c2855d00a3204a3fbc Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 23 Dec 2015 16:52:59 +0000 Subject: [PATCH 09/13] Get avatar display and uploads working --- src/components/structures/UserSettings.js | 111 +++++++----------- src/components/views/settings/ChangeAvatar.js | 35 ++++-- 2 files changed, 68 insertions(+), 78 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index b135ea747d..cef15dff5d 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -47,26 +47,7 @@ module.exports = React.createClass({ componentWillMount: 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", - }); - - // keep a copy of the original state in order to track changes - self.setState({ - originalState: self.state - }); - }, function(error) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: "Can't load user settings", - description: error.toString() - }); - }); + this._refreshFromServer(); }, componentDidMount: function() { @@ -78,47 +59,24 @@ module.exports = React.createClass({ dis.unregister(this.dispatcherRef); }, -/* - onSaveClicked: function(ev) { // XXX unused + _refreshFromServer: function() { var self = this; - var savePromises = []; - - // XXX: this is managed in ChangeAvatar.js, although could be moved out here in order - // to allow for the change to be staged alongside the rest of the form. - // - // if (this.state.originalState.avatarUrl !== this.state.avatarUrl) { - // savePromises.push( UserSettingsStore.saveAvatarUrl(this.state.avatarUrl) ); - // } - - if (this.state.originalState.threepids.length !== this.state.threepids.length || - this.state.originalState.threepids.every((element, index) => { - return element === this.state.threepids[index]; - })) { - savePromises.push( - UserSettingsStore.saveThreePids(this.state.threepids) - ); - } - - self.setState({ - phase: "UserSettings.SAVING", - }); - - q.all(savePromises).done(function(resps) { + q.all([ + UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids() + ]).done(function(resps) { self.setState({ + avatarUrl: resps[0].avatar_url, + threepids: resps[1].threepids, phase: "UserSettings.DISPLAY", }); - self.props.onClose(); }, function(error) { - self.setState({ - phase: "UserSettings.DISPLAY", - }); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { - title: "Can't save user settings", + title: "Can't load user settings", description: error.toString() }); - }); - }, */ + }); + }, onAction: function(payload) { if (payload.action === "notifier_enabled") { @@ -126,24 +84,26 @@ module.exports = React.createClass({ enableNotifications : UserSettingsStore.getEnableNotifications() }); } - }, - - editAvatar: function() { - var url = MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl); - var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); - var avatarDialog = ( -
- -
- -
-
- ); - this.avatarDialog = Modal.createDialogWithElement(avatarDialog); }, - onAvatarDialogCancel: function() { - this.avatarDialog.close(); + 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).catch(function(err) { + var errMsg = err.error || ""; + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Error", + description: "Failed to set avatar. " + errMsg + }); + }).finally(function() { + // dunno if the avatar changed, re-check it. + self._refreshFromServer(); + }).done(); }, onLogoutClicked: function(ev) { @@ -206,6 +166,10 @@ module.exports = React.createClass({ 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 (
@@ -240,8 +204,15 @@ module.exports = React.createClass({
-
+ +
+ +
diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index 2ae50a0cae..0add8aecbf 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -23,6 +23,8 @@ 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 }, Phases: { @@ -31,6 +33,12 @@ module.exports = React.createClass({ Error: "error", }, + getDefaultProps: function() { + return { + showUploadSection: true + }; + }, + getInitialState: function() { return { avatarUrl: this.props.initialAvatarUrl, @@ -55,7 +63,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 +75,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 +88,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,6 +118,17 @@ 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: @@ -114,11 +137,7 @@ module.exports = React.createClass({
{avatarImg}
-
- Upload new: - - {this.state.errorText} -
+ {uploadSection}
); case this.Phases.Uploading: From abb170ebde9144cd05dad13e36c97e1743806988 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 23 Dec 2015 17:06:30 +0000 Subject: [PATCH 10/13] Keep one source of truth (the Notifier) when toggling notification state. Fixes notifications. --- src/components/structures/UserSettings.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index cef15dff5d..da61f62114 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -46,7 +46,6 @@ module.exports = React.createClass({ componentWillMount: function() { var self = this; - this._refreshFromServer(); }, @@ -80,9 +79,7 @@ module.exports = React.createClass({ onAction: function(payload) { if (payload.action === "notifier_enabled") { - this.setState({ - enableNotifications : UserSettingsStore.getEnableNotifications() - }); + this.forceUpdate(); } }, @@ -143,11 +140,7 @@ module.exports = React.createClass({ }, onEnableNotificationsChange: function(event) { - // don't bother waiting for Save to be clicked, as that'd be silly - UserSettingsStore.setEnableNotifications( this.refs.enableNotifications.value ); - this.setState({ - enableNotifications : UserSettingsStore.getEnableNotifications() - }); + UserSettingsStore.setEnableNotifications(event.target.checked); }, render: function() { @@ -245,7 +238,7 @@ module.exports = React.createClass({
From 05d1d7c82dcabd91bf88657ed6623471a1854f3a Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 23 Dec 2015 17:30:25 +0000 Subject: [PATCH 11/13] Better error message for failing to set avatars with no connection --- src/components/structures/UserSettings.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index da61f62114..b1dc80e95d 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -90,17 +90,17 @@ module.exports = React.createClass({ console.error("No ChangeAvatar found to upload image to!"); return; } - changeAvatar.onFileSelected(ev).catch(function(err) { - var errMsg = err.error || ""; + 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 }); - }).finally(function() { - // dunno if the avatar changed, re-check it. - self._refreshFromServer(); - }).done(); + }); }, onLogoutClicked: function(ev) { From 5286ec170ff535f43a4fbb41554af71c3fcd4302 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 24 Dec 2015 09:20:16 +0000 Subject: [PATCH 12/13] Wrangle CSS to get avatar in right place --- src/components/structures/UserSettings.js | 4 ++-- src/components/views/settings/ChangeAvatar.js | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index b1dc80e95d..c8916bfd44 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -198,7 +198,7 @@ module.exports = React.createClass({
+ showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>