import { useCallback, useState, useEffect, useRef } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { Helmet } from 'react-helmet'; import { useParams, Link } from 'react-router-dom'; import { useDebouncedCallback } from 'use-debounce'; import AddIcon from '@/material-icons/400-24px/add.svg?react'; import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react'; import { fetchFollowing } from 'mastodon/actions/accounts'; import { importFetchedAccounts } from 'mastodon/actions/importer'; import { fetchList } from 'mastodon/actions/lists'; import { apiRequest } from 'mastodon/api'; import { apiGetAccounts, apiAddAccountToList, apiRemoveAccountFromList, } from 'mastodon/api/lists'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; import { Avatar } from 'mastodon/components/avatar'; import { Button } from 'mastodon/components/button'; import Column from 'mastodon/components/column'; import { ColumnHeader } from 'mastodon/components/column_header'; import { FollowersCounter } from 'mastodon/components/counters'; import { DisplayName } from 'mastodon/components/display_name'; import { Icon } from 'mastodon/components/icon'; import ScrollableList from 'mastodon/components/scrollable_list'; import { ShortNumber } from 'mastodon/components/short_number'; import { VerifiedBadge } from 'mastodon/components/verified_badge'; import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context'; import { me } from 'mastodon/initial_state'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; const messages = defineMessages({ heading: { id: 'column.list_members', defaultMessage: 'Manage list members' }, placeholder: { id: 'lists.search_placeholder', defaultMessage: 'Search people you follow', }, enterSearch: { id: 'lists.add_to_list', defaultMessage: 'Add to list' }, add: { id: 'lists.add_member', defaultMessage: 'Add' }, remove: { id: 'lists.remove_member', defaultMessage: 'Remove' }, back: { id: 'column_back_button.label', defaultMessage: 'Back' }, }); type Mode = 'remove' | 'add'; const ColumnSearchHeader: React.FC<{ onBack: () => void; onSubmit: (value: string) => void; }> = ({ onBack, onSubmit }) => { const intl = useIntl(); const [value, setValue] = useState(''); const handleChange = useCallback( ({ target: { value } }: React.ChangeEvent) => { setValue(value); onSubmit(value); }, [setValue, onSubmit], ); const handleSubmit = useCallback(() => { onSubmit(value); }, [onSubmit, value]); return (
); }; const AccountItem: React.FC<{ accountId: string; listId: string; partOfList: boolean; onToggle: (accountId: string) => void; }> = ({ accountId, listId, partOfList, onToggle }) => { const intl = useIntl(); const account = useAppSelector((state) => state.accounts.get(accountId)); const handleClick = useCallback(() => { if (partOfList) { void apiRemoveAccountFromList(listId, accountId); } else { void apiAddAccountToList(listId, accountId); } onToggle(accountId); }, [accountId, listId, partOfList, onToggle]); if (!account) { return null; } const firstVerifiedField = account.fields.find((item) => !!item.verified_at); return (
{' '} {firstVerifiedField && ( )}
); }; const ListMembers: React.FC<{ multiColumn?: boolean; }> = ({ multiColumn }) => { const dispatch = useAppDispatch(); const { id } = useParams<{ id: string }>(); const intl = useIntl(); const followingAccountIds = useAppSelector( (state) => state.user_lists.getIn(['following', me, 'items']) as string[], ); const [searching, setSearching] = useState(false); const [accountIds, setAccountIds] = useState([]); const [searchAccountIds, setSearchAccountIds] = useState([]); const [loading, setLoading] = useState(true); const [mode, setMode] = useState('remove'); useEffect(() => { if (id) { setLoading(true); dispatch(fetchList(id)); void apiGetAccounts(id) .then((data) => { dispatch(importFetchedAccounts(data)); setAccountIds(data.map((a) => a.id)); setLoading(false); return ''; }) .catch(() => { setLoading(false); }); dispatch(fetchFollowing(me)); } }, [dispatch, id]); const handleSearchClick = useCallback(() => { setMode('add'); }, [setMode]); const handleDismissSearchClick = useCallback(() => { setMode('remove'); setSearching(false); }, [setMode]); const handleAccountToggle = useCallback( (accountId: string) => { const partOfList = accountIds.includes(accountId); if (partOfList) { setAccountIds(accountIds.filter((id) => id !== accountId)); } else { setAccountIds([accountId, ...accountIds]); } }, [accountIds, setAccountIds], ); const searchRequestRef = useRef(null); const handleSearch = useDebouncedCallback( (value: string) => { if (searchRequestRef.current) { searchRequestRef.current.abort(); } if (value.trim().length === 0) { setSearching(false); return; } setLoading(true); searchRequestRef.current = new AbortController(); void apiRequest('GET', 'v1/accounts/search', { signal: searchRequestRef.current.signal, params: { q: value, resolve: false, following: true, }, }) .then((data) => { dispatch(importFetchedAccounts(data)); setSearchAccountIds(data.map((a) => a.id)); setLoading(false); setSearching(true); return ''; }) .catch(() => { setSearching(true); setLoading(false); }); }, 500, { leading: true, trailing: true }, ); let displayedAccountIds: string[]; if (mode === 'add') { displayedAccountIds = searching ? searchAccountIds : followingAccountIds; } else { displayedAccountIds = accountIds; } return ( {mode === 'remove' ? ( } /> ) : ( )}
) } emptyMessage={ mode === 'remove' ? ( <>
) : ( ) } > {displayedAccountIds.map((accountId) => ( ))} {intl.formatMessage(messages.heading)} ); }; // eslint-disable-next-line import/no-default-export export default ListMembers;