Implement data storage + UI behaviours to allow users to block domains

This commit is contained in:
June Rhodes 2016-11-27 16:49:53 +11:00
parent 295fc0e612
commit 829b774588
16 changed files with 221 additions and 7 deletions

View File

@ -0,0 +1,77 @@
import api, { getLinks } from '../api'
import Immutable from 'immutable';
export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST';
export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS';
export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL';
export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST';
export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS';
export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL';
export function blockDomain(domain) {
return (dispatch, getState) => {
dispatch(blockDomainRequest(domain));
api(getState).post(`/api/v1/domains/block?domain=${domain}`).then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
dispatch(blockDomainSuccess(response.data));
}).catch(error => {
dispatch(blockDomainFail(domain, error));
});
};
};
export function unblockDomain(domain) {
return (dispatch, getState) => {
dispatch(unblockDomainRequest(domain));
api(getState).post(`/api/v1/domains/unblock?domain=${domain}`).then(response => {
dispatch(unblockDomainSuccess(response.data));
}).catch(error => {
dispatch(unblockDomainFail(domain, error));
});
};
};
export function blockDomainRequest(id) {
return {
type: DOMAIN_BLOCK_REQUEST,
id
};
};
export function blockDomainSuccess(blocked_domains) {
return {
type: DOMAIN_BLOCK_SUCCESS,
blocked_domains: blocked_domains
};
};
export function blockDomainFail(error) {
return {
type: DOMAIN_BLOCK_FAIL,
error
};
};
export function unblockDomainRequest(id) {
return {
type: DOMAIN_UNBLOCK_REQUEST,
id
};
};
export function unblockDomainSuccess(blocked_domains) {
return {
type: DOMAIN_UNBLOCK_SUCCESS,
blocked_domains: blocked_domains
};
};
export function unblockDomainFail(error) {
return {
type: DOMAIN_UNBLOCK_FAIL,
error
};
};

View File

