Merge pull request #2492 from ClearlyClaire/glitch-soc/painful-backports

Port account rows design change from upstream
th-new
Claire 2023-12-03 09:51:07 +01:00 committed by GitHub
commit edd96ce786
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 191 additions and 55 deletions

View File

@ -1,15 +1,21 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { Skeleton } from 'flavours/glitch/components/skeleton'; import { EmptyAccount } from 'flavours/glitch/components/empty_account';
import { ShortNumber } from 'flavours/glitch/components/short_number';
import { VerifiedBadge } from 'flavours/glitch/components/verified_badge';
import { me } from '../initial_state'; import { me } from '../initial_state';
import { Avatar } from './avatar'; import { Avatar } from './avatar';
import { Button } from './button';
import { FollowersCounter } from './counters';
import { DisplayName } from './display_name'; import { DisplayName } from './display_name';
import { IconButton } from './icon_button'; import { IconButton } from './icon_button';
import Permalink from './permalink'; import Permalink from './permalink';
@ -18,13 +24,13 @@ import { RelativeTimestamp } from './relative_timestamp';
const messages = defineMessages({ const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' },
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' }, unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' }, block: { id: 'account.block_short', defaultMessage: 'Block' },
}); });
class Account extends ImmutablePureComponent { class Account extends ImmutablePureComponent {
@ -38,14 +44,16 @@ class Account extends ImmutablePureComponent {
onMuteNotifications: PropTypes.func.isRequired, onMuteNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
hidden: PropTypes.bool, hidden: PropTypes.bool,
minimal: PropTypes.bool,
actionIcon: PropTypes.string, actionIcon: PropTypes.string,
actionTitle: PropTypes.string, actionTitle: PropTypes.string,
defaultAction: PropTypes.string, defaultAction: PropTypes.string,
onActionClick: PropTypes.func, onActionClick: PropTypes.func,
withBio: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
size: 36, size: 46,
}; };
handleFollow = () => { handleFollow = () => {
@ -73,19 +81,10 @@ class Account extends ImmutablePureComponent {
}; };
render () { render () {
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size } = this.props; const { account, intl, hidden, withBio, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal } = this.props;
if (!account) { if (!account) {
return ( return <EmptyAccount size={size} minimal={minimal} />;
<div className='account'>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'><Skeleton width={36} height={36} /></div>
<DisplayName />
</div>
</div>
</div>
);
} }
if (hidden) { if (hidden) {
@ -99,61 +98,91 @@ class Account extends ImmutablePureComponent {
let buttons; let buttons;
if (onActionClick) { if (actionIcon && onActionClick) {
if (actionIcon) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />; buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
} } else if (!actionIcon && account.get('id') !== me && account.get('relationship', null) !== null) {
} else if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']); const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']); const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']); const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']); const muting = account.getIn(['relationship', 'muting']);
if (requested) { if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />; buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={this.handleFollow} />;
} else if (blocking) { } else if (blocking) {
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
} else if (muting) { } else if (muting) {
let hidingNotificationsButton; let hidingNotificationsButton;
if (account.getIn(['relationship', 'muting_notifications'])) { if (account.getIn(['relationship', 'muting_notifications'])) {
hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />; hidingNotificationsButton = <Button text={intl.formatMessage(messages.unmute_notifications)} onClick={this.handleUnmuteNotifications} />;
} else { } else {
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />; hidingNotificationsButton = <Button text={intl.formatMessage(messages.mute_notifications)} onClick={this.handleMuteNotifications} />;
} }
buttons = ( buttons = (
<> <>
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} /> <Button text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />
{hidingNotificationsButton} {hidingNotificationsButton}
</> </>
); );
} else if (defaultAction === 'mute') { } else if (defaultAction === 'mute') {
buttons = <IconButton icon='volume-off' title={intl.formatMessage(messages.mute, { name: account.get('username') })} onClick={this.handleMute} />; buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
} else if (defaultAction === 'block') { } else if (defaultAction === 'block') {
buttons = <IconButton icon='lock' title={intl.formatMessage(messages.block, { name: account.get('username') })} onClick={this.handleBlock} />; buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
} else if (!account.get('moved') || following) { } else if (!account.get('moved') || following) {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
} }
} }
let mute_expires_at; let muteTimeRemaining;
if (account.get('mute_expires_at')) { if (account.get('mute_expires_at')) {
mute_expires_at = <div><RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></div>; muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
}
let verification;
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
if (firstVerifiedField) {
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
} }
return ( return (
<div className='account'> <div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'> <div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}> <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={size} /></div> <div className='account__avatar-wrapper'>
{mute_expires_at} <Avatar account={account} size={size} />
<DisplayName account={account} /> </div>
<div className='account__contents'>
<DisplayName account={account} inline />
{!minimal && (
<div className='account__details'>
{account.get('followers_count') !== -1 && (
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} />
)} {verification} {muteTimeRemaining}
</div>
)}
</div>
</Permalink> </Permalink>
{buttons ?
{!minimal && (
<div className='account__relationship'> <div className='account__relationship'>
{buttons} {buttons}
</div> </div>
: null} )}
</div> </div>
{withBio && (account.get('note').length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
) : (
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
))}
</div> </div>
); );
} }

