Rework profile sections of user and room settings

Mostly by design request. Some is freehand, to be reviewed.
This commit is contained in:
Travis Ralston 2020-09-21 21:00:51 -06:00
parent 6fee3d8f4f
commit 4f983ad9a1
7 changed files with 200 additions and 47 deletions

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 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
// 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,34 @@ limitations under the License.
left: 0; left: 0;
right: 0; right: 0;
} }
.mx_AvatarSetting_avatarPlaceholder ~ .mx_AvatarSetting_uploadButton {
border: 1px solid $settings-profile-overlay-placeholder-fg-color;
}
.mx_AvatarSetting_uploadButton {
width: 32px;
height: 32px;
border-radius: 32px;
background-color: $settings-profile-placeholder-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-overlay-placeholder-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

@ -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,11 @@ 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 +182,20 @@ 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();
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,26 @@ 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 +181,20 @@ 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...",