From fa1ce61a06581845cff607b4a532657ad27774b8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 22 Jan 2019 13:28:33 -0700 Subject: [PATCH] Move profile settings to a dedicated component The tab component is getting a bit hard to navigate --- res/css/_components.scss | 1 + res/css/views/settings/_ProfileSettings.scss | 107 ++++++++++++ .../settings/tabs/_GeneralSettingsTab.scss | 100 ++--------- res/css/views/settings/tabs/_SettingsTab.scss | 16 ++ .../views/settings/ChangePassword.js | 2 +- .../views/settings/ProfileSettings.js | 155 ++++++++++++++++++ .../views/settings/tabs/GeneralSettingsTab.js | 138 +--------------- src/i18n/strings/en_EN.json | 6 +- 8 files changed, 297 insertions(+), 228 deletions(-) create mode 100644 res/css/views/settings/_ProfileSettings.scss create mode 100644 src/components/views/settings/ProfileSettings.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 2617cd7c4c..ee34eeb524 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -130,6 +130,7 @@ @import "./views/settings/_IntegrationsManager.scss"; @import "./views/settings/_KeyBackupPanel.scss"; @import "./views/settings/_Notifications.scss"; +@import "./views/settings/_ProfileSettings.scss"; @import "./views/settings/tabs/_GeneralSettingsTab.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; @import "./views/voip/_CallView.scss"; diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss new file mode 100644 index 0000000000..4bec429d55 --- /dev/null +++ b/res/css/views/settings/_ProfileSettings.scss @@ -0,0 +1,107 @@ +/* +Copyright 2019 New Vector 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. +*/ + +.mx_ProfileSettings_profile { + display: flex; +} + +.mx_ProfileSettings_controls { + flex-grow: 1; +} + +.mx_ProfileSettings_controls .mx_Field #profileDisplayName { + width: calc(100% - 20px); // subtract 10px padding on left and right +} + +.mx_ProfileSettings_avatar { + width: 88px; + height: 88px; + margin-left: 13px; + position: relative; + cursor: pointer; +} + +.mx_ProfileSettings_avatar > * { + display: block; + width: 88px; + height: 88px; + border-radius: 4px; +} + +.mx_ProfileSettings_avatar .mx_ProfileSettings_avatarPlaceholder { + background-color: $settings-profile-placeholder-bg-color; +} + +.mx_ProfileSettings_avatarOverlay { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: none; + text-align: center; + vertical-align: middle; + font-size: 10px; +} + +.mx_ProfileSettings_avatar:hover .mx_ProfileSettings_avatarOverlay { + display: inline-block; + opacity: 0.5 !important; + color: $settings-profile-overlay-fg-color !important; + background-color: $settings-profile-overlay-bg-color !important; +} + +.mx_ProfileSettings_avatarOverlay_show { + display: inline-block; + opacity: 1; + color: $settings-profile-overlay-placeholder-fg-color; + background-color: $settings-profile-overlay-placeholder-bg-color; +} + +.mx_ProfileSettings_avatarOverlayText { + display: block; + margin-top: 17px; + margin-bottom: 8px; +} + +.mx_ProfileSettings_avatarOverlayImgContainer { + position: relative; + width: 14px; + height: 14px; + margin: auto; +} + +.mx_ProfileSettings_avatarOverlayImg:before { + background-color: $settings-profile-overlay-placeholder-fg-color; + mask: url("$(res)/img/feather-icons/upload.svg"); + mask-repeat: no-repeat; + mask-size: 14px; + mask-position: center; + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} + +.mx_ProfileSettings_avatar:hover .mx_ProfileSettings_avatarOverlayImg:before { + background-color: $settings-profile-overlay-fg-color !important; +} + +.mx_ProfileSettings_avatarUpload { + display: none; +} diff --git a/res/css/views/settings/tabs/_GeneralSettingsTab.scss b/res/css/views/settings/tabs/_GeneralSettingsTab.scss index 097d81dab4..d1a31e37f8 100644 --- a/res/css/views/settings/tabs/_GeneralSettingsTab.scss +++ b/res/css/views/settings/tabs/_GeneralSettingsTab.scss @@ -1,94 +1,18 @@ -.mx_GeneralSettingsTab_profile { - display: flex; -} +/* +Copyright 2019 New Vector Ltd -.mx_GeneralSettingsTab_profileControls { - flex-grow: 1; -} +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 -.mx_GeneralSettingsTab_profileControls .mx_Field #profileDisplayName { - width: calc(100% - 20px); // subtract 10px padding on left and right -} + http://www.apache.org/licenses/LICENSE-2.0 -.mx_GeneralSettingsTab_profileAvatar { - width: 88px; - height: 88px; - margin-left: 13px; - position: relative; - cursor: pointer; -} - -.mx_GeneralSettingsTab_profileAvatar > * { - display: block; - width: 88px; - height: 88px; - border-radius: 4px; -} - -.mx_GeneralSettingsTab_profileAvatar .mx_GeneralSettingsTab_profileAvatarPlaceholder { - background-color: $settings-profile-placeholder-bg-color; -} - -.mx_GeneralSettingsTab_profileAvatarOverlay { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - display: none; - text-align: center; - vertical-align: middle; - font-size: 10px; -} - -.mx_GeneralSettingsTab_profileAvatar:hover .mx_GeneralSettingsTab_profileAvatarOverlay { - display: inline-block; - opacity: 0.5 !important; - color: $settings-profile-overlay-fg-color !important; - background-color: $settings-profile-overlay-bg-color !important; -} - -.mx_GeneralSettingsTab_profileAvatarOverlay_show { - display: inline-block; - opacity: 1; - color: $settings-profile-overlay-placeholder-fg-color; - background-color: $settings-profile-overlay-placeholder-bg-color; -} - -.mx_GeneralSettingsTab_profileAvatarOverlayText { - display: block; - margin-top: 17px; - margin-bottom: 8px; -} - -.mx_GeneralSettingsTab_profileAvatarOverlayImgContainer { - position: relative; - width: 14px; - height: 14px; - margin: auto; -} - -.mx_GeneralSettingsTab_profileAvatarOverlayImg:before { - background-color: $settings-profile-overlay-placeholder-fg-color; - mask: url("$(res)/img/feather-icons/upload.svg"); - mask-repeat: no-repeat; - mask-size: 14px; - mask-position: center; - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; -} - -.mx_GeneralSettingsTab_profileAvatar:hover .mx_GeneralSettingsTab_profileAvatarOverlayImg:before { - background-color: $settings-profile-overlay-fg-color !important; -} - -.mx_GeneralSettingsTab_profileAvatarUpload { - display: none; -} +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. +*/ .mx_GeneralSettingsTab_changePassword { display: block; diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index c86fb1f41e..3ea9d616ba 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -1,3 +1,19 @@ +/* +Copyright 2019 New Vector 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. +*/ + .mx_SettingsTab_heading { font-size: 20px; font-weight: 600; diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 50319850b5..ee9662ebc4 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd +Copyright 2018-2019 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js new file mode 100644 index 0000000000..6bffd92f85 --- /dev/null +++ b/src/components/views/settings/ProfileSettings.js @@ -0,0 +1,155 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import {_t} from "../../../languageHandler"; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import Field from "../elements/Field"; +import AccessibleButton from "../elements/AccessibleButton"; +import classNames from 'classnames'; + +export default class ProfileSettings extends React.Component { + constructor() { + super(); + + const client = MatrixClientPeg.get(); + const user = client.getUser(client.getUserId()); + let avatarUrl = user.avatarUrl; + if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false); + this.state = { + userId: user.userId, + originalDisplayName: user.displayName, + displayName: user.displayName, + originalAvatarUrl: avatarUrl, + avatarUrl: avatarUrl, + avatarFile: null, + enableProfileSave: false, + }; + } + + _uploadAvatar = (e) => { + e.stopPropagation(); + e.preventDefault(); + + this.refs.avatarUpload.click(); + }; + + _saveProfile = async (e) => { + e.stopPropagation(); + e.preventDefault(); + + if (!this.state.enableProfileSave) return; + this.setState({enableProfileSave: false}); + + const client = MatrixClientPeg.get(); + const newState = {}; + + // TODO: What do we do about errors? + + if (this.state.originalDisplayName !== this.state.displayName) { + await client.setDisplayName(this.state.displayName); + newState.originalDisplayName = this.state.displayName; + } + + if (this.state.avatarFile) { + const uri = await client.uploadContent(this.state.avatarFile); + await client.setAvatarUrl(uri); + newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false); + newState.originalAvatarUrl = newState.avatarUrl; + newState.avatarFile = null; + } + + newState.enableProfileSave = true; + this.setState(newState); + }; + + _onDisplayNameChanged = (e) => { + this.setState({ + displayName: e.target.value, + enableProfileSave: true, + }); + }; + + _onAvatarChanged = (e) => { + if (!e.target.files || !e.target.files.length) { + this.setState({ + avatarUrl: this.state.originalAvatarUrl, + avatarFile: null, + enableProfileSave: false, + }); + return; + } + + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onload = (ev) => { + this.setState({ + avatarUrl: ev.target.result, + avatarFile: file, + enableProfileSave: true, + }); + }; + reader.readAsDataURL(file); + }; + + render() { + // TODO: Why is rendering a box with an overlay so complicated? Can the DOM be reduced? + + let showOverlayAnyways = true; + let avatarElement =
; + if (this.state.avatarUrl) { + showOverlayAnyways = false; + avatarElement = {_t("Profile + } + + const avatarOverlayClasses = classNames({ + "mx_ProfileSettings_avatarOverlay": true, + "mx_ProfileSettings_avatarOverlay_show": showOverlayAnyways, + }); + let avatarHoverElement = ( +
+ {_t("Upload profile picture")} +
+
+
+
+ ); + + return ( +
+ +
+
+

{this.state.userId}

+ +
+
+ {avatarElement} + {avatarHoverElement} +
+
+ + {_t("Save")} + +
+ ); + } +} diff --git a/src/components/views/settings/tabs/GeneralSettingsTab.js b/src/components/views/settings/tabs/GeneralSettingsTab.js index aa99973130..e2d3a924d3 100644 --- a/src/components/views/settings/tabs/GeneralSettingsTab.js +++ b/src/components/views/settings/tabs/GeneralSettingsTab.js @@ -17,13 +17,11 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../languageHandler"; import MatrixClientPeg from "../../../../MatrixClientPeg"; -import Field from "../../elements/Field"; -import AccessibleButton from "../../elements/AccessibleButton"; -import classNames from 'classnames'; import GroupUserSettings from "../../groups/GroupUserSettings"; import PropTypes from "prop-types"; import {MatrixClient} from "matrix-js-sdk"; import { DragDropContext } from 'react-beautiful-dnd'; +import ProfileSettings from "../ProfileSettings"; const sdk = require('../../../../index'); const Modal = require("../../../../Modal"); @@ -34,20 +32,6 @@ export default class GeneralSettingsTab extends React.Component { constructor() { super(); - - const client = MatrixClientPeg.get(); - const user = client.getUser(client.getUserId()); - let avatarUrl = user.avatarUrl; - if (avatarUrl) avatarUrl = client.mxcUrlToHttp(avatarUrl, 96, 96, 'crop', false); - this.state = { - userId: user.userId, - originalDisplayName: user.displayName, - displayName: user.displayName, - originalAvatarUrl: avatarUrl, - avatarUrl: avatarUrl, - avatarFile: null, - enableProfileSave: false, - }; } getChildContext() { @@ -56,78 +40,6 @@ export default class GeneralSettingsTab extends React.Component { }; } - _uploadAvatar = (e) => { - e.stopPropagation(); - e.preventDefault(); - - this.refs.avatarUpload.click(); - }; - - _saveProfile = async (e) => { - e.stopPropagation(); - e.preventDefault(); - - if (!this.state.enableProfileSave) return; - this.setState({enableProfileSave: false}); - - const client = MatrixClientPeg.get(); - const newState = {}; - - // TODO: What do we do about errors? - - if (this.state.originalDisplayName !== this.state.displayName) { - await client.setDisplayName(this.state.displayName); - newState.originalDisplayName = this.state.displayName; - } - - if (this.state.avatarFile) { - const uri = await client.uploadContent(this.state.avatarFile); - await client.setAvatarUrl(uri); - newState.avatarUrl = client.mxcUrlToHttp(uri, 96, 96, 'crop', false); - newState.originalAvatarUrl = newState.avatarUrl; - newState.avatarFile = null; - } - - newState.enableProfileSave = true; - this.setState(newState); - }; - - _changePassword = async (e) => { - e.stopPropagation(); - e.preventDefault(); - - console.log(this.refs.currentPassword); - }; - - _onDisplayNameChanged = (e) => { - this.setState({ - displayName: e.target.value, - enableProfileSave: true, - }); - }; - - _onAvatarChanged = (e) => { - if (!e.target.files || !e.target.files.length) { - this.setState({ - avatarUrl: this.state.originalAvatarUrl, - avatarFile: null, - enableProfileSave: false, - }); - return; - } - - const file = e.target.files[0]; - const reader = new FileReader(); - reader.onload = (ev) => { - this.setState({ - avatarUrl: ev.target.result, - avatarFile: file, - enableProfileSave: true, - }); - }; - reader.readAsDataURL(file); - }; - _onPasswordChangeError = (err) => { // TODO: Figure out a design that doesn't involve replacing the current dialog let errMsg = err.error || ""; @@ -157,57 +69,11 @@ export default class GeneralSettingsTab extends React.Component { }; _renderProfileSection() { - // TODO: Why is rendering a box with an overlay so complicated? Can the DOM be reduced? - - let showOverlayAnyways = true; - let avatarElement =
; - if (this.state.avatarUrl) { - showOverlayAnyways = false; - avatarElement = {_t("Profile - } - - const avatarOverlayClasses = classNames({ - "mx_GeneralSettingsTab_profileAvatarOverlay": true, - "mx_GeneralSettingsTab_profileAvatarOverlay_show": showOverlayAnyways, - }); - let avatarHoverElement = ( -
- {_t("Upload profile picture")} -
-
-
-
- ); - - const form = ( -
- -
-
-

{this.state.userId}

- -
-
- {avatarElement} - {avatarHoverElement} -
-
- - {_t("Save")} - -
- ); - // HACK/TODO: Using DragDropContext feels wrong, but we need it. return (
{_t("Profile")} - {form} + {_t("Flair")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8e26c099da..dfbe9cc6a3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -402,13 +402,13 @@ "Off": "Off", "On": "On", "Noisy": "Noisy", - "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", - "Success": "Success", - "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them", "Profile picture": "Profile picture", "Upload profile picture": "Upload profile picture", "Display Name": "Display Name", "Save": "Save", + "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", + "Success": "Success", + "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them", "Profile": "Profile", "Flair": "Flair", "Account": "Account",