Merge remote-tracking branch 'mkljczk/locked-profile-header' into profile-page-nav
This commit is contained in:
commit
9cb3e682c1
4 changed files with 163 additions and 15 deletions
|
@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import Icon from 'soapbox/components/icon';
|
||||||
import IconButton from 'soapbox/components/icon_button';
|
import IconButton from 'soapbox/components/icon_button';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import {
|
import {
|
||||||
|
@ -18,16 +19,20 @@ import {
|
||||||
} from 'soapbox/utils/accounts';
|
} from 'soapbox/utils/accounts';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Avatar from 'soapbox/components/avatar';
|
import Avatar from 'soapbox/components/avatar';
|
||||||
|
import { getAcct } from 'soapbox/utils/accounts';
|
||||||
|
import { displayFqn } from 'soapbox/utils/state';
|
||||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
|
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
|
||||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||||
import { ProfileInfoPanel } from 'soapbox/features/ui/util/async-components';
|
import { ProfileInfoPanel } from 'soapbox/features/ui/util/async-components';
|
||||||
import { debounce } from 'lodash';
|
import { debounce, throttle } from 'lodash';
|
||||||
import StillImage from 'soapbox/components/still_image';
|
import StillImage from 'soapbox/components/still_image';
|
||||||
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';
|
||||||
import { openModal } from 'soapbox/actions/modal';
|
import { openModal } from 'soapbox/actions/modal';
|
||||||
|
import VerificationBadge from 'soapbox/components/verification_badge';
|
||||||
|
import Badge from 'soapbox/components/badge';
|
||||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
|
@ -68,6 +73,8 @@ const messages = defineMessages({
|
||||||
demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' },
|
demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' },
|
||||||
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe to notifications from @{name}' },
|
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe to notifications from @{name}' },
|
||||||
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' },
|
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' },
|
||||||
|
deactivated: { id: 'account.deactivated', defaultMessage: 'Deactivated' },
|
||||||
|
bot: { id: 'account.badges.bot', defaultMessage: 'Bot' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
|
@ -80,6 +87,8 @@ const mapStateToProps = state => {
|
||||||
me,
|
me,
|
||||||
meAccount: account,
|
meAccount: account,
|
||||||
features,
|
features,
|
||||||
|
displayFqn: displayFqn(state),
|
||||||
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -94,10 +103,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) => {
|
||||||
|
@ -109,19 +120,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({
|
||||||
|
@ -337,16 +363,18 @@ 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__avatar' />
|
<div className='account__header__card'>
|
||||||
|
<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'>
|
||||||
|
@ -369,6 +397,10 @@ 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 lockedIcon = account.get('locked') ? (<Icon id='lock' title={intl.formatMessage(messages.account_locked)} />) : '';
|
||||||
|
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 */ })}>
|
||||||
|
@ -385,12 +417,23 @@ 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 })}>
|
||||||
<Avatar account={account} size={avatarSize} />
|
<a className='account__header__avatar' href={account.get('avatar')} onClick={this.handleAvatarClick} target='_blank' aria-hidden={!isSmallScreen && isLocked}>
|
||||||
</a>
|
<Avatar account={account} size={avatarSize} />
|
||||||
|
</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)} {lockedIcon}</small> }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='account__header__extra__links'>
|
<div className='account__header__extra__links'>
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,9 @@ import LinkFooter from '../features/ui/components/link_footer';
|
||||||
import { getAcct } from 'soapbox/utils/accounts';
|
import { getAcct } from 'soapbox/utils/accounts';
|
||||||
import { displayFqn } from 'soapbox/utils/state';
|
import { displayFqn } from 'soapbox/utils/state';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
import { makeGetAccount } from '../selectors';
|
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { findAccountByUsername } from 'soapbox/selectors';
|
import classNames from 'classnames';
|
||||||
|
import { findAccountByUsername, makeGetAccount } from 'soapbox/selectors';
|
||||||
|
|
||||||
const mapStateToProps = (state, { params, withReplies = false }) => {
|
const mapStateToProps = (state, { params, withReplies = false }) => {
|
||||||
const username = params.username || '';
|
const username = params.username || '';
|
||||||
|
@ -76,13 +76,20 @@ class ProfilePage extends ImmutablePureComponent {
|
||||||
return <Redirect to={`/@${realAccount.get('acct')}`} />;
|
return <Redirect to={`/@${realAccount.get('acct')}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let headerMissing;
|
||||||
|
const header = account ? account.get('header', '') : undefined;
|
||||||
|
|
||||||
|
if (header) {
|
||||||
|
headerMissing = !header || ['/images/banner.png', '/headers/original/missing.png'].some(path => header.endsWith(path)) || !account.getIn(['pleroma', 'is_active'], true);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={bg && `page page--customization page--${bg}` || 'page'}>
|
<div className={bg && `page page--customization page--${bg}` || 'page'}>
|
||||||
{account && <Helmet>
|
{account && <Helmet>
|
||||||
<title>@{getAcct(account, displayFqn)}</title>
|
<title>@{getAcct(account, displayFqn)}</title>
|
||||||
</Helmet>}
|
</Helmet>}
|
||||||
|
|
||||||
<div className='page__top'>
|
<div className={classNames('page__top', { 'page__top__no-header': headerMissing })}>
|
||||||
<HeaderContainer accountId={accountId} username={accountUsername} />
|
<HeaderContainer accountId={accountId} username={accountUsername} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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: -130px;
|
||||||
|
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: 220px;
|
||||||
|
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: block;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__extra {
|
&__extra {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -355,6 +355,10 @@
|
||||||
@media (min-width: 896px) {
|
@media (min-width: 896px) {
|
||||||
top: -290px;
|
top: -290px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
|
||||||
|
&__no-header {
|
||||||
|
top: -75px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue