diff --git a/app/controllers/api/v1/antennas/exclude_tags_controller.rb b/app/controllers/api/v1/antennas/exclude_tags_controller.rb new file mode 100644 index 0000000000..bf9e087369 --- /dev/null +++ b/app/controllers/api/v1/antennas/exclude_tags_controller.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class Api::V1::Antennas::ExcludeTagsController < Api::BaseController + before_action -> { doorkeeper_authorize! :write, :'write:lists' } + + before_action :require_user! + before_action :set_antenna + + def create + new_tags = @antenna.exclude_tags || [] + tags.map(&:id).each do |tag| + raise Mastodon::ValidationError, I18n.t('antennas.errors.duplicate_tag') if new_tags.include?(tag) + + new_tags << tag + end + + raise Mastodon::ValidationError, I18n.t('antennas.errors.limit.tags') if new_tags.size > Antenna::TAGS_PER_ANTENNA_LIMIT + + @antenna.update!(exclude_tags: new_tags) + + render_empty + end + + def destroy + new_tags = @antenna.exclude_tags || [] + new_tags -= exist_tags.pluck(:id) + + @antenna.update!(exclude_tags: new_tags) + + render_empty + end + + private + + def set_antenna + @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) + end + + def tags + Tag.find_or_create_by_names(Array(resource_params[:tags])) + end + + def exist_tags + Tag.matching_name(Array(resource_params[:tags])) + end + + def resource_params + params.permit(tags: []) + end +end diff --git a/app/controllers/api/v1/antennas/tags_controller.rb b/app/controllers/api/v1/antennas/tags_controller.rb new file mode 100644 index 0000000000..fe0bb6b4eb --- /dev/null +++ b/app/controllers/api/v1/antennas/tags_controller.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class Api::V1::Antennas::TagsController < 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 + + def show + @tags = load_tags + @exclude_tags = load_exclude_tags + render json: { tags: @tags, exclude_tags: @exclude_tags.pluck(:name) } + end + + def create + ApplicationRecord.transaction do + tags.each do |tag| + @antenna.antenna_tags.create!(tag: tag, exclude: false) + @antenna.update!(any_tags: false) if @antenna.any_tags + end + end + + render_empty + end + + def destroy + AntennaTag.where(antenna: @antenna, tag: exist_tags).destroy_all + @antenna.update!(any_tags: true) unless @antenna.antenna_tags.where(exclude: false).any? + render_empty + end + + private + + def set_antenna + @antenna = Antenna.where(account: current_account).find(params[:antenna_id]) + end + + def load_tags + @antenna.tags.pluck(:name) + end + + def load_exclude_tags + Tag.where(id: @antenna.exclude_tags || []) + end + + def tags + Tag.find_or_create_by_names(Array(resource_params[:tags])) + end + + def exist_tags + Tag.matching_name(Array(resource_params[:tags])) + end + + def resource_params + params.permit(tags: []) + end +end diff --git a/app/javascript/mastodon/actions/antennas.js b/app/javascript/mastodon/actions/antennas.js index 3e561cbacb..2b0d5d042a 100644 --- a/app/javascript/mastodon/actions/antennas.js +++ b/app/javascript/mastodon/actions/antennas.js @@ -95,6 +95,26 @@ export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_REM 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_EDITOR_FETCH_TAGS_REQUEST = 'ANTENNA_EDITOR_FETCH_TAGS_REQUEST'; +export const ANTENNA_EDITOR_FETCH_TAGS_SUCCESS = 'ANTENNA_EDITOR_FETCH_TAGS_SUCCESS'; +export const ANTENNA_EDITOR_FETCH_TAGS_FAIL = 'ANTENNA_EDITOR_FETCH_TAGS_FAIL'; + +export const ANTENNA_EDITOR_ADD_TAG_REQUEST = 'ANTENNA_EDITOR_ADD_TAG_REQUEST'; +export const ANTENNA_EDITOR_ADD_TAG_SUCCESS = 'ANTENNA_EDITOR_ADD_TAG_SUCCESS'; +export const ANTENNA_EDITOR_ADD_TAG_FAIL = 'ANTENNA_EDITOR_ADD_TAG_FAIL'; + +export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS'; +export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL'; + +export const ANTENNA_EDITOR_REMOVE_TAG_REQUEST = 'ANTENNA_EDITOR_REMOVE_TAG_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_TAG_SUCCESS = 'ANTENNA_EDITOR_REMOVE_TAG_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_TAG_FAIL = 'ANTENNA_EDITOR_REMOVE_TAG_FAIL'; + +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS'; +export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL'; + export const ANTENNA_ADDER_RESET = 'ANTENNA_ADDER_RESET'; export const ANTENNA_ADDER_SETUP = 'ANTENNA_ADDER_SETUP'; @@ -739,6 +759,139 @@ export const removeExcludeKeywordFromAntennaFail = (antennaId, keyword, error) = error, }); +export const fetchAntennaTags = antennaId => (dispatch, getState) => { + dispatch(fetchAntennaTagsRequest(antennaId)); + + api(getState).get(`/api/v1/antennas/${antennaId}/tags`, { params: { limit: 0 } }).then(({ data }) => { + dispatch(fetchAntennaTagsSuccess(antennaId, data)); + }).catch(err => dispatch(fetchAntennaTagsFail(antennaId, err))); +}; + +export const fetchAntennaTagsRequest = id => ({ + type: ANTENNA_EDITOR_FETCH_TAGS_REQUEST, + id, +}); + +export const fetchAntennaTagsSuccess = (id, tags) => ({ + type: ANTENNA_EDITOR_FETCH_TAGS_SUCCESS, + id, + tags, +}); + +export const fetchAntennaTagsFail = (id, error) => ({ + type: ANTENNA_EDITOR_FETCH_TAGS_FAIL, + id, + error, +}); + +export const addTagToAntenna = (antennaId, tag) => (dispatch, getState) => { + dispatch(addTagToAntennaRequest(antennaId, tag)); + + api(getState).post(`/api/v1/antennas/${antennaId}/tags`, { tags: [tag] }) + .then(() => dispatch(addTagToAntennaSuccess(antennaId, tag))) + .catch(err => dispatch(addTagToAntennaFail(antennaId, tag, err))); +}; + +export const addTagToAntennaRequest = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_ADD_TAG_REQUEST, + antennaId, + tag, +}); + +export const addTagToAntennaSuccess = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_ADD_TAG_SUCCESS, + antennaId, + tag, +}); + +export const addTagToAntennaFail = (antennaId, tag, error) => ({ + type: ANTENNA_EDITOR_ADD_TAG_FAIL, + antennaId, + tag, + error, +}); + +export const removeTagFromAntenna = (antennaId, tag) => (dispatch, getState) => { + dispatch(removeTagFromAntennaRequest(antennaId, tag)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/tags`, { params: { tags: [tag] } }) + .then(() => dispatch(removeTagFromAntennaSuccess(antennaId, tag))) + .catch(err => dispatch(removeTagFromAntennaFail(antennaId, tag, err))); +}; + +export const removeTagFromAntennaRequest = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_REMOVE_TAG_REQUEST, + antennaId, + tag, +}); + +export const removeTagFromAntennaSuccess = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_REMOVE_TAG_SUCCESS, + antennaId, + tag, +}); + +export const removeTagFromAntennaFail = (antennaId, tag, error) => ({ + type: ANTENNA_EDITOR_REMOVE_TAG_FAIL, + antennaId, + tag, + error, +}); + +export const addExcludeTagToAntenna = (antennaId, tag) => (dispatch, getState) => { + dispatch(addExcludeTagToAntennaRequest(antennaId, tag)); + + api(getState).post(`/api/v1/antennas/${antennaId}/exclude_tags`, { tags: [tag] }) + .then(() => dispatch(addExcludeTagToAntennaSuccess(antennaId, tag))) + .catch(err => dispatch(addExcludeTagToAntennaFail(antennaId, tag, err))); +}; + +export const addExcludeTagToAntennaRequest = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST, + antennaId, + tag, +}); + +export const addExcludeTagToAntennaSuccess = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS, + antennaId, + tag, +}); + +export const addExcludeTagToAntennaFail = (antennaId, tag, error) => ({ + type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL, + antennaId, + tag, + error, +}); + +export const removeExcludeTagFromAntenna = (antennaId, tag) => (dispatch, getState) => { + dispatch(removeExcludeTagFromAntennaRequest(antennaId, tag)); + + api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_tags`, { params: { tags: [tag] } }) + .then(() => dispatch(removeExcludeTagFromAntennaSuccess(antennaId, tag))) + .catch(err => dispatch(removeExcludeTagFromAntennaFail(antennaId, tag, err))); +}; + +export const removeExcludeTagFromAntennaRequest = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST, + antennaId, + tag, +}); + +export const removeExcludeTagFromAntennaSuccess = (antennaId, tag) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS, + antennaId, + tag, +}); + +export const removeExcludeTagFromAntennaFail = (antennaId, tag, error) => ({ + type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL, + antennaId, + tag, + error, +}); + export const resetAntennaAdder = () => ({ type: ANTENNA_ADDER_RESET, }); diff --git a/app/javascript/mastodon/features/antenna_setting/index.jsx b/app/javascript/mastodon/features/antenna_setting/index.jsx index 7e0c236c83..83f21fd066 100644 --- a/app/javascript/mastodon/features/antenna_setting/index.jsx +++ b/app/javascript/mastodon/features/antenna_setting/index.jsx @@ -25,7 +25,12 @@ import { removeKeywordFromAntenna, addKeywordToAntenna, removeExcludeKeywordFromAntenna, - addExcludeKeywordToAntenna + addExcludeKeywordToAntenna, + fetchAntennaTags, + removeTagFromAntenna, + addTagToAntenna, + removeExcludeTagFromAntenna, + addExcludeTagToAntenna, } from 'mastodon/actions/antennas'; import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; import { fetchLists } from 'mastodon/actions/lists'; @@ -48,8 +53,10 @@ const messages = defineMessages({ placeholder: { id: 'antennas.select.placeholder', defaultMessage: 'Select list' }, addDomainLabel: { id: 'antennas.add_domain_placeholder', defaultMessage: 'New domain' }, addKeywordLabel: { id: 'antennas.add_keyword_placeholder', defaultMessage: 'New keyword' }, + addTagLabel: { id: 'antennas.add_tag_placeholder', defaultMessage: 'New tag' }, addDomainTitle: { id: 'antennas.add_domain', defaultMessage: 'Add domain' }, addKeywordTitle: { id: 'antennas.add_keyword', defaultMessage: 'Add keyword' }, + addTagTitle: { id: 'antennas.add_tag', defaultMessage: 'Add tag' }, accounts: { id: 'antennas.accounts', defaultMessage: '{count} accounts' }, domains: { id: 'antennas.domains', defaultMessage: '{count} domains' }, tags: { id: 'antennas.tags', defaultMessage: '{count} tags' }, @@ -62,6 +69,7 @@ const mapStateToProps = (state, props) => ({ lists: state.get('lists'), domains: state.getIn(['antennas', props.params.id, 'domains']) || ImmutableMap(), keywords: state.getIn(['antennas', props.params.id, 'keywords']) || ImmutableMap(), + tags: state.getIn(['antennas', props.params.id, 'tags']) || ImmutableMap(), }); class AntennaSetting extends PureComponent { @@ -79,6 +87,7 @@ class AntennaSetting extends PureComponent { lists: ImmutablePropTypes.map, domains: ImmutablePropTypes.map, keywords: ImmutablePropTypes.map, + tags: ImmutablePropTypes.map, intl: PropTypes.object.isRequired, }; @@ -87,6 +96,8 @@ class AntennaSetting extends PureComponent { excludeDomainName: '', keywordName: '', excludeKeywordName: '', + tagName: '', + excludeTagName: '', rangeRadioValue: null, contentRadioValue: null, }; @@ -118,6 +129,7 @@ class AntennaSetting extends PureComponent { dispatch(fetchAntenna(id)); dispatch(fetchAntennaDomains(id)); dispatch(fetchAntennaKeywords(id)); + dispatch(fetchAntennaTags(id)); dispatch(fetchLists()); } @@ -130,6 +142,7 @@ class AntennaSetting extends PureComponent { dispatch(fetchAntennaKeywords(id)); dispatch(fetchAntennaDomains(id)); dispatch(fetchAntennaKeywords(id)); + dispatch(fetchAntennaTags(id)); dispatch(fetchLists()); } } @@ -238,6 +251,15 @@ class AntennaSetting extends PureComponent { onKeywordRemove = (value) => this.props.dispatch(removeKeywordFromAntenna(this.props.params.id, value)); + onTagNameChanged = (value) => this.setState({ tagName: value }); + + onTagAdd = () => { + this.props.dispatch(addTagToAntenna(this.props.params.id, this.state.tagName)); + this.setState({ tagName: '' }); + }; + + onTagRemove = (value) => this.props.dispatch(removeTagFromAntenna(this.props.params.id, value)); + onExcludeDomainNameChanged = (value) => this.setState({ excludeDomainName: value }); onExcludeDomainAdd = () => { @@ -256,8 +278,17 @@ class AntennaSetting extends PureComponent { onExcludeKeywordRemove = (value) => this.props.dispatch(removeExcludeKeywordFromAntenna(this.props.params.id, value)); + onExcludeTagNameChanged = (value) => this.setState({ excludeTagName: value }); + + onExcludeTagAdd = () => { + this.props.dispatch(addExcludeTagToAntenna(this.props.params.id, this.state.excludeTagName)); + this.setState({ excludeTagName: '' }); + }; + + onExcludeTagRemove = (value) => this.props.dispatch(removeExcludeTagFromAntenna(this.props.params.id, value)); + render () { - const { columnId, multiColumn, antenna, lists, domains, keywords, intl } = this.props; + const { columnId, multiColumn, antenna, lists, domains, keywords, tags, intl } = this.props; const { id } = this.props.params; const pinned = !!columnId; const title = antenna ? antenna.get('title') : id; @@ -422,6 +453,19 @@ class AntennaSetting extends PureComponent { + {contentRadioValue.get('value') === 'tags' && ( + + )} + {contentRadioValue.get('value') === 'keywords' && ( +

+ )} diff --git a/app/javascript/mastodon/reducers/antennas.js b/app/javascript/mastodon/reducers/antennas.js index e6075dde90..7ef16f8dd2 100644 --- a/app/javascript/mastodon/reducers/antennas.js +++ b/app/javascript/mastodon/reducers/antennas.js @@ -19,6 +19,11 @@ import { ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS, ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS, ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS, + ANTENNA_EDITOR_ADD_TAG_SUCCESS, + ANTENNA_EDITOR_REMOVE_TAG_SUCCESS, + ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS, + ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS, + ANTENNA_EDITOR_FETCH_TAGS_SUCCESS, } from '../actions/antennas'; const initialState = ImmutableMap(); @@ -85,6 +90,16 @@ export default function antennas(state = initialState, action) { 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({ keywords: ImmutableList(action.keywords.keywords), exclude_keywords: ImmutableList(action.keywords.exclude_keywords) })); + case ANTENNA_EDITOR_ADD_TAG_SUCCESS: + return state.setIn([action.antennaId, 'tags_count'], state.getIn([action.antennaId, 'tags_count']) + 1).updateIn([action.antennaId, 'tags', 'tags'], tags => (ImmutableList(tags || [])).push(action.tag)); + case ANTENNA_EDITOR_REMOVE_TAG_SUCCESS: + return state.setIn([action.antennaId, 'tags_count'], state.getIn([action.antennaId, 'tags_count']) - 1).updateIn([action.antennaId, 'tags', 'tags'], tags => (ImmutableList(tags || [])).filterNot(tag => tag === action.tag)); + case ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS: + return state.updateIn([action.antennaId, 'tags', 'exclude_tags'], tags => (ImmutableList(tags || [])).push(action.tag)); + case ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS: + return state.updateIn([action.antennaId, 'tags', 'exclude_tags'], tags => (ImmutableList(tags || [])).filterNot(tag => tag === action.tag)); + case ANTENNA_EDITOR_FETCH_TAGS_SUCCESS: + return state.setIn([action.id, 'tags'], ImmutableMap({ tags: ImmutableList(action.tags.tags), exclude_tags: ImmutableList(action.tags.exclude_tags) })); default: return state; } diff --git a/app/models/antenna.rb b/app/models/antenna.rb index a52e78192d..73b79c9c97 100644 --- a/app/models/antenna.rb +++ b/app/models/antenna.rb @@ -37,6 +37,7 @@ class Antenna < ApplicationRecord has_many :antenna_domains, inverse_of: :antenna, dependent: :destroy has_many :antenna_tags, inverse_of: :antenna, dependent: :destroy + has_many :tags, through: :antenna_tags has_many :antenna_accounts, inverse_of: :antenna, dependent: :destroy has_many :accounts, through: :antenna_accounts diff --git a/config/routes/api.rb b/config/routes/api.rb index 04db787e69..4739d8eaf1 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -210,9 +210,11 @@ 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 :tags, only: [:show, :create, :destroy], controller: 'antennas/tags' 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' + resource :exclude_tags, only: [:create, :destroy], controller: 'antennas/exclude_tags' end resources :circles, only: [:index, :create, :show, :update, :destroy] do