diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 62dc3236a..4de9b6580 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -2,7 +2,7 @@ import classNames from 'classnames'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; -import { debounce } from 'lodash'; +import { debounce, throttle } from 'lodash'; import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; @@ -13,8 +13,11 @@ import { NavLink } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modal'; 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 StillImage from 'soapbox/components/still_image'; +import VerificationBadge from 'soapbox/components/verification_badge'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; import ActionButton from 'soapbox/features/ui/components/action_button'; import SubscriptionButton from 'soapbox/features/ui/components/subscription_button'; @@ -29,8 +32,10 @@ import { isRemote, getDomain, } from 'soapbox/utils/accounts'; +import { getAcct } from 'soapbox/utils/accounts'; import { getFeatures } from 'soapbox/utils/features'; import { shortNumberFormat } from 'soapbox/utils/numbers'; +import { displayFqn } from 'soapbox/utils/state'; const messages = defineMessages({ 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}' }, suggestUser: { id: 'admin.users.actions.suggest_user', defaultMessage: 'Suggest @{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 => { @@ -83,6 +90,8 @@ const mapStateToProps = state => { me, meAccount: account, features, + displayFqn: displayFqn(state), + }; }; @@ -97,10 +106,12 @@ class Header extends ImmutablePureComponent { intl: PropTypes.object.isRequired, username: PropTypes.string, features: PropTypes.object, + displayFqn: PropTypes.bool, }; state = { isSmallScreen: (window.innerWidth <= 895), + isLocked: false, } isStatusesPageActive = (match, location) => { @@ -112,19 +123,34 @@ class Header extends ImmutablePureComponent { } componentDidMount() { + window.addEventListener('scroll', this.handleScroll); window.addEventListener('resize', this.handleResize, { passive: true }); } componentWillUnmount() { + window.removeEventListener('scroll', this.handleScroll); window.removeEventListener('resize', this.handleResize); } + setRef = (c) => { + this.node = c; + } + handleResize = debounce(() => { this.setState({ isSmallScreen: (window.innerWidth <= 895) }); }, 5, { 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 = () => { const avatar_url = this.props.account.get('avatar'); const avatar = ImmutableMap({ @@ -515,16 +541,18 @@ class Header extends ImmutablePureComponent { } render() { - const { account, intl, username, me, features } = this.props; - const { isSmallScreen } = this.state; + const { account, displayFqn, intl, username, me, features } = this.props; + const { isSmallScreen, isLocked } = this.state; if (!account) { return (
-
+
-
+
+
+
{isSmallScreen && (
@@ -547,6 +575,9 @@ class Header extends ImmutablePureComponent { const avatarSize = isSmallScreen ? 90 : 200; 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 (
@@ -563,12 +594,28 @@ class Header extends ImmutablePureComponent {
}
-
+
- - - +
+ + + +
+ +
+ + {verified && } + {account.get('bot') && } + + @{getAcct(account, displayFqn)} + {account.get('locked') && ( + + )} + +
+
+
diff --git a/app/styles/components/account-header.scss b/app/styles/components/account-header.scss index 9e3a6951b..deaf47a3a 100644 --- a/app/styles/components/account-header.scss +++ b/app/styles/components/account-header.scss @@ -114,15 +114,67 @@ } } - &__avatar { - display: block; + @keyframes fadeIn { + 1% { + visibility: visible; + } + + 100% { + visibility: visible; + } + } + + @keyframes fadeOut { + 1% { + visibility: visible; + } + + 100% { + visibility: hidden; + } + } + + &__card { + display: flex; + flex-direction: column; position: absolute; left: 0; 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%; height: 200px; width: 200px; 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 .account__avatar { @@ -149,7 +201,6 @@ } @media screen and (max-width: 895px) { - top: -45px; left: 20px; left: max(20px + env(safe-area-inset-left)); 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 { display: flex; flex-direction: row;