diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index bd1757e827..717010ba74 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -5,3 +5,16 @@ export const apiSubmitAccountNote = (id: string, value: string) => apiRequestPost(`v1/accounts/${id}/note`, { comment: value, }); + +export const apiFollowAccount = ( + id: string, + params?: { + reblogs: boolean; + }, +) => + apiRequestPost(`v1/accounts/${id}/follow`, { + ...params, + }); + +export const apiUnfollowAccount = (id: string) => + apiRequestPost(`v1/accounts/${id}/unfollow`); diff --git a/app/javascript/mastodon/features/lists/members.tsx b/app/javascript/mastodon/features/lists/members.tsx index 80ea08e89a..e635304ed1 100644 --- a/app/javascript/mastodon/features/lists/members.tsx +++ b/app/javascript/mastodon/features/lists/members.tsx @@ -9,10 +9,12 @@ import { useDebouncedCallback } from 'use-debounce'; 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 { fetchFollowing, fetchRelationships } from 'mastodon/actions/accounts'; import { importFetchedAccounts } from 'mastodon/actions/importer'; import { fetchList } from 'mastodon/actions/lists'; +import { openModal } from 'mastodon/actions/modal'; import { apiRequest } from 'mastodon/api'; +import { apiFollowAccount } from 'mastodon/api/accounts'; import { apiGetAccounts, apiAddAccountToList, @@ -35,8 +37,8 @@ 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', + id: 'lists.search', + defaultMessage: 'Search', }, enterSearch: { id: 'lists.add_to_list', defaultMessage: 'Add to list' }, add: { id: 'lists.add_member', defaultMessage: 'Add' }, @@ -53,17 +55,50 @@ const AccountItem: React.FC<{ onToggle: (accountId: string) => void; }> = ({ accountId, listId, partOfList, onToggle }) => { const intl = useIntl(); + const dispatch = useAppDispatch(); const account = useAppSelector((state) => state.accounts.get(accountId)); + const relationship = useAppSelector((state) => + accountId ? state.relationships.get(accountId) : undefined, + ); + const following = relationship?.following || relationship?.requested; + + useEffect(() => { + if (accountId) { + dispatch(fetchRelationships([accountId])); + } + }, [dispatch, accountId]); const handleClick = useCallback(() => { if (partOfList) { void apiRemoveAccountFromList(listId, accountId); + onToggle(accountId); } else { - void apiAddAccountToList(listId, accountId); + if (following) { + void apiAddAccountToList(listId, accountId); + onToggle(accountId); + } else { + dispatch( + openModal({ + modalType: 'CONFIRM_FOLLOW_TO_LIST', + modalProps: { + accountId, + onConfirm: () => { + apiFollowAccount(accountId) + .then(() => apiAddAccountToList(listId, accountId)) + .then(() => { + onToggle(accountId); + return ''; + }) + .catch(() => { + // Nothing + }); + }, + }, + }), + ); + } } - - onToggle(accountId); - }, [accountId, listId, partOfList, onToggle]); + }, [dispatch, accountId, following, listId, partOfList, onToggle]); if (!account) { return null; @@ -193,8 +228,7 @@ const ListMembers: React.FC<{ signal: searchRequestRef.current.signal, params: { q: value, - resolve: false, - following: true, + resolve: true, }, }) .then((data) => { diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/follow_to_list.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/follow_to_list.tsx new file mode 100644 index 0000000000..b862a29827 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/follow_to_list.tsx @@ -0,0 +1,43 @@ +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import { useAppSelector } from 'mastodon/store'; + +import type { BaseConfirmationModalProps } from './confirmation_modal'; +import { ConfirmationModal } from './confirmation_modal'; + +const messages = defineMessages({ + title: { + id: 'confirmations.follow_to_list.title', + defaultMessage: 'Follow user?', + }, + confirm: { + id: 'confirmations.follow_to_list.confirm', + defaultMessage: 'Follow and add to list', + }, +}); + +export const ConfirmFollowToListModal: React.FC< + { + accountId: string; + onConfirm: () => void; + } & BaseConfirmationModalProps +> = ({ accountId, onConfirm, onClose }) => { + const intl = useIntl(); + const account = useAppSelector((state) => state.accounts.get(accountId)); + + return ( + @{account?.acct} }} + /> + } + confirm={intl.formatMessage(messages.confirm)} + onConfirm={onConfirm} + onClose={onClose} + /> + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts index 912c99a393..16478d0d11 100644 --- a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts @@ -6,3 +6,4 @@ export { ConfirmEditStatusModal } from './edit_status'; export { ConfirmUnfollowModal } from './unfollow'; export { ConfirmClearNotificationsModal } from './clear_notifications'; export { ConfirmLogOutModal } from './log_out'; +export { ConfirmFollowToListModal } from './follow_to_list'; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 8a97ec4565..87fc556804 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -36,6 +36,7 @@ import { ConfirmUnfollowModal, ConfirmClearNotificationsModal, ConfirmLogOutModal, + ConfirmFollowToListModal, } from './confirmation_modals'; import FocalPointModal from './focal_point_modal'; import ImageModal from './image_modal'; @@ -57,6 +58,7 @@ export const MODAL_COMPONENTS = { 'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }), 'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }), 'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }), + 'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }), 'MUTE': MuteModal, 'BLOCK': BlockModal, 'DOMAIN_BLOCK': DomainBlockModal, diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 6a44856837..3082c9976b 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -205,6 +205,9 @@ "confirmations.edit.confirm": "Edit", "confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.edit.title": "Overwrite post?", + "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?", "confirmations.logout.confirm": "Log out", "confirmations.logout.message": "Are you sure you want to log out?", "confirmations.logout.title": "Log out?", @@ -492,7 +495,7 @@ "lists.replies_policy.list": "Members of the list", "lists.replies_policy.none": "No one", "lists.save": "Save", - "lists.search_placeholder": "Search people you follow", + "lists.search": "Search", "lists.show_replies_to": "Include replies from list members to", "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading…",