Require following accounts before being able to add them to a collection (#37927)
This commit is contained in:
@@ -6,6 +6,9 @@ import { useHistory, useLocation } from 'react-router-dom';
|
|||||||
|
|
||||||
import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
|
import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
|
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 type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||||
import { Account } from 'mastodon/components/account';
|
import { Account } from 'mastodon/components/account';
|
||||||
import { Avatar } from 'mastodon/components/avatar';
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
@@ -18,11 +21,13 @@ import { Icon } from 'mastodon/components/icon';
|
|||||||
import { IconButton } from 'mastodon/components/icon_button';
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
import { useSearchAccounts } from 'mastodon/features/lists/use_search_accounts';
|
import { useSearchAccounts } from 'mastodon/features/lists/use_search_accounts';
|
||||||
|
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||||
|
import { me } from 'mastodon/initial_state';
|
||||||
import {
|
import {
|
||||||
addCollectionItem,
|
addCollectionItem,
|
||||||
removeCollectionItem,
|
removeCollectionItem,
|
||||||
} from 'mastodon/reducers/slices/collections';
|
} from 'mastodon/reducers/slices/collections';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { store, useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import type { TempCollectionState } from './state';
|
import type { TempCollectionState } from './state';
|
||||||
import { getCollectionEditorState } from './state';
|
import { getCollectionEditorState } from './state';
|
||||||
@@ -66,7 +71,7 @@ interface SuggestionItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SuggestedAccountItem: React.FC<SuggestionItem> = ({ id, isSelected }) => {
|
const SuggestedAccountItem: React.FC<SuggestionItem> = ({ id, isSelected }) => {
|
||||||
const account = useAppSelector((state) => state.accounts.get(id));
|
const account = useAccount(id);
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
@@ -130,6 +135,7 @@ export const CollectionAccounts: React.FC<{
|
|||||||
isLoading: isLoadingSuggestions,
|
isLoading: isLoadingSuggestions,
|
||||||
searchAccounts,
|
searchAccounts,
|
||||||
} = useSearchAccounts({
|
} = useSearchAccounts({
|
||||||
|
withRelationships: true,
|
||||||
filterResults: (account) =>
|
filterResults: (account) =>
|
||||||
// Only suggest accounts who allow being featured/recommended
|
// Only suggest accounts who allow being featured/recommended
|
||||||
account.feature_approval.current_user === 'automatic',
|
account.feature_approval.current_user === 'automatic',
|
||||||
@@ -157,14 +163,67 @@ export const CollectionAccounts: React.FC<{
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleAccountItem = useCallback((item: SuggestionItem) => {
|
const relationships = useAppSelector((state) => state.relationships);
|
||||||
setAccountIds((ids) =>
|
|
||||||
ids.includes(item.id)
|
const confirmFollowStatus = useCallback(
|
||||||
? ids.filter((id) => id !== item.id)
|
(accountId: string, onFollowing: () => void) => {
|
||||||
: [...ids, item.id],
|
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) => {
|
||||||
|
setAccountIds((ids) => ids.filter((id) => id !== accountId));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const addAccountItem = useCallback(
|
||||||
|
(accountId: string) => {
|
||||||
|
confirmFollowStatus(accountId, () => {
|
||||||
|
setAccountIds((ids) => [...ids, accountId]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[confirmFollowStatus],
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleAccountItem = useCallback(
|
||||||
|
(item: SuggestionItem) => {
|
||||||
|
if (addedAccountIds.includes(item.id)) {
|
||||||
|
removeAccountItem(item.id);
|
||||||
|
} else {
|
||||||
|
addAccountItem(item.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[addAccountItem, addedAccountIds, removeAccountItem],
|
||||||
|
);
|
||||||
|
|
||||||
const instantRemoveAccountItem = useCallback(
|
const instantRemoveAccountItem = useCallback(
|
||||||
(accountId: string) => {
|
(accountId: string) => {
|
||||||
const itemId = collectionItems?.find(
|
const itemId = collectionItems?.find(
|
||||||
@@ -187,19 +246,24 @@ export const CollectionAccounts: React.FC<{
|
|||||||
[collectionItems, dispatch, id, intl],
|
[collectionItems, dispatch, id, intl],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const instantAddAccountItem = useCallback(
|
||||||
|
(collectionId: string, accountId: string) => {
|
||||||
|
confirmFollowStatus(accountId, () => {
|
||||||
|
void dispatch(addCollectionItem({ collectionId, accountId }));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[confirmFollowStatus, dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
const instantToggleAccountItem = useCallback(
|
const instantToggleAccountItem = useCallback(
|
||||||
(item: SuggestionItem) => {
|
(item: SuggestionItem) => {
|
||||||
if (accountIds.includes(item.id)) {
|
if (accountIds.includes(item.id)) {
|
||||||
instantRemoveAccountItem(item.id);
|
instantRemoveAccountItem(item.id);
|
||||||
} else {
|
} else if (id) {
|
||||||
if (id) {
|
instantAddAccountItem(id, item.id);
|
||||||
void dispatch(
|
|
||||||
addCollectionItem({ collectionId: id, accountId: item.id }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[accountIds, dispatch, id, instantRemoveAccountItem],
|
[accountIds, id, instantAddAccountItem, instantRemoveAccountItem],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRemoveAccountItem = useCallback(
|
const handleRemoveAccountItem = useCallback(
|
||||||
@@ -207,10 +271,10 @@ export const CollectionAccounts: React.FC<{
|
|||||||
if (isEditMode) {
|
if (isEditMode) {
|
||||||
instantRemoveAccountItem(accountId);
|
instantRemoveAccountItem(accountId);
|
||||||
} else {
|
} else {
|
||||||
setAccountIds((ids) => ids.filter((id) => id !== accountId));
|
removeAccountItem(accountId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isEditMode, instantRemoveAccountItem],
|
[isEditMode, instantRemoveAccountItem, removeAccountItem],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
|
|||||||
@@ -2,19 +2,22 @@ import { useRef, useState } from 'react';
|
|||||||
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
import { fetchRelationships } from 'mastodon/actions/accounts';
|
||||||
import { importFetchedAccounts } from 'mastodon/actions/importer';
|
import { importFetchedAccounts } from 'mastodon/actions/importer';
|
||||||
import { apiRequest } from 'mastodon/api';
|
import { apiRequest } from 'mastodon/api';
|
||||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||||
import { useAppDispatch } from 'mastodon/store';
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
export function useSearchAccounts({
|
export function useSearchAccounts({
|
||||||
resetOnInputClear = true,
|
|
||||||
onSettled,
|
onSettled,
|
||||||
filterResults,
|
filterResults,
|
||||||
|
resetOnInputClear = true,
|
||||||
|
withRelationships = false,
|
||||||
}: {
|
}: {
|
||||||
onSettled?: (value: string) => void;
|
onSettled?: (value: string) => void;
|
||||||
filterResults?: (account: ApiAccountJSON) => boolean;
|
filterResults?: (account: ApiAccountJSON) => boolean;
|
||||||
resetOnInputClear?: boolean;
|
resetOnInputClear?: boolean;
|
||||||
|
withRelationships?: boolean;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@@ -52,8 +55,12 @@ export function useSearchAccounts({
|
|||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
const accounts = filterResults ? data.filter(filterResults) : data;
|
const accounts = filterResults ? data.filter(filterResults) : data;
|
||||||
|
const accountIds = accounts.map((a) => a.id);
|
||||||
dispatch(importFetchedAccounts(accounts));
|
dispatch(importFetchedAccounts(accounts));
|
||||||
setAccountIds(accounts.map((a) => a.id));
|
if (withRelationships) {
|
||||||
|
dispatch(fetchRelationships(accountIds));
|
||||||
|
}
|
||||||
|
setAccountIds(accountIds);
|
||||||
setLoadingState('idle');
|
setLoadingState('idle');
|
||||||
onSettled?.(value);
|
onSettled?.(value);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { useAppSelector } from 'mastodon/store';
|
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||||
|
|
||||||
import type { BaseConfirmationModalProps } from './confirmation_modal';
|
import type { BaseConfirmationModalProps } from './confirmation_modal';
|
||||||
import { ConfirmationModal } from './confirmation_modal';
|
import { ConfirmationModal } from './confirmation_modal';
|
||||||
@@ -23,7 +23,7 @@ export const ConfirmFollowToListModal: React.FC<
|
|||||||
} & BaseConfirmationModalProps
|
} & BaseConfirmationModalProps
|
||||||
> = ({ accountId, onConfirm, onClose }) => {
|
> = ({ accountId, onConfirm, onClose }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
const account = useAccount(accountId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export { ConfirmUnblockModal } from './unblock';
|
|||||||
export { ConfirmClearNotificationsModal } from './clear_notifications';
|
export { ConfirmClearNotificationsModal } from './clear_notifications';
|
||||||
export { ConfirmLogOutModal } from './log_out';
|
export { ConfirmLogOutModal } from './log_out';
|
||||||
export { ConfirmFollowToListModal } from './follow_to_list';
|
export { ConfirmFollowToListModal } from './follow_to_list';
|
||||||
|
export { ConfirmFollowToCollectionModal } from './follow_to_collection';
|
||||||
export { ConfirmMissingAltTextModal } from './missing_alt_text';
|
export { ConfirmMissingAltTextModal } from './missing_alt_text';
|
||||||
export { ConfirmRevokeQuoteModal } from './revoke_quote';
|
export { ConfirmRevokeQuoteModal } from './revoke_quote';
|
||||||
export { QuietPostQuoteInfoModal } from './quiet_post_quote_info';
|
export { QuietPostQuoteInfoModal } from './quiet_post_quote_info';
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
ConfirmClearNotificationsModal,
|
ConfirmClearNotificationsModal,
|
||||||
ConfirmLogOutModal,
|
ConfirmLogOutModal,
|
||||||
ConfirmFollowToListModal,
|
ConfirmFollowToListModal,
|
||||||
|
ConfirmFollowToCollectionModal,
|
||||||
ConfirmMissingAltTextModal,
|
ConfirmMissingAltTextModal,
|
||||||
ConfirmRevokeQuoteModal,
|
ConfirmRevokeQuoteModal,
|
||||||
QuietPostQuoteInfoModal,
|
QuietPostQuoteInfoModal,
|
||||||
@@ -67,6 +68,7 @@ export const MODAL_COMPONENTS = {
|
|||||||
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
|
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
|
||||||
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
|
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
|
||||||
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
|
'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_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
|
||||||
'CONFIRM_PRIVATE_QUOTE_NOTIFY': () => Promise.resolve({ default: PrivateQuoteNotify }),
|
'CONFIRM_PRIVATE_QUOTE_NOTIFY': () => Promise.resolve({ default: PrivateQuoteNotify }),
|
||||||
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),
|
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),
|
||||||
|
|||||||
@@ -390,6 +390,9 @@
|
|||||||
"confirmations.discard_draft.post.title": "Discard your draft post?",
|
"confirmations.discard_draft.post.title": "Discard your draft post?",
|
||||||
"confirmations.discard_edit_media.confirm": "Discard",
|
"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.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.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.message": "You need to be following {name} to add them to a list.",
|
||||||
"confirmations.follow_to_list.title": "Follow user?",
|
"confirmations.follow_to_list.title": "Follow user?",
|
||||||
|
|||||||
Reference in New Issue
Block a user