Merge pull request #5243 from matrix-org/travis/workflow/design/sep2120

Rework profile area for user and room settings to be more clear
This commit is contained in:
Travis Ralston 2020-09-24 08:39:01 -06:00 committed by GitHub
commit 2d46ca1d15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 220 additions and 61 deletions

View file

@ -18,6 +18,8 @@ limitations under the License.
@import "./_font-sizes.scss"; @import "./_font-sizes.scss";
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
:root { :root {
font-size: 10px; font-size: 10px;
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,13 +15,55 @@ limitations under the License.
*/ */
.mx_AvatarSetting_avatar { .mx_AvatarSetting_avatar {
width: $font-88px; width: 90px;
height: $font-88px; height: 90px;
margin-left: 13px; margin-top: 8px;
position: relative; position: relative;
.mx_AvatarSetting_hover {
transition: opacity $hover-transition;
// position to place the hover bg over the entire thing
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
pointer-events: none; // let the pointer fall through the underlying thing
line-height: 90px;
text-align: center;
> span {
color: #fff; // hardcoded to contrast with background
position: relative; // tricks the layout engine into putting this on top of the bg
font-weight: 500;
}
.mx_AvatarSetting_hoverBg {
// absolute position to lazily fill the entire container
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
opacity: 0.5;
background-color: $settings-profile-overlay-placeholder-fg-color;
border-radius: 90px;
}
}
&.mx_AvatarSetting_avatar_hovering .mx_AvatarSetting_hover {
opacity: 1;
}
&:not(.mx_AvatarSetting_avatar_hovering) .mx_AvatarSetting_hover {
opacity: 0;
}
& > * { & > * {
width: $font-88px;
box-sizing: border-box; box-sizing: border-box;
} }
@ -30,7 +72,7 @@ limitations under the License.
} }
.mx_AccessibleButton.mx_AccessibleButton_kind_link_sm { .mx_AccessibleButton.mx_AccessibleButton_kind_link_sm {
color: $button-danger-bg-color; width: 100%;
} }
& > img { & > img {
@ -41,8 +83,9 @@ limitations under the License.
& > img, & > img,
.mx_AvatarSetting_avatarPlaceholder { .mx_AvatarSetting_avatarPlaceholder {
display: block; display: block;
height: $font-88px; height: 90px;
border-radius: 4px; border-radius: 90px;
cursor: pointer;
} }
.mx_AvatarSetting_avatarPlaceholder::before { .mx_AvatarSetting_avatarPlaceholder::before {
@ -58,6 +101,29 @@ limitations under the License.
left: 0; left: 0;
right: 0; right: 0;
} }
.mx_AvatarSetting_uploadButton {
width: 32px;
height: 32px;
border-radius: 32px;
background-color: $settings-profile-button-bg-color;
position: absolute;
bottom: 0;
right: 0;
}
.mx_AvatarSetting_uploadButton::before {
content: "";
display: block;
width: 100%;
height: 100%;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 55%;
background-color: $settings-profile-button-fg-color;
mask-image: url('$(res)/img/feather-customised/edit.svg');
}
} }
.mx_AvatarSetting_avatar .mx_AvatarSetting_avatarPlaceholder { .mx_AvatarSetting_avatar .mx_AvatarSetting_avatarPlaceholder {

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -20,6 +20,13 @@ limitations under the License.
.mx_ProfileSettings_controls { .mx_ProfileSettings_controls {
flex-grow: 1; flex-grow: 1;
margin-right: 54px;
// We put the header under the controls with some minor styling to cheat
// alignment of the field with the avatar
.mx_SettingsTab_subheading {
margin-top: 0;
}
} }
.mx_ProfileSettings_controls .mx_Field #profileTopic { .mx_ProfileSettings_controls .mx_Field #profileTopic {
@ -41,3 +48,17 @@ limitations under the License.
.mx_ProfileSettings_avatarUpload { .mx_ProfileSettings_avatarUpload {
display: none; display: none;
} }
.mx_ProfileSettings_profileForm {
@mixin mx_Settings_fullWidthField;
border-bottom: 1px solid $menu-border-color;
}
.mx_ProfileSettings_buttons {
margin-top: 10px; // 18px is already accounted for by the <p> above the buttons
margin-bottom: 28px;
> .mx_AccessibleButton_kind_link {
padding-left: 0; // to align with left side
}
}

View file

@ -22,6 +22,13 @@ limitations under the License.
margin-top: 0; margin-top: 0;
} }
// TODO: Make this selector less painful
.mx_GeneralUserSettingsTab_accountSection .mx_SettingsTab_subheading:nth-child(n + 1),
.mx_GeneralUserSettingsTab_discovery .mx_SettingsTab_subheading:nth-child(n + 2),
.mx_SetIdServer .mx_SettingsTab_subheading {
margin-top: 24px;
}
.mx_GeneralUserSettingsTab_accountSection .mx_Spinner, .mx_GeneralUserSettingsTab_accountSection .mx_Spinner,
.mx_GeneralUserSettingsTab_discovery .mx_Spinner { .mx_GeneralUserSettingsTab_discovery .mx_Spinner {
// Move the spinner to the left side of the container (default center) // Move the spinner to the left side of the container (default center)

View file

@ -87,11 +87,10 @@ $dialog-background-bg-color: $header-panel-bg-color;
$lightbox-background-bg-color: #000; $lightbox-background-bg-color: #000;
$settings-grey-fg-color: #a2a2a2; $settings-grey-fg-color: #a2a2a2;
$settings-profile-placeholder-bg-color: #e7e7e7; $settings-profile-placeholder-bg-color: #21262c;
$settings-profile-overlay-bg-color: #000;
$settings-profile-overlay-placeholder-bg-color: transparent;
$settings-profile-overlay-fg-color: #fff;
$settings-profile-overlay-placeholder-fg-color: #454545; $settings-profile-overlay-placeholder-fg-color: #454545;
$settings-profile-button-bg-color: #e7e7e7;
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
$settings-subsection-fg-color: $text-secondary-color; $settings-subsection-fg-color: $text-secondary-color;
$topleftmenu-color: $text-primary-color; $topleftmenu-color: $text-primary-color;

View file

@ -86,10 +86,9 @@ $lightbox-background-bg-color: #000;
$settings-grey-fg-color: #a2a2a2; $settings-grey-fg-color: #a2a2a2;
$settings-profile-placeholder-bg-color: #e7e7e7; $settings-profile-placeholder-bg-color: #e7e7e7;
$settings-profile-overlay-bg-color: #000;
$settings-profile-overlay-placeholder-bg-color: transparent;
$settings-profile-overlay-fg-color: #fff;
$settings-profile-overlay-placeholder-fg-color: #454545; $settings-profile-overlay-placeholder-fg-color: #454545;
$settings-profile-button-bg-color: #e7e7e7;
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
$settings-subsection-fg-color: $text-secondary-color; $settings-subsection-fg-color: $text-secondary-color;
$topleftmenu-color: $text-primary-color; $topleftmenu-color: $text-primary-color;

View file

@ -144,10 +144,9 @@ $blockquote-fg-color: #777;
$settings-grey-fg-color: #a2a2a2; $settings-grey-fg-color: #a2a2a2;
$settings-profile-placeholder-bg-color: #e7e7e7; $settings-profile-placeholder-bg-color: #e7e7e7;
$settings-profile-overlay-bg-color: #000;
$settings-profile-overlay-placeholder-bg-color: transparent;
$settings-profile-overlay-fg-color: #fff;
$settings-profile-overlay-placeholder-fg-color: #2e2f32; $settings-profile-overlay-placeholder-fg-color: #2e2f32;
$settings-profile-button-bg-color: #e7e7e7;
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
$settings-subsection-fg-color: #61708b; $settings-subsection-fg-color: #61708b;
$voip-decline-color: #f48080; $voip-decline-color: #f48080;

View file

@ -137,11 +137,10 @@ $blockquote-bar-color: #ddd;
$blockquote-fg-color: #777; $blockquote-fg-color: #777;
$settings-grey-fg-color: #a2a2a2; $settings-grey-fg-color: #a2a2a2;
$settings-profile-placeholder-bg-color: #e7e7e7; $settings-profile-placeholder-bg-color: #f4f6fa;
$settings-profile-overlay-bg-color: #000;
$settings-profile-overlay-placeholder-bg-color: transparent;
$settings-profile-overlay-fg-color: #fff;
$settings-profile-overlay-placeholder-fg-color: #2e2f32; $settings-profile-overlay-placeholder-fg-color: #2e2f32;
$settings-profile-button-bg-color: #e7e7e7;
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
$settings-subsection-fg-color: #61708b; $settings-subsection-fg-color: #61708b;
$voip-decline-color: #f48080; $voip-decline-color: #f48080;

View file

@ -75,6 +75,15 @@ export default class RoomProfileSettings extends React.Component {
}); });
}; };
_clearProfile = async (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.state.enableProfileSave) return;
this._removeAvatar();
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
};
_saveProfile = async (e) => { _saveProfile = async (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -150,7 +159,12 @@ export default class RoomProfileSettings extends React.Component {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AvatarSetting = sdk.getComponent('settings.AvatarSetting'); const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
return ( return (
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}> <form
onSubmit={this._saveProfile}
autoComplete="off"
noValidate={true}
className="mx_ProfileSettings_profileForm"
>
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload" <input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" /> onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile"> <div className="mx_ProfileSettings_profile">
@ -169,10 +183,22 @@ export default class RoomProfileSettings extends React.Component {
uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined} uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} /> removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
</div> </div>
<AccessibleButton onClick={this._saveProfile} kind="primary" <div className="mx_ProfileSettings_buttons">
disabled={!this.state.enableProfileSave}> <AccessibleButton
onClick={this._clearProfile}
kind="link"
disabled={!this.state.enableProfileSave}
>
{_t("Cancel")}
</AccessibleButton>
<AccessibleButton
onClick={this._saveProfile}
kind="primary"
disabled={!this.state.enableProfileSave}
>
{_t("Save")} {_t("Save")}
</AccessibleButton> </AccessibleButton>
</div>
</form> </form>
); );
} }

