Rework profile sections of user and room settings
Mostly by design request. Some is freehand, to be reviewed.
This commit is contained in:
parent
6fee3d8f4f
commit
4f983ad9a1
7 changed files with 200 additions and 47 deletions
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,8 +63,16 @@ const AvatarSetting = ({avatarUrl, avatarAltText, avatarName, uploadAvatar, remo
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_AvatarSetting_avatar">
|
const avatarClasses = classNames({
|
||||||
|
"mx_AvatarSetting_avatar": true,
|
||||||
|
"mx_AvatarSetting_avatar_hovering": isHovering,
|
||||||
|
})
|
||||||
|
return <div className={avatarClasses}>
|
||||||
{avatarElement}
|
{avatarElement}
|
||||||
|
<div className="mx_AvatarSetting_hover">
|
||||||
|
<div className="mx_AvatarSetting_hoverBg" />
|
||||||
|
<span>{_t("Upload")}</span>
|
||||||
|
</div>
|
||||||
{uploadAvatarBtn}
|
{uploadAvatarBtn}
|
||||||
{removeAvatarBtn}
|
{removeAvatarBtn}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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...",
|
||||||
|
|
Loading…
Reference in a new issue