Allow viewing and severing relationships with suspended accounts (#27667)
parent
b87bfb8c96
commit
c451bbe249
|
@ -5,10 +5,11 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
|
|
||||||
def index
|
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
|
# .where doesn't guarantee that our results are in the same order
|
||||||
# we requested them, so return the "right" order to the requestor.
|
# 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
|
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -460,7 +460,7 @@ export function fetchRelationships(accountIds) {
|
||||||
|
|
||||||
dispatch(fetchRelationshipsRequest(newAccountIds));
|
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 }));
|
dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchRelationshipsFail(error));
|
dispatch(fetchRelationshipsFail(error));
|
||||||
|
|
|
@ -119,7 +119,7 @@ class Account extends ImmutablePureComponent {
|
||||||
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
|
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
|
||||||
} else if (defaultAction === 'block') {
|
} else if (defaultAction === 'block') {
|
||||||
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
|
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} />;
|
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,7 +289,7 @@ class Header extends ImmutablePureComponent {
|
||||||
lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />;
|
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.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({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
@ -299,7 +299,7 @@ class Header extends ImmutablePureComponent {
|
||||||
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
|
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({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
@ -347,8 +347,10 @@ 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.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!account.get('suspended')) {
|
||||||
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
|
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (signedIn && isRemote) {
|
if (signedIn && isRemote) {
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
@ -395,7 +397,7 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
<div className='account__header__image'>
|
<div className='account__header__image'>
|
||||||
<div className='account__header__info'>
|
<div className='account__header__info'>
|
||||||
{!suspended && info}
|
{info}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
|
{!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
|
||||||
|
@ -407,7 +409,6 @@ class Header extends ImmutablePureComponent {
|
||||||
<Avatar account={suspended || hidden ? undefined : account} size={90} />
|
<Avatar account={suspended || hidden ? undefined : account} size={90} />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{!suspended && (
|
|
||||||
<div className='account__header__tabs__buttons'>
|
<div className='account__header__tabs__buttons'>
|
||||||
{!hidden && (
|
{!hidden && (
|
||||||
<>
|
<>
|
||||||
|
@ -418,7 +419,6 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account__header__tabs__name'>
|
<div className='account__header__tabs__name'>
|
||||||
|
|
|
@ -301,6 +301,10 @@ namespace :api, format: false do
|
||||||
resources :statuses, only: [:show, :destroy]
|
resources :statuses, only: [:show, :destroy]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
namespace :accounts do
|
||||||
|
resources :relationships, only: :index
|
||||||
|
end
|
||||||
|
|
||||||
namespace :admin do
|
namespace :admin do
|
||||||
resources :accounts, only: [:index]
|
resources :accounts, only: [:index]
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
Loading…
Reference in New Issue