[Glitch] Require following accounts before being able to add them to a collection

Port 68a7cd404d to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion
2026-02-20 14:14:36 +01:00
committed by Claire
parent 85f72a3940
commit c16cbb98e5
6 changed files with 138 additions and 21 deletions

View File

@@ -6,6 +6,9 @@ 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 { showAlertForError } from 'flavours/glitch/actions/alerts';
import { openModal } from 'flavours/glitch/actions/modal';
import { apiFollowAccount } from 'flavours/glitch/api/accounts';
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
import { Account } from 'flavours/glitch/components/account';
import { Avatar } from 'flavours/glitch/components/avatar';
@@ -21,11 +24,13 @@ import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
import { useSearchAccounts } from 'flavours/glitch/features/lists/use_search_accounts';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import { me } from 'flavours/glitch/initial_state';
import {
addCollectionItem,
removeCollectionItem,
} from 'flavours/glitch/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import { store, useAppDispatch, useAppSelector } from 'flavours/glitch/store';
import type { TempCollectionState } from './state';
import { getCollectionEditorState } from './state';
@@ -69,7 +74,7 @@ interface SuggestionItem {
}
const SuggestedAccountItem: React.FC<SuggestionItem> = ({ id, isSelected }) => {
const account = useAppSelector((state) => state.accounts.get(id));
const account = useAccount(id);
if (!account) return null;
@@ -133,6 +138,7 @@ export const CollectionAccounts: React.FC<{
isLoading: isLoadingSuggestions,
searchAccounts,
} = useSearchAccounts({
withRelationships: true,
filterResults: (account) =>
// Only suggest accounts who allow being featured/recommended
account.feature_approval.current_user === 'automatic',
@@ -160,14 +166,67 @@ export const CollectionAccounts: React.FC<{
[],
);
const toggleAccountItem = useCallback((item: SuggestionItem) => {
setAccountIds((ids) =>
ids.includes(item.id)
? ids.filter((id) => id !== item.id)
: [...ids, item.id],
);
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) => {
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(
(accountId: string) => {
const itemId = collectionItems?.find(
@@ -190,19 +249,24 @@ export const CollectionAccounts: React.FC<{
[collectionItems, dispatch, id, intl],
);
const instantAddAccountItem = useCallback(
(collectionId: string, accountId: string) => {
confirmFollowStatus(accountId, () => {
void dispatch(addCollectionItem({ collectionId, accountId }));
});
},
[confirmFollowStatus, dispatch],
);
const instantToggleAccountItem = useCallback(
(item: SuggestionItem) => {
if (accountIds.includes(item.id)) {
instantRemoveAccountItem(item.id);
} else {
if (id) {
void dispatch(
addCollectionItem({ collectionId: id, accountId: item.id }),
);
}
} else if (id) {
instantAddAccountItem(id, item.id);
}
},
[accountIds, dispatch, id, instantRemoveAccountItem],
[accountIds, id, instantAddAccountItem, instantRemoveAccountItem],
);
const handleRemoveAccountItem = useCallback(
@@ -210,10 +274,10 @@ export const CollectionAccounts: React.FC<{
if (isEditMode) {
instantRemoveAccountItem(accountId);
} else {
setAccountIds((ids) => ids.filter((id) => id !== accountId));
removeAccountItem(accountId);
}
},
[isEditMode, instantRemoveAccountItem],
[isEditMode, instantRemoveAccountItem, removeAccountItem],
);
const handleSubmit = useCallback(

View File

@@ -2,19 +2,22 @@ import { useRef, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { fetchRelationships } from 'flavours/glitch/actions/accounts';
import { importFetchedAccounts } from 'flavours/glitch/actions/importer';
import { apiRequest } from 'flavours/glitch/api';
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
import { useAppDispatch } from 'flavours/glitch/store';
export function useSearchAccounts({
resetOnInputClear = true,
onSettled,
filterResults,
resetOnInputClear = true,
withRelationships = false,
}: {
onSettled?: (value: string) => void;
filterResults?: (account: ApiAccountJSON) => boolean;
resetOnInputClear?: boolean;
withRelationships?: boolean;
} = {}) {
const dispatch = useAppDispatch();
@@ -52,8 +55,12 @@ export function useSearchAccounts({
})
.then((data) => {
const accounts = filterResults ? data.filter(filterResults) : data;
const accountIds = accounts.map((a) => a.id);
dispatch(importFetchedAccounts(accounts));
setAccountIds(accounts.map((a) => a.id));
if (withRelationships) {
dispatch(fetchRelationships(accountIds));
}
setAccountIds(accountIds);
setLoadingState('idle');
onSettled?.(value);
})

View File

@@ -0,0 +1,43 @@
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useAccount } from 'flavours/glitch/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

@@ -1,6 +1,6 @@
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useAppSelector } from 'flavours/glitch/store';
import { useAccount } from 'flavours/glitch/hooks/useAccount';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
@@ -23,7 +23,7 @@ export const ConfirmFollowToListModal: React.FC<
} & BaseConfirmationModalProps
> = ({ accountId, onConfirm, onClose }) => {
const intl = useIntl();
const account = useAppSelector((state) => state.accounts.get(accountId));
const account = useAccount(accountId);
return (
<ConfirmationModal

View File

@@ -13,6 +13,7 @@ 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,6 +39,7 @@ import {
ConfirmClearNotificationsModal,
ConfirmLogOutModal,
ConfirmFollowToListModal,
ConfirmFollowToCollectionModal,
ConfirmMissingAltTextModal,
ConfirmRevokeQuoteModal,
QuietPostQuoteInfoModal,
@@ -73,6 +74,7 @@ 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 }),