diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js
index 2b8110d95..8f7d86d75 100644
--- a/app/soapbox/features/account/components/header.js
+++ b/app/soapbox/features/account/components/header.js
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import Icon from 'soapbox/components/icon';
import ImmutablePureComponent from 'react-immutable-pure-component';
import {
isStaff,
@@ -17,14 +18,18 @@ import {
} from 'soapbox/utils/accounts';
import classNames from 'classnames';
import Avatar from 'soapbox/components/avatar';
+import { getAcct } from 'soapbox/utils/accounts';
+import { displayFqn } from 'soapbox/utils/state';
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
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 ActionButton from 'soapbox/features/ui/components/action_button';
import SubscriptionButton from 'soapbox/features/ui/components/subscription_button';
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 { getFeatures } from 'soapbox/utils/features';
@@ -65,6 +70,8 @@ const messages = defineMessages({
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}' },
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 => {
@@ -77,6 +84,8 @@ const mapStateToProps = state => {
me,
meAccount: account,
features,
+ displayFqn: displayFqn(state),
+
};
};
@@ -91,10 +100,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) => {
@@ -106,19 +117,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({
@@ -295,16 +321,18 @@ class Header extends ImmutablePureComponent {
}
render() {
- const { account, 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 && (
@@ -326,6 +354,10 @@ class Header extends ImmutablePureComponent {
const avatarSize = isSmallScreen ? 90 : 200;
const deactivated = !account.getIn(['pleroma', 'is_active'], true);
+ const lockedIcon = account.get('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 (
@@ -342,12 +374,23 @@ class Header extends ImmutablePureComponent {
}
-
+
-
-
-
+
+
+
+
+
+
+
+
+ {verified && }
+ {account.get('bot') && }
+ { @{getAcct(account, displayFqn)} {lockedIcon} }
+
+
+
{isSmallScreen && (
diff --git a/app/soapbox/pages/profile_page.js b/app/soapbox/pages/profile_page.js
index 251bf7190..82657b1a2 100644
--- a/app/soapbox/pages/profile_page.js
+++ b/app/soapbox/pages/profile_page.js
@@ -19,6 +19,8 @@ import { displayFqn } from 'soapbox/utils/state';
import { getFeatures } from 'soapbox/utils/features';
import { makeGetAccount } from '../selectors';
import { Redirect } from 'react-router-dom';
+import classNames from 'classnames';
+
const mapStateToProps = (state, { params, withReplies = false }) => {
const username = params.username || '';
@@ -75,13 +77,20 @@ class ProfilePage extends ImmutablePureComponent {
return
;
}
+ 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 (
{account &&
@{getAcct(account, displayFqn)}
}
-
+
diff --git a/app/styles/components/account-header.scss b/app/styles/components/account-header.scss
index 12ca0146c..7b1ff4376 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: -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%;
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 {
@@ -163,6 +215,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 {
display: flex;
flex-direction: row;
diff --git a/app/styles/ui.scss b/app/styles/ui.scss
index 926900dea..dbad80cc7 100644
--- a/app/styles/ui.scss
+++ b/app/styles/ui.scss
@@ -351,6 +351,10 @@
@media (min-width: 896px) {
top: -290px;
position: sticky;
+
+ &__no-header {
+ top: -75px;
+ }
}
}