[Glitch] Profile editing: Name and bio

Port ed4787c1b1de1d7491719be0f45b7a7f16af8c0d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Echo 2026-02-19 14:53:29 +01:00 committed by Claire
parent eebed16f59
commit 3ac5b10dec
16 changed files with 602 additions and 61 deletions

View File

@ -6,7 +6,7 @@ import { EmojiHTML } from './emoji/html';
import { useElementHandledLink } from './status/handled_link'; import { useElementHandledLink } from './status/handled_link';
interface AccountBioProps { interface AccountBioProps {
className: string; className?: string;
accountId: string; accountId: string;
showDropdown?: boolean; showDropdown?: boolean;
} }

View File

@ -42,6 +42,13 @@ export const WithError: Story = {
}, },
}; };
export const AutoSize: Story = {
args: {
autoSize: true,
defaultValue: 'This textarea will grow as you type more lines.',
},
};
export const Plain: Story = { export const Plain: Story = {
render(args) { render(args) {
return <TextArea {...args} />; return <TextArea {...args} />;

View File

@ -3,12 +3,16 @@ import { forwardRef, useCallback } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import type { TextareaAutosizeProps } from 'react-textarea-autosize';
import TextAreaAutosize from 'react-textarea-autosize';
import { FormFieldWrapper } from './form_field_wrapper'; import { FormFieldWrapper } from './form_field_wrapper';
import type { CommonFieldWrapperProps } from './form_field_wrapper'; import type { CommonFieldWrapperProps } from './form_field_wrapper';
import classes from './text_input.module.scss'; import classes from './text_input.module.scss';
interface Props type TextAreaProps =
extends ComponentPropsWithoutRef<'textarea'>, CommonFieldWrapperProps {} | ({ autoSize?: false } & ComponentPropsWithoutRef<'textarea'>)
| ({ autoSize: true } & TextareaAutosizeProps);
/** /**
* A simple form field for multi-line text. * A simple form field for multi-line text.
@ -17,45 +21,56 @@ interface Props
* or optional (by explicitly setting `required={false}`) * or optional (by explicitly setting `required={false}`)
*/ */
export const TextAreaField = forwardRef<HTMLTextAreaElement, Props>( export const TextAreaField = forwardRef<
({ id, label, hint, required, hasError, ...otherProps }, ref) => ( HTMLTextAreaElement,
<FormFieldWrapper TextAreaProps & CommonFieldWrapperProps
label={label} >(({ id, label, hint, required, hasError, ...otherProps }, ref) => (
hint={hint} <FormFieldWrapper
required={required} label={label}
hasError={hasError} hint={hint}
inputId={id} required={required}
> hasError={hasError}
{(inputProps) => <TextArea {...otherProps} {...inputProps} ref={ref} />} inputId={id}
</FormFieldWrapper> >
), {(inputProps) => <TextArea {...otherProps} {...inputProps} ref={ref} />}
); </FormFieldWrapper>
));
TextAreaField.displayName = 'TextAreaField'; TextAreaField.displayName = 'TextAreaField';
export const TextArea = forwardRef< export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
HTMLTextAreaElement, ({ className, onKeyDown, autoSize, ...otherProps }, ref) => {
ComponentPropsWithoutRef<'textarea'> const handleSubmitHotkey = useCallback(
>(({ className, onKeyDown, ...otherProps }, ref) => { (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const handleSubmitHotkey = useCallback( onKeyDown?.(e);
(e: React.KeyboardEvent<HTMLTextAreaElement>) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
onKeyDown?.(e); const targetForm = e.currentTarget.form;
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { targetForm?.requestSubmit();
const targetForm = e.currentTarget.form; }
targetForm?.requestSubmit(); },
} [onKeyDown],
}, );
[onKeyDown],
);
return ( if (autoSize) {
<textarea return (
{...otherProps} <TextAreaAutosize
onKeyDown={handleSubmitHotkey} {...(otherProps as TextareaAutosizeProps)}
className={classNames(className, classes.input)} onKeyDown={handleSubmitHotkey}
ref={ref} className={classNames(className, classes.input)}
/> ref={ref}
); />
}); );
}
return (
<textarea
{...otherProps}
onKeyDown={handleSubmitHotkey}
className={classNames(className, classes.input)}
ref={ref}
/>
);
},
);
TextArea.displayName = 'TextArea'; TextArea.displayName = 'TextArea';

View File

@ -0,0 +1,94 @@
import { useCallback, useId, useRef, useState } from 'react';
import type { ChangeEventHandler, FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { TextArea } from '@/flavours/glitch/components/form_fields';
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
import { insertEmojiAtPosition } from '@/flavours/glitch/features/emoji/utils';
import type { BaseConfirmationModalProps } from '@/flavours/glitch/features/ui/components/confirmation_modals';
import { ConfirmationModal } from '@/flavours/glitch/features/ui/components/confirmation_modals';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
import classes from '../styles.module.scss';
import { CharCounter } from './char_counter';
import { EmojiPicker } from './emoji_picker';
const messages = defineMessages({
addTitle: {
id: 'account_edit.bio_modal.add_title',
defaultMessage: 'Add bio',
},
editTitle: {
id: 'account_edit.bio_modal.edit_title',
defaultMessage: 'Edit bio',
},
save: {
id: 'account_edit.save',
defaultMessage: 'Save',
},
});
const MAX_BIO_LENGTH = 500;
export const BioModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
const intl = useIntl();
const titleId = useId();
const counterId = useId();
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const accountId = useCurrentAccountId();
const account = useAccount(accountId);
const [newBio, setNewBio] = useState(account?.note_plain ?? '');
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = useCallback(
(event) => {
setNewBio(event.currentTarget.value);
},
[],
);
const handlePickEmoji = useCallback((emoji: string) => {
setNewBio((prev) => {
const position = textAreaRef.current?.selectionStart ?? prev.length;
return insertEmojiAtPosition(prev, emoji, position);
});
}, []);
if (!account) {
return <LoadingIndicator />;
}
return (
<ConfirmationModal
title={intl.formatMessage(
account.note_plain ? messages.editTitle : messages.addTitle,
)}
titleId={titleId}
confirm={intl.formatMessage(messages.save)}
onConfirm={onClose} // To be implemented
onClose={onClose}
noFocusButton
>
<div className={classes.inputWrapper}>
<TextArea
value={newBio}
ref={textAreaRef}
onChange={handleChange}
className={classes.inputText}
aria-labelledby={titleId}
aria-describedby={counterId}
// eslint-disable-next-line jsx-a11y/no-autofocus -- This is a modal, it's fine.
autoFocus
autoSize
/>
<EmojiPicker onPick={handlePickEmoji} />
</div>
<CharCounter
currentLength={newBio.length}
maxLength={MAX_BIO_LENGTH}
id={counterId}
/>
</ConfirmationModal>
);
};

View File

@ -0,0 +1,27 @@
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { polymorphicForwardRef } from '@/types/polymorphic';
import classes from '../styles.module.scss';
export const CharCounter = polymorphicForwardRef<
'p',
{ currentLength: number; maxLength: number }
>(({ currentLength, maxLength, as: Component = 'p' }, ref) => (
<Component
ref={ref}
className={classNames(
classes.counter,
currentLength > maxLength && classes.counterError,
)}
>
<FormattedMessage
id='account_edit.char_counter'
defaultMessage='{currentLength}/{maxLength} characters'
values={{ currentLength, maxLength }}
/>
</Component>
));
CharCounter.displayName = 'CharCounter';

View File

@ -0,0 +1,27 @@
import { useCallback } from 'react';
import type { FC } from 'react';
import { isPlainObject } from '@reduxjs/toolkit';
import EmojiPickerDropdown from '../../compose/containers/emoji_picker_dropdown_container';
export const EmojiPicker: FC<{ onPick: (emoji: string) => void }> = ({
onPick,
}) => {
const handlePick = useCallback(
(emoji: unknown) => {
if (isPlainObject(emoji)) {
if ('native' in emoji && typeof emoji.native === 'string') {
onPick(emoji.native);
} else if (
'shortcode' in emoji &&
typeof emoji.shortcode === 'string'
) {
onPick(`:${emoji.shortcode}:`);
}
}
},
[onPick],
);
return <EmojiPickerDropdown onPickEmoji={handlePick} />;
};

View File

@ -0,0 +1,87 @@
import { useCallback, useId, useRef, useState } from 'react';
import type { ChangeEventHandler, FC } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { TextInput } from '@/flavours/glitch/components/form_fields';
import { insertEmojiAtPosition } from '@/flavours/glitch/features/emoji/utils';
import type { BaseConfirmationModalProps } from '@/flavours/glitch/features/ui/components/confirmation_modals';
import { ConfirmationModal } from '@/flavours/glitch/features/ui/components/confirmation_modals';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
import classes from '../styles.module.scss';
import { CharCounter } from './char_counter';
import { EmojiPicker } from './emoji_picker';
const messages = defineMessages({
addTitle: {
id: 'account_edit.name_modal.add_title',
defaultMessage: 'Add display name',
},
editTitle: {
id: 'account_edit.name_modal.edit_title',
defaultMessage: 'Edit display name',
},
save: {
id: 'account_edit.save',
defaultMessage: 'Save',
},
});
const MAX_NAME_LENGTH = 30;
export const NameModal: FC<BaseConfirmationModalProps> = ({ onClose }) => {
const intl = useIntl();
const titleId = useId();
const counterId = useId();
const inputRef = useRef<HTMLInputElement>(null);
const accountId = useCurrentAccountId();
const account = useAccount(accountId);
const [newName, setNewName] = useState(account?.display_name ?? '');
const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(event) => {
setNewName(event.currentTarget.value);
},
[],
);
const handlePickEmoji = useCallback((emoji: string) => {
setNewName((prev) => {
const position = inputRef.current?.selectionStart ?? prev.length;
return insertEmojiAtPosition(prev, emoji, position);
});
}, []);
return (
<ConfirmationModal
title={intl.formatMessage(messages.editTitle)}
titleId={titleId}
confirm={intl.formatMessage(messages.save)}
onConfirm={onClose} // To be implemented
onClose={onClose}
noCloseOnConfirm
noFocusButton
>
<div className={classes.inputWrapper}>
<TextInput
value={newName}
ref={inputRef}
onChange={handleChange}
className={classes.inputText}
aria-labelledby={titleId}
aria-describedby={counterId}
// eslint-disable-next-line jsx-a11y/no-autofocus -- This is a modal, it's fine.
autoFocus
/>
<EmojiPicker onPick={handlePickEmoji} />
</div>
<CharCounter
currentLength={newName.length}
maxLength={MAX_NAME_LENGTH}
id={counterId}
/>
</ConfirmationModal>
);
};

