Profile editing: Add initial route (#37885)

This commit is contained in:
Echo
2026-02-17 16:45:24 +01:00
committed by GitHub
parent 371946fa80
commit 4b1f66418b
11 changed files with 127 additions and 10 deletions

View File

@@ -1,6 +1,8 @@
import { forwardRef, useRef, useImperativeHandle } from 'react';
import type { Ref } from 'react';
import classNames from 'classnames';
import { scrollTop } from 'mastodon/scroll';
export interface ColumnRef {
@@ -12,10 +14,11 @@ interface ColumnProps {
children?: React.ReactNode;
label?: string;
bindToDocument?: boolean;
className?: string;
}
export const Column = forwardRef<ColumnRef, ColumnProps>(
({ children, label, bindToDocument }, ref: Ref<ColumnRef>) => {
({ children, label, bindToDocument, className }, ref: Ref<ColumnRef>) => {
const nodeRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => ({
@@ -39,7 +42,12 @@ export const Column = forwardRef<ColumnRef, ColumnProps>(
}));
return (
<div role='region' aria-label={label} className='column' ref={nodeRef}>
<div
role='region'
aria-label={label}
className={classNames('column', className)}
ref={nodeRef}
>
{children}
</div>
);

View File

@@ -73,6 +73,7 @@ export interface Props {
iconComponent?: IconProp;
active?: boolean;
children?: React.ReactNode;
className?: string;
pinned?: boolean;
multiColumn?: boolean;
extraButton?: React.ReactNode;
@@ -91,6 +92,7 @@ export const ColumnHeader: React.FC<Props> = ({
iconComponent,
active,
children,
className,
pinned,
multiColumn,
extraButton,
@@ -141,7 +143,7 @@ export const ColumnHeader: React.FC<Props> = ({
onPin?.();
}, [history, pinned, onPin]);
const wrapperClassName = classNames('column-header__wrapper', {
const wrapperClassName = classNames('column-header__wrapper', className, {
active,
});
@@ -256,7 +258,8 @@ export const ColumnHeader: React.FC<Props> = ({
}
const hasIcon = icon && iconComponent;
const hasTitle = hasIcon && title;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const hasTitle = (hasIcon || backButton) && title;
const component = (
<div className={wrapperClassName}>
@@ -270,7 +273,7 @@ export const ColumnHeader: React.FC<Props> = ({
className='column-header__title'
type='button'
>
{!backButton && (
{!backButton && hasIcon && (
<Icon
id={icon}
icon={iconComponent}

View File

@@ -3,8 +3,10 @@ import { useCallback, useEffect } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import { useIdentity } from '@/mastodon/identity_context';
import { isClientFeatureEnabled } from '@/mastodon/utils/environment';
import {
fetchRelationships,
followAccount,
@@ -158,14 +160,24 @@ export const FollowButton: React.FC<{
}
if (accountId === me) {
const buttonClasses = classNames(className, 'button button-secondary', {
'button--compact': compact,
});
if (isClientFeatureEnabled('profile_editing')) {
return (
<Link to='/profile/edit' className={buttonClasses}>
{label}
</Link>
);
}
return (
<a
href='/settings/profile'
target='_blank'
rel='noopener'
className={classNames(className, 'button button-secondary', {
'button--compact': compact,
})}
className={buttonClasses}
>
{label}
</a>

View File

@@ -0,0 +1,53 @@
import type { FC } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { Column } from '@/mastodon/components/column';
import { ColumnHeader } from '@/mastodon/components/column_header';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
import classes from './styles.module.scss';
export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const accountId = useCurrentAccountId();
const account = useAccount(accountId);
const intl = useIntl();
if (!accountId) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
if (!account) {
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<LoadingIndicator />
</Column>
);
}
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<ColumnHeader
title={intl.formatMessage({
id: 'account_edit.column_title',
defaultMessage: 'Edit Profile',
})}
className={classes.header}
showBackButton
extraButton={
<Link to={`/@${account.acct}`} className='button'>
<FormattedMessage
id='account_edit.column_button'
defaultMessage='Done'
/>
</Link>
}
/>
</Column>
);
};

View File

@@ -0,0 +1,26 @@
.column {
border: 1px solid var(--color-border-primary);
border-top-width: 0;
}
.header {
:global(.column-header__buttons) {
align-items: center;
padding-inline-end: 16px;
height: auto;
}
}
.nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 24px 24px 12px;
> h1 {
flex-grow: 1;
font-weight: 600;
font-size: 15px;
}
}

View File

@@ -22,7 +22,7 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
import { layoutFromWindow } from 'mastodon/is_mobile';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report';
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
import { isClientFeatureEnabled, isServerFeatureEnabled } from '@/mastodon/utils/environment';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { clearHeight } from '../../actions/height_cache';
@@ -80,6 +80,7 @@ import {
TermsOfService,
AccountFeatured,
AccountAbout,
AccountEdit,
Quotes,
} from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context';
@@ -232,6 +233,8 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
{isClientFeatureEnabled('profile_editing') && <WrappedRoute key="edit" path='/profile/edit' component={AccountEdit} content={children} />}
<WrappedRoute path={['/start', '/start/profile']} exact component={OnboardingProfile} content={children} />
<WrappedRoute path='/start/follows' component={OnboardingFollows} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} />

View File

@@ -92,6 +92,11 @@ export function AccountAbout() {
.then((module) => ({ default: module.AccountAbout }));
}
export function AccountEdit() {
return import('../../account_edit')
.then((module) => ({ default: module.AccountEdit }));
}
export function Followers () {
return import('../../followers');
}

View File

@@ -55,3 +55,7 @@ export function useAccountId() {
return accountId satisfies AccountId;
}
export function useCurrentAccountId() {
return useAppSelector((state) => state.meta.get('me', null) as string | null);
}

View File

@@ -141,6 +141,8 @@
"account.unmute": "Unmute @{name}",
"account.unmute_notifications_short": "Unmute notifications",
"account.unmute_short": "Unmute",
"account_edit.column_button": "Done",
"account_edit.column_title": "Edit Profile",
"account_note.placeholder": "Click to add note",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",

View File

@@ -18,7 +18,7 @@ export function isServerFeatureEnabled(feature: ServerFeatures) {
return initialState?.features.includes(feature) ?? false;
}
type ClientFeatures = 'collections';
type ClientFeatures = 'collections' | 'profile_editing';
export function isClientFeatureEnabled(feature: ClientFeatures) {
try {

View File

@@ -25,6 +25,7 @@
/notifications_v2/(*any)
/notifications/(*any)
/pinned
/profile/(*any)
/public
/public/local
/public/remote