Redesign: Display follow button in column header on profile page (#40460)

This commit is contained in:
diondiondion
2026-09-17 14:58:43 +00:00
committed by GitHub
parent 8a90cc0c88
commit 0f6ef0d471
4 changed files with 124 additions and 20 deletions

View File

@@ -3,12 +3,12 @@ import type { FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useLocation } from 'react-router-dom';
import { followAccount } from '@/mastodon/actions/accounts';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useFollowReference } from '@/mastodon/hooks/useFollowReference';
import { getAccountHidden } from '@/mastodon/selectors/accounts';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { isRedesignEnabled } from '@/mastodon/utils/environment';
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
@@ -83,10 +83,8 @@ const AccountButtonsOther: FC<
});
}
}, [accountUrl]);
const { state } = useLocation<{
reference?: string;
} | null>();
const reference = state?.reference ?? 'profile';
const reference = useFollowReference('profile');
if (!account) {
return null;
@@ -99,8 +97,10 @@ const AccountButtonsOther: FC<
<>
{!isMovedAndUnfollowedAccount && (
<FollowButton
compact={isRedesignEnabled()}
accountId={accountId}
className={classes.followButton}
withUnmute={false}
labelLength='long'
reference={reference}
/>

View File

@@ -107,7 +107,7 @@ const BaseButton: React.FC<BaseButtonProps> = ({
);
};
type ButtonProps = BaseButtonProps & {
export type ButtonProps = BaseButtonProps & {
leadingIcon?: IconProp;
trailingIcon?: IconProp;
};

View File

@@ -18,6 +18,11 @@ import { me } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import { useBreakpoint } from '../features/ui/hooks/useBreakpoint';
import { isRedesignEnabled } from '../utils/environment';
import { Button as RedesignButton } from './button/redesign';
import type { ButtonProps as RedesignButtonProps } from './button/redesign';
import type { MastodonLocationDescriptor } from './router';
const longMessages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@@ -55,21 +60,29 @@ const shortMessages = {
}),
};
export const FollowButton: React.FC<{
interface FollowButtonOptions {
accountId?: string;
compact?: boolean;
labelLength?: 'auto' | 'short' | 'long';
className?: string;
withUnmute?: boolean;
reference?: string;
}> = ({
}
interface FollowButtonReturn {
label: React.ReactNode;
onClick: (() => void) | undefined;
link: MastodonLocationDescriptor | undefined;
disabled: boolean;
following: boolean;
secondary: boolean;
hidden: boolean;
}
export function useFollowButton({
accountId,
compact,
labelLength = 'auto',
className,
withUnmute = true,
reference,
}) => {
}: FollowButtonOptions): FollowButtonReturn {
const intl = useIntl();
const dispatch = useAppDispatch();
const { signedIn } = useIdentity();
@@ -79,7 +92,9 @@ export const FollowButton: React.FC<{
const relationship = useAppSelector((state) =>
accountId ? state.relationships.get(accountId) : undefined,
);
const following = relationship?.following || relationship?.requested;
const following = relationship?.following || relationship?.requested || false;
const secondary = following || relationship?.blocking || false;
const link = accountId === me ? '/profile/edit' : undefined;
useEffect(() => {
if (accountId && signedIn) {
@@ -87,7 +102,7 @@ export const FollowButton: React.FC<{
}
}, [dispatch, accountId, signedIn]);
const handleClick = useCallback(() => {
const onClick = useCallback(() => {
if (!signedIn) {
dispatch(
openModal({
@@ -148,7 +163,7 @@ export const FollowButton: React.FC<{
? messages.followRequest
: messages.follow;
let label;
let label: React.ReactNode;
let disabled =
relationship?.blocked_by || account?.suspended || !!account?.moved;
@@ -176,7 +191,30 @@ export const FollowButton: React.FC<{
label = intl.formatMessage(followMessage);
}
if (accountId === me) {
const isMovedAndUnfollowedAccount =
(account?.moved && !relationship?.following) || false;
return {
onClick,
link,
label,
disabled,
following,
secondary,
hidden: isMovedAndUnfollowedAccount,
};
}
const FollowButtonLegacy: React.FC<
FollowButtonOptions & {
compact?: boolean;
className?: string;
}
> = ({ compact, className, ...props }) => {
const { onClick, link, label, disabled, following, secondary } =
useFollowButton(props);
if (link) {
const buttonClasses = classNames(className, 'button button-secondary', {
'button--compact': compact,
});
@@ -190,9 +228,9 @@ export const FollowButton: React.FC<{
return (
<Button
onClick={handleClick}
onClick={onClick}
disabled={disabled}
secondary={following || relationship?.blocking}
secondary={secondary}
compact={compact}
className={classNames(className, { 'button--destructive': following })}
>
@@ -200,3 +238,52 @@ export const FollowButton: React.FC<{
</Button>
);
};
const FollowButtonRedesign: React.FC<
FollowButtonOptions &
Pick<RedesignButtonProps, 'size' | 'color' | 'className'>
> = ({ accountId, labelLength, withUnmute, reference, ...buttonProps }) => {
const { onClick, link, label, disabled, secondary, hidden } = useFollowButton(
{
accountId,
labelLength,
withUnmute,
reference,
},
);
if (hidden) {
return null;
}
if (link) {
return (
<RedesignButton as='link' to={link} {...buttonProps}>
{label}
</RedesignButton>
);
}
return (
<RedesignButton
{...buttonProps}
onClick={onClick}
disabled={disabled}
variant={secondary ? 'tonal' : 'solid'}
>
{label}
</RedesignButton>
);
};
export const FollowButton: React.FC<
FollowButtonOptions & {
compact?: boolean;
className?: string;
}
> = ({ compact, ...props }) => {
if (isRedesignEnabled()) {
return <FollowButtonRedesign {...props} size={compact ? 'sm' : 'md'} />;
}
return <FollowButtonLegacy compact={compact} {...props} />;
};

View File

@@ -0,0 +1,17 @@
import { useLocation } from 'react-router-dom';
import type { LocationState } from '../components/router';
/**
* Gets the current `reference` state from location state if present,
* otherwise returns the passed-in fallback reference.
*
* `reference` is a string that we use to anonymously & locally track
* where a follow is coming from, helping us understand which UI features
* people actually use to discover people.
*/
export function useFollowReference(fallbackReference: string) {
const { state } = useLocation<LocationState>();
return state?.reference ?? fallbackReference;
}