Files
mastodon-sakyey/app/javascript/mastodon/features/account_edit/index.tsx

309 lines
10 KiB
TypeScript
Raw Normal View History

import { useCallback, useEffect } from 'react';
import type { FC } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
2026-02-19 14:53:29 +01:00
import type { ModalType } from '@/mastodon/actions/modal';
import { openModal } from '@/mastodon/actions/modal';
import { AccountBio } from '@/mastodon/components/account_bio';
2026-02-19 14:53:29 +01:00
import { Avatar } from '@/mastodon/components/avatar';
import { Button } from '@/mastodon/components/button';
import { DismissibleCallout } from '@/mastodon/components/callout/dismissible';
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
import { EmojiHTML } from '@/mastodon/components/emoji/html';
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
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';
import { fetchProfile } from '@/mastodon/reducers/slices/profile_edit';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
import { EditButton } from './components/edit_button';
import { AccountField } from './components/field';
import { AccountFieldActions } from './components/field_actions';
import { AccountImageEdit } from './components/image_edit';
2026-02-19 14:53:29 +01:00
import { AccountEditSection } from './components/section';
import classes from './styles.module.scss';
export const messages = defineMessages({
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 youd like to share.',
},
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.',
},
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.',
},
});
export const AccountEdit: FC = () => {
const accountId = useCurrentAccountId();
const account = useAccount(accountId);
const intl = useIntl();
2026-02-19 14:53:29 +01:00
const dispatch = useAppDispatch();
const { profile } = useAppSelector((state) => state.profileEdit);
useEffect(() => {
void dispatch(fetchProfile());
}, [dispatch]);
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]);
const handleCustomFieldAdd = useCallback(() => {
handleOpenModal('ACCOUNT_EDIT_FIELD_EDIT');
}, [handleOpenModal]);
const handleCustomFieldReorder = useCallback(() => {
handleOpenModal('ACCOUNT_EDIT_FIELDS_REORDER');
}, [handleOpenModal]);
const handleCustomFieldsVerifiedHelp = useCallback(() => {
handleOpenModal('ACCOUNT_EDIT_VERIFY_LINKS');
}, [handleOpenModal]);
const handleProfileDisplayEdit = useCallback(() => {
handleOpenModal('ACCOUNT_EDIT_PROFILE_DISPLAY');
}, [handleOpenModal]);
2026-02-19 14:53:29 +01:00
const history = useHistory();
const handleFeaturedTagsEdit = useCallback(() => {
history.push('/profile/featured_tags');
}, [history]);
// 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) {
return <AccountEditEmptyColumn notFound={!accountId} />;
}
const headerSrc = autoPlayGif ? profile.header : profile.headerStatic;
const hasName = !!profile.displayName;
const hasBio = !!profile.bio;
const hasFields = profile.fields.length > 0;
const hasTags = profile.featuredTags.length > 0;
2026-02-19 14:53:29 +01:00
return (
<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='' />}
<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>
<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}
/>
}
>
<AccountBio
showDropdown
accountId={profile.id}
className={classes.bio}
/>
</AccountEditSection>
<AccountEditSection
title={messages.customFieldsTitle}
description={messages.customFieldsPlaceholder}
showDescription={!hasFields}
buttons={
<>
<Button
className={classes.editButton}
onClick={handleCustomFieldReorder}
disabled={profile.fields.length <= 1}
>
<FormattedMessage
id='account_edit.custom_fields.reorder_button'
defaultMessage='Reorder fields'
/>
</Button>
<EditButton
item={messages.customFieldsName}
onClick={handleCustomFieldAdd}
disabled={profile.fields.length >= maxFieldCount}
/>
</>
}
>
{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>
)}
<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>
<AccountEditSection
title={messages.featuredHashtagsTitle}
description={messages.featuredHashtagsPlaceholder}
showDescription={!hasTags}
buttons={
<EditButton
onClick={handleFeaturedTagsEdit}
edit={hasTags}
item={messages.featuredHashtagsItem}
/>
}
>
{profile.featuredTags.map((tag) => `#${tag.name}`).join(', ')}
</AccountEditSection>
<AccountEditSection
title={messages.profileTabTitle}
description={messages.profileTabSubtitle}
showDescription
buttons={
<Button
className={classes.editButton}
onClick={handleProfileDisplayEdit}
>
<FormattedMessage
id='account_edit.profile_tab.button_label'
defaultMessage='Customize'
/>
</Button>
}
/>
</CustomEmojiProvider>
</AccountEditColumn>
);
};