2026-02-23 16:53:49 +01:00
|
|
|
|
import { useCallback, useEffect } from 'react';
|
2026-02-17 16:45:24 +01:00
|
|
|
|
import type { FC } from 'react';
|
|
|
|
|
|
|
2026-02-27 14:36:19 +01:00
|
|
|
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
2026-02-17 16:45:24 +01:00
|
|
|
|
|
2026-02-23 16:53:49 +01:00
|
|
|
|
import { useHistory } from 'react-router-dom';
|
2026-02-17 16:45:24 +01:00
|
|
|
|
|
2026-02-19 14:53:29 +01:00
|
|
|
|
import type { ModalType } from '@/mastodon/actions/modal';
|
|
|
|
|
|
import { openModal } from '@/mastodon/actions/modal';
|
2026-03-16 16:56:30 +01:00
|
|
|
|
import { AccountBio } from '@/mastodon/components/account_bio';
|
2026-02-19 14:53:29 +01:00
|
|
|
|
import { Avatar } from '@/mastodon/components/avatar';
|
2026-02-27 14:36:19 +01:00
|
|
|
|
import { Button } from '@/mastodon/components/button';
|
2026-03-05 11:48:19 +01:00
|
|
|
|
import { DismissibleCallout } from '@/mastodon/components/callout/dismissible';
|
2026-02-26 14:55:10 +01:00
|
|
|
|
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
|
|
|
|
|
|
import { EmojiHTML } from '@/mastodon/components/emoji/html';
|
|
|
|
|
|
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
|
2026-02-17 16:45:24 +01:00
|
|
|
|
import { useAccount } from '@/mastodon/hooks/useAccount';
|
|
|
|
|
|
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
2026-02-19 14:53:29 +01:00
|
|
|
|
import { autoPlayGif } from '@/mastodon/initial_state';
|
2026-03-02 17:32:08 +01:00
|
|
|
|
import { fetchProfile } from '@/mastodon/reducers/slices/profile_edit';
|
2026-02-23 16:53:49 +01:00
|
|
|
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
2026-02-17 16:45:24 +01:00
|
|
|
|
|
2026-02-23 16:53:49 +01:00
|
|
|
|
import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
|
|
|
|
|
|
import { EditButton } from './components/edit_button';
|
2026-03-06 13:57:06 +01:00
|
|
|
|
import { AccountField } from './components/field';
|
2026-03-05 11:48:19 +01:00
|
|
|
|
import { AccountFieldActions } from './components/field_actions';
|
2026-03-12 11:42:29 +01:00
|
|
|
|
import { AccountImageEdit } from './components/image_edit';
|
2026-02-19 14:53:29 +01:00
|
|
|
|
import { AccountEditSection } from './components/section';
|
2026-02-17 16:45:24 +01:00
|
|
|
|
import classes from './styles.module.scss';
|
|
|
|
|
|
|
2026-02-27 14:36:19 +01:00
|
|
|
|
export const messages = defineMessages({
|
2026-02-23 16:53:49 +01:00
|
|
|
|
columnTitle: {
|
|
|
|
|
|
id: 'account_edit.column_title',
|
|
|
|
|
|
defaultMessage: 'Edit Profile',
|
|
|
|
|
|
},
|
2026-02-19 14:53:29 +01:00
|
|
|
|
displayNameTitle: {
|
|
|
|
|
|
id: 'account_edit.display_name.title',
|
|
|
|
|
|
defaultMessage: 'Display name',
|
|
|
|
|
|
},
|
|
|
|
|
|
displayNamePlaceholder: {
|
|
|
|
|
|
id: 'account_edit.display_name.placeholder',
|
|
|
|
|
|
defaultMessage:
|
|
|
|
|
|
'Your display name is how your name appears on your profile and in timelines.',
|
|
|
|
|
|
},
|
|
|
|
|
|
bioTitle: {
|
|
|
|
|
|
id: 'account_edit.bio.title',
|
|
|
|
|
|
defaultMessage: 'Bio',
|
|
|
|
|
|
},
|
|
|
|
|
|
bioPlaceholder: {
|
|
|
|
|
|
id: 'account_edit.bio.placeholder',
|
|
|
|
|
|
defaultMessage: 'Add a short introduction to help others identify you.',
|
|
|
|
|
|
},
|
|
|
|
|
|
customFieldsTitle: {
|
|
|
|
|
|
id: 'account_edit.custom_fields.title',
|
|
|
|
|
|
defaultMessage: 'Custom fields',
|
|
|
|
|
|
},
|
|
|
|
|
|
customFieldsPlaceholder: {
|
|
|
|
|
|
id: 'account_edit.custom_fields.placeholder',
|
|
|
|
|
|
defaultMessage:
|
|
|
|
|
|
'Add your pronouns, external links, or anything else you’d like to share.',
|
|
|
|
|
|
},
|
2026-03-05 11:48:19 +01:00
|
|
|
|
customFieldsName: {
|
|
|
|
|
|
id: 'account_edit.custom_fields.name',
|
|
|
|
|
|
defaultMessage: 'field',
|
|
|
|
|
|
},
|
|
|
|
|
|
customFieldsTipTitle: {
|
|
|
|
|
|
id: 'account_edit.custom_fields.tip_title',
|
|
|
|
|
|
defaultMessage: 'Tip: Adding verified links',
|
|
|
|
|
|
},
|
2026-02-19 14:53:29 +01:00
|
|
|
|
featuredHashtagsTitle: {
|
|
|
|
|
|
id: 'account_edit.featured_hashtags.title',
|
|
|
|
|
|
defaultMessage: 'Featured hashtags',
|
|
|
|
|
|
},
|
|
|
|
|
|
featuredHashtagsPlaceholder: {
|
|
|
|
|
|
id: 'account_edit.featured_hashtags.placeholder',
|
|
|
|
|
|
defaultMessage:
|
|
|
|
|
|
'Help others identify, and have quick access to, your favorite topics.',
|
|
|
|
|
|
},
|
2026-02-23 16:53:49 +01:00
|
|
|
|
featuredHashtagsItem: {
|
|
|
|
|
|
id: 'account_edit.featured_hashtags.item',
|
|
|
|
|
|
defaultMessage: 'hashtags',
|
|
|
|
|
|
},
|
2026-02-19 14:53:29 +01:00
|
|
|
|
profileTabTitle: {
|
|
|
|
|
|
id: 'account_edit.profile_tab.title',
|
|
|
|
|
|
defaultMessage: 'Profile tab settings',
|
|
|
|
|
|
},
|
|
|
|
|
|
profileTabSubtitle: {
|
|
|
|
|
|
id: 'account_edit.profile_tab.subtitle',
|
|
|
|
|
|
defaultMessage: 'Customize the tabs on your profile and what they display.',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-23 16:53:49 +01:00
|
|
|
|
export const AccountEdit: FC = () => {
|
2026-02-17 16:45:24 +01:00
|
|
|
|
const accountId = useCurrentAccountId();
|
|
|
|
|
|
const account = useAccount(accountId);
|
|
|
|
|
|
const intl = useIntl();
|
|
|
|
|
|
|
2026-02-19 14:53:29 +01:00
|
|
|
|
const dispatch = useAppDispatch();
|
2026-02-23 16:53:49 +01:00
|
|
|
|
|
2026-03-02 17:32:08 +01:00
|
|
|
|
const { profile } = useAppSelector((state) => state.profileEdit);
|
2026-02-23 16:53:49 +01:00
|
|
|
|
useEffect(() => {
|
2026-02-26 14:55:10 +01:00
|
|
|
|
void dispatch(fetchProfile());
|
2026-02-23 16:53:49 +01:00
|
|
|
|
}, [dispatch]);
|
|
|
|
|
|
|
2026-03-06 13:57:06 +01:00
|
|
|
|
const maxFieldCount = useAppSelector(
|
|
|
|
|
|
(state) =>
|
|
|
|
|
|
(state.server.getIn([
|
|
|
|
|
|
'server',
|
|
|
|
|
|
'configuration',
|
|
|
|
|
|
'accounts',
|
|
|
|
|
|
'max_profile_fields',
|
|
|
|
|
|
]) as number | undefined) ?? 4,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-02-19 14:53:29 +01:00
|
|
|
|
const handleOpenModal = useCallback(
|
|
|
|
|
|
(type: ModalType, props?: Record<string, unknown>) => {
|
|
|
|
|
|
dispatch(openModal({ modalType: type, modalProps: props ?? {} }));
|
|
|
|
|
|
},
|
|
|
|
|
|
[dispatch],
|
|
|
|
|
|
);
|
|
|
|
|
|
const handleNameEdit = useCallback(() => {
|
|
|
|
|
|
handleOpenModal('ACCOUNT_EDIT_NAME');
|
|
|
|
|
|
}, [handleOpenModal]);
|
|
|
|
|
|
const handleBioEdit = useCallback(() => {
|
|
|
|
|
|
handleOpenModal('ACCOUNT_EDIT_BIO');
|
|
|
|
|
|
}, [handleOpenModal]);
|
2026-03-06 13:57:06 +01:00
|
|
|
|
const handleCustomFieldAdd = useCallback(() => {
|
|
|
|
|
|
handleOpenModal('ACCOUNT_EDIT_FIELD_EDIT');
|
|
|
|
|
|
}, [handleOpenModal]);
|
|
|
|
|
|
const handleCustomFieldReorder = useCallback(() => {
|
|
|
|
|
|
handleOpenModal('ACCOUNT_EDIT_FIELDS_REORDER');
|
|
|
|
|
|
}, [handleOpenModal]);
|
2026-03-05 11:48:19 +01:00
|
|
|
|
const handleCustomFieldsVerifiedHelp = useCallback(() => {
|
|
|
|
|
|
handleOpenModal('ACCOUNT_EDIT_VERIFY_LINKS');
|
|
|
|
|
|
}, [handleOpenModal]);
|
2026-02-27 14:36:19 +01:00
|
|
|
|
const handleProfileDisplayEdit = useCallback(() => {
|
|
|
|
|
|
handleOpenModal('ACCOUNT_EDIT_PROFILE_DISPLAY');
|
|
|
|
|
|
}, [handleOpenModal]);
|
2026-02-19 14:53:29 +01:00
|
|
|
|
|
2026-02-23 16:53:49 +01:00
|
|
|
|
const history = useHistory();
|
|
|
|
|
|
const handleFeaturedTagsEdit = useCallback(() => {
|
|
|
|
|
|
history.push('/profile/featured_tags');
|
|
|
|
|
|
}, [history]);
|
2026-02-17 16:45:24 +01:00
|
|
|
|
|
2026-02-26 14:55:10 +01:00
|
|
|
|
// Normally we would use the account emoji, but we want all custom emojis to be available to render after editing.
|
|
|
|
|
|
const emojis = useAppSelector((state) => state.custom_emojis);
|
|
|
|
|
|
const htmlHandlers = useElementHandledLink({
|
|
|
|
|
|
hashtagAccountId: profile?.id,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!accountId || !account || !profile) {
|
2026-02-23 16:53:49 +01:00
|
|
|
|
return <AccountEditEmptyColumn notFound={!accountId} />;
|
2026-02-17 16:45:24 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 14:55:10 +01:00
|
|
|
|
const headerSrc = autoPlayGif ? profile.header : profile.headerStatic;
|
|
|
|
|
|
const hasName = !!profile.displayName;
|
|
|
|
|
|
const hasBio = !!profile.bio;
|
2026-03-05 11:48:19 +01:00
|
|
|
|
const hasFields = profile.fields.length > 0;
|
2026-03-02 17:32:08 +01:00
|
|
|
|
const hasTags = profile.featuredTags.length > 0;
|
2026-02-19 14:53:29 +01:00
|
|
|
|
|
2026-02-17 16:45:24 +01:00
|
|
|
|
return (
|
2026-02-23 16:53:49 +01:00
|
|
|
|
<AccountEditColumn
|
|
|
|
|
|
title={intl.formatMessage(messages.columnTitle)}
|
|
|
|
|
|
to={`/@${account.acct}`}
|
|
|
|
|
|
>
|
2026-02-19 14:53:29 +01:00
|
|
|
|
<header>
|
|
|
|
|
|
<div className={classes.profileImage}>
|
|
|
|
|
|
{headerSrc && <img src={headerSrc} alt='' />}
|
2026-03-12 11:42:29 +01:00
|
|
|
|
<AccountImageEdit location='header' />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={classes.avatar}>
|
|
|
|
|
|
<Avatar account={account} size={80} />
|
|
|
|
|
|
<AccountImageEdit location='avatar' />
|
2026-02-19 14:53:29 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
2026-02-26 14:55:10 +01:00
|
|
|
|
<CustomEmojiProvider emojis={emojis}>
|
|
|
|
|
|
<AccountEditSection
|
|
|
|
|
|
title={messages.displayNameTitle}
|
|
|
|
|
|
description={messages.displayNamePlaceholder}
|
|
|
|
|
|
showDescription={!hasName}
|
|
|
|
|
|
buttons={
|
|
|
|
|
|
<EditButton
|
|
|
|
|
|
onClick={handleNameEdit}
|
|
|
|
|
|
item={messages.displayNameTitle}
|
|
|
|
|
|
edit={hasName}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<EmojiHTML htmlString={profile.displayName} {...htmlHandlers} />
|
|
|
|
|
|
</AccountEditSection>
|
|
|
|
|
|
|
|
|
|
|
|
<AccountEditSection
|
|
|
|
|
|
title={messages.bioTitle}
|
|
|
|
|
|
description={messages.bioPlaceholder}
|
|
|
|
|
|
showDescription={!hasBio}
|
|
|
|
|
|
buttons={
|
|
|
|
|
|
<EditButton
|
|
|
|
|
|
onClick={handleBioEdit}
|
|
|
|
|
|
item={messages.bioTitle}
|
|
|
|
|
|
edit={hasBio}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2026-03-16 16:56:30 +01:00
|
|
|
|
<AccountBio
|
|
|
|
|
|
showDropdown
|
|
|
|
|
|
accountId={profile.id}
|
|
|
|
|
|
className={classes.bio}
|
|
|
|
|
|
/>
|
2026-02-26 14:55:10 +01:00
|
|
|
|
</AccountEditSection>
|
|
|
|
|
|
|
|
|
|
|
|
<AccountEditSection
|
|
|
|
|
|
title={messages.customFieldsTitle}
|
|
|
|
|
|
description={messages.customFieldsPlaceholder}
|
2026-03-05 11:48:19 +01:00
|
|
|
|
showDescription={!hasFields}
|
2026-03-06 13:57:06 +01:00
|
|
|
|
buttons={
|
|
|
|
|
|
<>
|
2026-03-11 14:52:44 +01:00
|
|
|
|
<Button
|
|
|
|
|
|
className={classes.editButton}
|
|
|
|
|
|
onClick={handleCustomFieldReorder}
|
|
|
|
|
|
disabled={profile.fields.length <= 1}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FormattedMessage
|
|
|
|
|
|
id='account_edit.custom_fields.reorder_button'
|
|
|
|
|
|
defaultMessage='Reorder fields'
|
2026-03-05 11:48:19 +01:00
|
|
|
|
/>
|
2026-03-11 14:52:44 +01:00
|
|
|
|
</Button>
|
|
|
|
|
|
<EditButton
|
|
|
|
|
|
item={messages.customFieldsName}
|
|
|
|
|
|
onClick={handleCustomFieldAdd}
|
|
|
|
|
|
disabled={profile.fields.length >= maxFieldCount}
|
|
|
|
|
|
/>
|
2026-03-06 13:57:06 +01:00
|
|
|
|
</>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{hasFields && (
|
|
|
|
|
|
<ol>
|
|
|
|
|
|
{profile.fields.map((field) => (
|
|
|
|
|
|
<li key={field.id} className={classes.field}>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<AccountField {...field} {...htmlHandlers} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<AccountFieldActions
|
|
|
|
|
|
item={intl.formatMessage(messages.customFieldsName)}
|
|
|
|
|
|
id={field.id}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ol>
|
|
|
|
|
|
)}
|
2026-03-05 11:48:19 +01:00
|
|
|
|
<Button
|
|
|
|
|
|
onClick={handleCustomFieldsVerifiedHelp}
|
|
|
|
|
|
className={classes.verifiedLinkHelpButton}
|
|
|
|
|
|
plain
|
|
|
|
|
|
>
|
|
|
|
|
|
<FormattedMessage
|
|
|
|
|
|
id='account_edit.custom_fields.verified_hint'
|
|
|
|
|
|
defaultMessage='How do I add a verified link?'
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
{!hasFields && (
|
|
|
|
|
|
<DismissibleCallout
|
|
|
|
|
|
id='profile_edit_fields_tip'
|
|
|
|
|
|
title={intl.formatMessage(messages.customFieldsTipTitle)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FormattedMessage
|
|
|
|
|
|
id='account_edit.custom_fields.tip_content'
|
|
|
|
|
|
defaultMessage='You can easily add credibility to your Mastodon account by verifying links to any websites you own.'
|
|
|
|
|
|
/>
|
|
|
|
|
|
</DismissibleCallout>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</AccountEditSection>
|
2026-02-26 14:55:10 +01:00
|
|
|
|
|
|
|
|
|
|
<AccountEditSection
|
|
|
|
|
|
title={messages.featuredHashtagsTitle}
|
|
|
|
|
|
description={messages.featuredHashtagsPlaceholder}
|
|
|
|
|
|
showDescription={!hasTags}
|
|
|
|
|
|
buttons={
|
|
|
|
|
|
<EditButton
|
|
|
|
|
|
onClick={handleFeaturedTagsEdit}
|
|
|
|
|
|
edit={hasTags}
|
|
|
|
|
|
item={messages.featuredHashtagsItem}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2026-03-02 17:32:08 +01:00
|
|
|
|
{profile.featuredTags.map((tag) => `#${tag.name}`).join(', ')}
|
2026-02-26 14:55:10 +01:00
|
|
|
|
</AccountEditSection>
|
|
|
|
|
|
|
|
|
|
|
|
<AccountEditSection
|
|
|
|
|
|
title={messages.profileTabTitle}
|
|
|
|
|
|
description={messages.profileTabSubtitle}
|
|
|
|
|
|
showDescription
|
2026-02-27 14:36:19 +01:00
|
|
|
|
buttons={
|
|
|
|
|
|
<Button
|
|
|
|
|
|
className={classes.editButton}
|
|
|
|
|
|
onClick={handleProfileDisplayEdit}
|
|
|
|
|
|
>
|
|
|
|
|
|
<FormattedMessage
|
|
|
|
|
|
id='account_edit.profile_tab.button_label'
|
|
|
|
|
|
defaultMessage='Customize'
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
}
|
2026-02-26 14:55:10 +01:00
|
|
|
|
/>
|
|
|
|
|
|
</CustomEmojiProvider>
|
2026-02-23 16:53:49 +01:00
|
|
|
|
</AccountEditColumn>
|
2026-02-17 16:45:24 +01:00
|
|
|
|
);
|
|
|
|
|
|
};
|