View File

@ -0,0 +1,33 @@
import React from 'react';
import classNames from 'classnames';
import { DisplayName } from 'flavours/glitch/components/display_name';
import { Skeleton } from 'flavours/glitch/components/skeleton';
interface Props {
size?: number;
minimal?: boolean;
}
export const EmptyAccount: React.FC<Props> = ({
size = 46,
minimal = false,
}) => {
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'>
<Skeleton width={size} height={size} />
</div>
<div>
<DisplayName />
<Skeleton width='7ch' />
</div>
</div>
</div>
</div>
);
};

View File

@ -63,7 +63,7 @@ class ServerBanner extends PureComponent {
<div className='server-banner__meta__column'> <div className='server-banner__meta__column'>
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4> <h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} /> <Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
</div> </div>
<div className='server-banner__meta__column'> <div className='server-banner__meta__column'>

View File

@ -0,0 +1,27 @@
import { Icon } from './icon';
const domParser = new DOMParser();
const stripRelMe = (html: string) => {
const document = domParser.parseFromString(html, 'text/html').documentElement;
document.querySelectorAll<HTMLAnchorElement>('a[rel]').forEach((link) => {
link.rel = link.rel
.split(' ')
.filter((x: string) => x !== 'me')
.join(' ');
});
const body = document.querySelector('body');
return body ? { __html: body.innerHTML } : undefined;
};
interface Props {
link: string;
}
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
<span className='verified-badge'>
<Icon id='check' className='verified-badge__mark' />
<span dangerouslySetInnerHTML={stripRelMe(link)} />
</span>
);

View File

@ -128,7 +128,7 @@ class About extends PureComponent {
<div className='about__meta__column'> <div className='about__meta__column'>
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4> <h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} /> <Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
</div> </div>
<hr className='about__meta__divider' /> <hr className='about__meta__divider' />

View File

@ -6,17 +6,33 @@
.account__display-name { .account__display-name {
flex: 1 1 auto; flex: 1 1 auto;
display: block; display: flex;
align-items: center;
gap: 10px;
color: $darker-text-color; color: $darker-text-color;
overflow: hidden; overflow: hidden;
text-decoration: none; text-decoration: none;
font-size: 14px; font-size: 14px;
&--with-note { .display-name {
strong { margin-bottom: 4px;
}
.display-name strong {
display: inline; display: inline;
} }
} }
&--minimal {
.account__display-name {
.display-name {
margin-bottom: 0;
}
.display-name strong {
display: block;
}
}
} }
&__note { &__note {
@ -41,12 +57,12 @@
.account__wrapper { .account__wrapper {
display: flex; display: flex;
gap: 10px;
align-items: center;
} }
.account__avatar-wrapper { .account__avatar-wrapper {
float: left; float: left;
margin-inline-start: 12px;
margin-inline-end: 12px;
} }
.account__avatar { .account__avatar {
@ -129,9 +145,10 @@
} }
.account__relationship { .account__relationship {
height: 18px;
padding: 10px;
white-space: nowrap; white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
} }
.account__header__wrapper { .account__header__wrapper {
@ -749,6 +766,36 @@
} }
} }
.account__contents {
overflow: hidden;
}
.account__details {
display: flex;
flex-wrap: wrap;
column-gap: 1em;
}
.verified-badge {
display: inline-flex;
align-items: center;
color: $valid-value-color;
gap: 4px;
overflow: hidden;
white-space: nowrap;
> span {
overflow: hidden;
text-overflow: ellipsis;
}
a {
color: inherit;
font-weight: 500;
text-decoration: none;
}
}
.moved-account-banner, .moved-account-banner,
.follow-request-banner, .follow-request-banner,
.account-memorial-banner { .account-memorial-banner {

View File

@ -626,7 +626,7 @@
.status__display-name, .status__display-name,
.account__display-name { .account__display-name {
strong { .display-name strong {
color: $primary-text-color; color: $primary-text-color;
} }
} }
@ -641,12 +641,12 @@ a.status__display-name,
.reply-indicator__display-name, .reply-indicator__display-name,
.detailed-status__display-name, .detailed-status__display-name,
.account__display-name { .account__display-name {
&:hover strong { &:hover .display-name strong {
text-decoration: underline; text-decoration: underline;
} }
} }
.account__display-name strong { .account__display-name .display-name strong {
display: block; display: block;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;