Implement new Collection inclusion rules in Collection accounts editor (#38719)

This commit is contained in:
diondiondion
2026-04-16 20:05:36 +02:00
committed by GitHub
parent 0e6180a5af
commit a40b071640
7 changed files with 41 additions and 127 deletions

View File

@@ -4,11 +4,8 @@ import { FormattedMessage, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { showAlertForError } from 'mastodon/actions/alerts';
import { openModal } from 'mastodon/actions/modal';
import { apiFollowAccount } from 'mastodon/api/accounts';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { Account } from 'mastodon/components/account';
import { AccountListItem } from 'mastodon/components/account_list_item';
import { Avatar } from 'mastodon/components/avatar';
import { Button } from 'mastodon/components/button';
import { DisplayName } from 'mastodon/components/display_name';
@@ -21,19 +18,18 @@ import {
} from 'mastodon/components/scrollable_list/components';
import { useAccount } from 'mastodon/hooks/useAccount';
import { useSearchAccounts } from 'mastodon/hooks/useSearchAccounts';
import { me } from 'mastodon/initial_state';
import {
addCollectionItem,
getCollectionItemIds,
removeCollectionItem,
updateCollectionEditorField,
} from 'mastodon/reducers/slices/collections';
import { store, useAppDispatch, useAppSelector } from 'mastodon/store';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
import classes from './styles.module.scss';
import { WizardStepTitle } from './wizard_step_title';
const MAX_ACCOUNT_COUNT = 3;
const MAX_ACCOUNT_COUNT = 25;
const AddedAccountItem: React.FC<{
accountId: string;
@@ -43,20 +39,24 @@ const AddedAccountItem: React.FC<{
onRemove(accountId);
}, [accountId, onRemove]);
return (
<Account minimal key={accountId} id={accountId}>
const renderButton = useCallback(
() => (
<Button compact secondary onClick={handleRemoveAccount}>
<FormattedMessage
id='collections.remove_account'
defaultMessage='Remove'
/>
</Button>
</Account>
),
[handleRemoveAccount],
);
return <AccountListItem accountId={accountId} renderButton={renderButton} />;
};
interface SuggestionItem {
id: string;
isDisabled?: boolean;
}
const SuggestedAccountItem: React.FC<SuggestionItem> = ({ id }) => {
@@ -77,6 +77,7 @@ const renderAccountItem = (item: SuggestionItem) => (
);
const getItemId = (item: SuggestionItem) => item.id;
const getIsItemDisabled = (item: SuggestionItem) => item.isDisabled ?? false;
export const CollectionAccounts: React.FC<{
collection?: ApiCollectionJSON | null;
@@ -106,21 +107,22 @@ export const CollectionAccounts: React.FC<{
const hasMaxAccounts = accountIds.length === MAX_ACCOUNT_COUNT;
const {
accountIds: suggestedAccountIds,
accounts: suggestedAccounts,
isLoading: isLoadingSuggestions,
searchAccounts,
resetAccounts,
} = useSearchAccounts({
withRelationships: true,
filterResults: (account) =>
!accountIds.includes(account.id) &&
// Only suggest accounts who allow being featured/recommended
account.feature_approval.current_user === 'automatic',
// Don't suggest accounts that were already added
filterResults: (account) => !accountIds.includes(account.id),
});
const suggestedItems = suggestedAccountIds.map((id) => ({
const suggestedItems = suggestedAccounts.map(({ id, feature_approval }) => ({
id,
isDisabled: accountIds.includes(id),
// Disable accounts who can't be added to a collection
isDisabled: !['automatic', 'manual'].includes(
feature_approval.current_user,
),
}));
const handleSearchValueChange = useCallback(
@@ -140,43 +142,6 @@ export const CollectionAccounts: React.FC<{
[],
);
const relationships = useAppSelector((state) => state.relationships);
const confirmFollowStatus = useCallback(
(accountId: string, onFollowing: () => void) => {
const relationship = relationships.get(accountId);
if (!relationship) {
return;
}
if (
accountId === me ||
relationship.following ||
relationship.requested
) {
onFollowing();
} else {
dispatch(
openModal({
modalType: 'CONFIRM_FOLLOW_TO_COLLECTION',
modalProps: {
accountId,
onConfirm: () => {
apiFollowAccount(accountId)
.then(onFollowing)
.catch((err: unknown) => {
store.dispatch(showAlertForError(err));
});
},
},
}),
);
}
},
[dispatch, relationships],
);
const removeAccountItem = useCallback(
(accountId: string) => {
dispatch(
@@ -191,16 +156,14 @@ export const CollectionAccounts: React.FC<{
const addAccountItem = useCallback(
(item: SuggestionItem) => {
confirmFollowStatus(item.id, () => {
dispatch(
updateCollectionEditorField({
field: 'accountIds',
value: [...accountIds, item.id],
}),
);
});
dispatch(
updateCollectionEditorField({
field: 'accountIds',
value: [...accountIds, item.id],
}),
);
},
[accountIds, confirmFollowStatus, dispatch],
[accountIds, dispatch],
);
const instantRemoveAccountItem = useCallback(
@@ -227,15 +190,13 @@ export const CollectionAccounts: React.FC<{
const instantAddAccountItem = useCallback(
(item: SuggestionItem) => {
confirmFollowStatus(item.id, () => {
if (id) {
void dispatch(
addCollectionItem({ collectionId: id, accountId: item.id }),
);
}
});
if (id) {
void dispatch(
addCollectionItem({ collectionId: id, accountId: item.id }),
);
}
},
[confirmFollowStatus, dispatch, id],
[dispatch, id],
);
const handleRemoveAccountItem = useCallback(
@@ -307,6 +268,7 @@ export const CollectionAccounts: React.FC<{
isLoading={isLoadingSuggestions}
items={suggestedItems}
getItemId={getItemId}
getIsItemDisabled={getIsItemDisabled}
renderItem={renderAccountItem}
onSelectItem={handleSelectItem}
status={

View File

@@ -164,7 +164,7 @@ const ListMembers: React.FC<{
const [mode, setMode] = useState<Mode>('remove');
const {
accountIds: searchAccountIds,
accounts: accountsFromSearch,
isLoading: loadingSearchResults,
searchAccounts: handleSearch,
} = useSearchAccounts({
@@ -177,6 +177,7 @@ const ListMembers: React.FC<{
}
},
});
const accountIdsFromSearch = accountsFromSearch.map((item) => item.id);
useEffect(() => {
if (id) {
@@ -220,7 +221,7 @@ const ListMembers: React.FC<{
let displayedAccountIds: string[];
if (mode === 'add' && searching) {
displayedAccountIds = searchAccountIds;
displayedAccountIds = accountIdsFromSearch;
} else {
displayedAccountIds = accountIds;
}

View File

@@ -1,43 +0,0 @@
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useAccount } from 'mastodon/hooks/useAccount';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
title: {
id: 'confirmations.follow_to_collection.title',
defaultMessage: 'Follow account?',
},
confirm: {
id: 'confirmations.follow_to_collection.confirm',
defaultMessage: 'Follow and add to collection',
},
});
export const ConfirmFollowToCollectionModal: React.FC<
{
accountId: string;
onConfirm: () => void;
} & BaseConfirmationModalProps
> = ({ accountId, onConfirm, onClose }) => {
const intl = useIntl();
const account = useAccount(accountId);
return (
<ConfirmationModal
title={intl.formatMessage(messages.title)}
message={
<FormattedMessage
id='confirmations.follow_to_collection.message'
defaultMessage='You need to be following {name} to add them to a collection.'
values={{ name: <strong>@{account?.acct}</strong> }}
/>
}
confirm={intl.formatMessage(messages.confirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View File

@@ -13,7 +13,6 @@ export { ConfirmUnblockModal } from './unblock';
export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out';
export { ConfirmFollowToListModal } from './follow_to_list';
export { ConfirmFollowToCollectionModal } from './follow_to_collection';
export { ConfirmMissingAltTextModal } from './missing_alt_text';
export { ConfirmRevokeQuoteModal } from './revoke_quote';
export { QuietPostQuoteInfoModal } from './quiet_post_quote_info';

View File

@@ -39,7 +39,6 @@ import {
ConfirmClearNotificationsModal,
ConfirmLogOutModal,
ConfirmFollowToListModal,
ConfirmFollowToCollectionModal,
ConfirmMissingAltTextModal,
ConfirmRevokeQuoteModal,
QuietPostQuoteInfoModal,
@@ -69,7 +68,6 @@ export const MODAL_COMPONENTS = {
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
'CONFIRM_FOLLOW_TO_COLLECTION': () => Promise.resolve({ default: ConfirmFollowToCollectionModal }),
'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
'CONFIRM_PRIVATE_QUOTE_NOTIFY': () => Promise.resolve({ default: PrivateQuoteNotify }),
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),

View File

@@ -21,7 +21,7 @@ export function useSearchAccounts({
} = {}) {
const dispatch = useAppDispatch();
const [accountIds, setAccountIds] = useState<string[]>([]);
const [accounts, setAccounts] = useState<ApiAccountJSON[]>([]);
const [loadingState, setLoadingState] = useState<
'idle' | 'loading' | 'error'
>('idle');
@@ -37,7 +37,7 @@ export function useSearchAccounts({
if (value.trim().length === 0) {
onSettled?.('');
if (resetOnInputClear) {
setAccountIds([]);
setAccounts([]);
}
return;
}
@@ -60,7 +60,7 @@ export function useSearchAccounts({
if (withRelationships) {
dispatch(fetchRelationships(accountIds));
}
setAccountIds(accountIds);
setAccounts(accounts);
setLoadingState('idle');
onSettled?.(value);
})
@@ -74,13 +74,13 @@ export function useSearchAccounts({
);
const resetAccounts = useCallback(() => {
setAccountIds([]);
setAccounts([]);
}, []);
return {
searchAccounts,
resetAccounts,
accountIds,
accounts,
isLoading: loadingState === 'loading',
isError: loadingState === 'error',
};

View File

@@ -505,9 +505,6 @@
"confirmations.discard_draft.post.title": "Discard your draft post?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.follow_to_collection.confirm": "Follow and add to collection",
"confirmations.follow_to_collection.message": "You need to be following {name} to add them to a collection.",
"confirmations.follow_to_collection.title": "Follow account?",
"confirmations.follow_to_list.confirm": "Follow and add to list",
"confirmations.follow_to_list.message": "You need to be following {name} to add them to a list.",
"confirmations.follow_to_list.title": "Follow user?",