@ -8,10 +8,11 @@ const messages = defineMessages({
mention: { id: 'account.mention', defaultMessage: 'Mention' },
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' },
unblock_domain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
block: { id: 'account.block', defaultMessage: 'Block' },
block: { id: 'account.block', defaultMessage: 'Block user' },
block_domain: { id: 'account.block_domain', defaultMessage: 'Block domain' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
block: { id: 'account.block', defaultMessage: 'Block' }
});
const outerStyle = {
@ -41,6 +42,7 @@ const ActionBar = React.createClass({
me: React.PropTypes.number.isRequired,
onFollow: React.PropTypes.func.isRequired,
onBlock: React.PropTypes.func.isRequired,
onBlockDomain: React.PropTypes.func.isRequired,
onMention: React.PropTypes.func.isRequired
},
@ -55,6 +57,8 @@ const ActionBar = React.createClass({
if (account.get('id') === me) {
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
} else if (account.getIn(['relationship', 'blocking_domain'])) {
// Do not show per account block / unblock if the whole domain is filtered.
} else if (account.getIn(['relationship', 'blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblock), action: this.props.onBlock });
} else if (account.getIn(['relationship', 'following'])) {
@ -63,6 +67,12 @@ const ActionBar = React.createClass({
menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock });
}
if (account.getIn(['relationship', 'blocking_domain'])) {
menu.push({ text: intl.formatMessage(messages.unblock_domain), action: this.props.onBlockDomain });
} else if (account.get('acct').indexOf('@') != -1 /* Is this account from an external instance? */) {
menu.push({ text: intl.formatMessage(messages.block_domain), action: this.props.onBlockDomain });
}
return (
<div style={outerStyle}>
<div style={outerDropdownStyle}>

View File

@ -10,6 +10,10 @@ import {
fetchAccountTimeline,
expandAccountTimeline
} from '../../actions/accounts';
import {
blockDomain,
unblockDomain
} from '../../actions/domains';
import { mentionCompose } from '../../actions/compose';
import Header from './components/header';
import {
@ -69,6 +73,24 @@ const Account = React.createClass({
}
},
handleBlockDomain () {
let domain = null;
const acct = this.props.account.get('acct');
if (acct.indexOf('@') == -1) {
// same domain as current user... ?
// we should not hit here because the UI should not show the option.
return;
}
domain = acct.substring(acct.indexOf('@') + 1);
if (this.props.account.getIn(['relationship', 'blocking_domain'])) {
this.props.dispatch(unblockDomain(domain));
} else {
this.props.dispatch(blockDomain(domain));
}
},
handleMention () {
this.props.dispatch(mentionCompose(this.props.account));
},
@ -88,7 +110,7 @@ const Account = React.createClass({
<Column>
<ColumnBackButton />
<Header account={account} me={me} onFollow={this.handleFollow} />
<ActionBar account={account} me={me} onBlock={this.handleBlock} onMention={this.handleMention} />
<ActionBar account={account} me={me} onBlock={this.handleBlock} onBlockDomain={this.handleBlockDomain} onMention={this.handleMention} />
{this.props.children}
</Column>

View File

@ -14,10 +14,11 @@ const en = {
"account.mention": "Mention",
"account.edit_profile": "Edit profile",
"account.unblock": "Unblock",
"account.unblock_domain": "Unblock domain",
"account.unfollow": "Unfollow",
"account.block": "Block",
"account.block": "Block user",
"account.block_domain": "Block domain",
"account.follow": "Follow",
"account.block": "Block",
"account.posts": "Posts",
"account.follows": "Follows",
"account.followers": "Followers",

View File

@ -85,10 +85,11 @@ class Api::V1::AccountsController < ApiController
def relationships
ids = params[:id].is_a?(Enumerable) ? params[:id].map(&:to_i) : [params[:id].to_i]
@accounts = Account.where(id: ids).select('id')
@accounts = Account.where(id: ids).select('id, domain')
@following = Account.following_map(ids, current_user.account_id)
@followed_by = Account.followed_by_map(ids, current_user.account_id)
@blocking = Account.blocking_map(ids, current_user.account_id)
@blocking_domain = Account.blocking_domains_map(Account.where(id: ids).select('domain'), current_user.account_id)
end
def search
@ -110,6 +111,7 @@ class Api::V1::AccountsController < ApiController
@following = Account.following_map([@account.id], current_user.account_id)
@followed_by = Account.followed_by_map([@account.id], current_user.account_id)
@blocking = Account.blocking_map([@account.id], current_user.account_id)
@blocking_domain = Account.blocking_domains_map([@account.domain], current_user.account_id)
end
def cache(raw)

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
class Api::V1::DomainsController < ApiController
before_action -> { doorkeeper_authorize! :read }
before_action -> { doorkeeper_authorize! :write }
before_action :require_user!
respond_to :json
def blocks
@domains = AccountDomainBlock.where(account: current_account)
end
def block
AccountDomainBlockService.new.call(current_user.account, params[:domain])
@domains = AccountDomainBlock.where(account: current_account)
render action: :index
end
def unblock
AccountDomainUnblockService.new.call(current_user.account, params[:domain])
@domains = AccountDomainBlock.where(account: current_account)
render action: :index
end
end

View File

@ -43,6 +43,8 @@ class Account < ApplicationRecord
# Block relationships
has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy
has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account
has_many :block_domain_relationships, class_name: 'AccountDomainBlock', foreign_key: 'account_id', dependent: :destroy
has_many :blocked_domains, -> { order('account_domain_blocks.id desc') }, through: :block_domain_relationships, source: :domain
has_many :media_attachments, dependent: :destroy
@ -64,6 +66,10 @@ class Account < ApplicationRecord
block_relationships.where(target_account: other_account).first_or_create!(target_account: other_account)
end
def block_domain!(domain)
block_domain_relationships.where(domain: domain).first_or_create!(domain: domain)
end
def unfollow!(other_account)
follow = active_relationships.find_by(target_account: other_account)
follow&.destroy
@ -74,6 +80,11 @@ class Account < ApplicationRecord
block&.destroy
end
def unblock_domain!(domain)
block = block_domain_relationships.find_by(domain: domain)
block&.destroy
end
def following?(other_account)
following.include?(other_account)
end
@ -166,6 +177,10 @@ class Account < ApplicationRecord
def blocking_map(target_account_ids, account_id)
Block.where(target_account_id: target_account_ids).where(account_id: account_id).map { |b| [b.target_account_id, true] }.to_h
end
def blocking_domains_map(domains, account_id)
AccountDomainBlock.where(domain: domains).where(account_id: account_id).map { |d| [d.domain, true] }.to_h
end
end
before_create do

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
class AccountDomainBlock < ApplicationRecord
belongs_to :account
validates :domain, presence: true, uniqueness: true
validates :account_id, uniqueness: { scope: :domain }
def self.blocked?(account, domain)
where(domain: domain, account: account).exists?
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AccountDomainBlockService < BaseService
def call(account, target_domain)
account.block_domain!(target_domain)
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AccountDomainUnblockService < BaseService
def call(account, target_domain)
account.unblock_domain!(target_domain)
end
end

View File

@ -4,3 +4,4 @@ attribute :id
node(:following) { |account| @following[account.id] || false }
node(:followed_by) { |account| @followed_by[account.id] || false }
node(:blocking) { |account| @blocking[account.id] || false }
node(:blocking_domain) { |account| @blocking_domain[account.domain] || false }

View File

@ -0,0 +1,2 @@
collection @domains
extends('api/v1/domains/show')

View File

@ -0,0 +1,3 @@
object @domain
attributes :id, :domain, :account_id

View File

@ -76,6 +76,14 @@ Rails.application.routes.draw do
resources :notifications, only: [:index]
resources :domains, only: [] do
collection do
get :blocks
post :block
post :unblock
end
end
resources :accounts, only: [:show] do
collection do
get :relationships

View File

@ -0,0 +1,12 @@
class CreateAccountDomainBlocks < ActiveRecord::Migration[5.0]
def change
create_table :account_domain_blocks do |t|
t.integer :account_id, null: false
t.string :domain, null: false, default: ''
t.timestamps null: false
end
add_index :account_domain_blocks, [:account_id, :domain], unique: true
end
end

View File

@ -10,11 +10,19 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161123093447) do
ActiveRecord::Schema.define(version: 20161127152000) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "account_domain_blocks", force: :cascade do |t|
t.integer "account_id", null: false
t.string "domain", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id", "domain"], name: "index_account_domain_blocks_on_account_id_and_domain", unique: true, using: :btree
end
create_table "accounts", force: :cascade do |t|
t.string "username", default: "", null: false
t.string "domain"