Merge commit '8e7c3973dc67835e0ce9c7073b9344ec19d3910a' into glitch-soc/merge-upstream
This commit is contained in:
commit
eebed16f59
151 files changed
+1519
-687
No files matched your search
@@ -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 '@/mastodon/components/form_fields';
|
||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import { insertEmojiAtPosition } from '@/mastodon/features/emoji/utils';
|
||||
import type { BaseConfirmationModalProps } from '@/mastodon/features/ui/components/confirmation_modals';
|
||||
import { ConfirmationModal } from '@/mastodon/features/ui/components/confirmation_modals';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useCurrentAccountId } from '@/mastodon/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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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 '@/mastodon/components/form_fields';
|
||||
import { insertEmojiAtPosition } from '@/mastodon/features/emoji/utils';
|
||||
import type { BaseConfirmationModalProps } from '@/mastodon/features/ui/components/confirmation_modals';
|
||||
import { ConfirmationModal } from '@/mastodon/features/ui/components/confirmation_modals';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useCurrentAccountId } from '@/mastodon/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>
|
||||
);
|
||||
};
|
||||
@@ -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 '@/mastodon/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>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +1,92 @@
|
||||
import { useCallback } 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 type { ModalType } from '@/mastodon/actions/modal';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { AccountBio } from '@/mastodon/components/account_bio';
|
||||
import { Avatar } from '@/mastodon/components/avatar';
|
||||
import { Column } from '@/mastodon/components/column';
|
||||
import { ColumnHeader } from '@/mastodon/components/column_header';
|
||||
import { DisplayNameSimple } from '@/mastodon/components/display_name/simple';
|
||||
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 { autoPlayGif } from '@/mastodon/initial_state';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
import { AccountEditSection } from './components/section';
|
||||
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 you’d 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 }) => {
|
||||
const accountId = useCurrentAccountId();
|
||||
const account = useAccount(accountId);
|
||||
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) {
|
||||
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 (
|
||||
<Column bindToDocument={!multiColumn} className={classes.column}>
|
||||
<ColumnHeader
|
||||
@@ -37,7 +108,7 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
||||
id: 'account_edit.column_title',
|
||||
defaultMessage: 'Edit Profile',
|
||||
})}
|
||||
className={classes.header}
|
||||
className={classes.columnHeader}
|
||||
showBackButton
|
||||
extraButton={
|
||||
<Link to={`/@${account.acct}`} className='button'>
|
||||
@@ -48,6 +119,48 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
.columnHeader {
|
||||
:global(.column-header__buttons) {
|
||||
align-items: center;
|
||||
padding-inline-end: 16px;
|
||||
@@ -11,16 +11,100 @@
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 24px 24px 12px;
|
||||
.profileImage {
|
||||
height: 120px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
overflow: hidden;
|
||||
|
||||
> h1 {
|
||||
flex-grow: 1;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
@container (width >= 500px) {
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
> 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);
|
||||
}
|
||||
@@ -153,7 +153,7 @@ const InnerNodeModal: FC<{
|
||||
onConfirm={handleSave}
|
||||
updating={state === 'saving'}
|
||||
disabled={!isDirty}
|
||||
closeWhenConfirm={false}
|
||||
noCloseOnConfirm
|
||||
noFocusButton
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -86,11 +86,9 @@ const InnerTimeline: FC<{ accountId: string; multiColumn: boolean }> = ({
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
if (accountId) {
|
||||
if (!timeline) {
|
||||
dispatch(expandTimelineByKey({ key }));
|
||||
}
|
||||
dispatch(expandTimelineByKey({ key }));
|
||||
}
|
||||
}, [accountId, dispatch, key, timeline]);
|
||||
}, [accountId, dispatch, key]);
|
||||
|
||||
const handleLoadMore = useCallback(
|
||||
(maxId: number) => {
|
||||
|
||||
@@ -13,8 +13,9 @@ import { CollectionMenu } from './collection_menu';
|
||||
|
||||
export const CollectionMetaData: React.FC<{
|
||||
collection: ApiCollectionJSON;
|
||||
extended?: boolean;
|
||||
className?: string;
|
||||
}> = ({ collection, className }) => {
|
||||
}> = ({ collection, extended, className }) => {
|
||||
return (
|
||||
<ul className={classNames(classes.metaList, className)}>
|
||||
<FormattedMessage
|
||||
@@ -23,6 +24,30 @@ export const CollectionMetaData: React.FC<{
|
||||
values={{ count: collection.item_count }}
|
||||
tagName='li'
|
||||
/>
|
||||
{extended && (
|
||||
<>
|
||||
{collection.discoverable ? (
|
||||
<FormattedMessage
|
||||
id='collections.visibility_public'
|
||||
defaultMessage='Public'
|
||||
tagName='li'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='collections.visibility_unlisted'
|
||||
defaultMessage='Unlisted'
|
||||
tagName='li'
|
||||
/>
|
||||
)}
|
||||
{collection.sensitive && (
|
||||
<FormattedMessage
|
||||
id='collections.sensitive'
|
||||
defaultMessage='Sensitive'
|
||||
tagName='li'
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<FormattedMessage
|
||||
id='collections.last_updated_at'
|
||||
defaultMessage='Last updated: {date}'
|
||||
|
||||
@@ -55,10 +55,6 @@ export const CollectionMenu: React.FC<{
|
||||
text: intl.formatMessage(editorMessages.editDetails),
|
||||
to: `/collections/${id}/edit/details`,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(editorMessages.editSettings),
|
||||
to: `/collections/${id}/edit/settings`,
|
||||
},
|
||||
null,
|
||||
{
|
||||
text: intl.formatMessage(messages.delete),
|
||||
|
||||
@@ -114,6 +114,7 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
|
||||
{description && <p className={classes.description}>{description}</p>}
|
||||
<AuthorNote id={collection.account_id} />
|
||||
<CollectionMetaData
|
||||
extended
|
||||
collection={collection}
|
||||
className={classes.metaData}
|
||||
/>
|
||||
|
||||
@@ -129,7 +129,11 @@ export const CollectionAccounts: React.FC<{
|
||||
accountIds: suggestedAccountIds,
|
||||
isLoading: isLoadingSuggestions,
|
||||
searchAccounts,
|
||||
} = useSearchAccounts();
|
||||
} = useSearchAccounts({
|
||||
filterResults: (account) =>
|
||||
// Only suggest accounts who allow being featured/recommended
|
||||
account.feature_approval.current_user === 'automatic',
|
||||
});
|
||||
|
||||
const suggestedItems = suggestedAccountIds.map((id) => ({
|
||||
id,
|
||||
|
||||
@@ -4,15 +4,26 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import type {
|
||||
ApiCollectionJSON,
|
||||
ApiCreateCollectionPayload,
|
||||
ApiUpdateCollectionPayload,
|
||||
} from 'mastodon/api_types/collections';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { FormStack, TextAreaField } from 'mastodon/components/form_fields';
|
||||
import {
|
||||
CheckboxField,
|
||||
Fieldset,
|
||||
FormStack,
|
||||
RadioButtonField,
|
||||
TextAreaField,
|
||||
} from 'mastodon/components/form_fields';
|
||||
import { TextInputField } from 'mastodon/components/form_fields/text_input_field';
|
||||
import { updateCollection } from 'mastodon/reducers/slices/collections';
|
||||
import {
|
||||
createCollection,
|
||||
updateCollection,
|
||||
} from 'mastodon/reducers/slices/collections';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import type { TempCollectionState } from './state';
|
||||
@@ -27,12 +38,21 @@ export const CollectionDetails: React.FC<{
|
||||
const history = useHistory();
|
||||
const location = useLocation<TempCollectionState>();
|
||||
|
||||
const { id, initialName, initialDescription, initialTopic, initialItemIds } =
|
||||
getCollectionEditorState(collection, location.state);
|
||||
const {
|
||||
id,
|
||||
initialName,
|
||||
initialDescription,
|
||||
initialTopic,
|
||||
initialItemIds,
|
||||
initialDiscoverable,
|
||||
initialSensitive,
|
||||
} = getCollectionEditorState(collection, location.state);
|
||||
|
||||
const [name, setName] = useState(initialName);
|
||||
const [description, setDescription] = useState(initialDescription);
|
||||
const [topic, setTopic] = useState(initialTopic);
|
||||
const [discoverable, setDiscoverable] = useState(initialDiscoverable);
|
||||
const [sensitive, setSensitive] = useState(initialSensitive);
|
||||
|
||||
const handleNameChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -55,6 +75,20 @@ export const CollectionDetails: React.FC<{
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDiscoverableChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDiscoverable(event.target.value === 'public');
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSensitiveChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSensitive(event.target.checked);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -65,108 +99,208 @@ export const CollectionDetails: React.FC<{
|
||||
name,
|
||||
description,
|
||||
tag_name: topic || null,
|
||||
discoverable,
|
||||
sensitive,
|
||||
};
|
||||
|
||||
void dispatch(updateCollection({ payload })).then(() => {
|
||||
history.push(`/collections/${id}`);
|
||||
history.goBack();
|
||||
});
|
||||
} else {
|
||||
const payload: Partial<ApiCreateCollectionPayload> = {
|
||||
const payload: ApiCreateCollectionPayload = {
|
||||
name,
|
||||
description,
|
||||
tag_name: topic || null,
|
||||
discoverable,
|
||||
sensitive,
|
||||
account_ids: initialItemIds,
|
||||
};
|
||||
if (topic) {
|
||||
payload.tag_name = topic;
|
||||
}
|
||||
|
||||
history.replace('/collections/new', payload);
|
||||
history.push('/collections/new/settings', payload);
|
||||
void dispatch(
|
||||
createCollection({
|
||||
payload,
|
||||
}),
|
||||
).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
history.replace(
|
||||
`/collections/${result.payload.collection.id}/edit/details`,
|
||||
);
|
||||
history.push(`/collections/${result.payload.collection.id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
[id, name, description, topic, dispatch, history, initialItemIds],
|
||||
[
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
topic,
|
||||
discoverable,
|
||||
sensitive,
|
||||
dispatch,
|
||||
history,
|
||||
initialItemIds,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<FormStack as='form' onSubmit={handleSubmit}>
|
||||
{!id && (
|
||||
<WizardStepHeader
|
||||
step={2}
|
||||
title={
|
||||
<form onSubmit={handleSubmit} className={classes.form}>
|
||||
<FormStack className={classes.formFieldStack}>
|
||||
{!id && (
|
||||
<WizardStepHeader
|
||||
step={2}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='collections.create.basic_details_title'
|
||||
defaultMessage='Basic details'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<TextInputField
|
||||
required
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.create.basic_details_title'
|
||||
defaultMessage='Basic details'
|
||||
id='collections.collection_name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<TextInputField
|
||||
required
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.collection_name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.name_length_hint'
|
||||
defaultMessage='40 characters limit'
|
||||
/>
|
||||
}
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
maxLength={40}
|
||||
/>
|
||||
|
||||
<TextAreaField
|
||||
required
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.collection_description'
|
||||
defaultMessage='Description'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.description_length_hint'
|
||||
defaultMessage='100 characters limit'
|
||||
/>
|
||||
}
|
||||
value={description}
|
||||
onChange={handleDescriptionChange}
|
||||
maxLength={100}
|
||||
/>
|
||||
|
||||
<TextInputField
|
||||
required={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.collection_topic'
|
||||
defaultMessage='Topic'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.topic_hint'
|
||||
defaultMessage='Add a hashtag that helps others understand the main topic of this collection.'
|
||||
/>
|
||||
}
|
||||
value={topic}
|
||||
onChange={handleTopicChange}
|
||||
maxLength={40}
|
||||
/>
|
||||
|
||||
<div className={classes.actionWrapper}>
|
||||
<Button type='submit'>
|
||||
{id ? (
|
||||
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
||||
) : (
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.continue'
|
||||
defaultMessage='Continue'
|
||||
id='collections.name_length_hint'
|
||||
defaultMessage='40 characters limit'
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
value={name}
|
||||
onChange={handleNameChange}
|
||||
maxLength={40}
|
||||
/>
|
||||
|
||||
<TextAreaField
|
||||
required
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.collection_description'
|
||||
defaultMessage='Description'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.description_length_hint'
|
||||
defaultMessage='100 characters limit'
|
||||
/>
|
||||
}
|
||||
value={description}
|
||||
onChange={handleDescriptionChange}
|
||||
maxLength={100}
|
||||
/>
|
||||
|
||||
<TextInputField
|
||||
required={false}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.collection_topic'
|
||||
defaultMessage='Topic'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.topic_hint'
|
||||
defaultMessage='Add a hashtag that helps others understand the main topic of this collection.'
|
||||
/>
|
||||
}
|
||||
value={topic}
|
||||
onChange={handleTopicChange}
|
||||
maxLength={40}
|
||||
/>
|
||||
|
||||
<Fieldset
|
||||
legend={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_title'
|
||||
defaultMessage='Visibility'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RadioButtonField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_public'
|
||||
defaultMessage='Public'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_public_hint'
|
||||
defaultMessage='Discoverable in search results and other areas where recommendations appear.'
|
||||
/>
|
||||
}
|
||||
value='public'
|
||||
checked={discoverable}
|
||||
onChange={handleDiscoverableChange}
|
||||
/>
|
||||
<RadioButtonField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_unlisted'
|
||||
defaultMessage='Unlisted'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_unlisted_hint'
|
||||
defaultMessage='Visible to anyone with a link. Hidden from search results and recommendations.'
|
||||
/>
|
||||
}
|
||||
value='unlisted'
|
||||
checked={!discoverable}
|
||||
onChange={handleDiscoverableChange}
|
||||
/>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset
|
||||
legend={
|
||||
<FormattedMessage
|
||||
id='collections.content_warning'
|
||||
defaultMessage='Content warning'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CheckboxField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.mark_as_sensitive'
|
||||
defaultMessage='Mark as sensitive'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.mark_as_sensitive_hint'
|
||||
defaultMessage="Hides the collection's description and accounts behind a content warning. The collection name will still be visible."
|
||||
/>
|
||||
}
|
||||
checked={sensitive}
|
||||
onChange={handleSensitiveChange}
|
||||
/>
|
||||
</Fieldset>
|
||||
</FormStack>
|
||||
|
||||
<div className={classes.stickyFooter}>
|
||||
<div className={classes.actionWrapper}>
|
||||
<Button type='submit'>
|
||||
{id ? (
|
||||
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='collections.create_collection'
|
||||
defaultMessage='Create collection'
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FormStack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -21,7 +21,6 @@ import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { CollectionAccounts } from './accounts';
|
||||
import { CollectionDetails } from './details';
|
||||
import { CollectionSettings } from './settings';
|
||||
|
||||
export const messages = defineMessages({
|
||||
create: {
|
||||
@@ -34,20 +33,12 @@ export const messages = defineMessages({
|
||||
},
|
||||
editDetails: {
|
||||
id: 'collections.edit_details',
|
||||
defaultMessage: 'Edit basic details',
|
||||
defaultMessage: 'Edit details',
|
||||
},
|
||||
manageAccounts: {
|
||||
id: 'collections.manage_accounts',
|
||||
defaultMessage: 'Manage accounts',
|
||||
},
|
||||
manageAccountsLong: {
|
||||
id: 'collections.manage_accounts_in_collection',
|
||||
defaultMessage: 'Manage accounts in this collection',
|
||||
},
|
||||
editSettings: {
|
||||
id: 'collections.edit_settings',
|
||||
defaultMessage: 'Edit settings',
|
||||
},
|
||||
});
|
||||
|
||||
function usePageTitle(id: string | undefined) {
|
||||
@@ -62,8 +53,6 @@ function usePageTitle(id: string | undefined) {
|
||||
return messages.manageAccounts;
|
||||
} else if (matchPath(location.pathname, { path: `${path}/details` })) {
|
||||
return messages.editDetails;
|
||||
} else if (matchPath(location.pathname, { path: `${path}/settings` })) {
|
||||
return messages.editSettings;
|
||||
} else {
|
||||
throw new Error('No page title defined for route');
|
||||
}
|
||||
@@ -117,11 +106,6 @@ export const CollectionEditorPage: React.FC<{
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
render={() => <CollectionDetails collection={collection} />}
|
||||
/>
|
||||
<Route
|
||||
path={`${path}/settings`}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
render={() => <CollectionSettings collection={collection} />}
|
||||
/>
|
||||
</Switch>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import type {
|
||||
ApiCollectionJSON,
|
||||
ApiCreateCollectionPayload,
|
||||
ApiUpdateCollectionPayload,
|
||||
} from 'mastodon/api_types/collections';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import {
|
||||
Fieldset,
|
||||
FormStack,
|
||||
CheckboxField,
|
||||
RadioButtonField,
|
||||
} from 'mastodon/components/form_fields';
|
||||
import {
|
||||
createCollection,
|
||||
updateCollection,
|
||||
} from 'mastodon/reducers/slices/collections';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import type { TempCollectionState } from './state';
|
||||
import { getCollectionEditorState } from './state';
|
||||
import classes from './styles.module.scss';
|
||||
import { WizardStepHeader } from './wizard_step_header';
|
||||
|
||||
export const CollectionSettings: React.FC<{
|
||||
collection?: ApiCollectionJSON | null;
|
||||
}> = ({ collection }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const location = useLocation<TempCollectionState>();
|
||||
|
||||
const { id, initialDiscoverable, initialSensitive, ...editorState } =
|
||||
getCollectionEditorState(collection, location.state);
|
||||
|
||||
const [discoverable, setDiscoverable] = useState(initialDiscoverable);
|
||||
const [sensitive, setSensitive] = useState(initialSensitive);
|
||||
|
||||
const handleDiscoverableChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDiscoverable(event.target.value === 'public');
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSensitiveChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSensitive(event.target.checked);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (id) {
|
||||
const payload: ApiUpdateCollectionPayload = {
|
||||
id,
|
||||
discoverable,
|
||||
sensitive,
|
||||
};
|
||||
|
||||
void dispatch(updateCollection({ payload })).then(() => {
|
||||
history.push(`/collections/${id}`);
|
||||
});
|
||||
} else {
|
||||
const payload: ApiCreateCollectionPayload = {
|
||||
name: editorState.initialName,
|
||||
description: editorState.initialDescription,
|
||||
discoverable,
|
||||
sensitive,
|
||||
account_ids: editorState.initialItemIds,
|
||||
};
|
||||
if (editorState.initialTopic) {
|
||||
payload.tag_name = editorState.initialTopic;
|
||||
}
|
||||
|
||||
void dispatch(
|
||||
createCollection({
|
||||
payload,
|
||||
}),
|
||||
).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
history.replace(
|
||||
`/collections/${result.payload.collection.id}/edit/settings`,
|
||||
);
|
||||
history.push(`/collections`);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
[id, discoverable, sensitive, dispatch, history, editorState],
|
||||
);
|
||||
|
||||
return (
|
||||
<FormStack as='form' onSubmit={handleSubmit}>
|
||||
{!id && (
|
||||
<WizardStepHeader
|
||||
step={3}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='collections.create.settings_title'
|
||||
defaultMessage='Settings'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Fieldset
|
||||
legend={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_title'
|
||||
defaultMessage='Visibility'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<RadioButtonField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_public'
|
||||
defaultMessage='Public'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_public_hint'
|
||||
defaultMessage='Discoverable in search results and other areas where recommendations appear.'
|
||||
/>
|
||||
}
|
||||
value='public'
|
||||
checked={discoverable}
|
||||
onChange={handleDiscoverableChange}
|
||||
/>
|
||||
<RadioButtonField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_unlisted'
|
||||
defaultMessage='Unlisted'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.visibility_unlisted_hint'
|
||||
defaultMessage='Visible to anyone with a link. Hidden from search results and recommendations.'
|
||||
/>
|
||||
}
|
||||
value='unlisted'
|
||||
checked={!discoverable}
|
||||
onChange={handleDiscoverableChange}
|
||||
/>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset
|
||||
legend={
|
||||
<FormattedMessage
|
||||
id='collections.content_warning'
|
||||
defaultMessage='Content warning'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CheckboxField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='collections.mark_as_sensitive'
|
||||
defaultMessage='Mark as sensitive'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='collections.mark_as_sensitive_hint'
|
||||
defaultMessage="Hides the collection's description and accounts behind a content warning. The collection name will still be visible."
|
||||
/>
|
||||
}
|
||||
checked={sensitive}
|
||||
onChange={handleSensitiveChange}
|
||||
/>
|
||||
</Fieldset>
|
||||
|
||||
<div className={classes.actionWrapper}>
|
||||
<Button type='submit'>
|
||||
{id ? (
|
||||
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='collections.create_collection'
|
||||
defaultMessage='Create collection'
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</FormStack>
|
||||
);
|
||||
};
|
||||
@@ -12,7 +12,7 @@ export const WizardStepHeader: React.FC<{
|
||||
<FormattedMessage
|
||||
id='collections.create.steps'
|
||||
defaultMessage='Step {step}/{total}'
|
||||
values={{ step, total: 3 }}
|
||||
values={{ step, total: 2 }}
|
||||
>
|
||||
{(content) => <p className={classes.step}>{content}</p>}
|
||||
</FormattedMessage>
|
||||
|
||||
@@ -67,6 +67,24 @@ export function emojiToUnicodeHex(emoji: string): string {
|
||||
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() {
|
||||
return 'unicodeSets' in RegExp.prototype;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ import { useAppDispatch } from 'mastodon/store';
|
||||
export function useSearchAccounts({
|
||||
resetOnInputClear = true,
|
||||
onSettled,
|
||||
filterResults,
|
||||
}: {
|
||||
onSettled?: (value: string) => void;
|
||||
filterResults?: (account: ApiAccountJSON) => boolean;
|
||||
resetOnInputClear?: boolean;
|
||||
} = {}) {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -49,8 +51,9 @@ export function useSearchAccounts({
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
setAccountIds(data.map((a) => a.id));
|
||||
const accounts = filterResults ? data.filter(filterResults) : data;
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
setAccountIds(accounts.map((a) => a.id));
|
||||
setLoadingState('idle');
|
||||
onSettled?.(value);
|
||||
})
|
||||
|
||||
+10
-6
@@ -11,20 +11,23 @@ export interface BaseConfirmationModalProps {
|
||||
export const ConfirmationModal: React.FC<
|
||||
{
|
||||
title: React.ReactNode;
|
||||
titleId?: string;
|
||||
message?: React.ReactNode;
|
||||
confirm: React.ReactNode;
|
||||
cancel?: React.ReactNode;
|
||||
secondary?: React.ReactNode;
|
||||
onSecondary?: () => void;
|
||||
onConfirm: () => void;
|
||||
closeWhenConfirm?: boolean;
|
||||
noCloseOnConfirm?: boolean;
|
||||
extraContent?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
updating?: boolean;
|
||||
disabled?: boolean;
|
||||
noFocusButton?: boolean;
|
||||
} & BaseConfirmationModalProps
|
||||
> = ({
|
||||
title,
|
||||
titleId,
|
||||
message,
|
||||
confirm,
|
||||
cancel,
|
||||
@@ -32,19 +35,20 @@ export const ConfirmationModal: React.FC<
|
||||
onConfirm,
|
||||
secondary,
|
||||
onSecondary,
|
||||
closeWhenConfirm = true,
|
||||
extraContent,
|
||||
children,
|
||||
updating,
|
||||
disabled,
|
||||
noCloseOnConfirm = false,
|
||||
noFocusButton = false,
|
||||
}) => {
|
||||
const handleClick = useCallback(() => {
|
||||
if (closeWhenConfirm) {
|
||||
if (!noCloseOnConfirm) {
|
||||
onClose();
|
||||
}
|
||||
|
||||
onConfirm();
|
||||
}, [onClose, onConfirm, closeWhenConfirm]);
|
||||
}, [onClose, onConfirm, noCloseOnConfirm]);
|
||||
|
||||
const handleSecondary = useCallback(() => {
|
||||
onClose();
|
||||
@@ -55,10 +59,10 @@ export const ConfirmationModal: React.FC<
|
||||
<div className='modal-root__modal safety-action-modal'>
|
||||
<div className='safety-action-modal__top'>
|
||||
<div className='safety-action-modal__confirmation'>
|
||||
<h1>{title}</h1>
|
||||
<h1 id={titleId}>{title}</h1>
|
||||
{message && <p>{message}</p>}
|
||||
|
||||
{extraContent}
|
||||
{extraContent ?? children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export type { BaseConfirmationModalProps } from './confirmation_modal';
|
||||
export { ConfirmationModal } from './confirmation_modal';
|
||||
export { ConfirmDeleteStatusModal } from './delete_status';
|
||||
export { ConfirmDeleteListModal } from './delete_list';
|
||||
|
||||
+2
-2
@@ -33,7 +33,7 @@ const messages = defineMessages({
|
||||
/**
|
||||
* [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"
|
||||
* 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.
|
||||
* This prevents the modal from being dismissed permanently when just confirming.
|
||||
*/
|
||||
@@ -65,13 +65,13 @@ export const QuietPostQuoteInfoModal: React.FC<{ status: Status }> = ({
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
closeWhenConfirm={false} // [1]
|
||||
title={intl.formatMessage(messages.title)}
|
||||
message={intl.formatMessage(messages.message)}
|
||||
confirm={intl.formatMessage(messages.got_it)}
|
||||
cancel={intl.formatMessage(messages.dismiss)}
|
||||
onConfirm={confirm}
|
||||
onClose={dismiss}
|
||||
noCloseOnConfirm
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -88,6 +88,8 @@ export const MODAL_COMPONENTS = {
|
||||
'ANNUAL_REPORT': AnnualReportModal,
|
||||
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
|
||||
'ACCOUNT_NOTE': () => import('@/mastodon/features/account_timeline/modals/note_modal').then(module => ({ default: module.AccountNoteModal })),
|
||||
'ACCOUNT_EDIT_NAME': () => import('@/mastodon/features/account_edit/components/name_modal').then(module => ({ default: module.NameModal })),
|
||||
'ACCOUNT_EDIT_BIO': () => import('@/mastodon/features/account_edit/components/bio_modal').then(module => ({ default: module.BioModal })),
|
||||
};
|
||||
|
||||
export default class ModalRoot extends PureComponent {
|
||||
|
||||
Reference in new issue
Block a user