View File

@ -0,0 +1,62 @@
import type { FC, ReactNode } from 'react';
import type { MessageDescriptor } from 'react-intl';
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import { IconButton } from '@/flavours/glitch/components/icon_button';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import classes from '../styles.module.scss';
const buttonMessage = defineMessage({
id: 'account_edit.section_edit_button',
defaultMessage: 'Edit',
});
interface AccountEditSectionProps {
title: MessageDescriptor;
description?: MessageDescriptor;
showDescription?: boolean;
onEdit?: () => void;
children?: ReactNode;
className?: string;
extraButtons?: ReactNode;
}
export const AccountEditSection: FC<AccountEditSectionProps> = ({
title,
description,
showDescription,
onEdit,
children,
className,
extraButtons,
}) => {
const intl = useIntl();
return (
<section className={classNames(className, classes.section)}>
<header className={classes.sectionHeader}>
<h3 className={classes.sectionTitle}>
<FormattedMessage {...title} />
</h3>
{onEdit && (
<IconButton
icon='pencil'
iconComponent={EditIcon}
onClick={onEdit}
title={`${intl.formatMessage(buttonMessage)} ${intl.formatMessage(title)}`}
/>
)}
{extraButtons}
</header>
{showDescription && (
<p className={classes.sectionSubtitle}>
<FormattedMessage {...description} />
</p>
)}
{children}
</section>
);
};

