import { useCallback, useMemo, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { useHistory, useLocation } from 'react-router-dom'; import CancelIcon from '@/material-icons/400-24px/cancel.svg?react'; import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import type { ApiCollectionJSON } from 'mastodon/api_types/collections'; import { Account } from 'mastodon/components/account'; import { Avatar } from 'mastodon/components/avatar'; import { Button } from 'mastodon/components/button'; import { Callout } from 'mastodon/components/callout'; import { DisplayName } from 'mastodon/components/display_name'; import { EmptyState } from 'mastodon/components/empty_state'; import { FormStack, ComboboxField } from 'mastodon/components/form_fields'; import { Icon } from 'mastodon/components/icon'; import { IconButton } from 'mastodon/components/icon_button'; import ScrollableList from 'mastodon/components/scrollable_list'; import { useSearchAccounts } from 'mastodon/features/lists/use_search_accounts'; import { addCollectionItem, removeCollectionItem, } from 'mastodon/reducers/slices/collections'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; import type { TempCollectionState } from './state'; import { getCollectionEditorState } from './state'; import classes from './styles.module.scss'; import { WizardStepHeader } from './wizard_step_header'; const MIN_ACCOUNT_COUNT = 1; const MAX_ACCOUNT_COUNT = 25; const AddedAccountItem: React.FC<{ accountId: string; isRemovable: boolean; onRemove: (id: string) => void; }> = ({ accountId, isRemovable, onRemove }) => { const intl = useIntl(); const handleRemoveAccount = useCallback(() => { onRemove(accountId); }, [accountId, onRemove]); return ( {isRemovable && ( )} ); }; interface SuggestionItem { id: string; isSelected: boolean; } const SuggestedAccountItem: React.FC = ({ id, isSelected }) => { const account = useAppSelector((state) => state.accounts.get(id)); if (!account) return null; return ( <> {isSelected && ( )} ); }; const renderAccountItem = (item: SuggestionItem) => ( ); const getItemId = (item: SuggestionItem) => item.id; const getIsItemSelected = (item: SuggestionItem) => item.isSelected; export const CollectionAccounts: React.FC<{ collection?: ApiCollectionJSON | null; }> = ({ collection }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const history = useHistory(); const location = useLocation(); const { id, initialItemIds } = getCollectionEditorState( collection, location.state, ); const isEditMode = !!id; const collectionItems = collection?.items; const [searchValue, setSearchValue] = useState(''); // This state is only used when creating a new collection. // In edit mode, the collection will be updated instantly const [addedAccountIds, setAccountIds] = useState(initialItemIds); const accountIds = useMemo( () => isEditMode ? (collectionItems ?.map((item) => item.account_id) .filter((id): id is string => !!id) ?? []) : addedAccountIds, [isEditMode, collectionItems, addedAccountIds], ); const hasMaxAccounts = accountIds.length === MAX_ACCOUNT_COUNT; const hasMinAccounts = accountIds.length === MIN_ACCOUNT_COUNT; const hasTooFewAccounts = accountIds.length < MIN_ACCOUNT_COUNT; const canSubmit = !hasTooFewAccounts; const { accountIds: suggestedAccountIds, isLoading: isLoadingSuggestions, searchAccounts, } = useSearchAccounts(); const suggestedItems = suggestedAccountIds.map((id) => ({ id, isSelected: accountIds.includes(id), })); const handleSearchValueChange = useCallback( (e: React.ChangeEvent) => { setSearchValue(e.target.value); searchAccounts(e.target.value); }, [searchAccounts], ); const handleSearchKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); } }, [], ); const toggleAccountItem = useCallback((item: SuggestionItem) => { setAccountIds((ids) => ids.includes(item.id) ? ids.filter((id) => id !== item.id) : [...ids, item.id], ); }, []); const instantRemoveAccountItem = useCallback( (accountId: string) => { const itemId = collectionItems?.find( (item) => item.account_id === accountId, )?.id; if (itemId && id) { if ( window.confirm( intl.formatMessage({ id: 'collections.confirm_account_removal', defaultMessage: 'Are you sure you want to remove this account from this collection?', }), ) ) { void dispatch(removeCollectionItem({ collectionId: id, itemId })); } } }, [collectionItems, dispatch, id, intl], ); const instantToggleAccountItem = useCallback( (item: SuggestionItem) => { if (accountIds.includes(item.id)) { instantRemoveAccountItem(item.id); } else { if (id) { void dispatch( addCollectionItem({ collectionId: id, accountId: item.id }), ); } } }, [accountIds, dispatch, id, instantRemoveAccountItem], ); const handleRemoveAccountItem = useCallback( (accountId: string) => { if (isEditMode) { instantRemoveAccountItem(accountId); } else { setAccountIds((ids) => ids.filter((id) => id !== accountId)); } }, [isEditMode, instantRemoveAccountItem], ); const handleSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault(); if (!canSubmit) { return; } if (!id) { history.push(`/collections/new/details`, { account_ids: accountIds, }); } }, [canSubmit, id, history, accountIds], ); return (
{!id && ( } description={ } /> )} } hint={ hasMaxAccounts ? ( ) : undefined } value={hasMaxAccounts ? '' : searchValue} onChange={handleSearchValueChange} onKeyDown={handleSearchKeyDown} disabled={hasMaxAccounts} isLoading={isLoadingSuggestions} items={suggestedItems} getItemId={getItemId} getIsItemSelected={getIsItemSelected} renderItem={renderAccountItem} onSelectItem={ isEditMode ? instantToggleAccountItem : toggleAccountItem } /> {hasMinAccounts && ( )}
} message={ } /> } // TODO: Re-add `bindToDocument={!multiColumn}` > {accountIds.map((accountId) => ( ))}
{!isEditMode && (
{hasTooFewAccounts ? ( ) : (
{(text) => (
{text}
)}
{canSubmit && ( )}
)}
)}
); };