diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb index 3061fcb7e7..0ac7858e16 100644 --- a/app/controllers/api/v1/accounts/search_controller.rb +++ b/app/controllers/api/v1/accounts/search_controller.rb @@ -18,6 +18,7 @@ class Api::V1::Accounts::SearchController < Api::BaseController limit: limit_param(DEFAULT_ACCOUNTS_LIMIT), resolve: truthy_param?(:resolve), following: truthy_param?(:following), + follower: truthy_param?(:follower), offset: params[:offset] ) end 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/controllers/api/v1/antennas/keywords_controller.rb b/app/controllers/api/v1/antennas/keywords_controller.rb index d486d1a585..b35fb535dc 100644 --- a/app/controllers/api/v1/antennas/keywords_controller.rb +++ b/app/controllers/api/v1/antennas/keywords_controller.rb @@ -21,6 +21,8 @@ class Api::V1::Antennas::KeywordsController < Api::BaseController new_keywords << keyword end + raise Mastodon::ValidationError, I18n.t('antennas.errors.limit.keywords') if new_keywords.size > Antenna::KEYWORDS_PER_ANTENNA_LIMIT + @antenna.update!(keywords: new_keywords, any_keywords: new_keywords.empty?) render_empty diff --git a/app/controllers/api/v1/circles/accounts_controller.rb b/app/controllers/api/v1/circles/accounts_controller.rb new file mode 100644 index 0000000000..e0d43bd950 --- /dev/null +++ b/app/controllers/api/v1/circles/accounts_controller.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +class Api::V1::Circles::AccountsController < 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_circle + + after_action :insert_pagination_headers, only: :show + + def show + @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer + end + + def create + ApplicationRecord.transaction do + circle_accounts.each do |account| + @circle.accounts << account + end + end + + render_empty + end + + def destroy + CircleAccount.where(circle: @circle, account_id: account_ids).destroy_all + render_empty + end + + private + + def set_circle + @circle = Circle.where(account: current_account).find(params[:circle_id]) + end + + def load_accounts + if unlimited? + @circle.accounts.without_suspended.includes(:account_stat).all + else + @circle.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) + end + end + + def circle_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_circle_accounts_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + return if unlimited? + + api_v1_circle_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/circles_controller.rb b/app/controllers/api/v1/circles_controller.rb new file mode 100644 index 0000000000..53c9adf14e --- /dev/null +++ b/app/controllers/api/v1/circles_controller.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class Api::V1::CirclesController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:index, :show] + + before_action :require_user! + before_action :set_circle, except: [:index, :create] + + rescue_from ArgumentError do |e| + render json: { error: e.to_s }, status: 422 + end + + def index + @circles = Circle.where(account: current_account).all + render json: @circles, each_serializer: REST::CircleSerializer + end + + def show + render json: @circle, serializer: REST::CircleSerializer + end + + def create + @circle = Circle.create!(circle_params.merge(account: current_account)) + render json: @circle, serializer: REST::CircleSerializer + end + + def update + @circle.update!(circle_params) + render json: @circle, serializer: REST::CircleSerializer + end + + def destroy + @circle.destroy! + render_empty + end + + private + + def set_circle + @circle = Circle.where(account: current_account).find(params[:id]) + end + + def circle_params + params.permit(:title) + end +end diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 70145c983a..4071c34799 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -31,7 +31,8 @@ class Api::V1::ListsController < Api::BaseController end def destroy - raise Mastodon::ValidationError, I18n.t('antennas.errors.remove_list_with_antenna') if Antenna.where(list_id: @list.id).any? + antenna = Antenna.find_by(list_id: @list.id) + antenna.update!(list_id: 0) if antenna.present? @list.destroy! render_empty diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index db9fcce905..1df399405a 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -67,6 +67,7 @@ class Api::V1::StatusesController < Api::BaseController visibility: status_params[:visibility], force_visibility: status_params[:force_visibility], searchability: status_params[:searchability], + circle_id: status_params[:circle_id], language: status_params[:language], scheduled_at: status_params[:scheduled_at], application: doorkeeper_token.application, @@ -144,6 +145,7 @@ class Api::V1::StatusesController < Api::BaseController :visibility, :force_visibility, :searchability, + :circle_id, :language, :markdown, :scheduled_at, 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/actions/circles.js b/app/javascript/mastodon/actions/circles.js new file mode 100644 index 0000000000..6a52e541c9 --- /dev/null +++ b/app/javascript/mastodon/actions/circles.js @@ -0,0 +1,372 @@ +import api from '../api'; + +import { showAlertForError } from './alerts'; +import { importFetchedAccounts } from './importer'; + +export const CIRCLE_FETCH_REQUEST = 'CIRCLE_FETCH_REQUEST'; +export const CIRCLE_FETCH_SUCCESS = 'CIRCLE_FETCH_SUCCESS'; +export const CIRCLE_FETCH_FAIL = 'CIRCLE_FETCH_FAIL'; + +export const CIRCLES_FETCH_REQUEST = 'CIRCLES_FETCH_REQUEST'; +export const CIRCLES_FETCH_SUCCESS = 'CIRCLES_FETCH_SUCCESS'; +export const CIRCLES_FETCH_FAIL = 'CIRCLES_FETCH_FAIL'; + +export const CIRCLE_EDITOR_TITLE_CHANGE = 'CIRCLE_EDITOR_TITLE_CHANGE'; +export const CIRCLE_EDITOR_RESET = 'CIRCLE_EDITOR_RESET'; +export const CIRCLE_EDITOR_SETUP = 'CIRCLE_EDITOR_SETUP'; + +export const CIRCLE_CREATE_REQUEST = 'CIRCLE_CREATE_REQUEST'; +export const CIRCLE_CREATE_SUCCESS = 'CIRCLE_CREATE_SUCCESS'; +export const CIRCLE_CREATE_FAIL = 'CIRCLE_CREATE_FAIL'; + +export const CIRCLE_UPDATE_REQUEST = 'CIRCLE_UPDATE_REQUEST'; +export const CIRCLE_UPDATE_SUCCESS = 'CIRCLE_UPDATE_SUCCESS'; +export const CIRCLE_UPDATE_FAIL = 'CIRCLE_UPDATE_FAIL'; + +export const CIRCLE_DELETE_REQUEST = 'CIRCLE_DELETE_REQUEST'; +export const CIRCLE_DELETE_SUCCESS = 'CIRCLE_DELETE_SUCCESS'; +export const CIRCLE_DELETE_FAIL = 'CIRCLE_DELETE_FAIL'; + +export const CIRCLE_ACCOUNTS_FETCH_REQUEST = 'CIRCLE_ACCOUNTS_FETCH_REQUEST'; +export const CIRCLE_ACCOUNTS_FETCH_SUCCESS = 'CIRCLE_ACCOUNTS_FETCH_SUCCESS'; +export const CIRCLE_ACCOUNTS_FETCH_FAIL = 'CIRCLE_ACCOUNTS_FETCH_FAIL'; + +export const CIRCLE_EDITOR_SUGGESTIONS_CHANGE = 'CIRCLE_EDITOR_SUGGESTIONS_CHANGE'; +export const CIRCLE_EDITOR_SUGGESTIONS_READY = 'CIRCLE_EDITOR_SUGGESTIONS_READY'; +export const CIRCLE_EDITOR_SUGGESTIONS_CLEAR = 'CIRCLE_EDITOR_SUGGESTIONS_CLEAR'; + +export const CIRCLE_EDITOR_ADD_REQUEST = 'CIRCLE_EDITOR_ADD_REQUEST'; +export const CIRCLE_EDITOR_ADD_SUCCESS = 'CIRCLE_EDITOR_ADD_SUCCESS'; +export const CIRCLE_EDITOR_ADD_FAIL = 'CIRCLE_EDITOR_ADD_FAIL'; + +export const CIRCLE_EDITOR_REMOVE_REQUEST = 'CIRCLE_EDITOR_REMOVE_REQUEST'; +export const CIRCLE_EDITOR_REMOVE_SUCCESS = 'CIRCLE_EDITOR_REMOVE_SUCCESS'; +export const CIRCLE_EDITOR_REMOVE_FAIL = 'CIRCLE_EDITOR_REMOVE_FAIL'; + +export const CIRCLE_ADDER_RESET = 'CIRCLE_ADDER_RESET'; +export const CIRCLE_ADDER_SETUP = 'CIRCLE_ADDER_SETUP'; + +export const CIRCLE_ADDER_CIRCLES_FETCH_REQUEST = 'CIRCLE_ADDER_CIRCLES_FETCH_REQUEST'; +export const CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS = 'CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS'; +export const CIRCLE_ADDER_CIRCLES_FETCH_FAIL = 'CIRCLE_ADDER_CIRCLES_FETCH_FAIL'; + +export const fetchCircle = id => (dispatch, getState) => { + if (getState().getIn(['circles', id])) { + return; + } + + dispatch(fetchCircleRequest(id)); + + api(getState).get(`/api/v1/circles/${id}`) + .then(({ data }) => dispatch(fetchCircleSuccess(data))) + .catch(err => dispatch(fetchCircleFail(id, err))); +}; + +export const fetchCircleRequest = id => ({ + type: CIRCLE_FETCH_REQUEST, + id, +}); + +export const fetchCircleSuccess = circle => ({ + type: CIRCLE_FETCH_SUCCESS, + circle, +}); + +export const fetchCircleFail = (id, error) => ({ + type: CIRCLE_FETCH_FAIL, + id, + error, +}); + +export const fetchCircles = () => (dispatch, getState) => { + dispatch(fetchCirclesRequest()); + + api(getState).get('/api/v1/circles') + .then(({ data }) => dispatch(fetchCirclesSuccess(data))) + .catch(err => dispatch(fetchCirclesFail(err))); +}; + +export const fetchCirclesRequest = () => ({ + type: CIRCLES_FETCH_REQUEST, +}); + +export const fetchCirclesSuccess = circles => ({ + type: CIRCLES_FETCH_SUCCESS, + circles, +}); + +export const fetchCirclesFail = error => ({ + type: CIRCLES_FETCH_FAIL, + error, +}); + +export const submitCircleEditor = shouldReset => (dispatch, getState) => { + const circleId = getState().getIn(['circleEditor', 'circleId']); + const title = getState().getIn(['circleEditor', 'title']); + + if (circleId === null) { + dispatch(createCircle(title, shouldReset)); + } else { + dispatch(updateCircle(circleId, title, shouldReset)); + } +}; + +export const setupCircleEditor = circleId => (dispatch, getState) => { + dispatch({ + type: CIRCLE_EDITOR_SETUP, + circle: getState().getIn(['circles', circleId]), + }); + + dispatch(fetchCircleAccounts(circleId)); +}; + +export const changeCircleEditorTitle = value => ({ + type: CIRCLE_EDITOR_TITLE_CHANGE, + value, +}); + +export const createCircle = (title, shouldReset) => (dispatch, getState) => { + dispatch(createCircleRequest()); + + api(getState).post('/api/v1/circles', { title }).then(({ data }) => { + dispatch(createCircleSuccess(data)); + + if (shouldReset) { + dispatch(resetCircleEditor()); + } + }).catch(err => dispatch(createCircleFail(err))); +}; + +export const createCircleRequest = () => ({ + type: CIRCLE_CREATE_REQUEST, +}); + +export const createCircleSuccess = circle => ({ + type: CIRCLE_CREATE_SUCCESS, + circle, +}); + +export const createCircleFail = error => ({ + type: CIRCLE_CREATE_FAIL, + error, +}); + +export const updateCircle = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { + dispatch(updateCircleRequest(id)); + + api(getState).put(`/api/v1/circles/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { + dispatch(updateCircleSuccess(data)); + + if (shouldReset) { + dispatch(resetCircleEditor()); + } + }).catch(err => dispatch(updateCircleFail(id, err))); +}; + +export const updateCircleRequest = id => ({ + type: CIRCLE_UPDATE_REQUEST, + id, +}); + +export const updateCircleSuccess = circle => ({ + type: CIRCLE_UPDATE_SUCCESS, + circle, +}); + +export const updateCircleFail = (id, error) => ({ + type: CIRCLE_UPDATE_FAIL, + id, + error, +}); + +export const resetCircleEditor = () => ({ + type: CIRCLE_EDITOR_RESET, +}); + +export const deleteCircle = id => (dispatch, getState) => { + dispatch(deleteCircleRequest(id)); + + api(getState).delete(`/api/v1/circles/${id}`) + .then(() => dispatch(deleteCircleSuccess(id))) + .catch(err => dispatch(deleteCircleFail(id, err))); +}; + +export const deleteCircleRequest = id => ({ + type: CIRCLE_DELETE_REQUEST, + id, +}); + +export const deleteCircleSuccess = id => ({ + type: CIRCLE_DELETE_SUCCESS, + id, +}); + +export const deleteCircleFail = (id, error) => ({ + type: CIRCLE_DELETE_FAIL, + id, + error, +}); + +export const fetchCircleAccounts = circleId => (dispatch, getState) => { + dispatch(fetchCircleAccountsRequest(circleId)); + + api(getState).get(`/api/v1/circles/${circleId}/accounts`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchCircleAccountsSuccess(circleId, data)); + }).catch(err => dispatch(fetchCircleAccountsFail(circleId, err))); +}; + +export const fetchCircleAccountsRequest = id => ({ + type: CIRCLE_ACCOUNTS_FETCH_REQUEST, + id, +}); + +export const fetchCircleAccountsSuccess = (id, accounts, next) => ({ + type: CIRCLE_ACCOUNTS_FETCH_SUCCESS, + id, + accounts, + next, +}); + +export const fetchCircleAccountsFail = (id, error) => ({ + type: CIRCLE_ACCOUNTS_FETCH_FAIL, + id, + error, +}); + +export const fetchCircleSuggestions = q => (dispatch, getState) => { + const params = { + q, + resolve: false, + follower: true, + }; + + api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchCircleSuggestionsReady(q, data)); + }).catch(error => dispatch(showAlertForError(error))); +}; + +export const fetchCircleSuggestionsReady = (query, accounts) => ({ + type: CIRCLE_EDITOR_SUGGESTIONS_READY, + query, + accounts, +}); + +export const clearCircleSuggestions = () => ({ + type: CIRCLE_EDITOR_SUGGESTIONS_CLEAR, +}); + +export const changeCircleSuggestions = value => ({ + type: CIRCLE_EDITOR_SUGGESTIONS_CHANGE, + value, +}); + +export const addToCircleEditor = accountId => (dispatch, getState) => { + dispatch(addToCircle(getState().getIn(['circleEditor', 'circleId']), accountId)); +}; + +export const addToCircle = (circleId, accountId) => (dispatch, getState) => { + dispatch(addToCircleRequest(circleId, accountId)); + + api(getState).post(`/api/v1/circles/${circleId}/accounts`, { account_ids: [accountId] }) + .then(() => dispatch(addToCircleSuccess(circleId, accountId))) + .catch(err => dispatch(addToCircleFail(circleId, accountId, err))); +}; + +export const addToCircleRequest = (circleId, accountId) => ({ + type: CIRCLE_EDITOR_ADD_REQUEST, + circleId, + accountId, +}); + +export const addToCircleSuccess = (circleId, accountId) => ({ + type: CIRCLE_EDITOR_ADD_SUCCESS, + circleId, + accountId, +}); + +export const addToCircleFail = (circleId, accountId, error) => ({ + type: CIRCLE_EDITOR_ADD_FAIL, + circleId, + accountId, + error, +}); + +export const removeFromCircleEditor = accountId => (dispatch, getState) => { + dispatch(removeFromCircle(getState().getIn(['circleEditor', 'circleId']), accountId)); +}; + +export const removeFromCircle = (circleId, accountId) => (dispatch, getState) => { + dispatch(removeFromCircleRequest(circleId, accountId)); + + api(getState).delete(`/api/v1/circles/${circleId}/accounts`, { params: { account_ids: [accountId] } }) + .then(() => dispatch(removeFromCircleSuccess(circleId, accountId))) + .catch(err => dispatch(removeFromCircleFail(circleId, accountId, err))); +}; + +export const removeFromCircleRequest = (circleId, accountId) => ({ + type: CIRCLE_EDITOR_REMOVE_REQUEST, + circleId, + accountId, +}); + +export const removeFromCircleSuccess = (circleId, accountId) => ({ + type: CIRCLE_EDITOR_REMOVE_SUCCESS, + circleId, + accountId, +}); + +export const removeFromCircleFail = (circleId, accountId, error) => ({ + type: CIRCLE_EDITOR_REMOVE_FAIL, + circleId, + accountId, + error, +}); + +export const resetCircleAdder = () => ({ + type: CIRCLE_ADDER_RESET, +}); + +export const setupCircleAdder = accountId => (dispatch, getState) => { + dispatch({ + type: CIRCLE_ADDER_SETUP, + account: getState().getIn(['accounts', accountId]), + }); + dispatch(fetchCircles()); + dispatch(fetchAccountCircles(accountId)); +}; + +export const fetchAccountCircles = accountId => (dispatch, getState) => { + dispatch(fetchAccountCirclesRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/circles`) + .then(({ data }) => dispatch(fetchAccountCirclesSuccess(accountId, data))) + .catch(err => dispatch(fetchAccountCirclesFail(accountId, err))); +}; + +export const fetchAccountCirclesRequest = id => ({ + type:CIRCLE_ADDER_CIRCLES_FETCH_REQUEST, + id, +}); + +export const fetchAccountCirclesSuccess = (id, circles) => ({ + type: CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS, + id, + circles, +}); + +export const fetchAccountCirclesFail = (id, err) => ({ + type: CIRCLE_ADDER_CIRCLES_FETCH_FAIL, + id, + err, +}); + +export const addToCircleAdder = circleId => (dispatch, getState) => { + dispatch(addToCircle(circleId, getState().getIn(['circleAdder', 'accountId']))); +}; + +export const removeFromCircleAdder = circleId => (dispatch, getState) => { + dispatch(removeFromCircle(circleId, getState().getIn(['circleAdder', 'accountId']))); +}; + diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index cf695f79b4..381cfdd42f 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -75,6 +75,8 @@ export const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE'; export const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE'; export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE'; +export const COMPOSE_CIRCLE_CHANGE = 'COMPOSE_CIRCLE_CHANGE'; + export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL'; export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION'; @@ -211,6 +213,7 @@ export function submitCompose(routerHistory) { markdown: getState().getIn(['compose', 'markdown']), visibility: getState().getIn(['compose', 'privacy']), searchability: getState().getIn(['compose', 'searchability']), + circle_id: getState().getIn(['compose', 'circle_id']), poll: getState().getIn(['compose', 'poll'], null), language: getState().getIn(['compose', 'language']), }, @@ -837,3 +840,10 @@ export function changePollSettings(expiresIn, isMultiple) { isMultiple, }; } + +export function changeCircle(circleId) { + return { + type: COMPOSE_CIRCLE_CHANGE, + circleId, + }; +} diff --git a/app/javascript/mastodon/components/icon_button.tsx b/app/javascript/mastodon/components/icon_button.tsx index 9dbee2cc24..ee63fba0e9 100644 --- a/app/javascript/mastodon/components/icon_button.tsx +++ b/app/javascript/mastodon/components/icon_button.tsx @@ -27,6 +27,7 @@ interface Props { obfuscateCount?: boolean; href?: string; ariaHidden: boolean; + data_id?: string; } interface States { activate: boolean; @@ -108,6 +109,7 @@ export class IconButton extends PureComponent { obfuscateCount, href, ariaHidden, + data_id, } = this.props; const { activate, deactivate } = this.state; @@ -160,6 +162,7 @@ export class IconButton extends PureComponent { style={style} tabIndex={tabIndex} disabled={disabled} + data-id={data_id} > {contents} diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 93195e61ea..43c31dca0e 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -72,6 +72,7 @@ const messages = defineMessages({ private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' }, mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' }, + circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle members only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, }); @@ -403,6 +404,7 @@ class Status extends ImmutablePureComponent { 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) }, 'limited': { icon: 'get-pocket', text: intl.formatMessage(messages.limited_short) }, 'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) }, + 'circle': { icon: 'user-circle', text: intl.formatMessage(messages.circle_short) }, 'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) }, }; diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index da76bd1c0b..579432794d 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -8,6 +8,7 @@ import { Provider as ReduxProvider } from 'react-redux'; import { ScrollContext } from 'react-router-scroll-4'; +import { fetchCircles } from 'mastodon/actions/circles'; import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis'; import { fetchReactionDeck } from 'mastodon/actions/reaction_deck'; import { hydrateStore } from 'mastodon/actions/store'; @@ -27,6 +28,7 @@ store.dispatch(hydrateAction); if (initialState.meta.me) { store.dispatch(fetchCustomEmojis()); store.dispatch(fetchReactionDeck()); + store.dispatch(fetchCircles()); } const createIdentityContext = state => ({ diff --git a/app/javascript/mastodon/features/antenna_editor/components/account.jsx b/app/javascript/mastodon/features/antenna_editor/components/account.jsx index 94daad4523..ea3a950b2d 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,14 @@ class Account extends ImmutablePureComponent { }; render () { - const { account, intl, onRemove, onAdd, added } = this.props; + const { account, intl, isExclude, onRemove, onAdd, onExcludeRemove, onExcludeAdd, added } = this.props; 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/components/radio_panel.jsx b/app/javascript/mastodon/features/antenna_setting/components/radio_panel.jsx new file mode 100644 index 0000000000..57a88eba65 --- /dev/null +++ b/app/javascript/mastodon/features/antenna_setting/components/radio_panel.jsx @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { injectIntl } from 'react-intl'; + +import classNames from 'classnames'; + + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; + +class RadioPanel extends PureComponent { + + static propTypes = { + values: ImmutablePropTypes.list.isRequired, + value: PropTypes.object.isRequired, + intl: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, + }; + + handleChange = e => { + const value = e.currentTarget.getAttribute('data-value'); + + if (value !== this.props.value.get('value')) { + this.props.onChange(value); + } + }; + + render () { + const { values, value } = this.props; + + return ( +
+ {values.map((val) => ( + + ))} +
+ ); + } + +} + +export default connect()(injectIntl(RadioPanel)); diff --git a/app/javascript/mastodon/features/antenna_setting/components/text_list.jsx b/app/javascript/mastodon/features/antenna_setting/components/text_list.jsx index 7f4d5b67ac..cce7bf70f3 100644 --- a/app/javascript/mastodon/features/antenna_setting/components/text_list.jsx +++ b/app/javascript/mastodon/features/antenna_setting/components/text_list.jsx @@ -59,6 +59,11 @@ class TextList extends PureComponent { this.props.onAdd(); }; + handleSubmit = (e) => { + e.preventDefault(); + this.handleAdd(); + }; + render () { const { icon, value, values, disabled, label, title } = this.props; @@ -68,7 +73,7 @@ class TextList extends PureComponent { ))} -
+