Merge branch 'profile-avatar-switcher' into 'develop'

Improve account headers

See merge request soapbox-pub/soapbox-fe!801
improve-ci
marcin mikołajczak 2022-01-25 00:00:39 +00:00
commit 72cc60f880
2 zmienionych plików z 153 dodań i 12 usunięć

Wyświetl plik

@ -2,7 +2,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { debounce } from 'lodash'; import { debounce, throttle } from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
@ -13,8 +13,11 @@ import { NavLink } from 'react-router-dom';
import { openModal } from 'soapbox/actions/modal'; import { openModal } from 'soapbox/actions/modal';
import Avatar from 'soapbox/components/avatar'; import Avatar from 'soapbox/components/avatar';
import Badge from 'soapbox/components/badge';
import Icon from 'soapbox/components/icon';
import IconButton from 'soapbox/components/icon_button'; import IconButton from 'soapbox/components/icon_button';
import StillImage from 'soapbox/components/still_image'; import StillImage from 'soapbox/components/still_image';
import VerificationBadge from 'soapbox/components/verification_badge';
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
import ActionButton from 'soapbox/features/ui/components/action_button'; import ActionButton from 'soapbox/features/ui/components/action_button';
import SubscriptionButton from 'soapbox/features/ui/components/subscription_button'; import SubscriptionButton from 'soapbox/features/ui/components/subscription_button';
@ -29,8 +32,10 @@ import {
isRemote, isRemote,
getDomain, getDomain,
} from 'soapbox/utils/accounts'; } from 'soapbox/utils/accounts';
import { getAcct } from 'soapbox/utils/accounts';
import { getFeatures } from 'soapbox/utils/features'; import { getFeatures } from 'soapbox/utils/features';
import { shortNumberFormat } from 'soapbox/utils/numbers'; import { shortNumberFormat } from 'soapbox/utils/numbers';
import { displayFqn } from 'soapbox/utils/state';
const messages = defineMessages({ const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
@ -71,6 +76,8 @@ const messages = defineMessages({
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' }, unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' },
suggestUser: { id: 'admin.users.actions.suggest_user', defaultMessage: 'Suggest @{name}' }, suggestUser: { id: 'admin.users.actions.suggest_user', defaultMessage: 'Suggest @{name}' },
unsuggestUser: { id: 'admin.users.actions.unsuggest_user', defaultMessage: 'Unsuggest @{name}' }, unsuggestUser: { id: 'admin.users.actions.unsuggest_user', defaultMessage: 'Unsuggest @{name}' },
deactivated: { id: 'account.deactivated', defaultMessage: 'Deactivated' },
bot: { id: 'account.badges.bot', defaultMessage: 'Bot' },
}); });
const mapStateToProps = state => { const mapStateToProps = state => {
@ -83,6 +90,8 @@ const mapStateToProps = state => {
me, me,
meAccount: account, meAccount: account,
features, features,
displayFqn: displayFqn(state),
}; };
}; };
@ -97,10 +106,12 @@ class Header extends ImmutablePureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
username: PropTypes.string, username: PropTypes.string,
features: PropTypes.object, features: PropTypes.object,
displayFqn: PropTypes.bool,
}; };
state = { state = {
isSmallScreen: (window.innerWidth <= 895), isSmallScreen: (window.innerWidth <= 895),
isLocked: false,
} }
isStatusesPageActive = (match, location) => { isStatusesPageActive = (match, location) => {
@ -112,19 +123,34 @@ class Header extends ImmutablePureComponent {
} }
componentDidMount() { componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize, { passive: true }); window.addEventListener('resize', this.handleResize, { passive: true });
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
} }
setRef = (c) => {
this.node = c;
}
handleResize = debounce(() => { handleResize = debounce(() => {
this.setState({ isSmallScreen: (window.innerWidth <= 895) }); this.setState({ isSmallScreen: (window.innerWidth <= 895) });
}, 5, { }, 5, {
trailing: true, trailing: true,
}); });
handleScroll = throttle(() => {
const { top } = this.node.getBoundingClientRect();
const isLocked = top <= 60;
if (this.state.isLocked !== isLocked) {
this.setState({ isLocked });
}
}, 100, { trailing: true });
onAvatarClick = () => { onAvatarClick = () => {
const avatar_url = this.props.account.get('avatar'); const avatar_url = this.props.account.get('avatar');
const avatar = ImmutableMap({ const avatar = ImmutableMap({
@ -515,17 +541,19 @@ class Header extends ImmutablePureComponent {
} }
render() { render() {
const { account, intl, username, me, features } = this.props; const { account, displayFqn, intl, username, me, features } = this.props;
const { isSmallScreen } = this.state; const { isSmallScreen, isLocked } = this.state;
if (!account) { if (!account) {
return ( return (
<div className='account__header'> <div className='account__header'>
<div className='account__header__image account__header__image--none' /> <div className='account__header__image account__header__image--none' />
<div className='account__header__bar'> <div className='account__header__bar' ref={this.setRef}>
<div className='account__header__extra'> <div className='account__header__extra'>
<div className='account__header__card'>
<div className='account__header__avatar' /> <div className='account__header__avatar' />
</div> </div>
</div>
{isSmallScreen && ( {isSmallScreen && (
<div className='account-mobile-container account-mobile-container--nonuser'> <div className='account-mobile-container account-mobile-container--nonuser'>
<BundleContainer fetchComponent={ProfileInfoPanel}> <BundleContainer fetchComponent={ProfileInfoPanel}>
@ -547,6 +575,9 @@ class Header extends ImmutablePureComponent {
const avatarSize = isSmallScreen ? 90 : 200; const avatarSize = isSmallScreen ? 90 : 200;
const deactivated = !account.getIn(['pleroma', 'is_active'], true); const deactivated = !account.getIn(['pleroma', 'is_active'], true);
const displayNameHtml = deactivated ? { __html: intl.formatMessage(messages.deactivated) } : { __html: account.get('display_name_html') };
const verified = account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified');
return ( return (
<div className={classNames('account__header', { inactive: !!account.get('moved'), deactivated: deactivated })}> <div className={classNames('account__header', { inactive: !!account.get('moved'), deactivated: deactivated })}>
<div className={classNames('account__header__image', { /* 'account__header__image--none': headerMissing || deactivated */ })}> <div className={classNames('account__header__image', { /* 'account__header__image--none': headerMissing || deactivated */ })}>
@ -563,12 +594,28 @@ class Header extends ImmutablePureComponent {
</div>} </div>}
</div> </div>
<div className='account__header__bar'> <div className='account__header__bar' ref={this.setRef}>
<div className='account__header__extra'> <div className='account__header__extra'>
<a className='account__header__avatar' href={account.get('avatar')} onClick={this.handleAvatarClick} target='_blank'> <div className={classNames('account__header__card', { 'is-locked': !isSmallScreen && isLocked })}>
<a className='account__header__avatar' href={account.get('avatar')} onClick={this.handleAvatarClick} target='_blank' aria-hidden={!isSmallScreen && isLocked}>
<Avatar account={account} size={avatarSize} /> <Avatar account={account} size={avatarSize} />
</a> </a>
<div className='account__header__name' aria-hidden={isSmallScreen || !isLocked}>
<Avatar account={account} size={40} />
<div>
<span dangerouslySetInnerHTML={displayNameHtml} className={classNames('profile-info-panel__name-content', { 'with-badge': verified })} />
{verified && <VerificationBadge />}
{account.get('bot') && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
<small>
@{getAcct(account, displayFqn)}
{account.get('locked') && (
<Icon src={require('@tabler/icons/icons/lock.svg')} title={intl.formatMessage(messages.account_locked)} />
)}
</small>
</div>
</div>
</div>
<div className='account__header__extra__links'> <div className='account__header__extra__links'>

Wyświetl plik

@ -114,15 +114,67 @@
} }
} }
&__avatar { @keyframes fadeIn {
display: block; 1% {
visibility: visible;
}
100% {
visibility: visible;
}
}
@keyframes fadeOut {
1% {
visibility: visible;
}
100% {
visibility: hidden;
}
}
&__card {
display: flex;
flex-direction: column;
position: absolute; position: absolute;
left: 0; left: 0;
top: -90px; top: -90px;
&.is-locked {
.account__header__avatar {
top: -20px;
opacity: 0;
animation: 0.3s fadeOut;
animation-fill-mode: forwards;
}
.account__header__name {
top: 90px;
opacity: 1;
animation: 0.3s fadeIn;
animation-fill-mode: forwards;
}
}
@media screen and (max-width: 895px) {
top: -45px;
left: 10px;
}
}
&__avatar {
display: block;
position: absolute;
top: 0;
border-radius: 50%; border-radius: 50%;
height: 200px; height: 200px;
width: 200px; width: 200px;
background-color: var(--foreground-color); background-color: var(--foreground-color);
opacity: 1;
animation: 0.3s fadeIn;
animation-fill-mode: forwards;
transition: top 0.3s, opacity 0.15s;
// NOTE - patch fix for avatar size. Wrapper may not be needed when I do polish up on the page // NOTE - patch fix for avatar size. Wrapper may not be needed when I do polish up on the page
.account__avatar { .account__avatar {
@ -149,7 +201,6 @@
} }
@media screen and (max-width: 895px) { @media screen and (max-width: 895px) {
top: -45px;
left: 20px; left: 20px;
left: max(20px + env(safe-area-inset-left)); left: max(20px + env(safe-area-inset-left));
height: 90px; height: 90px;
@ -163,6 +214,49 @@
} }
} }
&__name {
display: flex;
align-items: center;
column-gap: 10px;
width: 265px;
height: 74px;
position: absolute;
top: 100px;
opacity: 0;
animation: 0.3s fadeOut;
animation-fill-mode: forwards;
transition: top 0.3s, opacity 0.15s;
div:nth-child(2) {
width: calc(100% - 50px);
color: var(--primary-text-color);
span:first-of-type {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 18px;
line-height: 1.25;
font-weight: 600;
&.with-badge {
max-width: calc(100% - 20px);
}
}
small {
display: flex;
font-size: 14px;
color: var(--primary-text-color--faint);
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
&__extra { &__extra {
display: flex; display: flex;
flex-direction: row; flex-direction: row;