View File

@ -1,23 +1,92 @@
import { useCallback } from 'react';
import type { FC } from 'react'; import type { FC } from 'react';
import { FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import type { ModalType } from '@/flavours/glitch/actions/modal';
import { openModal } from '@/flavours/glitch/actions/modal';
import { AccountBio } from '@/flavours/glitch/components/account_bio';
import { Avatar } from '@/flavours/glitch/components/avatar';
import { Column } from '@/flavours/glitch/components/column'; import { Column } from '@/flavours/glitch/components/column';
import { ColumnHeader } from '@/flavours/glitch/components/column_header'; import { ColumnHeader } from '@/flavours/glitch/components/column_header';
import { DisplayNameSimple } from '@/flavours/glitch/components/display_name/simple';
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator'; import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
import BundleColumnError from '@/flavours/glitch/features/ui/components/bundle_column_error'; import BundleColumnError from '@/flavours/glitch/features/ui/components/bundle_column_error';
import { useAccount } from '@/flavours/glitch/hooks/useAccount'; import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId'; import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
import { autoPlayGif } from '@/flavours/glitch/initial_state';
import { useAppDispatch } from '@/flavours/glitch/store';
import { AccountEditSection } from './components/section';
import classes from './styles.module.scss'; import classes from './styles.module.scss';
const messages = defineMessages({
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.',
},
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.',
},
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<{ multiColumn: boolean }> = ({ multiColumn }) => { export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const accountId = useCurrentAccountId(); const accountId = useCurrentAccountId();
const account = useAccount(accountId); const account = useAccount(accountId);
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch();
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]);
if (!accountId) { if (!accountId) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />; return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
} }
@ -30,6 +99,8 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
); );
} }
const headerSrc = autoPlayGif ? account.header : account.header_static;
return ( return (
<Column bindToDocument={!multiColumn} className={classes.column}> <Column bindToDocument={!multiColumn} className={classes.column}>
<ColumnHeader <ColumnHeader
@ -37,7 +108,7 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
id: 'account_edit.column_title', id: 'account_edit.column_title',
defaultMessage: 'Edit Profile', defaultMessage: 'Edit Profile',
})} })}
className={classes.header} className={classes.columnHeader}
showBackButton showBackButton
extraButton={ extraButton={
<Link to={`/@${account.acct}`} className='button'> <Link to={`/@${account.acct}`} className='button'>
@ -48,6 +119,48 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
</Link> </Link>
} }
/> />
<header>
<div className={classes.profileImage}>
{headerSrc && <img src={headerSrc} alt='' />}
</div>
<Avatar account={account} size={80} className={classes.avatar} />
</header>
<AccountEditSection
title={messages.displayNameTitle}
description={messages.displayNamePlaceholder}
showDescription={account.display_name.length === 0}
onEdit={handleNameEdit}
>
<DisplayNameSimple account={account} />
</AccountEditSection>
<AccountEditSection
title={messages.bioTitle}
description={messages.bioPlaceholder}
showDescription={!account.note_plain}
onEdit={handleBioEdit}
>
<AccountBio accountId={accountId} />
</AccountEditSection>
<AccountEditSection
title={messages.customFieldsTitle}
description={messages.customFieldsPlaceholder}
showDescription
/>
<AccountEditSection
title={messages.featuredHashtagsTitle}
description={messages.featuredHashtagsPlaceholder}
showDescription
/>
<AccountEditSection
title={messages.profileTabTitle}
description={messages.profileTabSubtitle}
showDescription
/>
</Column> </Column>
); );
}; };

