* Make private toots get PuSHed to subscription URLs that belong to domains where you have approved followers * Authorized followers controller, stub for bulk action * Soft block in the background * Add simple test for new controller * Rename Settings::FollowersController to Settings::FollowerDomainsController, paginate results, rename "private" post setting to "followers-only", fix pagination style, improve post privacy preferences style, improve warning style * Extract compose form warnings into own container, show warning when posting to followers-only with unlocked accountclosed-social-glitch-2
@ -0,0 +1,25 @@ | |||
import PropTypes from 'prop-types'; | |||
class Warning extends React.PureComponent { | |||
constructor (props) { | |||
super(props); | |||
} | |||
render () { | |||
const { message } = this.props; | |||
return ( | |||
<div className='compose-form__warning'> | |||
{message} | |||
</div> | |||
); | |||
} | |||
} | |||
Warning.propTypes = { | |||
message: PropTypes.node.isRequired | |||
}; | |||
export default Warning; |
@ -0,0 +1,48 @@ | |||
import { connect } from 'react-redux'; | |||
import Warning from '../components/warning'; | |||
import { createSelector } from 'reselect'; | |||
import PropTypes from 'prop-types'; | |||
import { FormattedMessage } from 'react-intl'; | |||
const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig)); | |||
const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => { | |||
return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : []; | |||
}); | |||
const mapStateToProps = state => { | |||
const mentionedUsernames = getMentionedUsernames(state); | |||
const mentionedUsernamesWithDomains = getMentionedDomains(state); | |||
return { | |||
needsLeakWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null, | |||
mentionedDomains: mentionedUsernamesWithDomains, | |||
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked']) | |||
}; | |||
}; | |||
const WarningWrapper = ({ needsLeakWarning, needsLockWarning, mentionedDomains }) => { | |||
if (needsLockWarning) { | |||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />; | |||
} else if (needsLeakWarning) { | |||
return ( | |||
<Warning | |||
message={<FormattedMessage | |||
id='compose_form.privacy_disclaimer' | |||
defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}} to not leak your status?' | |||
values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.length }} | |||
/>} | |||
/> | |||
); | |||
} | |||
return null; | |||
}; | |||
WarningWrapper.propTypes = { | |||
needsLeakWarning: PropTypes.bool, | |||
needsLockWarning: PropTypes.bool, | |||
mentionedDomains: PropTypes.array.isRequired, | |||
}; | |||
export default connect(mapStateToProps)(WarningWrapper); |
@ -0,0 +1,28 @@ | |||
# frozen_string_literal: true | |||
class Settings::FollowerDomainsController < ApplicationController | |||
layout 'admin' | |||
before_action :authenticate_user! | |||
def show | |||
@account = current_account | |||
@domains = current_account.followers.reorder(nil).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10) | |||
end | |||
def update | |||
domains = bulk_params[:select] || [] | |||
domains.each do |domain| | |||
SoftBlockDomainFollowersWorker.perform_async(current_account.id, domain) | |||
end | |||
redirect_to settings_follower_domains_path, notice: I18n.t('followers.success', count: domains.size) | |||
end | |||
private | |||
def bulk_params | |||
params.permit(select: []) | |||
end | |||
end |
@ -0,0 +1,33 @@ | |||
- content_for :page_title do | |||
= t('settings.followers') | |||
= form_tag settings_follower_domains_path, method: :patch, class: 'table-form' do | |||
- unless @account.locked? | |||
.warning | |||
%strong | |||
= fa_icon('warning') | |||
= t('followers.unlocked_warning_title') | |||
= t('followers.unlocked_warning_html', lock_link: link_to(t('followers.lock_link'), settings_profile_url)) | |||
%p= t('followers.explanation_html') | |||
%p= t('followers.true_privacy_html') | |||
%table.table | |||
%thead | |||
%tr | |||
%th | |||
%th= t('followers.domain') | |||
%th= t('followers.followers_count') | |||
%tbody | |||
- @domains.each do |domain| | |||
%tr | |||
%td | |||
= check_box_tag 'select[]', domain.domain, false, disabled: !@account.locked? unless domain.domain.nil? | |||
%td | |||
%samp= domain.domain.presence || Rails.configuration.x.local_domain | |||
%td= number_with_delimiter domain.accounts_from_domain | |||
.action-pagination | |||
.actions | |||
= button_tag t('followers.purge'), type: :submit, class: 'button', disabled: !@account.locked? | |||
= paginate @domains |
@ -0,0 +1,13 @@ | |||
# frozen_string_literal: true | |||
class SoftBlockDomainFollowersWorker | |||
include Sidekiq::Worker | |||
sidekiq_options queue: 'pull' | |||
def perform(account_id, domain) | |||
Account.find(account_id).followers.where(domain: domain).pluck(:id).each do |follower_id| | |||
SoftBlockWorker.perform_async(account_id, follower_id) | |||
end | |||
end | |||
end |
@ -0,0 +1,17 @@ | |||
# frozen_string_literal: true | |||
class SoftBlockWorker | |||
include Sidekiq::Worker | |||
sidekiq_options queue: 'pull' | |||
def perform(account_id, target_account_id) | |||
account = Account.find(account_id) | |||
target_account = Account.find(target_account_id) | |||
BlockService.new.call(account, target_account) | |||
UnblockService.new.call(account, target_account) | |||
rescue ActiveRecord::RecordNotFound | |||
true | |||
end | |||
end |
@ -0,0 +1,34 @@ | |||
require 'rails_helper' | |||
describe Settings::FollowerDomainsController do | |||
let(:user) { Fabricate(:user) } | |||
before do | |||
sign_in user, scope: :user | |||
end | |||
describe 'GET #show' do | |||
it 'returns http success' do | |||
get :show | |||
expect(response).to have_http_status(:success) | |||
end | |||
end | |||
describe 'PATCH #update' do | |||
let(:poopfeast) { Fabricate(:account, username: 'poopfeast', domain: 'example.com', salmon_url: 'http://example.com/salmon') } | |||
before do | |||
stub_request(:post, 'http://example.com/salmon').to_return(status: 200) | |||
poopfeast.follow!(user.account) | |||
patch :update, params: { select: ['example.com'] } | |||
end | |||
it 'redirects back to followers page' do | |||
expect(response).to redirect_to(settings_follower_domains_path) | |||
end | |||
it 'soft-blocks followers from selected domains' do | |||
expect(poopfeast.following?(user.account)).to be false | |||
end | |||
end | |||
end |