From 25af09f60b9d956f1b53dfff44b145aaed0327df Mon Sep 17 00:00:00 2001 From: KMY Date: Mon, 21 Aug 2023 11:14:11 +0900 Subject: [PATCH] Add antenna excluding settings --- .../antennas/exclude_accounts_controller.rb | 104 ++++++ .../v1/antennas/exclude_domains_controller.rb | 46 +++ .../antennas/exclude_keywords_controller.rb | 46 +++ app/javascript/mastodon/actions/antennas.js | 350 +++++++++++++++--- .../antenna_editor/components/account.jsx | 18 +- .../features/antenna_editor/index.jsx | 17 +- .../features/antenna_setting/index.jsx | 72 +++- .../mastodon/reducers/antenna_editor.js | 25 ++ app/javascript/mastodon/reducers/antennas.js | 14 + .../styles/mastodon/components.scss | 6 + config/routes/api.rb | 3 + 11 files changed, 627 insertions(+), 74 deletions(-) create mode 100644 app/controllers/api/v1/antennas/exclude_accounts_controller.rb create mode 100644 app/controllers/api/v1/antennas/exclude_domains_controller.rb create mode 100644 app/controllers/api/v1/antennas/exclude_keywords_controller.rb diff --git a/app/controllers/api/v1/antennas/exclude_accounts_controller.rb b/app/controllers/api/v1/antennas/exclude_accounts_controller.rb new file mode 100644 index 0000000000..cdb9173c11 --- /dev/null +++ b/app/controllers/api/v1/antennas/exclude_accounts_controller.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +class Api::V1::Antennas::ExcludeAccountsController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show] + before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show] + + before_action :require_user! + before_action :set_antenna + + after_action :insert_pagination_headers, only: :show + + def show + @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer + end + + def create + new_accounts = @antenna.exclude_accounts || [] + antenna_accounts.each do |account| + raise Mastodon::ValidationError, I18n.t('antennas.errors.duplicate_account') if new_accounts.include?(account.id) + + new_accounts << account.id + end + + raise Mastodon::ValidationError, I18n.t('antennas.errors.limit.accounts') if new_accounts.size > Antenna::ACCOUNTS_PER_ANTENNA_LIMIT + + @antenna.update!(exclude_accounts: new_accounts) + + render_empty + end + + def destroy + new_accounts = @antenna.exclude_accounts || [] + new_accounts -= antenna_accounts.pluck(:id) + + @antenna.update!(exclude_accounts: new_accounts) + + render_empty + end + + private + + def set_antenna + @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) + end + + def load_accounts + return [] if @antenna.exclude_accounts.nil? + + if unlimited? + Account.where(id: @antenna.exclude_accounts).without_suspended.includes(:account_stat).all + else + Account.where(id: @antenna.exclude_accounts).without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) + end + end + + def antenna_accounts + Account.find(account_ids) + end + + def account_ids + Array(resource_params[:account_ids]) + end + + def resource_params + params.permit(account_ids: []) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + return if unlimited? + + api_v1_list_accounts_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + return if unlimited? + + api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? + end + + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id + end + + def records_continue? + @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) + end + + def pagination_params(core_params) + params.slice(:limit).permit(:limit).merge(core_params) + end + + def unlimited? + params[:limit] == '0' + end +end diff --git a/app/controllers/api/v1/antennas/exclude_domains_controller.rb b/app/controllers/api/v1/antennas/exclude_domains_controller.rb new file mode 100644 index 0000000000..235a44d593 --- /dev/null +++ b/app/controllers/api/v1/antennas/exclude_domains_controller.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class Api::V1::Antennas::ExcludeDomainsController < Api::BaseController + before_action -> { doorkeeper_authorize! :write, :'write:lists' } + + before_action :require_user! + before_action :set_antenna + + def create + new_domains = @antenna.exclude_domains || [] + domains.each do |domain| + raise Mastodon::ValidationError, I18n.t('antennas.errors.duplicate_domain') if new_domains.include?(domain) + + new_domains << domain + end + + raise Mastodon::ValidationError, I18n.t('antennas.errors.limit.domains') if new_domains.size > Antenna::KEYWORDS_PER_ANTENNA_LIMIT + + @antenna.update!(exclude_domains: new_domains) + + render_empty + end + + def destroy + new_domains = @antenna.exclude_domains || [] + new_domains -= domains + + @antenna.update!(exclude_domains: new_domains) + + render_empty + end + + private + + def set_antenna + @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) + end + + def domains + Array(resource_params[:domains]) + end + + def resource_params + params.permit(domains: []) + end +end diff --git a/app/controllers/api/v1/antennas/exclude_keywords_controller.rb b/app/controllers/api/v1/antennas/exclude_keywords_controller.rb new file mode 100644 index 0000000000..171dac5f80 --- /dev/null +++ b/app/controllers/api/v1/antennas/exclude_keywords_controller.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class Api::V1::Antennas::ExcludeKeywordsController < Api::BaseController + before_action -> { doorkeeper_authorize! :write, :'write:lists' } + + before_action :require_user! + before_action :set_antenna + + def create + new_keywords = @antenna.exclude_keywords || [] + keywords.each do |keyword| + raise Mastodon::ValidationError, I18n.t('antennas.errors.duplicate_keyword') if new_keywords.include?(keyword) + + new_keywords << keyword + end + + raise Mastodon::ValidationError, I18n.t('antennas.errors.limit.keywords') if new_keywords.size > Antenna::KEYWORDS_PER_ANTENNA_LIMIT + + @antenna.update!(exclude_keywords: new_keywords) + + render_empty + end + + def destroy + new_keywords = @antenna.exclude_keywords || [] + new_keywords -= keywords + + @antenna.update!(exclude_keywords: new_keywords) + + render_empty + end + + private + + def set_antenna + @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) + end + + def keywords + Array(resource_params[:keywords]) + end + + def resource_params + params.permit(keywords: []) + end +end diff --git a/app/javascript/mastodon/actions/antennas.js b/app/javascript/mastodon/actions/antennas.js index 37e6b13802..3e561cbacb 100644 --- a/app/javascript/mastodon/actions/antennas.js +++ b/app/javascript/mastodon/actions/antennas.js @@ -43,6 +43,18 @@ export const ANTENNA_EDITOR_REMOVE_REQUEST = 'ANTENNA_EDITOR_REMOVE_REQUEST'; export const ANTENNA_EDITOR_REMOVE_SUCCESS = 'ANTENNA_EDITOR_REMOVE_SUCCESS'; export const ANTENNA_EDITOR_REMOVE_FAIL = 'ANTENNA_EDITOR_REMOVE_FAIL'; +export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST'; +export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS'; +export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL'; + +export const ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_FAIL'; + +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL'; + export const ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST = 'ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST'; export const ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS = 'ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS'; export const ANTENNA_EDITOR_FETCH_DOMAINS_FAIL = 'ANTENNA_EDITOR_FETCH_DOMAINS_FAIL'; @@ -51,10 +63,18 @@ export const ANTENNA_EDITOR_ADD_DOMAIN_REQUEST = 'ANTENNA_EDITOR_ADD_DOMAIN_REQU export const ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS'; export const ANTENNA_EDITOR_ADD_DOMAIN_FAIL = 'ANTENNA_EDITOR_ADD_DOMAIN_FAIL'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDEDOMAIN_REQUEST'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL'; + export const ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST'; export const ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS'; export const ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL = 'ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL'; + export const ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST = 'ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST'; export const ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS = 'ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS'; export const ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL = 'ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL'; @@ -63,10 +83,18 @@ export const ANTENNA_EDITOR_ADD_KEYWORD_REQUEST = 'ANTENNA_EDITOR_ADD_KEYWORD_RE export const ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS'; export const ANTENNA_EDITOR_ADD_KEYWORD_FAIL = 'ANTENNA_EDITOR_ADD_KEYWORD_FAIL'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL'; + export const ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST'; export const ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS'; export const ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL = 'ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL'; + export const ANTENNA_ADDER_RESET = 'ANTENNA_ADDER_RESET'; export const ANTENNA_ADDER_SETUP = 'ANTENNA_ADDER_SETUP'; @@ -144,6 +172,15 @@ export const setupAntennaEditor = antennaId => (dispatch, getState) => { dispatch(fetchAntennaAccounts(antennaId)); }; +export const setupExcludeAntennaEditor = antennaId => (dispatch, getState) => { + dispatch({ + type: ANTENNA_EDITOR_SETUP, + antenna: getState().getIn(['antennas', antennaId]), + }); + + dispatch(fetchAntennaExcludeAccounts(antennaId)); +}; + export const changeAntennaEditorTitle = value => ({ type: ANTENNA_EDITOR_TITLE_CHANGE, value, @@ -258,6 +295,33 @@ export const fetchAntennaAccountsFail = (id, error) => ({ error, }); +export const fetchAntennaExcludeAccounts = antennaId => (dispatch, getState) => { + dispatch(fetchAntennaExcludeAccountsRequest(antennaId)); + + api(getState).get(`/api/v1/antennas/${antennaId}/exclude_accounts`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchAntennaExcludeAccountsSuccess(antennaId, data)); + }).catch(err => dispatch(fetchAntennaExcludeAccountsFail(antennaId, err))); +}; + +export const fetchAntennaExcludeAccountsRequest = id => ({ + type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST, + id, +}); + +export const fetchAntennaExcludeAccountsSuccess = (id, accounts, next) => ({ + type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +export const fetchAntennaExcludeAccountsFail = (id, error) => ({ + type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL, + id, + error, +}); + export const fetchAntennaSuggestions = q => (dispatch, getState) => { const params = { q, @@ -316,6 +380,99 @@ export const addToAntennaFail = (antennaId, accountId, error) => ({ error, }); +export const addExcludeToAntennaEditor = accountId => (dispatch, getState) => { + dispatch(addExcludeToAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); +}; + +export const addExcludeToAntenna = (antennaId, accountId) => (dispatch, getState) => { + dispatch(addExcludeToAntennaRequest(antennaId, accountId)); + + api(getState).post(`/api/v1/antennas/${antennaId}/exclude_accounts`, { account_ids: [accountId] }) + .then(() => dispatch(addExcludeToAntennaSuccess(antennaId, accountId))) + .catch(err => dispatch(addExcludeToAntennaFail(antennaId, accountId, err))); +}; + +export const addExcludeToAntennaRequest = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST, + antennaId, + accountId, +}); + +export const addExcludeToAntennaSuccess = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS, + antennaId, + accountId, +}); + +export const addExcludeToAntennaFail = (antennaId, accountId, error) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_FAIL, + antennaId, + accountId, + error, +}); + +export const removeFromAntennaEditor = accountId => (dispatch, getState) => { + dispatch(removeFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); +}; + +export const removeFromAntenna = (antennaId, accountId) => (dispatch, getState) => { + dispatch(removeFromAntennaRequest(antennaId, accountId)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/accounts`, { params: { account_ids: [accountId] } }) + .then(() => dispatch(removeFromAntennaSuccess(antennaId, accountId))) + .catch(err => dispatch(removeFromAntennaFail(antennaId, accountId, err))); +}; + +export const removeFromAntennaRequest = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_REMOVE_REQUEST, + antennaId, + accountId, +}); + +export const removeFromAntennaSuccess = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_REMOVE_SUCCESS, + antennaId, + accountId, +}); + +export const removeFromAntennaFail = (antennaId, accountId, error) => ({ + type: ANTENNA_EDITOR_REMOVE_FAIL, + antennaId, + accountId, + error, +}); + +export const removeExcludeFromAntennaEditor = accountId => (dispatch, getState) => { + dispatch(removeExcludeFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); +}; + +export const removeExcludeFromAntenna = (antennaId, accountId) => (dispatch, getState) => { + dispatch(removeExcludeFromAntennaRequest(antennaId, accountId)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_accounts`, { params: { account_ids: [accountId] } }) + .then(() => dispatch(removeExcludeFromAntennaSuccess(antennaId, accountId))) + .catch(err => dispatch(removeExcludeFromAntennaFail(antennaId, accountId, err))); +}; + +export const removeExcludeFromAntennaRequest = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST, + antennaId, + accountId, +}); + +export const removeExcludeFromAntennaSuccess = (antennaId, accountId) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS, + antennaId, + accountId, +}); + +export const removeExcludeFromAntennaFail = (antennaId, accountId, error) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL, + antennaId, + accountId, + error, +}); + export const fetchAntennaDomains = antennaId => (dispatch, getState) => { dispatch(fetchAntennaDomainsRequest(antennaId)); @@ -368,6 +525,87 @@ export const addDomainToAntennaFail = (antennaId, domain, error) => ({ error, }); +export const removeDomainFromAntenna = (antennaId, domain) => (dispatch, getState) => { + dispatch(removeDomainFromAntennaRequest(antennaId, domain)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/domains`, { params: { domains: [domain] } }) + .then(() => dispatch(removeDomainFromAntennaSuccess(antennaId, domain))) + .catch(err => dispatch(removeDomainFromAntennaFail(antennaId, domain, err))); +}; + +export const removeDomainFromAntennaRequest = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST, + antennaId, + domain, +}); + +export const removeDomainFromAntennaSuccess = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS, + antennaId, + domain, +}); + +export const removeDomainFromAntennaFail = (antennaId, domain, error) => ({ + type: ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL, + antennaId, + domain, + error, +}); + +export const addExcludeDomainToAntenna = (antennaId, domain) => (dispatch, getState) => { + dispatch(addExcludeDomainToAntennaRequest(antennaId, domain)); + + api(getState).post(`/api/v1/antennas/${antennaId}/exclude_domains`, { domains: [domain] }) + .then(() => dispatch(addExcludeDomainToAntennaSuccess(antennaId, domain))) + .catch(err => dispatch(addExcludeDomainToAntennaFail(antennaId, domain, err))); +}; + +export const addExcludeDomainToAntennaRequest = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_REQUEST, + antennaId, + domain, +}); + +export const addExcludeDomainToAntennaSuccess = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS, + antennaId, + domain, +}); + +export const addExcludeDomainToAntennaFail = (antennaId, domain, error) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL, + antennaId, + domain, + error, +}); + +export const removeExcludeDomainFromAntenna = (antennaId, domain) => (dispatch, getState) => { + dispatch(removeExcludeDomainFromAntennaRequest(antennaId, domain)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_domains`, { params: { domains: [domain] } }) + .then(() => dispatch(removeExcludeDomainFromAntennaSuccess(antennaId, domain))) + .catch(err => dispatch(removeExcludeDomainFromAntennaFail(antennaId, domain, err))); +}; + +export const removeExcludeDomainFromAntennaRequest = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST, + antennaId, + domain, +}); + +export const removeExcludeDomainFromAntennaSuccess = (antennaId, domain) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS, + antennaId, + domain, +}); + +export const removeExcludeDomainFromAntennaFail = (antennaId, domain, error) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL, + antennaId, + domain, + error, +}); + export const fetchAntennaKeywords = antennaId => (dispatch, getState) => { dispatch(fetchAntennaKeywordsRequest(antennaId)); @@ -420,64 +658,6 @@ export const addKeywordToAntennaFail = (antennaId, keyword, error) => ({ error, }); -export const removeFromAntennaEditor = accountId => (dispatch, getState) => { - dispatch(removeFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId)); -}; - -export const removeFromAntenna = (antennaId, accountId) => (dispatch, getState) => { - dispatch(removeFromAntennaRequest(antennaId, accountId)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/accounts`, { params: { account_ids: [accountId] } }) - .then(() => dispatch(removeFromAntennaSuccess(antennaId, accountId))) - .catch(err => dispatch(removeFromAntennaFail(antennaId, accountId, err))); -}; - -export const removeFromAntennaRequest = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_REMOVE_REQUEST, - antennaId, - accountId, -}); - -export const removeFromAntennaSuccess = (antennaId, accountId) => ({ - type: ANTENNA_EDITOR_REMOVE_SUCCESS, - antennaId, - accountId, -}); - -export const removeFromAntennaFail = (antennaId, accountId, error) => ({ - type: ANTENNA_EDITOR_REMOVE_FAIL, - antennaId, - accountId, - error, -}); - -export const removeDomainFromAntenna = (antennaId, domain) => (dispatch, getState) => { - dispatch(removeDomainFromAntennaRequest(antennaId, domain)); - - api(getState).delete(`/api/v1/antennas/${antennaId}/domains`, { params: { domains: [domain] } }) - .then(() => dispatch(removeDomainFromAntennaSuccess(antennaId, domain))) - .catch(err => dispatch(removeDomainFromAntennaFail(antennaId, domain, err))); -}; - -export const removeDomainFromAntennaRequest = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST, - antennaId, - domain, -}); - -export const removeDomainFromAntennaSuccess = (antennaId, domain) => ({ - type: ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS, - antennaId, - domain, -}); - -export const removeDomainFromAntennaFail = (antennaId, domain, error) => ({ - type: ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL, - antennaId, - domain, - error, -}); - export const removeKeywordFromAntenna = (antennaId, keyword) => (dispatch, getState) => { dispatch(removeKeywordFromAntennaRequest(antennaId, keyword)); @@ -505,6 +685,60 @@ export const removeKeywordFromAntennaFail = (antennaId, keyword, error) => ({ error, }); +export const addExcludeKeywordToAntenna = (antennaId, keyword) => (dispatch, getState) => { + dispatch(addExcludeKeywordToAntennaRequest(antennaId, keyword)); + + api(getState).post(`/api/v1/antennas/${antennaId}/exclude_keywords`, { keywords: [keyword] }) + .then(() => dispatch(addExcludeKeywordToAntennaSuccess(antennaId, keyword))) + .catch(err => dispatch(addExcludeKeywordToAntennaFail(antennaId, keyword, err))); +}; + +export const addExcludeKeywordToAntennaRequest = (antennaId, keyword) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST, + antennaId, + keyword, +}); + +export const addExcludeKeywordToAntennaSuccess = (antennaId, keyword) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS, + antennaId, + keyword, +}); + +export const addExcludeKeywordToAntennaFail = (antennaId, keyword, error) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL, + antennaId, + keyword, + error, +}); + +export const removeExcludeKeywordFromAntenna = (antennaId, keyword) => (dispatch, getState) => { + dispatch(removeExcludeKeywordFromAntennaRequest(antennaId, keyword)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_keywords`, { params: { keywords: [keyword] } }) + .then(() => dispatch(removeExcludeKeywordFromAntennaSuccess(antennaId, keyword))) + .catch(err => dispatch(removeExcludeKeywordFromAntennaFail(antennaId, keyword, err))); +}; + +export const removeExcludeKeywordFromAntennaRequest = (antennaId, keyword) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST, + antennaId, + keyword, +}); + +export const removeExcludeKeywordFromAntennaSuccess = (antennaId, keyword) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS, + antennaId, + keyword, +}); + +export const removeExcludeKeywordFromAntennaFail = (antennaId, keyword, error) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL, + antennaId, + keyword, + error, +}); + export const resetAntennaAdder = () => ({ type: ANTENNA_ADDER_RESET, }); diff --git a/app/javascript/mastodon/features/antenna_editor/components/account.jsx b/app/javascript/mastodon/features/antenna_editor/components/account.jsx index 94daad4523..94cb4ea6a5 100644 --- a/app/javascript/mastodon/features/antenna_editor/components/account.jsx +++ b/app/javascript/mastodon/features/antenna_editor/components/account.jsx @@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; -import { removeFromAntennaEditor, addToAntennaEditor } from '../../../actions/antennas'; +import { removeFromAntennaEditor, addToAntennaEditor, removeExcludeFromAntennaEditor, addExcludeToAntennaEditor } from '../../../actions/antennas'; import { Avatar } from '../../../components/avatar'; import { DisplayName } from '../../../components/display_name'; import { IconButton } from '../../../components/icon_button'; @@ -20,9 +20,9 @@ const messages = defineMessages({ const makeMapStateToProps = () => { const getAccount = makeGetAccount(); - const mapStateToProps = (state, { accountId, added }) => ({ + const mapStateToProps = (state, { accountId, added, isExclude }) => ({ account: getAccount(state, accountId), - added: typeof added === 'undefined' ? state.getIn(['antennaEditor', 'accounts', 'items']).includes(accountId) : added, + added: typeof added === 'undefined' ? state.getIn(['antennaEditor', isExclude ? 'excludeAccounts' : 'accounts', 'items']).includes(accountId) : added, }); return mapStateToProps; @@ -31,15 +31,20 @@ const makeMapStateToProps = () => { const mapDispatchToProps = (dispatch, { accountId }) => ({ onRemove: () => dispatch(removeFromAntennaEditor(accountId)), onAdd: () => dispatch(addToAntennaEditor(accountId)), + onExcludeRemove: () => dispatch(removeExcludeFromAntennaEditor(accountId)), + onExcludeAdd: () => dispatch(addExcludeToAntennaEditor(accountId)), }); class Account extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, + isExclude: PropTypes.bool.isRequired, intl: PropTypes.object.isRequired, onRemove: PropTypes.func.isRequired, onAdd: PropTypes.func.isRequired, + onExcludeRemove: PropTypes.func.isRequired, + onExcludeAdd: PropTypes.func.isRequired, added: PropTypes.bool, }; @@ -48,14 +53,15 @@ class Account extends ImmutablePureComponent { }; render () { - const { account, intl, onRemove, onAdd, added } = this.props; + const { account, intl, isExclude, onRemove, onAdd, onExcludeRemove, onExcludeAdd, added } = this.props; + console.dir(isExclude) let button; if (added) { - button = ; + button = ; } else { - button = ; + button = ; } return ( diff --git a/app/javascript/mastodon/features/antenna_editor/index.jsx b/app/javascript/mastodon/features/antenna_editor/index.jsx index 0e0846859b..d558161171 100644 --- a/app/javascript/mastodon/features/antenna_editor/index.jsx +++ b/app/javascript/mastodon/features/antenna_editor/index.jsx @@ -8,20 +8,20 @@ import { connect } from 'react-redux'; import spring from 'react-motion/lib/spring'; -import { setupAntennaEditor, clearAntennaSuggestions, resetAntennaEditor } from '../../actions/antennas'; +import { setupAntennaEditor, setupExcludeAntennaEditor, clearAntennaSuggestions, resetAntennaEditor } from '../../actions/antennas'; import Motion from '../ui/util/optional_motion'; import Account from './components/account'; import EditAntennaForm from './components/edit_antenna_form'; import Search from './components/search'; -const mapStateToProps = state => ({ - accountIds: state.getIn(['antennaEditor', 'accounts', 'items']), +const mapStateToProps = (state, { isExclude }) => ({ + accountIds: state.getIn(['antennaEditor', isExclude ? 'excludeAccounts' : 'accounts', 'items']), searchAccountIds: state.getIn(['antennaEditor', 'suggestions', 'items']), }); -const mapDispatchToProps = dispatch => ({ - onInitialize: antennaId => dispatch(setupAntennaEditor(antennaId)), +const mapDispatchToProps = (dispatch, { isExclude }) => ({ + onInitialize: antennaId => dispatch(isExclude ? setupExcludeAntennaEditor(antennaId) : setupAntennaEditor(antennaId)), onClear: () => dispatch(clearAntennaSuggestions()), onReset: () => dispatch(resetAntennaEditor()), }); @@ -30,6 +30,7 @@ class AntennaEditor extends ImmutablePureComponent { static propTypes = { antennaId: PropTypes.string.isRequired, + isExclude: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, onInitialize: PropTypes.func.isRequired, @@ -50,7 +51,7 @@ class AntennaEditor extends ImmutablePureComponent { } render () { - const { accountIds, searchAccountIds, onClear } = this.props; + const { accountIds, searchAccountIds, onClear, isExclude } = this.props; const showSearch = searchAccountIds.size > 0; return ( @@ -61,7 +62,7 @@ class AntennaEditor extends ImmutablePureComponent {
- {accountIds.map(accountId => )} + {accountIds.map(accountId => )}
{showSearch &&
} @@ -69,7 +70,7 @@ class AntennaEditor extends ImmutablePureComponent { {({ x }) => (
- {searchAccountIds.map(accountId => )} + {searchAccountIds.map(accountId => )}
)}
diff --git a/app/javascript/mastodon/features/antenna_setting/index.jsx b/app/javascript/mastodon/features/antenna_setting/index.jsx index 4cfa2e3e51..dede69e6cc 100644 --- a/app/javascript/mastodon/features/antenna_setting/index.jsx +++ b/app/javascript/mastodon/features/antenna_setting/index.jsx @@ -12,7 +12,21 @@ import { connect } from 'react-redux'; import Select, { NonceProvider } from 'react-select'; import Toggle from 'react-toggle'; -import { fetchAntenna, deleteAntenna, updateAntenna, addDomainToAntenna, removeDomainFromAntenna, fetchAntennaDomains, fetchAntennaKeywords, removeKeywordFromAntenna, addKeywordToAntenna } from 'mastodon/actions/antennas'; +import { + fetchAntenna, + deleteAntenna, + updateAntenna, + addDomainToAntenna, + removeDomainFromAntenna, + addExcludeDomainToAntenna, + removeExcludeDomainFromAntenna, + fetchAntennaDomains, + fetchAntennaKeywords, + removeKeywordFromAntenna, + addKeywordToAntenna, + removeExcludeKeywordFromAntenna, + addExcludeKeywordToAntenna +} from 'mastodon/actions/antennas'; import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; import { fetchLists } from 'mastodon/actions/lists'; import { openModal } from 'mastodon/actions/modal'; @@ -69,7 +83,9 @@ class AntennaSetting extends PureComponent { state = { domainName: '', + excludeDomainName: '', keywordName: '', + excludeKeywordName: '', rangeRadioValue: null, contentRadioValue: null, }; @@ -124,7 +140,14 @@ class AntennaSetting extends PureComponent { handleEditClick = () => { this.props.dispatch(openModal({ modalType: 'ANTENNA_EDITOR', - modalProps: { antennaId: this.props.params.id }, + modalProps: { antennaId: this.props.params.id, isExclude: false }, + })); + }; + + handleExcludeEditClick = () => { + this.props.dispatch(openModal({ + modalType: 'ANTENNA_EDITOR', + modalProps: { antennaId: this.props.params.id, isExclude: true }, })); }; @@ -212,6 +235,24 @@ class AntennaSetting extends PureComponent { onKeywordRemove = (value) => this.props.dispatch(removeKeywordFromAntenna(this.props.params.id, value)); + onExcludeDomainNameChanged = (value) => this.setState({ excludeDomainName: value }); + + onExcludeDomainAdd = () => { + this.props.dispatch(addExcludeDomainToAntenna(this.props.params.id, this.state.excludeDomainName)); + this.setState({ excludeDomainName: '' }); + }; + + onExcludeDomainRemove = (value) => this.props.dispatch(removeExcludeDomainFromAntenna(this.props.params.id, value)); + + onExcludeKeywordNameChanged = (value) => this.setState({ excludeKeywordName: value }); + + onExcludeKeywordAdd = () => { + this.props.dispatch(addExcludeKeywordToAntenna(this.props.params.id, this.state.excludeKeywordName)); + this.setState({ excludeKeywordName: '' }); + }; + + onExcludeKeywordRemove = (value) => this.props.dispatch(removeExcludeKeywordFromAntenna(this.props.params.id, value)); + render () { const { columnId, multiColumn, antenna, lists, domains, keywords, intl } = this.props; const { id } = this.props.params; @@ -354,6 +395,7 @@ class AntennaSetting extends PureComponent { {!isStl && ( <> +

{rangeRadioValue.get('value') === 'accounts' &&
diff --git a/app/javascript/mastodon/reducers/antenna_editor.js b/app/javascript/mastodon/reducers/antenna_editor.js index 86c0f187f7..7a164070be 100644 --- a/app/javascript/mastodon/reducers/antenna_editor.js +++ b/app/javascript/mastodon/reducers/antenna_editor.js @@ -13,11 +13,16 @@ import { ANTENNA_ACCOUNTS_FETCH_REQUEST, ANTENNA_ACCOUNTS_FETCH_SUCCESS, ANTENNA_ACCOUNTS_FETCH_FAIL, + ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST, + ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS, + ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL, ANTENNA_EDITOR_SUGGESTIONS_READY, ANTENNA_EDITOR_SUGGESTIONS_CLEAR, ANTENNA_EDITOR_SUGGESTIONS_CHANGE, ANTENNA_EDITOR_ADD_SUCCESS, ANTENNA_EDITOR_REMOVE_SUCCESS, + ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS, + ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS, } from '../actions/antennas'; const initialState = ImmutableMap({ @@ -33,6 +38,12 @@ const initialState = ImmutableMap({ isLoading: false, }), + excludeAccounts: ImmutableMap({ + items: ImmutableList(), + loaded: false, + isLoading: false, + }), + suggestions: ImmutableMap({ value: '', items: ImmutableList(), @@ -80,6 +91,16 @@ export default function antennaEditorReducer(state = initialState, action) { map.set('loaded', true); map.set('items', ImmutableList(action.accounts.map(item => item.id))); })); + case ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST: + return state.setIn(['excludeAccounts', 'isLoading'], true); + case ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL: + return state.setIn(['excludeAccounts', 'isLoading'], false); + case ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS: + return state.update('excludeAccounts', accounts => accounts.withMutations(map => { + map.set('isLoading', false); + map.set('loaded', true); + map.set('items', ImmutableList(action.accounts.map(item => item.id))); + })); case ANTENNA_EDITOR_SUGGESTIONS_CHANGE: return state.setIn(['suggestions', 'value'], action.value); case ANTENNA_EDITOR_SUGGESTIONS_READY: @@ -93,6 +114,10 @@ export default function antennaEditorReducer(state = initialState, action) { return state.updateIn(['accounts', 'items'], antenna => antenna.unshift(action.accountId)); case ANTENNA_EDITOR_REMOVE_SUCCESS: return state.updateIn(['accounts', 'items'], antenna => antenna.filterNot(item => item === action.accountId)); + case ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS: + return state.updateIn(['excludeAccounts', 'items'], antenna => antenna.unshift(action.accountId)); + case ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS: + return state.updateIn(['excludeAccounts', 'items'], antenna => antenna.filterNot(item => item === action.accountId)); default: return state; } diff --git a/app/javascript/mastodon/reducers/antennas.js b/app/javascript/mastodon/reducers/antennas.js index 524053107b..babc5c140f 100644 --- a/app/javascript/mastodon/reducers/antennas.js +++ b/app/javascript/mastodon/reducers/antennas.js @@ -11,9 +11,13 @@ import { ANTENNA_EDITOR_REMOVE_SUCCESS, ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS, ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS, + ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS, + ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS, ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS, ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS, ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS, + ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS, + ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS, ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS, } from '../actions/antennas'; @@ -28,7 +32,9 @@ const normalizeAntenna = (state, antenna) => { let s = state.set(antenna.id, fromJS(antenna)); if (old) { s = s.setIn([antenna.id, 'domains'], old.get('domains')); + s = s.setIn([antenna.id, 'exclude_domains'], old.get('exclude_domains')); s = s.setIn([antenna.id, 'keywords'], old.get('keywords')); + s = s.setIn([antenna.id, 'exclude_keywords'], old.get('exclude_keywords')); s = s.setIn([antenna.id, 'accounts_count'], old.get('accounts_count')); s = s.setIn([antenna.id, 'domains_count'], old.get('domains_count')); s = s.setIn([antenna.id, 'keywords_count'], old.get('keywords_count')); @@ -63,12 +69,20 @@ export default function antennas(state = initialState, action) { return state.setIn([action.antennaId, 'domains_count'], state.getIn([action.antennaId, 'domains_count']) + 1).updateIn([action.antennaId, 'domains', 'domains'], domains => (ImmutableList(domains || [])).push(action.domain)); case ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS: return state.setIn([action.antennaId, 'domains_count'], state.getIn([action.antennaId, 'domains_count']) - 1).updateIn([action.antennaId, 'domains', 'domains'], domains => (ImmutableList(domains || [])).filterNot(domain => domain === action.domain)); + case ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS: + return state.updateIn([action.antennaId, 'domains', 'exclude_domains'], domains => (ImmutableList(domains || [])).push(action.domain)); + case ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS: + return state.updateIn([action.antennaId, 'domains', 'exclude_domains'], domains => (ImmutableList(domains || [])).filterNot(domain => domain === action.domain)); case ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS: return state.setIn([action.id, 'domains'], ImmutableMap(action.domains)); case ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS: return state.setIn([action.antennaId, 'keywords_count'], state.getIn([action.antennaId, 'keywords_count']) + 1).updateIn([action.antennaId, 'keywords', 'keywords'], keywords => (ImmutableList(keywords || [])).push(action.keyword)); case ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS: return state.setIn([action.antennaId, 'keywords_count'], state.getIn([action.antennaId, 'keywords_count']) - 1).updateIn([action.antennaId, 'keywords', 'keywords'], keywords => (ImmutableList(keywords || [])).filterNot(keyword => keyword === action.keyword)); + case ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS: + return state.updateIn([action.antennaId, 'keywords', 'exclude_keywords'], keywords => (ImmutableList(keywords || [])).push(action.keyword)); + case ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS: + return state.updateIn([action.antennaId, 'keywords', 'exclude_keywords'], keywords => (ImmutableList(keywords || [])).filterNot(keyword => keyword === action.keyword)); case ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS: return state.setIn([action.id, 'keywords'], ImmutableMap(action.keywords)); default: diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 03cbaf7aa5..2464c92e38 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -7480,6 +7480,12 @@ noscript { .antenna-setting { margin: 8px 16px 32px; + h2 { + font-size: 20px; + margin: 40px 0 16px; + font-weight: bold; + } + h3 { font-size: 16px; margin: 40px 0 16px; diff --git a/config/routes/api.rb b/config/routes/api.rb index 26c8f04350..7737e7c9ea 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -210,6 +210,9 @@ namespace :api, format: false do resource :accounts, only: [:show, :create, :destroy], controller: 'antennas/accounts' resource :domains, only: [:show, :create, :destroy], controller: 'antennas/domains' resource :keywords, only: [:show, :create, :destroy], controller: 'antennas/keywords' + resource :exclude_accounts, only: [:show, :create, :destroy], controller: 'antennas/exclude_accounts' + resource :exclude_domains, only: [:create, :destroy], controller: 'antennas/exclude_domains' + resource :exclude_keywords, only: [:create, :destroy], controller: 'antennas/exclude_keywords' end namespace :featured_tags do