Implement new design for uploading/removing avatars

pull/21833/head
Michael Telatynski 2019-12-16 14:58:12 +00:00
parent 2569b78db3
commit 9fa2680dc6
7 changed files with 186 additions and 174 deletions

View File

@ -175,6 +175,7 @@
@import "./views/rooms/_Stickers.scss"; @import "./views/rooms/_Stickers.scss";
@import "./views/rooms/_TopUnreadMessagesBar.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss";
@import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/rooms/_WhoIsTypingTile.scss";
@import "./views/settings/_AvatarSetting.scss";
@import "./views/settings/_CrossSigningPanel.scss"; @import "./views/settings/_CrossSigningPanel.scss";
@import "./views/settings/_DevicesPanel.scss"; @import "./views/settings/_DevicesPanel.scss";
@import "./views/settings/_EmailAddresses.scss"; @import "./views/settings/_EmailAddresses.scss";

View File

@ -0,0 +1,82 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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_AvatarSetting_avatar {
width: 88px;
height: 88px;
margin-left: 13px;
position: relative;
& > * {
width: 88px;
box-sizing: border-box;
}
.mx_AccessibleButton.mx_AccessibleButton_kind_primary {
margin-top: 8px;
div {
position: relative;
height: 12px;
width: 12px;
display: inline;
padding-right: 6px; // 0.5 * 12px
left: -6px; // 0.5 * 12px
top: 3px;
}
div::before {
content: '';
position: absolute;
height: 12px;
width: 12px;
background-color: $button-primary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/feather-customised/upload.svg');
}
}
.mx_AccessibleButton.mx_AccessibleButton_kind_link_sm {
color: $button-danger-bg-color;
}
& > img,
.mx_AvatarSetting_avatarPlaceholder {
display: block;
height: 88px;
border-radius: 4px;
}
.mx_AvatarSetting_avatarPlaceholder::before {
background-color: $settings-profile-overlay-placeholder-fg-color;
mask: url("$(res)/img/feather-customised/user.svg");
mask-repeat: no-repeat;
mask-size: 36px;
mask-position: center;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}
.mx_AvatarSetting_avatar .mx_AvatarSetting_avatarPlaceholder {
background-color: $settings-profile-placeholder-bg-color;
}

View File

@ -38,91 +38,6 @@ limitations under the License.
} }
} }
.mx_ProfileSettings_avatar {
width: 88px;
height: 88px;
margin-left: 13px;
position: relative;
}
.mx_ProfileSettings_avatar > * {
display: block;
width: 88px;
height: 88px;
border-radius: 4px;
}
.mx_ProfileSettings_avatar .mx_ProfileSettings_avatarOverlay_disabled {
cursor: default;
}
.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;
cursor: pointer;
}
.mx_ProfileSettings_avatar:hover .mx_ProfileSettings_avatarOverlay:not(.mx_ProfileSettings_avatarOverlay_disabled) {
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_noAvatarText {
display: block;
margin: 34px auto auto;
}
.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-customised/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 { .mx_ProfileSettings_avatarUpload {
display: none; display: none;
} }

View File

@ -19,8 +19,7 @@ import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import MatrixClientPeg from "../../../MatrixClientPeg"; import MatrixClientPeg from "../../../MatrixClientPeg";
import Field from "../elements/Field"; import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton"; import sdk from "../../../index";
import classNames from 'classnames';
// TODO: Merge with ProfileSettings? // TODO: Merge with ProfileSettings?
export default class RoomProfileSettings extends React.Component { export default class RoomProfileSettings extends React.Component {
@ -62,13 +61,20 @@ export default class RoomProfileSettings extends React.Component {
this._avatarUpload = createRef(); this._avatarUpload = createRef();
} }
_uploadAvatar = (e) => { _uploadAvatar = () => {
e.stopPropagation();
e.preventDefault();
this._avatarUpload.current.click(); this._avatarUpload.current.click();
}; };
_removeAvatar = () => {
// clear file upload field so same file can be selected
this._avatarUpload.current.value = "";
this.setState({
avatarUrl: undefined,
avatarFile: undefined,
enableProfileSave: true,
});
};
_saveProfile = async (e) => { _saveProfile = async (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -139,45 +145,8 @@ export default class RoomProfileSettings extends React.Component {
}; };
render() { render() {
// TODO: Why is rendering a box with an overlay so complicated? Can the DOM be reduced? const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
let showOverlayAnyways = true;
let avatarElement = <div className="mx_ProfileSettings_avatarPlaceholder" />;
if (this.state.avatarUrl) {
showOverlayAnyways = false;
avatarElement = <img src={this.state.avatarUrl}
alt={_t("Room avatar")} />;
}
const avatarOverlayClasses = classNames({
"mx_ProfileSettings_avatarOverlay": true,
"mx_ProfileSettings_avatarOverlay_show": showOverlayAnyways,
});
let avatarHoverElement = (
<div className={avatarOverlayClasses} onClick={this._uploadAvatar}>
<span className="mx_ProfileSettings_avatarOverlayText">{_t("Upload room avatar")}</span>
<div className="mx_ProfileSettings_avatarOverlayImgContainer">
<div className="mx_ProfileSettings_avatarOverlayImg" />
</div>
</div>
);
if (!this.state.canSetAvatar) {
if (!showOverlayAnyways) {
avatarHoverElement = null;
} else {
const disabledOverlayClasses = classNames({
"mx_ProfileSettings_avatarOverlay": true,
"mx_ProfileSettings_avatarOverlay_show": true,
"mx_ProfileSettings_avatarOverlay_disabled": true,
});
avatarHoverElement = (
<div className={disabledOverlayClasses}>
<span className="mx_ProfileSettings_noAvatarText">{_t("No room avatar")}</span>
</div>
);
}
}
return ( return (
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}> <form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload" <input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
@ -191,10 +160,11 @@ export default class RoomProfileSettings extends React.Component {
type="text" value={this.state.topic} autoComplete="off" type="text" value={this.state.topic} autoComplete="off"
onChange={this._onTopicChanged} element="textarea" /> onChange={this._onTopicChanged} element="textarea" />
</div> </div>
<div className="mx_ProfileSettings_avatar"> <AvatarSetting
{avatarElement} avatarUrl={this.state.avatarUrl}
{avatarHoverElement} avatarAltText={_t("Room avatar")}
</div> uploadAvatar={this.state.canSetAvatar ? this._uploadAvatar : undefined}
removeAvatar={this.state.canSetAvatar ? this._removeAvatar : undefined} />
</div> </div>
<AccessibleButton onClick={this._saveProfile} kind="primary" <AccessibleButton onClick={this._saveProfile} kind="primary"
disabled={!this.state.enableProfileSave}> disabled={!this.state.enableProfileSave}>

View File

@ -0,0 +1,61 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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 PropTypes from "prop-types";
import sdk from "../../../index";
import {_t} from "../../../languageHandler";
const AvatarSetting = ({avatarUrl, avatarAltText, uploadAvatar, removeAvatar}) => {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let avatarElement = <div className="mx_AvatarSetting_avatarPlaceholder" />;
if (avatarUrl) {
avatarElement = <img src={avatarUrl} alt={avatarAltText} />;
}
let uploadAvatarBtn;
if (uploadAvatar) {
// insert an empty div to be the host for a css mask containing the upload.svg
uploadAvatarBtn = <AccessibleButton onClick={uploadAvatar} kind="primary">
<div>&nbsp;</div>
{_t("Upload")}
</AccessibleButton>;
}
let removeAvatarBtn;
if (avatarUrl && removeAvatar) {
removeAvatarBtn = <AccessibleButton onClick={removeAvatar} kind="link_sm">
{_t("Remove")}
</AccessibleButton>;
}
return <div className="mx_AvatarSetting_avatar">
{ avatarElement }
{ uploadAvatarBtn }
{ removeAvatarBtn }
</div>;
};
AvatarSetting.propTypes = {
avatarUrl: PropTypes.string,
uploadAvatar: PropTypes.func,
removeAvatar: PropTypes.func,
avatarAltText: PropTypes.string.isRequired,
};
export default AvatarSetting;

View File

@ -18,10 +18,9 @@ import React, {createRef} from 'react';
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import MatrixClientPeg from "../../../MatrixClientPeg"; import MatrixClientPeg from "../../../MatrixClientPeg";
import Field from "../elements/Field"; import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import classNames from 'classnames';
import {User} from "matrix-js-sdk"; import {User} from "matrix-js-sdk";
import { getHostingLink } from '../../../utils/HostingLink'; import { getHostingLink } from '../../../utils/HostingLink';
import sdk from "../../../index";
export default class ProfileSettings extends React.Component { export default class ProfileSettings extends React.Component {
constructor() { constructor() {
@ -52,13 +51,20 @@ export default class ProfileSettings extends React.Component {
this._avatarUpload = createRef(); this._avatarUpload = createRef();
} }
_uploadAvatar = (e) => { _uploadAvatar = () => {
e.stopPropagation();
e.preventDefault();
this._avatarUpload.current.click(); this._avatarUpload.current.click();
}; };
_removeAvatar = () => {
// clear file upload field so same file can be selected
this._avatarUpload.current.value = "";
this.setState({
avatarUrl: undefined,
avatarFile: undefined,
enableProfileSave: true,
});
};
_saveProfile = async (e) => { _saveProfile = async (e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -117,29 +123,6 @@ export default class ProfileSettings extends React.Component {
}; };
render() { render() {
// TODO: Why is rendering a box with an overlay so complicated? Can the DOM be reduced?
let showOverlayAnyways = true;
let avatarElement = <div className="mx_ProfileSettings_avatarPlaceholder" />;
if (this.state.avatarUrl) {
showOverlayAnyways = false;
avatarElement = <img src={this.state.avatarUrl}
alt={_t("Profile picture")} />;
}
const avatarOverlayClasses = classNames({
"mx_ProfileSettings_avatarOverlay": true,
"mx_ProfileSettings_avatarOverlay_show": showOverlayAnyways,
});
const avatarHoverElement = (
<div className={avatarOverlayClasses} onClick={this._uploadAvatar}>
<span className="mx_ProfileSettings_avatarOverlayText">{_t("Upload profile picture")}</span>
<div className="mx_ProfileSettings_avatarOverlayImgContainer">
<div className="mx_ProfileSettings_avatarOverlayImg" />
</div>
</div>
);
const hostingSignupLink = getHostingLink('user-settings'); const hostingSignupLink = getHostingLink('user-settings');
let hostingSignup = null; let hostingSignup = null;
if (hostingSignupLink) { if (hostingSignupLink) {
@ -156,6 +139,8 @@ export default class ProfileSettings extends React.Component {
</span>; </span>;
} }
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AvatarSetting = sdk.getComponent('settings.AvatarSetting');
return ( return (
<form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}> <form onSubmit={this._saveProfile} autoComplete="off" noValidate={true}>
<input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload" <input type="file" ref={this._avatarUpload} className="mx_ProfileSettings_avatarUpload"
@ -170,10 +155,11 @@ export default class ProfileSettings extends React.Component {
type="text" value={this.state.displayName} autoComplete="off" type="text" value={this.state.displayName} autoComplete="off"
onChange={this._onDisplayNameChanged} /> onChange={this._onDisplayNameChanged} />
</div> </div>
<div className="mx_ProfileSettings_avatar"> <AvatarSetting
{avatarElement} avatarUrl={this.state.avatarUrl}
{avatarHoverElement} avatarAltText={_t("Profile picture")}
</div> uploadAvatar={this._uploadAvatar}
removeAvatar={this._removeAvatar} />
</div> </div>
<AccessibleButton onClick={this._saveProfile} kind="primary" <AccessibleButton onClick={this._saveProfile} kind="primary"
disabled={!this.state.enableProfileSave}> disabled={!this.state.enableProfileSave}>

View File

@ -499,6 +499,8 @@
"Pin": "Pin", "Pin": "Pin",
"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",
"Failed to upload profile picture!": "Failed to upload profile picture!", "Failed to upload profile picture!": "Failed to upload profile picture!",
"Upload new:": "Upload new:", "Upload new:": "Upload new:",
"No display name": "No display name", "No display name": "No display name",
@ -597,10 +599,9 @@
"Off": "Off", "Off": "Off",
"On": "On", "On": "On",
"Noisy": "Noisy", "Noisy": "Noisy",
"Profile picture": "Profile picture",
"Upload profile picture": "Upload profile picture",
"<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",
"Display Name": "Display Name", "Display Name": "Display Name",
"Profile picture": "Profile picture",
"Save": "Save", "Save": "Save",
"Identity Server URL must be HTTPS": "Identity Server URL must be HTTPS", "Identity Server URL must be HTTPS": "Identity Server URL must be HTTPS",
"Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)", "Not a valid Identity Server (status code %(code)s)": "Not a valid Identity Server (status code %(code)s)",
@ -691,7 +692,6 @@
"User rules": "User rules", "User rules": "User rules",
"Close": "Close", "Close": "Close",
"You have not ignored anyone.": "You have not ignored anyone.", "You have not ignored anyone.": "You have not ignored anyone.",
"Remove": "Remove",
"You are currently ignoring:": "You are currently ignoring:", "You are currently ignoring:": "You are currently ignoring:",
"You are not subscribed to any lists": "You are not subscribed to any lists", "You are not subscribed to any lists": "You are not subscribed to any lists",
"Unsubscribe": "Unsubscribe", "Unsubscribe": "Unsubscribe",
@ -1097,11 +1097,9 @@
"Showing flair for these communities:": "Showing flair for these communities:", "Showing flair for these communities:": "Showing flair for these communities:",
"This room is not showing flair for any communities": "This room is not showing flair for any communities", "This room is not showing flair for any communities": "This room is not showing flair for any communities",
"New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)", "New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)",
"Room avatar": "Room avatar",
"Upload room avatar": "Upload room avatar",
"No room avatar": "No room avatar",
"Room Name": "Room Name", "Room Name": "Room Name",
"Room Topic": "Room Topic", "Room Topic": "Room Topic",
"Room avatar": "Room avatar",
"You have <a>enabled</a> URL previews by default.": "You have <a>enabled</a> URL previews by default.", "You have <a>enabled</a> URL previews by default.": "You have <a>enabled</a> URL previews by default.",
"You have <a>disabled</a> URL previews by default.": "You have <a>disabled</a> URL previews by default.", "You have <a>disabled</a> URL previews by default.": "You have <a>disabled</a> URL previews by default.",
"URL previews are enabled by default for participants in this room.": "URL previews are enabled by default for participants in this room.", "URL previews are enabled by default for participants in this room.": "URL previews are enabled by default for participants in this room.",
@ -1545,7 +1543,6 @@
"Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)", "Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)",
"Upload files": "Upload files", "Upload files": "Upload files",
"Upload all": "Upload all", "Upload all": "Upload all",
"Upload": "Upload",
"This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.", "This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "This file is <b>too large</b> to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.",
"These files are <b>too large</b> to upload. The file size limit is %(limit)s.": "These files are <b>too large</b> to upload. The file size limit is %(limit)s.", "These files are <b>too large</b> to upload. The file size limit is %(limit)s.": "These files are <b>too large</b> to upload. The file size limit is %(limit)s.",
"Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.": "Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.", "Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.": "Some files are <b>too large</b> to be uploaded. The file size limit is %(limit)s.",