View File

@ -3,7 +3,7 @@
border-top-width: 0; border-top-width: 0;
} }
.header { .columnHeader {
:global(.column-header__buttons) { :global(.column-header__buttons) {
align-items: center; align-items: center;
padding-inline-end: 16px; padding-inline-end: 16px;
@ -11,16 +11,100 @@
} }
} }
.nav { .profileImage {
display: flex; height: 120px;
align-items: center; background: var(--color-bg-secondary);
justify-content: space-between; border-bottom: 1px solid var(--color-border-primary);
gap: 8px; overflow: hidden;
padding: 24px 24px 12px;
> h1 { @container (width >= 500px) {
flex-grow: 1; height: 160px;
font-weight: 600; }
font-size: 15px;
> img {
object-fit: cover;
object-position: top center;
width: 100%;
height: 100%;
} }
} }
.avatar {
margin-top: -64px;
margin-left: 18px;
border: 1px solid var(--color-border-primary);
}
.section {
padding: 20px;
border-bottom: 1px solid var(--color-border-primary);
font-size: 15px;
}
.sectionHeader {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
> button {
border: 1px solid var(--color-border-primary);
border-radius: 8px;
box-sizing: border-box;
padding: 4px;
svg {
width: 20px;
height: 20px;
}
}
}
.sectionTitle {
flex-grow: 1;
font-size: 17px;
font-weight: 600;
}
.sectionSubtitle {
color: var(--color-text-secondary);
}
.inputWrapper {
position: relative;
}
// Override input styles
.inputWrapper .inputText {
font-size: 15px;
padding-right: 32px;
}
textarea.inputText {
min-height: 82px;
height: 100%;
// 160px is approx the height of the modal header and footer
max-height: calc(80vh - 160px);
}
.inputWrapper :global(.emoji-picker-dropdown) {
position: absolute;
bottom: 10px;
right: 8px;
height: 24px;
z-index: 1;
:global(.icon-button) {
color: var(--color-text-secondary);
}
}
.counter {
margin-top: 4px;
font-size: 13px;
}
.counterError {
color: var(--color-text-error);
}