View file

@ -14,25 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useCallback} from "react"; import React, {useState} from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import * as sdk from "../../../index";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import Modal from "../../../Modal"; import AccessibleButton from "../elements/AccessibleButton";
import classNames from "classnames";
const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar}) => { const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar}) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const [isHovering, setIsHovering] = useState(false);
const hoveringProps = {
onMouseEnter: () => setIsHovering(true),
onMouseLeave: () => setIsHovering(false),
};
const openImageView = useCallback(() => { let avatarElement = <AccessibleButton
const ImageView = sdk.getComponent("elements.ImageView"); element="div"
Modal.createDialog(ImageView, { onClick={uploadAvatar}
src: avatarUrl, className="mx_AvatarSetting_avatarPlaceholder"
name: avatarName, {...hoveringProps}
}, "mx_Dialog_lightbox"); />;
}, [avatarUrl, avatarName]);
let avatarElement = <div className="mx_AvatarSetting_avatarPlaceholder" />;
if (avatarUrl) { if (avatarUrl) {
avatarElement = ( avatarElement = (
<AccessibleButton <AccessibleButton
@ -40,16 +40,20 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
src={avatarUrl} src={avatarUrl}
alt={avatarAltText} alt={avatarAltText}
aria-label={avatarAltText} aria-label={avatarAltText}
onClick={openImageView} /> onClick={uploadAvatar}
{...hoveringProps}
/>
); );
} }
let uploadAvatarBtn; let uploadAvatarBtn;
if (uploadAvatar) { if (uploadAvatar) {
// insert an empty div to be the host for a css mask containing the upload.svg // insert an empty div to be the host for a css mask containing the upload.svg
uploadAvatarBtn = <AccessibleButton onClick={uploadAvatar} kind="primary"> uploadAvatarBtn = <AccessibleButton
{_t("Upload")} onClick={uploadAvatar}
</AccessibleButton>; className='mx_AvatarSetting_uploadButton'
{...hoveringProps}
/>;
} }
let removeAvatarBtn; let removeAvatarBtn;
@ -59,10 +63,18 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
</AccessibleButton>; </AccessibleButton>;
} }
return <div className="mx_AvatarSetting_avatar"> const avatarClasses = classNames({
{ avatarElement } "mx_AvatarSetting_avatar": true,
{ uploadAvatarBtn } "mx_AvatarSetting_avatar_hovering": isHovering,
{ removeAvatarBtn } });
return <div className={avatarClasses}>
{avatarElement}
<div className="mx_AvatarSetting_hover">
<div className="mx_AvatarSetting_hoverBg" />
<span>{_t("Upload")}</span>
</div>
{uploadAvatarBtn}
{removeAvatarBtn}
</div>; </div>;
}; };

