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 {