View File

@ -153,7 +153,7 @@ const InnerNodeModal: FC<{
onConfirm={handleSave} onConfirm={handleSave}
updating={state === 'saving'} updating={state === 'saving'}
disabled={!isDirty} disabled={!isDirty}
closeWhenConfirm={false} noCloseOnConfirm
noFocusButton noFocusButton
/> />
); );

View File

@ -67,6 +67,24 @@ export function emojiToUnicodeHex(emoji: string): string {
return codes.join('-'); return codes.join('-');
} }
const CHARS_ALLOWED_AROUND_EMOJI =
// eslint-disable-next-line no-control-regex
/[>< …\u0009-\u000d\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/;
// TODO: Move to picker file when that's being built out.
export function insertEmojiAtPosition(
text: string,
emoji: string,
position = text.length,
): string {
const isShortcode = isCustomEmoji(emoji);
const needsSpace =
isShortcode &&
position > 0 &&
!CHARS_ALLOWED_AROUND_EMOJI.test(text[position - 1] ?? '');
return `${text.slice(0, position)}${needsSpace ? ' ' : ''}${emoji} ${text.slice(position)}`;
}
function supportsRegExpSets() { function supportsRegExpSets() {
return 'unicodeSets' in RegExp.prototype; return 'unicodeSets' in RegExp.prototype;
} }

View File

@ -19,20 +19,23 @@ const messages = defineMessages({
export const ConfirmationModal: React.FC< export const ConfirmationModal: React.FC<
{ {
title: React.ReactNode; title: React.ReactNode;
titleId?: string;
message?: React.ReactNode; message?: React.ReactNode;
confirm: React.ReactNode; confirm: React.ReactNode;
cancel?: React.ReactNode; cancel?: React.ReactNode;
secondary?: React.ReactNode; secondary?: React.ReactNode;
onSecondary?: () => void; onSecondary?: () => void;
onConfirm: () => void; onConfirm: () => void;
closeWhenConfirm?: boolean; noCloseOnConfirm?: boolean;
extraContent?: React.ReactNode; extraContent?: React.ReactNode;
children?: React.ReactNode;
updating?: boolean; updating?: boolean;
disabled?: boolean; disabled?: boolean;
noFocusButton?: boolean; noFocusButton?: boolean;
} & BaseConfirmationModalProps } & BaseConfirmationModalProps
> = ({ > = ({
title, title,
titleId,
message, message,
confirm, confirm,
cancel, cancel,
@ -40,19 +43,20 @@ export const ConfirmationModal: React.FC<
onConfirm, onConfirm,
secondary, secondary,
onSecondary, onSecondary,
closeWhenConfirm = true,
extraContent, extraContent,
children,
updating, updating,
disabled, disabled,
noCloseOnConfirm = false,
noFocusButton = false, noFocusButton = false,
}) => { }) => {
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (closeWhenConfirm) { if (!noCloseOnConfirm) {
onClose(); onClose();
} }
onConfirm(); onConfirm();
}, [onClose, onConfirm, closeWhenConfirm]); }, [onClose, onConfirm, noCloseOnConfirm]);
const handleSecondary = useCallback(() => { const handleSecondary = useCallback(() => {
onClose(); onClose();
@ -63,10 +67,10 @@ export const ConfirmationModal: React.FC<
<div className='modal-root__modal safety-action-modal'> <div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'> <div className='safety-action-modal__top'>
<div className='safety-action-modal__confirmation'> <div className='safety-action-modal__confirmation'>
<h1>{title}</h1> <h1 id={titleId}>{title}</h1>
{message && <p>{message}</p>} {message && <p>{message}</p>}
{extraContent} {extraContent ?? children}
</div> </div>
</div> </div>

View File

@ -1,3 +1,4 @@
export type { BaseConfirmationModalProps } from './confirmation_modal';
export { ConfirmationModal } from './confirmation_modal'; export { ConfirmationModal } from './confirmation_modal';
export { ConfirmDeleteStatusModal } from './delete_status'; export { ConfirmDeleteStatusModal } from './delete_status';
export { ConfirmDeleteListModal } from './delete_list'; export { ConfirmDeleteListModal } from './delete_list';

View File

@ -33,7 +33,7 @@ const messages = defineMessages({
/** /**
* [1] Since we only want this modal to have two buttons "Don't ask again" and * [1] Since we only want this modal to have two buttons "Don't ask again" and
* "Got it" , we have to use the `onClose` handler to handle the "Don't ask again" * "Got it" , we have to use the `onClose` handler to handle the "Don't ask again"
* functionality. Because of this, we need to set `closeWhenConfirm` to false and * functionality. Because of this, we need to set `noCloseOnConfirm` to true and
* close the modal manually. * close the modal manually.
* This prevents the modal from being dismissed permanently when just confirming. * This prevents the modal from being dismissed permanently when just confirming.
*/ */
@ -65,13 +65,13 @@ export const QuietPostQuoteInfoModal: React.FC<{ status: Status }> = ({
return ( return (
<ConfirmationModal <ConfirmationModal
closeWhenConfirm={false} // [1]
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}
message={intl.formatMessage(messages.message)} message={intl.formatMessage(messages.message)}
confirm={intl.formatMessage(messages.got_it)} confirm={intl.formatMessage(messages.got_it)}
cancel={intl.formatMessage(messages.dismiss)} cancel={intl.formatMessage(messages.dismiss)}
onConfirm={confirm} onConfirm={confirm}
onClose={dismiss} onClose={dismiss}
noCloseOnConfirm
/> />
); );
}; };

View File

@ -96,6 +96,8 @@ export const MODAL_COMPONENTS = {
'ANNUAL_REPORT': AnnualReportModal, 'ANNUAL_REPORT': AnnualReportModal,
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }), 'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
'ACCOUNT_NOTE': () => import('@/flavours/glitch/features/account_timeline/modals/note_modal').then(module => ({ default: module.AccountNoteModal })), 'ACCOUNT_NOTE': () => import('@/flavours/glitch/features/account_timeline/modals/note_modal').then(module => ({ default: module.AccountNoteModal })),
'ACCOUNT_EDIT_NAME': () => import('@/flavours/glitch/features/account_edit/components/name_modal').then(module => ({ default: module.NameModal })),
'ACCOUNT_EDIT_BIO': () => import('@/flavours/glitch/features/account_edit/components/bio_modal').then(module => ({ default: module.BioModal })),
}; };
export default class ModalRoot extends PureComponent { export default class ModalRoot extends PureComponent {