View file

@ -65,6 +65,15 @@ export default class ProfileSettings extends React.Component {
}); });
}; };
_clearProfile = async (e) => {
e.stopPropagation();
e.preventDefault();
if (!this.state.enableProfileSave) return;
this._removeAvatar();
this.setState({enableProfileSave: false, displayName: this.state.originalDisplayName});
};
_saveProfile = async (e) => { _saveProfile = async (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -144,18 +153,27 @@ export default class ProfileSettings extends React.Component {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AvatarSetting = sdk.getComponent('settings.AvatarSetting'); const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
return ( return (
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}> <form
onSubmit={this._saveProfile}
autoComplete="off"
noValidate={true}
className="mx_ProfileSettings_profileForm"
>
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload" <input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
onChange={this._onAvatarChanged} accept="image/*" /> onChange={this._onAvatarChanged} accept="image/*" />
<div className="mx_ProfileSettings_profile"> <div className="mx_ProfileSettings_profile">
<div className="mx_ProfileSettings_controls"> <div className="mx_ProfileSettings_controls">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
<Field
label={_t("Display Name")}
type="text" value={this.state.displayName}
autoComplete="off"
onChange={this._onDisplayNameChanged}
/>
<p> <p>
{this.state.userId} {this.state.userId}
{hostingSignup} {hostingSignup}
</p> </p>
<Field label={_t("Display Name")}
type="text" value={this.state.displayName} autoComplete="off"
onChange={this._onDisplayNameChanged} />
</div> </div>
<AvatarSetting <AvatarSetting
avatarUrl={this.state.avatarUrl} avatarUrl={this.state.avatarUrl}
@ -164,10 +182,22 @@ export default class ProfileSettings extends React.Component {
uploadAvatar={this._uploadAvatar} uploadAvatar={this._uploadAvatar}
removeAvatar={this._removeAvatar} /> removeAvatar={this._removeAvatar} />
</div> </div>
<AccessibleButton onClick={this._saveProfile} kind="primary" <div className="mx_ProfileSettings_buttons">
disabled={!this.state.enableProfileSave}> <AccessibleButton
onClick={this._clearProfile}
kind="link"
disabled={!this.state.enableProfileSave}
>
{_t("Cancel")}
</AccessibleButton>
<AccessibleButton
onClick={this._saveProfile}
kind="primary"
disabled={!this.state.enableProfileSave}
>
{_t("Save")} {_t("Save")}
</AccessibleButton> </AccessibleButton>
</div>
</form> </form>
); );
} }

View file

@ -221,7 +221,6 @@ export default class GeneralUserSettingsTab extends React.Component {
_renderProfileSection() { _renderProfileSection() {
return ( return (
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Profile")}</span>
<ProfileSettings /> <ProfileSettings />
</div> </div>
); );

View file

@ -624,8 +624,8 @@
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)", "From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
"Decline (%(counter)s)": "Decline (%(counter)s)", "Decline (%(counter)s)": "Decline (%(counter)s)",
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:", "Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
"Upload": "Upload",
"Remove": "Remove", "Remove": "Remove",
"Upload": "Upload",
"This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.", "This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
"This bridge is managed by <user />.": "This bridge is managed by <user />.", "This bridge is managed by <user />.": "This bridge is managed by <user />.",
"Workspace: %(networkName)s": "Workspace: %(networkName)s", "Workspace: %(networkName)s": "Workspace: %(networkName)s",
@ -722,6 +722,7 @@
"On": "On", "On": "On",
"Noisy": "Noisy", "Noisy": "Noisy",
"<a>Upgrade</a> to your own domain": "<a>Upgrade</a> to your own domain", "<a>Upgrade</a> to your own domain": "<a>Upgrade</a> to your own domain",
"Profile": "Profile",
"Display Name": "Display Name", "Display Name": "Display Name",
"Profile picture": "Profile picture", "Profile picture": "Profile picture",
"Save": "Save", "Save": "Save",
@ -822,7 +823,6 @@
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
"Success": "Success", "Success": "Success",
"Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them", "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them",
"Profile": "Profile",
"Email addresses": "Email addresses", "Email addresses": "Email addresses",
"Phone numbers": "Phone numbers", "Phone numbers": "Phone numbers",
"Set a new account password...": "Set a new account password...", "Set a new account password...": "Set a new account password...",