diff options
author | Claire <claire.github-309c@sitedethib.com> | 2023-11-09 15:50:25 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-09 14:50:25 +0000 |
commit | c451bbe249475b937906f8f4d85c1f1eb9804343 (patch) | |
tree | 4ae58d30dd50d19d7e4da8ec3f3d603ff3acaa05 | |
parent | b87bfb8c96c8491f1228e0258d05119f3420db05 (diff) |
Allow viewing and severing relationships with suspended accounts (#27667)
-rw-r--r-- | app/controllers/api/v1/accounts/relationships_controller.rb | 5 | ||||
-rw-r--r-- | app/javascript/mastodon/actions/accounts.js | 2 | ||||
-rw-r--r-- | app/javascript/mastodon/components/account.jsx | 2 | ||||
-rw-r--r-- | app/javascript/mastodon/features/account/components/header.jsx | 30 | ||||
-rw-r--r-- | config/routes/api.rb | 4 | ||||
-rw-r--r-- | spec/controllers/api/v1/accounts/relationships_controller_spec.rb | 102 | ||||
-rw-r--r-- | spec/requests/api/v1/accounts/relationships_spec.rb | 133 |
7 files changed, 157 insertions, 121 deletions
diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index 503f85c97d7..320084efb5c 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -5,10 +5,11 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController before_action :require_user! def index - accounts = Account.without_suspended.where(id: account_ids).select('id') + scope = Account.where(id: account_ids).select('id') + scope.merge!(Account.without_suspended) unless truthy_param?(:with_suspended) # .where doesn't guarantee that our results are in the same order # we requested them, so return the "right" order to the requestor. - @accounts = accounts.index_by(&:id).values_at(*account_ids).compact + @accounts = scope.index_by(&:id).values_at(*account_ids).compact render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships end diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index 4a985a41eff..e0448f004c7 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -460,7 +460,7 @@ export function fetchRelationships(accountIds) { dispatch(fetchRelationshipsRequest(newAccountIds)); - api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { + api(getState).get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { dispatch(fetchRelationshipsSuccess({ relationships: response.data })); }).catch(error => { dispatch(fetchRelationshipsFail(error)); diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index aa18ce79a5e..f82dd9153a9 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -119,7 +119,7 @@ class Account extends ImmutablePureComponent { buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />; } else if (defaultAction === 'block') { buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />; - } else if (!account.get('moved') || following) { + } else if (!account.get('suspended') && !account.get('moved') || following) { buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />; } } diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index e546c756934..7594135a4ea 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -289,7 +289,7 @@ class Header extends ImmutablePureComponent { lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />; } - if (signedIn && account.get('id') !== me) { + if (signedIn && account.get('id') !== me && !account.get('suspended')) { menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); menu.push(null); @@ -299,7 +299,7 @@ class Header extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') }); } - if ('share' in navigator) { + if ('share' in navigator && !account.get('suspended')) { menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); menu.push(null); } @@ -347,7 +347,9 @@ class Header extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true }); } - menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true }); + if (!account.get('suspended')) { + menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true }); + } } if (signedIn && isRemote) { @@ -395,7 +397,7 @@ class Header extends ImmutablePureComponent { <div className='account__header__image'> <div className='account__header__info'> - {!suspended && info} + {info} </div> {!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />} @@ -407,18 +409,16 @@ class Header extends ImmutablePureComponent { <Avatar account={suspended || hidden ? undefined : account} size={90} /> </a> - {!suspended && ( - <div className='account__header__tabs__buttons'> - {!hidden && ( - <> - {actionBtn} - {bellBtn} - </> - )} + <div className='account__header__tabs__buttons'> + {!hidden && ( + <> + {actionBtn} + {bellBtn} + </> + )} - <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' /> - </div> - )} + <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' /> + </div> </div> <div className='account__header__tabs__name'> diff --git a/config/routes/api.rb b/config/routes/api.rb index 0fe9f69abcd..ebfa60a690a 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -301,6 +301,10 @@ namespace :api, format: false do resources :statuses, only: [:show, :destroy] end + namespace :accounts do + resources :relationships, only: :index + end + namespace :admin do resources :accounts, only: [:index] end diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb deleted file mode 100644 index 5ba6f2a1f8d..00000000000 --- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb +++ /dev/null @@ -1,102 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe Api::V1::Accounts::RelationshipsController do - render_views - - let(:user) { Fabricate(:user) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:follows') } - - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #index' do - let(:simon) { Fabricate(:account) } - let(:lewis) { Fabricate(:account) } - - before do - user.account.follow!(simon) - lewis.follow!(user.account) - end - - context 'when provided only one ID' do - before do - get :index, params: { id: simon.id } - end - - it 'returns JSON with correct data', :aggregate_failures do - json = body_as_json - - expect(response).to have_http_status(200) - expect(json).to be_a Enumerable - expect(json.first[:following]).to be true - expect(json.first[:followed_by]).to be false - end - end - - context 'when provided multiple IDs' do - before do - get :index, params: { id: [simon.id, lewis.id] } - end - - it 'returns http success' do - expect(response).to have_http_status(200) - end - - context 'when there is returned JSON data' do - let(:json) { body_as_json } - - it 'returns an enumerable json with correct elements', :aggregate_failures do - expect(json).to be_a Enumerable - - expect_simon_item_one - expect_lewis_item_two - end - - def expect_simon_item_one - expect(json.first[:id]).to eq simon.id.to_s - expect(json.first[:following]).to be true - expect(json.first[:showing_reblogs]).to be true - expect(json.first[:followed_by]).to be false - expect(json.first[:muting]).to be false - expect(json.first[:requested]).to be false - expect(json.first[:domain_blocking]).to be false - end - - def expect_lewis_item_two - expect(json.second[:id]).to eq lewis.id.to_s - expect(json.second[:following]).to be false - expect(json.second[:showing_reblogs]).to be false - expect(json.second[:followed_by]).to be true - expect(json.second[:muting]).to be false - expect(json.second[:requested]).to be false - expect(json.second[:domain_blocking]).to be false - end - end - - it 'returns JSON with correct data on cached requests too' do - get :index, params: { id: [simon.id] } - - json = body_as_json - - expect(json).to be_a Enumerable - expect(json.first[:following]).to be true - expect(json.first[:showing_reblogs]).to be true - end - - it 'returns JSON with correct data after change too' do - user.account.unfollow!(simon) - - get :index, params: { id: [simon.id] } - - json = body_as_json - - expect(json).to be_a Enumerable - expect(json.first[:following]).to be false - expect(json.first[:showing_reblogs]).to be false - end - end - end -end diff --git a/spec/requests/api/v1/accounts/relationships_spec.rb b/spec/requests/api/v1/accounts/relationships_spec.rb new file mode 100644 index 00000000000..bb78e3b3e48 --- /dev/null +++ b/spec/requests/api/v1/accounts/relationships_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'GET /api/v1/accounts/relationships' do + subject do + get '/api/v1/accounts/relationships', headers: headers, params: params + end + + let(:user) { Fabricate(:user) } + let(:scopes) { 'read:follows' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + let(:simon) { Fabricate(:account) } + let(:lewis) { Fabricate(:account) } + let(:bob) { Fabricate(:account, suspended: true) } + + before do + user.account.follow!(simon) + lewis.follow!(user.account) + end + + context 'when provided only one ID' do + let(:params) { { id: simon.id } } + + it 'returns JSON with correct data', :aggregate_failures do + subject + + json = body_as_json + + expect(response).to have_http_status(200) + expect(json).to be_a Enumerable + expect(json.first[:following]).to be true + expect(json.first[:followed_by]).to be false + end + end + + context 'when provided multiple IDs' do + let(:params) { { id: [simon.id, lewis.id, bob.id] } } + + context 'when there is returned JSON data' do + let(:json) { body_as_json } + + context 'with default parameters' do + it 'returns an enumerable json with correct elements, excluding suspended accounts', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(json).to be_a Enumerable + expect(json.size).to eq 2 + + expect_simon_item_one + expect_lewis_item_two + end + end + + context 'with `with_suspended` parameter' do + let(:params) { { id: [simon.id, lewis.id, bob.id], with_suspended: true } } + + it 'returns an enumerable json with correct elements, including suspended accounts', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(json).to be_a Enumerable + expect(json.size).to eq 3 + + expect_simon_item_one + expect_lewis_item_two + expect_bob_item_three + end + end + + def expect_simon_item_one + expect(json.first[:id]).to eq simon.id.to_s + expect(json.first[:following]).to be true + expect(json.first[:showing_reblogs]).to be true + expect(json.first[:followed_by]).to be false + expect(json.first[:muting]).to be false + expect(json.first[:requested]).to be false + expect(json.first[:domain_blocking]).to be false + end + + def expect_lewis_item_two + expect(json.second[:id]).to eq lewis.id.to_s + expect(json.second[:following]).to be false + expect(json.second[:showing_reblogs]).to be false + expect(json.second[:followed_by]).to be true + expect(json.second[:muting]).to be false + expect(json.second[:requested]).to be false + expect(json.second[:domain_blocking]).to be false + end + + def expect_bob_item_three + expect(json.third[:id]).to eq bob.id.to_s + expect(json.third[:following]).to be false + expect(json.third[:showing_reblogs]).to be false + expect(json.third[:followed_by]).to be false + expect(json.third[:muting]).to be false + expect(json.third[:requested]).to be false + expect(json.third[:domain_blocking]).to be false + end + end + + it 'returns JSON with correct data on cached requests too' do + subject + subject + + expect(response).to have_http_status(200) + + json = body_as_json + + expect(json).to be_a Enumerable + expect(json.first[:following]).to be true + expect(json.first[:showing_reblogs]).to be true + end + + it 'returns JSON with correct data after change too' do + subject + user.account.unfollow!(simon) + + get '/api/v1/accounts/relationships', headers: headers, params: { id: [simon.id] } + + expect(response).to have_http_status(200) + + json = body_as_json + + expect(json).to be_a Enumerable + expect(json.first[:following]).to be false + expect(json.first[:showing_reblogs]).to be false + end + end +end |