fix: Fix cramped layout of follower recommendations on small viewports (#34967)
This commit is contained in:
parent
1623d54ec0
commit
2c828748a3
6 changed files with 266 additions and 170 deletions
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -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']),
|
|
||||||
};
|
|
124
app/javascript/mastodon/features/explore/components/card.tsx
Normal file
124
app/javascript/mastodon/features/explore/components/card.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue