diff --git a/app/javascript/mastodon/actions/accounts_typed.ts b/app/javascript/mastodon/actions/accounts_typed.ts index 058a68a099..fcdec97e08 100644 --- a/app/javascript/mastodon/actions/accounts_typed.ts +++ b/app/javascript/mastodon/actions/accounts_typed.ts @@ -1,7 +1,9 @@ import { createAction } from '@reduxjs/toolkit'; +import { apiRemoveAccountFromFollowers } from 'mastodon/api/accounts'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; export const revealAccount = createAction<{ id: string; @@ -95,3 +97,10 @@ export const fetchRelationshipsSuccess = createAction( 'relationships/fetch/SUCCESS', actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>, ); + +export const removeAccountFromFollowers = createDataLoadingThunk( + 'accounts/remove_from_followers', + ({ accountId }: { accountId: string }) => + apiRemoveAccountFromFollowers(accountId), + (relationship) => ({ relationship }), +); diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index 717010ba74..c574a47459 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -18,3 +18,8 @@ export const apiFollowAccount = ( export const apiUnfollowAccount = (id: string) => apiRequestPost(`v1/accounts/${id}/unfollow`); + +export const apiRemoveAccountFromFollowers = (id: string) => + apiRequestPost( + `v1/accounts/${id}/remove_from_followers`, + ); diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index 7f5f9d964f..9d4825d302 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -18,6 +18,7 @@ import { unmuteAccount, pinAccount, unpinAccount, + removeAccountFromFollowers, } from 'mastodon/actions/accounts'; import { initBlockModal } from 'mastodon/actions/blocks'; import { mentionCompose, directCompose } from 'mastodon/actions/compose'; @@ -152,6 +153,23 @@ const messages = defineMessages({ id: 'account.open_original_page', defaultMessage: 'Open original page', }, + removeFromFollowers: { + id: 'account.remove_from_followers', + defaultMessage: 'Remove {name} from followers', + }, + confirmRemoveFromFollowersTitle: { + id: 'confirmations.remove_from_followers.title', + defaultMessage: 'Remove follower?', + }, + confirmRemoveFromFollowersMessage: { + id: 'confirmations.remove_from_followers.message', + defaultMessage: + '{name} will stop following you. Are you sure you want to proceed?', + }, + confirmRemoveFromFollowersButton: { + id: 'confirmations.remove_from_followers.confirm', + defaultMessage: 'Remove follower', + }, }); const titleFromAccount = (account: Account) => { @@ -494,6 +512,39 @@ export const AccountHeader: React.FC<{ arr.push(null); } + if (relationship?.followed_by) { + const handleRemoveFromFollowers = () => { + dispatch( + openModal({ + modalType: 'CONFIRM', + modalProps: { + title: intl.formatMessage( + messages.confirmRemoveFromFollowersTitle, + ), + message: intl.formatMessage( + messages.confirmRemoveFromFollowersMessage, + { name: {account.acct} }, + ), + confirm: intl.formatMessage( + messages.confirmRemoveFromFollowersButton, + ), + onConfirm: () => { + void dispatch(removeAccountFromFollowers({ accountId })); + }, + }, + }), + ); + }; + + arr.push({ + text: intl.formatMessage(messages.removeFromFollowers, { + name: account.username, + }), + action: handleRemoveFromFollowers, + dangerous: true, + }); + } + if (relationship?.muting) { arr.push({ text: intl.formatMessage(messages.unmute, { @@ -592,6 +643,8 @@ export const AccountHeader: React.FC<{ return arr; }, [ + dispatch, + accountId, account, relationship, permissions, diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 9df0c8422b..d9a0c8eb03 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -62,6 +62,7 @@ "account.open_original_page": "Open original page", "account.posts": "Posts", "account.posts_with_replies": "Posts and replies", + "account.remove_from_followers": "Remove {name} from followers", "account.report": "Report @{name}", "account.requested": "Awaiting approval. Click to cancel follow request", "account.requested_follow": "{name} has requested to follow you", @@ -233,6 +234,9 @@ "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.", "confirmations.redraft.title": "Delete & redraft post?", + "confirmations.remove_from_followers.confirm": "Remove follower", + "confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?", + "confirmations.remove_from_followers.title": "Remove follower?", "confirmations.reply.confirm": "Reply", "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.reply.title": "Overwrite post?", diff --git a/app/javascript/mastodon/reducers/relationships.ts b/app/javascript/mastodon/reducers/relationships.ts index dcca11b203..9df81c75ea 100644 --- a/app/javascript/mastodon/reducers/relationships.ts +++ b/app/javascript/mastodon/reducers/relationships.ts @@ -24,6 +24,7 @@ import { pinAccountSuccess, unpinAccountSuccess, fetchRelationshipsSuccess, + removeAccountFromFollowers, } from '../actions/accounts_typed'; import { blockDomainSuccess, @@ -109,7 +110,8 @@ export const relationshipsReducer: Reducer = ( unmuteAccountSuccess.match(action) || pinAccountSuccess.match(action) || unpinAccountSuccess.match(action) || - isFulfilled(submitAccountNote)(action) + isFulfilled(submitAccountNote)(action) || + isFulfilled(removeAccountFromFollowers)(action) ) return normalizeRelationship(state, action.payload.relationship); else if (fetchRelationshipsSuccess.match(action))