fix: Fix cramped layout of follower recommendations on small viewports (#34967)

This commit is contained in:
diondiondion 2025-06-11 17:15:12 +02:00 committed by GitHub
parent 1623d54ec0
commit 2c828748a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 266 additions and 170 deletions

View file

@ -71,6 +71,7 @@ interface AccountProps {
minimal?: boolean; minimal?: boolean;
defaultAction?: 'block' | 'mute'; defaultAction?: 'block' | 'mute';
withBio?: boolean; withBio?: boolean;
withMenu?: boolean;
} }
export const Account: React.FC<AccountProps> = ({ export const Account: React.FC<AccountProps> = ({
@ -80,6 +81,7 @@ export const Account: React.FC<AccountProps> = ({
minimal, minimal,
defaultAction, defaultAction,
withBio, withBio,
withMenu = true,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const { signedIn } = useIdentity(); const { signedIn } = useIdentity();
@ -225,9 +227,10 @@ export const Account: React.FC<AccountProps> = ({
); );
} }
let button: React.ReactNode, dropdown: React.ReactNode; let button: React.ReactNode;
let dropdown: React.ReactNode;
if (menu.length > 0) { if (menu.length > 0 && withMenu) {
dropdown = ( dropdown = (
<Dropdown <Dropdown
items={menu} items={menu}
@ -279,8 +282,17 @@ export const Account: React.FC<AccountProps> = ({
} }
return ( return (
<div className={classNames('account', { 'account--minimal': minimal })}> <div
<div className='account__wrapper'> className={classNames('account', {
'account--minimal': minimal,
})}
>
<div
className={classNames('account__wrapper', {
'account__wrapper--with-bio': account && withBio,
})}
>
<div className='account__info-wrapper'>
<Link <Link
className='account__display-name' className='account__display-name'
title={account?.acct} title={account?.acct}
@ -316,14 +328,6 @@ export const Account: React.FC<AccountProps> = ({
</div> </div>
</Link> </Link>
{!minimal && (
<div className='account__relationship'>
{dropdown}
{button}
</div>
)}
</div>
{account && {account &&
withBio && withBio &&
(account.note.length > 0 ? ( (account.note.length > 0 ? (
@ -340,5 +344,14 @@ export const Account: React.FC<AccountProps> = ({
</div> </div>
))} ))}
</div> </div>
{!minimal && (
<div className='account__relationship'>
{dropdown}
{button}
</div>
)}
</div>
</div>
); );
}; };

View file

@ -18,6 +18,7 @@ interface Props {
withLink?: boolean; withLink?: boolean;
counter?: number | string; counter?: number | string;
counterBorderColor?: string; counterBorderColor?: string;
className?: string;
} }
export const Avatar: React.FC<Props> = ({ export const Avatar: React.FC<Props> = ({
@ -27,6 +28,7 @@ export const Avatar: React.FC<Props> = ({
inline = false, inline = false,
withLink = false, withLink = false,
style: styleFromParent, style: styleFromParent,
className,
counter, counter,
counterBorderColor, counterBorderColor,
}) => { }) => {
@ -52,7 +54,7 @@ export const Avatar: React.FC<Props> = ({
const avatar = ( const avatar = (
<div <div
className={classNames('account__avatar', { className={classNames(className, 'account__avatar', {
'account__avatar--inline': inline, 'account__avatar--inline': inline,
'account__avatar--loading': loading, 'account__avatar--loading': loading,
})} })}

View file

@ -1,75 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { dismissSuggestion } from 'mastodon/actions/suggestions';
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { FollowButton } from 'mastodon/components/follow_button';
import { IconButton } from 'mastodon/components/icon_button';
import { domain } from 'mastodon/initial_state';
const messages = defineMessages({
dismiss: { id: 'follow_suggestions.dismiss', defaultMessage: "Don't show again" },
});
export const Card = ({ id, source }) => {
const intl = useIntl();
const account = useSelector(state => state.getIn(['accounts', id]));
const dispatch = useDispatch();
const handleDismiss = useCallback(() => {
dispatch(dismissSuggestion({ accountId: id }));
}, [id, dispatch]);
let label;
switch (source) {
case 'friends_of_friends':
label = <FormattedMessage id='follow_suggestions.friends_of_friends_longer' defaultMessage='Popular among people you follow' />;
break;
case 'similar_to_recently_followed':
label = <FormattedMessage id='follow_suggestions.similar_to_recently_followed_longer' defaultMessage='Similar to profiles you recently followed' />;
break;
case 'featured':
label = <FormattedMessage id='follow_suggestions.featured_longer' defaultMessage='Hand-picked by the {domain} team' values={{ domain }} />;
break;
case 'most_followed':
label = <FormattedMessage id='follow_suggestions.popular_suggestion_longer' defaultMessage='Popular on {domain}' values={{ domain }} />;
break;
case 'most_interactions':
label = <FormattedMessage id='follow_suggestions.popular_suggestion_longer' defaultMessage='Popular on {domain}' values={{ domain }} />;
break;
}
return (
<div className='explore__suggestions__card'>
<div className='explore__suggestions__card__source'>
{label}
</div>
<div className='explore__suggestions__card__body'>
<Link to={`/@${account.get('acct')}`} data-hover-card-account={account.id}><Avatar account={account} size={48} /></Link>
<div className='explore__suggestions__card__body__main'>
<div className='explore__suggestions__card__body__main__name-button'>
<Link className='explore__suggestions__card__body__main__name-button__name' to={`/@${account.get('acct')}`} data-hover-card-account={account.id}><DisplayName account={account} /></Link>
<IconButton iconComponent={CloseIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
<FollowButton accountId={account.get('id')} />
</div>
</div>
</div>
</div>
);
};
Card.propTypes = {
id: PropTypes.string.isRequired,
source: PropTypes.oneOf(['friends_of_friends', 'similar_to_recently_followed', 'featured', 'most_followed', 'most_interactions']),
};

View file

@ -0,0 +1,124 @@
import { useCallback } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { Link } from 'react-router-dom';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { dismissSuggestion } from 'mastodon/actions/suggestions';
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { FollowButton } from 'mastodon/components/follow_button';
import { IconButton } from 'mastodon/components/icon_button';
import { domain } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
dismiss: {
id: 'follow_suggestions.dismiss',
defaultMessage: "Don't show again",
},
});
type SuggestionSource =
| 'friends_of_friends'
| 'similar_to_recently_followed'
| 'featured'
| 'most_followed'
| 'most_interactions';
export const Card: React.FC<{ id: string; source: SuggestionSource }> = ({
id,
source,
}) => {
const intl = useIntl();
const account = useAppSelector((state) => state.accounts.get(id));
const dispatch = useAppDispatch();
const handleDismiss = useCallback(() => {
void dispatch(dismissSuggestion({ accountId: id }));
}, [id, dispatch]);
let label;
switch (source) {
case 'friends_of_friends':
label = (
<FormattedMessage
id='follow_suggestions.friends_of_friends_longer'
defaultMessage='Popular among people you follow'
/>
);
break;
case 'similar_to_recently_followed':
label = (
<FormattedMessage
id='follow_suggestions.similar_to_recently_followed_longer'
defaultMessage='Similar to profiles you recently followed'
/>
);
break;
case 'featured':
label = (
<FormattedMessage
id='follow_suggestions.featured_longer'
defaultMessage='Hand-picked by the {domain} team'
values={{ domain }}
/>
);
break;
case 'most_followed':
label = (
<FormattedMessage
id='follow_suggestions.popular_suggestion_longer'
defaultMessage='Popular on {domain}'
values={{ domain }}
/>
);
break;
case 'most_interactions':
label = (
<FormattedMessage
id='follow_suggestions.popular_suggestion_longer'
defaultMessage='Popular on {domain}'
values={{ domain }}
/>
);
break;
}
if (!account) {
return null;
}
return (
<div className='explore-suggestions-card'>
<div className='explore-suggestions-card__source'>{label}</div>
<div className='explore-suggestions-card__body'>
<Link
to={`/@${account.get('acct')}`}
data-hover-card-account={account.id}
className='explore-suggestions-card__link'
>
<Avatar
account={account}
size={48}
className='explore-suggestions-card__avatar'
/>
<DisplayName account={account} />
</Link>
<div className='explore-suggestions-card__actions'>
<IconButton
icon='close'
iconComponent={CloseIcon}
onClick={handleDismiss}
title={intl.formatMessage(messages.dismiss)}
className='explore-suggestions-card__dismiss-button'
/>
<FollowButton accountId={account.get('id')} />
</div>
</div>
</div>
);
};

View file

@ -170,7 +170,7 @@ export const Follows: React.FC<{
} }
> >
{displayedAccountIds.map((accountId) => ( {displayedAccountIds.map((accountId) => (
<Account id={accountId} key={accountId} withBio /> <Account id={accountId} key={accountId} withBio withMenu={false} />
))} ))}
</ScrollableList> </ScrollableList>

View file

@ -2133,6 +2133,16 @@ body > [data-popper-placement] {
display: flex; display: flex;
gap: 10px; gap: 10px;
align-items: center; align-items: center;
justify-content: end;
}
.account__wrapper--with-bio {
align-items: start;
}
.account__info-wrapper {
flex: 1 1 auto;
min-width: 0;
} }
.account__avatar { .account__avatar {
@ -2141,6 +2151,11 @@ body > [data-popper-placement] {
border-radius: var(--avatar-border-radius); border-radius: var(--avatar-border-radius);
background: var(--surface-background-color); background: var(--surface-background-color);
@container (width < 360px) {
width: 35px !important;
height: 35px !important;
}
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -2266,7 +2281,7 @@ a .account__avatar {
} }
.account__relationship, .account__relationship,
.explore__suggestions__card { .explore-suggestions-card {
.icon-button { .icon-button {
border: 1px solid var(--background-border-color); border: 1px solid var(--background-border-color);
border-radius: 4px; border-radius: 4px;
@ -3217,7 +3232,7 @@ a.account__display-name {
} }
} }
.explore__suggestions__card { .explore-suggestions-card {
padding: 12px 16px; padding: 12px 16px;
gap: 8px; gap: 8px;
display: flex; display: flex;
@ -3229,42 +3244,44 @@ a.account__display-name {
} }
&__source { &__source {
padding-inline-start: 60px;
font-size: 13px; font-size: 13px;
line-height: 16px; line-height: 16px;
color: $dark-text-color; color: $dark-text-color;
text-overflow: ellipsis;
overflow: hidden; @container (width >= 400px) {
white-space: nowrap; padding-inline-start: 60px;
}
} }
&__body { &__body {
display: flex; display: flex;
gap: 12px; gap: 12px;
align-items: center; align-items: center;
justify-content: end;
&__main {
flex: 1 1 auto;
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
&__name-button {
display: flex;
align-items: center;
gap: 8px;
&__name {
display: block;
color: inherit;
text-decoration: none;
flex: 1 1 auto;
min-width: 0;
} }
.button { &__avatar {
min-width: 80px; flex-shrink: 0;
@container (width < 360px) {
width: 35px !important;
height: 35px !important;
}
}
&__link {
flex: 1 1 auto;
display: flex;
gap: 12px;
align-items: center;
text-decoration: none;
min-width: 0;
&:hover,
&:focus-visible {
.display-name__html {
text-decoration: underline;
}
} }
.display-name { .display-name {
@ -3282,6 +3299,21 @@ a.account__display-name {
} }
} }
} }
&__actions {
display: flex;
align-items: center;
gap: 8px;
justify-content: end;
.button {
min-width: 80px;
}
}
&__dismiss-button {
@container (width < 400px) {
display: none;
} }
} }
} }