diff --git a/app/controllers/api/v1/antennas/accounts_controller.rb b/app/controllers/api/v1/antennas/accounts_controller.rb
index abeb4a6e4e..4353d3e4e9 100644
--- a/app/controllers/api/v1/antennas/accounts_controller.rb
+++ b/app/controllers/api/v1/antennas/accounts_controller.rb
@@ -1,13 +1,18 @@
# frozen_string_literal: true
class Api::V1::Antennas::AccountsController < Api::BaseController
- # before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show]
- before_action -> { doorkeeper_authorize! :write, :'write:lists' } # , except: [:show]
+ 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
+ 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
@@ -32,6 +37,14 @@ class Api::V1::Antennas::AccountsController < Api::BaseController
@antenna = Antenna.where(account: current_account).find(params[:antenna_id])
end
+ def load_accounts
+ if unlimited?
+ @antenna.accounts.without_suspended.includes(:account_stat).all
+ else
+ @antenna.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
diff --git a/app/controllers/api/v1/antennas_controller.rb b/app/controllers/api/v1/antennas_controller.rb
index 2e47af4a44..7c4fd1d329 100644
--- a/app/controllers/api/v1/antennas_controller.rb
+++ b/app/controllers/api/v1/antennas_controller.rb
@@ -5,7 +5,7 @@ class Api::V1::AntennasController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:index, :show]
before_action :require_user!
- before_action :set_antenna, except: [:index]
+ before_action :set_antenna, except: [:index, :create]
rescue_from ArgumentError do |e|
render json: { error: e.to_s }, status: 422
@@ -20,9 +20,28 @@ class Api::V1::AntennasController < Api::BaseController
render json: @antenna, serializer: REST::AntennaSerializer
end
+ def create
+ @antenna = Antenna.create!(antenna_params.merge(account: current_account, list_id: 0))
+ render json: @antenna, serializer: REST::AntennaSerializer
+ end
+
+ def update
+ @antenna.update!(antenna_params)
+ render json: @antenna, serializer: REST::AntennaSerializer
+ end
+
+ def destroy
+ @antenna.destroy!
+ render_empty
+ end
+
private
def set_antenna
@antenna = Antenna.where(account: current_account).find(params[:id])
end
+
+ def antenna_params
+ params.permit(:title, :stl, :with_media_only, :ignore_reblog)
+ end
end
diff --git a/app/javascript/mastodon/actions/antennas.js b/app/javascript/mastodon/actions/antennas.js
index d8a6a405d6..163b52b3b2 100644
--- a/app/javascript/mastodon/actions/antennas.js
+++ b/app/javascript/mastodon/actions/antennas.js
@@ -1,29 +1,82 @@
import api from '../api';
+import { showAlertForError } from './alerts';
import { importFetchedAccounts } from './importer';
+export const ANTENNA_FETCH_REQUEST = 'ANTENNA_FETCH_REQUEST';
+export const ANTENNA_FETCH_SUCCESS = 'ANTENNA_FETCH_SUCCESS';
+export const ANTENNA_FETCH_FAIL = 'ANTENNA_FETCH_FAIL';
+
export const ANTENNAS_FETCH_REQUEST = 'ANTENNAS_FETCH_REQUEST';
export const ANTENNAS_FETCH_SUCCESS = 'ANTENNAS_FETCH_SUCCESS';
export const ANTENNAS_FETCH_FAIL = 'ANTENNAS_FETCH_FAIL';
+export const ANTENNA_EDITOR_TITLE_CHANGE = 'ANTENNA_EDITOR_TITLE_CHANGE';
+export const ANTENNA_EDITOR_RESET = 'ANTENNA_EDITOR_RESET';
+export const ANTENNA_EDITOR_SETUP = 'ANTENNA_EDITOR_SETUP';
+
+export const ANTENNA_CREATE_REQUEST = 'ANTENNA_CREATE_REQUEST';
+export const ANTENNA_CREATE_SUCCESS = 'ANTENNA_CREATE_SUCCESS';
+export const ANTENNA_CREATE_FAIL = 'ANTENNA_CREATE_FAIL';
+
+export const ANTENNA_UPDATE_REQUEST = 'ANTENNA_UPDATE_REQUEST';
+export const ANTENNA_UPDATE_SUCCESS = 'ANTENNA_UPDATE_SUCCESS';
+export const ANTENNA_UPDATE_FAIL = 'ANTENNA_UPDATE_FAIL';
+
+export const ANTENNA_DELETE_REQUEST = 'ANTENNA_DELETE_REQUEST';
+export const ANTENNA_DELETE_SUCCESS = 'ANTENNA_DELETE_SUCCESS';
+export const ANTENNA_DELETE_FAIL = 'ANTENNA_DELETE_FAIL';
+
export const ANTENNA_ACCOUNTS_FETCH_REQUEST = 'ANTENNA_ACCOUNTS_FETCH_REQUEST';
export const ANTENNA_ACCOUNTS_FETCH_SUCCESS = 'ANTENNA_ACCOUNTS_FETCH_SUCCESS';
export const ANTENNA_ACCOUNTS_FETCH_FAIL = 'ANTENNA_ACCOUNTS_FETCH_FAIL';
-export const ANTENNA_EDITOR_ADD_ACCOUNT_REQUEST = 'ANTENNA_EDITOR_ADD_ACCOUNT_REQUEST';
-export const ANTENNA_EDITOR_ADD_ACCOUNT_SUCCESS = 'ANTENNA_EDITOR_ADD_ACCOUNT_SUCCESS';
-export const ANTENNA_EDITOR_ADD_ACCOUNT_FAIL = 'ANTENNA_EDITOR_ADD_ACCOUNT_FAIL';
+export const ANTENNA_EDITOR_SUGGESTIONS_CHANGE = 'ANTENNA_EDITOR_SUGGESTIONS_CHANGE';
+export const ANTENNA_EDITOR_SUGGESTIONS_READY = 'ANTENNA_EDITOR_SUGGESTIONS_READY';
+export const ANTENNA_EDITOR_SUGGESTIONS_CLEAR = 'ANTENNA_EDITOR_SUGGESTIONS_CLEAR';
-export const ANTENNA_EDITOR_REMOVE_ACCOUNT_REQUEST = 'ANTENNA_EDITOR_REMOVE_ACCOUNT_REQUEST';
-export const ANTENNA_EDITOR_REMOVE_ACCOUNT_SUCCESS = 'ANTENNA_EDITOR_REMOVE_ACCOUNT_SUCCESS';
-export const ANTENNA_EDITOR_REMOVE_ACCOUNT_FAIL = 'ANTENNA_EDITOR_REMOVE_ACCOUNT_FAIL';
+export const ANTENNA_EDITOR_ADD_REQUEST = 'ANTENNA_EDITOR_ADD_REQUEST';
+export const ANTENNA_EDITOR_ADD_SUCCESS = 'ANTENNA_EDITOR_ADD_SUCCESS';
+export const ANTENNA_EDITOR_ADD_FAIL = 'ANTENNA_EDITOR_ADD_FAIL';
+
+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_ADDER_RESET = 'ANTENNA_ADDER_RESET';
+export const ANTENNA_ADDER_SETUP = 'ANTENNA_ADDER_SETUP';
export const ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST = 'ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST';
export const ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS = 'ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS';
export const ANTENNA_ADDER_ANTENNAS_FETCH_FAIL = 'ANTENNA_ADDER_ANTENNAS_FETCH_FAIL';
-export const ANTENNA_ADDER_RESET = 'ANTENNA_ADDER_RESET';
-export const ANTENNA_ADDER_SETUP = 'ANTENNA_ADDER_SETUP';
+export const fetchAntenna = id => (dispatch, getState) => {
+ if (getState().getIn(['antennas', id])) {
+ return;
+ }
+
+ dispatch(fetchAntennaRequest(id));
+
+ api(getState).get(`/api/v1/antennas/${id}`)
+ .then(({ data }) => dispatch(fetchAntennaSuccess(data)))
+ .catch(err => dispatch(fetchAntennaFail(id, err)));
+};
+
+export const fetchAntennaRequest = id => ({
+ type: ANTENNA_FETCH_REQUEST,
+ id,
+});
+
+export const fetchAntennaSuccess = antenna => ({
+ type: ANTENNA_FETCH_SUCCESS,
+ antenna,
+});
+
+export const fetchAntennaFail = (id, error) => ({
+ type: ANTENNA_FETCH_FAIL,
+ id,
+ error,
+});
export const fetchAntennas = () => (dispatch, getState) => {
dispatch(fetchAntennasRequest());
@@ -47,6 +100,113 @@ export const fetchAntennasFail = error => ({
error,
});
+export const submitAntennaEditor = shouldReset => (dispatch, getState) => {
+ const antennaId = getState().getIn(['antennaEditor', 'antennaId']);
+ const title = getState().getIn(['antennaEditor', 'title']);
+
+ if (antennaId === null) {
+ dispatch(createAntenna(title, shouldReset));
+ } else {
+ dispatch(updateAntenna(antennaId, title, shouldReset));
+ }
+};
+
+export const setupAntennaEditor = antennaId => (dispatch, getState) => {
+ dispatch({
+ type: ANTENNA_EDITOR_SETUP,
+ antenna: getState().getIn(['antennas', antennaId]),
+ });
+
+ dispatch(fetchAntennaAccounts(antennaId));
+};
+
+export const changeAntennaEditorTitle = value => ({
+ type: ANTENNA_EDITOR_TITLE_CHANGE,
+ value,
+});
+
+export const createAntenna = (title, shouldReset) => (dispatch, getState) => {
+ dispatch(createAntennaRequest());
+
+ api(getState).post('/api/v1/antennas', { title }).then(({ data }) => {
+ dispatch(createAntennaSuccess(data));
+
+ if (shouldReset) {
+ dispatch(resetAntennaEditor());
+ }
+ }).catch(err => dispatch(createAntennaFail(err)));
+};
+
+export const createAntennaRequest = () => ({
+ type: ANTENNA_CREATE_REQUEST,
+});
+
+export const createAntennaSuccess = antenna => ({
+ type: ANTENNA_CREATE_SUCCESS,
+ antenna,
+});
+
+export const createAntennaFail = error => ({
+ type: ANTENNA_CREATE_FAIL,
+ error,
+});
+
+export const updateAntenna = (id, title, shouldReset, stl, with_media_only, ignore_reblog) => (dispatch, getState) => {
+ dispatch(updateAntennaRequest(id));
+
+ api(getState).put(`/api/v1/antennas/${id}`, { title, stl, with_media_only, ignore_reblog }).then(({ data }) => {
+ dispatch(updateAntennaSuccess(data));
+
+ if (shouldReset) {
+ dispatch(resetAntennaEditor());
+ }
+ }).catch(err => dispatch(updateAntennaFail(id, err)));
+};
+
+export const updateAntennaRequest = id => ({
+ type: ANTENNA_UPDATE_REQUEST,
+ id,
+});
+
+export const updateAntennaSuccess = antenna => ({
+ type: ANTENNA_UPDATE_SUCCESS,
+ antenna,
+});
+
+export const updateAntennaFail = (id, error) => ({
+ type: ANTENNA_UPDATE_FAIL,
+ id,
+ error,
+});
+
+export const resetAntennaEditor = () => ({
+ type: ANTENNA_EDITOR_RESET,
+});
+
+export const deleteAntenna = id => (dispatch, getState) => {
+ dispatch(deleteAntennaRequest(id));
+
+ api(getState).delete(`/api/v1/antennas/${id}`)
+ .then(() => dispatch(deleteAntennaSuccess(id)))
+ .catch(err => dispatch(deleteAntennaFail(id, err)));
+};
+
+export const deleteAntennaRequest = id => ({
+ type: ANTENNA_DELETE_REQUEST,
+ id,
+});
+
+export const deleteAntennaSuccess = id => ({
+ type: ANTENNA_DELETE_SUCCESS,
+ id,
+});
+
+export const deleteAntennaFail = (id, error) => ({
+ type: ANTENNA_DELETE_FAIL,
+ id,
+ error,
+});
+
export const fetchAntennaAccounts = antennaId => (dispatch, getState) => {
dispatch(fetchAntennaAccountsRequest(antennaId));
@@ -74,97 +234,95 @@ export const fetchAntennaAccountsFail = (id, error) => ({
error,
});
-export const addAccountToAntenna = (antennaId, accountId) => (dispatch, getState) => {
- dispatch(addAccountToAntennaRequest(antennaId, accountId));
+export const fetchAntennaSuggestions = q => (dispatch, getState) => {
+ const params = {
+ q,
+ resolve: false,
+ };
+
+ api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
+ dispatch(importFetchedAccounts(data));
+ dispatch(fetchAntennaSuggestionsReady(q, data));
+ }).catch(error => dispatch(showAlertForError(error)));
+};
+
+export const fetchAntennaSuggestionsReady = (query, accounts) => ({
+ type: ANTENNA_EDITOR_SUGGESTIONS_READY,
+ query,
+ accounts,
+});
+
+export const clearAntennaSuggestions = () => ({
+ type: ANTENNA_EDITOR_SUGGESTIONS_CLEAR,
+});
+
+export const changeAntennaSuggestions = value => ({
+ type: ANTENNA_EDITOR_SUGGESTIONS_CHANGE,
+ value,
+});
+
+export const addToAntennaEditor = accountId => (dispatch, getState) => {
+ dispatch(addToAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId));
+};
+
+export const addToAntenna = (antennaId, accountId) => (dispatch, getState) => {
+ dispatch(addToAntennaRequest(antennaId, accountId));
api(getState).post(`/api/v1/antennas/${antennaId}/accounts`, { account_ids: [accountId] })
- .then(() => dispatch(addAccountToAntennaSuccess(antennaId, accountId)))
- .catch(err => dispatch(addAccountToAntennaFail(antennaId, accountId, err)));
+ .then(() => dispatch(addToAntennaSuccess(antennaId, accountId)))
+ .catch(err => dispatch(addToAntennaFail(antennaId, accountId, err)));
};
-export const addAccountToAntennaRequest = (antennaId, accountId) => ({
- type: ANTENNA_EDITOR_ADD_ACCOUNT_REQUEST,
+export const addToAntennaRequest = (antennaId, accountId) => ({
+ type: ANTENNA_EDITOR_ADD_REQUEST,
antennaId,
accountId,
});
-export const addAccountToAntennaSuccess = (antennaId, accountId) => ({
- type: ANTENNA_EDITOR_ADD_ACCOUNT_SUCCESS,
+export const addToAntennaSuccess = (antennaId, accountId) => ({
+ type: ANTENNA_EDITOR_ADD_SUCCESS,
antennaId,
accountId,
});
-export const addAccountToAntennaFail = (antennaId, accountId, error) => ({
- type: ANTENNA_EDITOR_ADD_ACCOUNT_FAIL,
+export const addToAntennaFail = (antennaId, accountId, error) => ({
+ type: ANTENNA_EDITOR_ADD_FAIL,
antennaId,
accountId,
error,
});
-export const removeAccountFromAntennaEditor = accountId => (dispatch, getState) => {
- dispatch(removeAccountFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId));
+export const removeFromAntennaEditor = accountId => (dispatch, getState) => {
+ dispatch(removeFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId));
};
-export const removeAccountFromAntenna = (antennaId, accountId) => (dispatch, getState) => {
- dispatch(removeAccountFromAntennaRequest(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(removeAccountFromAntennaSuccess(antennaId, accountId)))
- .catch(err => dispatch(removeAccountFromAntennaFail(antennaId, accountId, err)));
+ .then(() => dispatch(removeFromAntennaSuccess(antennaId, accountId)))
+ .catch(err => dispatch(removeFromAntennaFail(antennaId, accountId, err)));
};
-export const removeAccountFromAntennaRequest = (antennaId, accountId) => ({
- type: ANTENNA_EDITOR_REMOVE_ACCOUNT_REQUEST,
+export const removeFromAntennaRequest = (antennaId, accountId) => ({
+ type: ANTENNA_EDITOR_REMOVE_REQUEST,
antennaId,
accountId,
});
-export const removeAccountFromAntennaSuccess = (antennaId, accountId) => ({
- type: ANTENNA_EDITOR_REMOVE_ACCOUNT_SUCCESS,
+export const removeFromAntennaSuccess = (antennaId, accountId) => ({
+ type: ANTENNA_EDITOR_REMOVE_SUCCESS,
antennaId,
accountId,
});
-export const removeAccountFromAntennaFail = (antennaId, accountId, error) => ({
- type: ANTENNA_EDITOR_REMOVE_ACCOUNT_FAIL,
+export const removeFromAntennaFail = (antennaId, accountId, error) => ({
+ type: ANTENNA_EDITOR_REMOVE_FAIL,
antennaId,
accountId,
error,
});
-export const addToAntennaAdder = antennaId => (dispatch, getState) => {
- dispatch(addAccountToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
-};
-
-export const removeFromAntennaAdder = antennaId => (dispatch, getState) => {
- dispatch(removeAccountFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
-};
-
-export const fetchAccountAntennas = accountId => (dispatch, getState) => {
- dispatch(fetchAccountAntennasRequest(accountId));
-
- api(getState).get(`/api/v1/accounts/${accountId}/antennas`)
- .then(({ data }) => dispatch(fetchAccountAntennasSuccess(accountId, data)))
- .catch(err => dispatch(fetchAccountAntennasFail(accountId, err)));
-};
-
-export const fetchAccountAntennasRequest = id => ({
- type: ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST,
- id,
-});
-
-export const fetchAccountAntennasSuccess = (id, antennas) => ({
- type: ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS,
- id,
- antennas,
-});
-
-export const fetchAccountAntennasFail = (id, err) => ({
- type: ANTENNA_ADDER_ANTENNAS_FETCH_FAIL,
- id,
- err,
-});
-
export const resetAntennaAdder = () => ({
type: ANTENNA_ADDER_RESET,
});
@@ -178,3 +336,36 @@ export const setupAntennaAdder = accountId => (dispatch, getState) => {
dispatch(fetchAccountAntennas(accountId));
};
+export const fetchAccountAntennas = accountId => (dispatch, getState) => {
+ dispatch(fetchAccountAntennasRequest(accountId));
+
+ api(getState).get(`/api/v1/accounts/${accountId}/antennas`)
+ .then(({ data }) => dispatch(fetchAccountAntennasSuccess(accountId, data)))
+ .catch(err => dispatch(fetchAccountAntennasFail(accountId, err)));
+};
+
+export const fetchAccountAntennasRequest = id => ({
+ type:ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST,
+ id,
+});
+
+export const fetchAccountAntennasSuccess = (id, antennas) => ({
+ type: ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS,
+ id,
+ antennas,
+});
+
+export const fetchAccountAntennasFail = (id, err) => ({
+ type: ANTENNA_ADDER_ANTENNAS_FETCH_FAIL,
+ id,
+ err,
+});
+
+export const addToAntennaAdder = antennaId => (dispatch, getState) => {
+ dispatch(addToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
+};
+
+export const removeFromAntennaAdder = antennaId => (dispatch, getState) => {
+ dispatch(removeFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
+};
+
diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js
index b0789cd426..e5c606f4ec 100644
--- a/app/javascript/mastodon/actions/lists.js
+++ b/app/javascript/mastodon/actions/lists.js
@@ -238,7 +238,6 @@ export const fetchListSuggestions = q => (dispatch, getState) => {
const params = {
q,
resolve: false,
- limit: 4,
following: true,
};
diff --git a/app/javascript/mastodon/features/antenna_adder/components/account.jsx b/app/javascript/mastodon/features/antenna_adder/components/account.jsx
deleted file mode 100644
index 31a2e96379..0000000000
--- a/app/javascript/mastodon/features/antenna_adder/components/account.jsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { injectIntl } from 'react-intl';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import ImmutablePureComponent from 'react-immutable-pure-component';
-import { connect } from 'react-redux';
-
-import { Avatar } from '../../../components/avatar';
-import { DisplayName } from '../../../components/display_name';
-import { makeGetAccount } from '../../../selectors';
-
-const makeMapStateToProps = () => {
- const getAccount = makeGetAccount();
-
- const mapStateToProps = (state, { accountId }) => ({
- account: getAccount(state, accountId),
- });
-
- return mapStateToProps;
-};
-
-class Account extends ImmutablePureComponent {
-
- static propTypes = {
- account: ImmutablePropTypes.map.isRequired,
- };
-
- render () {
- const { account } = this.props;
- return (
-
- );
- }
-
-}
-
-export default connect(makeMapStateToProps)(injectIntl(Account));
diff --git a/app/javascript/mastodon/features/antenna_adder/components/antenna.jsx b/app/javascript/mastodon/features/antenna_adder/components/antenna.jsx
index d2833d3e0d..2c90c311c1 100644
--- a/app/javascript/mastodon/features/antenna_adder/components/antenna.jsx
+++ b/app/javascript/mastodon/features/antenna_adder/components/antenna.jsx
@@ -6,14 +6,14 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-import { Icon } from 'mastodon/components/icon';
+import { Icon } from 'mastodon/components/icon';
import { removeFromAntennaAdder, addToAntennaAdder } from '../../../actions/antennas';
-import { IconButton } from '../../../components/icon_button';
+import { IconButton } from '../../../components/icon_button';
const messages = defineMessages({
- remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
- add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
+ remove: { id: 'antennas.account.remove', defaultMessage: 'Remove from antenna' },
+ add: { id: 'antennas.account.add', defaultMessage: 'Add to antenna' },
});
const MapStateToProps = (state, { antennaId, added }) => ({
diff --git a/app/javascript/mastodon/features/antenna_adder/index.jsx b/app/javascript/mastodon/features/antenna_adder/index.jsx
index b8ec30d0e1..a3d6c34a4c 100644
--- a/app/javascript/mastodon/features/antenna_adder/index.jsx
+++ b/app/javascript/mastodon/features/antenna_adder/index.jsx
@@ -8,8 +8,9 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setupAntennaAdder, resetAntennaAdder } from '../../actions/antennas';
+import NewAntennaForm from '../antennas/components/new_antenna_form';
+import Account from '../list_adder/components/account';
-import Account from './components/account';
import Antenna from './components/antenna';
// hack
@@ -60,8 +61,11 @@ class AntennaAdder extends ImmutablePureComponent {
+
+
+
- {antennaIds.map(AntennaId =>
)}
+ {antennaIds.map(antennaId =>
)}
);
diff --git a/app/javascript/mastodon/features/antenna_editor/components/account.jsx b/app/javascript/mastodon/features/antenna_editor/components/account.jsx
new file mode 100644
index 0000000000..94daad4523
--- /dev/null
+++ b/app/javascript/mastodon/features/antenna_editor/components/account.jsx
@@ -0,0 +1,79 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+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 { Avatar } from '../../../components/avatar';
+import { DisplayName } from '../../../components/display_name';
+import { IconButton } from '../../../components/icon_button';
+import { makeGetAccount } from '../../../selectors';
+
+const messages = defineMessages({
+ remove: { id: 'antennas.account.remove', defaultMessage: 'Remove from antenna' },
+ add: { id: 'antennas.account.add', defaultMessage: 'Add to antenna' },
+});
+
+const makeMapStateToProps = () => {
+ const getAccount = makeGetAccount();
+
+ const mapStateToProps = (state, { accountId, added }) => ({
+ account: getAccount(state, accountId),
+ added: typeof added === 'undefined' ? state.getIn(['antennaEditor', 'accounts', 'items']).includes(accountId) : added,
+ });
+
+ return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { accountId }) => ({
+ onRemove: () => dispatch(removeFromAntennaEditor(accountId)),
+ onAdd: () => dispatch(addToAntennaEditor(accountId)),
+});
+
+class Account extends ImmutablePureComponent {
+
+ static propTypes = {
+ account: ImmutablePropTypes.map.isRequired,
+ intl: PropTypes.object.isRequired,
+ onRemove: PropTypes.func.isRequired,
+ onAdd: PropTypes.func.isRequired,
+ added: PropTypes.bool,
+ };
+
+ static defaultProps = {
+ added: false,
+ };
+
+ render () {
+ const { account, intl, onRemove, onAdd, added } = this.props;
+
+ let button;
+
+ if (added) {
+ button = ;
+ } else {
+ button = ;
+ }
+
+ return (
+
+ );
+ }
+
+}
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(Account));
diff --git a/app/javascript/mastodon/features/antenna_editor/components/edit_antenna_form.jsx b/app/javascript/mastodon/features/antenna_editor/components/edit_antenna_form.jsx
new file mode 100644
index 0000000000..df921f9dea
--- /dev/null
+++ b/app/javascript/mastodon/features/antenna_editor/components/edit_antenna_form.jsx
@@ -0,0 +1,73 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { changeAntennaEditorTitle, submitAntennaEditor } from '../../../actions/antennas';
+import { IconButton } from '../../../components/icon_button';
+
+const messages = defineMessages({
+ title: { id: 'antennas.edit.submit', defaultMessage: 'Change title' },
+});
+
+const mapStateToProps = state => ({
+ value: state.getIn(['antennaEditor', 'title']),
+ disabled: !state.getIn(['antennaEditor', 'isChanged']) || !state.getIn(['antennaEditor', 'title']),
+});
+
+const mapDispatchToProps = dispatch => ({
+ onChange: value => dispatch(changeAntennaEditorTitle(value)),
+ onSubmit: () => dispatch(submitAntennaEditor(false)),
+});
+
+class AntennaForm extends PureComponent {
+
+ static propTypes = {
+ value: PropTypes.string.isRequired,
+ disabled: PropTypes.bool,
+ intl: PropTypes.object.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ };
+
+ handleChange = e => {
+ this.props.onChange(e.target.value);
+ };
+
+ handleSubmit = e => {
+ e.preventDefault();
+ this.props.onSubmit();
+ };
+
+ handleClick = () => {
+ this.props.onSubmit();
+ };
+
+ render () {
+ const { value, disabled, intl } = this.props;
+
+ const title = intl.formatMessage(messages.title);
+
+ return (
+
+ );
+ }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AntennaForm));
diff --git a/app/javascript/mastodon/features/antenna_editor/components/search.jsx b/app/javascript/mastodon/features/antenna_editor/components/search.jsx
new file mode 100644
index 0000000000..f6b32c5171
--- /dev/null
+++ b/app/javascript/mastodon/features/antenna_editor/components/search.jsx
@@ -0,0 +1,81 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import classNames from 'classnames';
+
+import { connect } from 'react-redux';
+
+import { Icon } from 'mastodon/components/icon';
+
+import { fetchAntennaSuggestions, clearAntennaSuggestions, changeAntennaSuggestions } from '../../../actions/antennas';
+
+const messages = defineMessages({
+ search: { id: 'antennas.search', defaultMessage: 'Search among people you follow' },
+});
+
+const mapStateToProps = state => ({
+ value: state.getIn(['antennaEditor', 'suggestions', 'value']),
+});
+
+const mapDispatchToProps = dispatch => ({
+ onSubmit: value => dispatch(fetchAntennaSuggestions(value)),
+ onClear: () => dispatch(clearAntennaSuggestions()),
+ onChange: value => dispatch(changeAntennaSuggestions(value)),
+});
+
+class Search extends PureComponent {
+
+ static propTypes = {
+ intl: PropTypes.object.isRequired,
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onClear: PropTypes.func.isRequired,
+ };
+
+ handleChange = e => {
+ this.props.onChange(e.target.value);
+ };
+
+ handleKeyUp = e => {
+ if (e.keyCode === 13) {
+ this.props.onSubmit(this.props.value);
+ }
+ };
+
+ handleClear = () => {
+ this.props.onClear();
+ };
+
+ render () {
+ const { value, intl } = this.props;
+ const hasValue = value.length > 0;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Search));
diff --git a/app/javascript/mastodon/features/antenna_editor/index.jsx b/app/javascript/mastodon/features/antenna_editor/index.jsx
new file mode 100644
index 0000000000..0e0846859b
--- /dev/null
+++ b/app/javascript/mastodon/features/antenna_editor/index.jsx
@@ -0,0 +1,83 @@
+import PropTypes from 'prop-types';
+
+import { injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+
+import spring from 'react-motion/lib/spring';
+
+import { setupAntennaEditor, 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']),
+ searchAccountIds: state.getIn(['antennaEditor', 'suggestions', 'items']),
+});
+
+const mapDispatchToProps = dispatch => ({
+ onInitialize: antennaId => dispatch(setupAntennaEditor(antennaId)),
+ onClear: () => dispatch(clearAntennaSuggestions()),
+ onReset: () => dispatch(resetAntennaEditor()),
+});
+
+class AntennaEditor extends ImmutablePureComponent {
+
+ static propTypes = {
+ antennaId: PropTypes.string.isRequired,
+ onClose: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ onInitialize: PropTypes.func.isRequired,
+ onClear: PropTypes.func.isRequired,
+ onReset: PropTypes.func.isRequired,
+ accountIds: ImmutablePropTypes.list.isRequired,
+ searchAccountIds: ImmutablePropTypes.list.isRequired,
+ };
+
+ componentDidMount () {
+ const { onInitialize, antennaId } = this.props;
+ onInitialize(antennaId);
+ }
+
+ componentWillUnmount () {
+ const { onReset } = this.props;
+ onReset();
+ }
+
+ render () {
+ const { accountIds, searchAccountIds, onClear } = this.props;
+ const showSearch = searchAccountIds.size > 0;
+
+ return (
+
+
+
+
+
+
+
+ {accountIds.map(accountId =>
)}
+
+
+ {showSearch &&
}
+
+
+ {({ x }) => (
+
+ {searchAccountIds.map(accountId =>
)}
+
+ )}
+
+
+
+ );
+ }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AntennaEditor));
diff --git a/app/javascript/mastodon/features/antenna_setting/index.jsx b/app/javascript/mastodon/features/antenna_setting/index.jsx
new file mode 100644
index 0000000000..1eb9e05b5f
--- /dev/null
+++ b/app/javascript/mastodon/features/antenna_setting/index.jsx
@@ -0,0 +1,256 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import Toggle from 'react-toggle';
+
+import { fetchAntenna, deleteAntenna, updateAntenna } from 'mastodon/actions/antennas';
+import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
+import { openModal } from 'mastodon/actions/modal';
+import Button from 'mastodon/components/button';
+import Column from 'mastodon/components/column';
+import ColumnHeader from 'mastodon/components/column_header';
+import { Icon } from 'mastodon/components/icon';
+import { LoadingIndicator } from 'mastodon/components/loading_indicator';
+import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
+
+const messages = defineMessages({
+ deleteMessage: { id: 'confirmations.delete_antenna.message', defaultMessage: 'Are you sure you want to permanently delete this antenna?' },
+ deleteConfirm: { id: 'confirmations.delete_antenna.confirm', defaultMessage: 'Delete' },
+ editAccounts: { id: 'antennas.edit_accounts', defaultMessage: 'Edit accounts' },
+});
+
+const mapStateToProps = (state, props) => ({
+ antenna: state.getIn(['antennas', props.params.id]),
+});
+
+class AntennaSetting extends PureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ params: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ columnId: PropTypes.string,
+ multiColumn: PropTypes.bool,
+ antenna: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
+ intl: PropTypes.object.isRequired,
+ };
+
+ handlePin = () => {
+ const { columnId, dispatch } = this.props;
+
+ if (columnId) {
+ dispatch(removeColumn(columnId));
+ } else {
+ dispatch(addColumn('ANTENNA', { id: this.props.params.id }));
+ this.context.router.history.push('/');
+ }
+ };
+
+ handleMove = (dir) => {
+ const { columnId, dispatch } = this.props;
+ dispatch(moveColumn(columnId, dir));
+ };
+
+ handleHeaderClick = () => {
+ this.column.scrollTop();
+ };
+
+ componentDidMount () {
+ const { dispatch } = this.props;
+ const { id } = this.props.params;
+
+ dispatch(fetchAntenna(id));
+ }
+
+ UNSAFE_componentWillReceiveProps (nextProps) {
+ const { dispatch } = this.props;
+ const { id } = nextProps.params;
+
+ if (id !== this.props.params.id) {
+ dispatch(fetchAntenna(id));
+ }
+ }
+
+ setRef = c => {
+ this.column = c;
+ };
+
+ handleEditClick = () => {
+ this.props.dispatch(openModal({
+ modalType: 'ANTENNA_EDITOR',
+ modalProps: { antennaId: this.props.params.id },
+ }));
+ };
+
+ handleEditAntennaClick = () => {
+ window.open(`/antennas/${this.props.params.id}/edit`, '_blank');
+ }
+
+ handleDeleteClick = () => {
+ const { dispatch, columnId, intl } = this.props;
+ const { id } = this.props.params;
+
+ dispatch(openModal({
+ modalType: 'CONFIRM',
+ modalProps: {
+ message: intl.formatMessage(messages.deleteMessage),
+ confirm: intl.formatMessage(messages.deleteConfirm),
+ onConfirm: () => {
+ dispatch(deleteAntenna(id));
+
+ if (columnId) {
+ dispatch(removeColumn(columnId));
+ } else {
+ this.context.router.history.push('/antennasw');
+ }
+ },
+ },
+ }));
+ };
+
+ onStlToggle = ({ target }) => {
+ const { dispatch } = this.props;
+ const { id } = this.props.params;
+ dispatch(updateAntenna(id, undefined, false, target.checked, undefined, undefined));
+ };
+
+ onMediaOnlyToggle = ({ target }) => {
+ const { dispatch } = this.props;
+ const { id } = this.props.params;
+ dispatch(updateAntenna(id, undefined, false, undefined, target.checked, undefined));
+ };
+
+ onIgnoreReblogToggle = ({ target }) => {
+ const { dispatch } = this.props;
+ const { id } = this.props.params;
+ dispatch(updateAntenna(id, undefined, false, undefined, undefined, target.checked));
+ };
+
+ render () {
+ const { columnId, multiColumn, antenna, intl } = this.props;
+ const { id } = this.props.params;
+ const pinned = !!columnId;
+ const title = antenna ? antenna.get('title') : id;
+ const isStl = antenna ? antenna.get('stl') : undefined;
+ const isMediaOnly = antenna ? antenna.get('with_media_only') : undefined;
+ const isIgnoreReblog = antenna ? antenna.get('ignore_reblog') : undefined;
+
+ if (typeof antenna === 'undefined') {
+ return (
+
+
+
+
+
+ );
+ } else if (antenna === false) {
+ return (
+
+ );
+ }
+
+ let columnSettings;
+ if (!isStl) {
+ columnSettings = (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ )
+ }
+
+ let stlAlert;
+ if (isStl) {
+ stlAlert = (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {columnSettings}
+
+
+ {stlAlert}
+ {!isStl && (
+
+ {antenna.get('list') ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ )}
+
+
+ {title}
+
+
+
+ );
+ }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(AntennaSetting));
diff --git a/app/javascript/mastodon/features/antennas/components/new_antenna_form.jsx b/app/javascript/mastodon/features/antennas/components/new_antenna_form.jsx
new file mode 100644
index 0000000000..1c71a1ef89
--- /dev/null
+++ b/app/javascript/mastodon/features/antennas/components/new_antenna_form.jsx
@@ -0,0 +1,80 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { defineMessages, injectIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+
+import { changeAntennaEditorTitle, submitAntennaEditor } from 'mastodon/actions/antennas';
+import Button from 'mastodon/components/button';
+
+const messages = defineMessages({
+ label: { id: 'antennas.new.title_placeholder', defaultMessage: 'New antenna title' },
+ title: { id: 'antennas.new.create', defaultMessage: 'Add antenna' },
+});
+
+const mapStateToProps = state => ({
+ value: state.getIn(['antennaEditor', 'title']),
+ disabled: state.getIn(['antennaEditor', 'isSubmitting']),
+});
+
+const mapDispatchToProps = dispatch => ({
+ onChange: value => dispatch(changeAntennaEditorTitle(value)),
+ onSubmit: () => dispatch(submitAntennaEditor(true)),
+});
+
+class NewAntennaForm extends PureComponent {
+
+ static propTypes = {
+ value: PropTypes.string.isRequired,
+ disabled: PropTypes.bool,
+ intl: PropTypes.object.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ };
+
+ handleChange = e => {
+ this.props.onChange(e.target.value);
+ };
+
+ handleSubmit = e => {
+ e.preventDefault();
+ this.props.onSubmit();
+ };
+
+ handleClick = () => {
+ this.props.onSubmit();
+ };
+
+ render () {
+ const { value, disabled, intl } = this.props;
+
+ const label = intl.formatMessage(messages.label);
+ const title = intl.formatMessage(messages.title);
+
+ return (
+
+ );
+ }
+
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(NewAntennaForm));
diff --git a/app/javascript/mastodon/features/antennas/index.jsx b/app/javascript/mastodon/features/antennas/index.jsx
new file mode 100644
index 0000000000..220b3f47fa
--- /dev/null
+++ b/app/javascript/mastodon/features/antennas/index.jsx
@@ -0,0 +1,93 @@
+import PropTypes from 'prop-types';
+
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+
+import { Helmet } from 'react-helmet';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+
+import { fetchAntennas } from 'mastodon/actions/antennas';
+import Column from 'mastodon/components/column';
+import ColumnHeader from 'mastodon/components/column_header';
+import { LoadingIndicator } from 'mastodon/components/loading_indicator';
+import ScrollableList from 'mastodon/components/scrollable_list';
+import ColumnLink from 'mastodon/features/ui/components/column_link';
+import ColumnSubheading from 'mastodon/features/ui/components/column_subheading';
+
+import NewAntennaForm from './components/new_antenna_form';
+
+const messages = defineMessages({
+ heading: { id: 'column.antennas', defaultMessage: 'Antennas' },
+ subheading: { id: 'antennas.subheading', defaultMessage: 'Your antennas' },
+});
+
+const getOrderedAntennas = createSelector([state => state.get('antennas')], antennas => {
+ if (!antennas) {
+ return antennas;
+ }
+
+ return antennas.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
+});
+
+const mapStateToProps = state => ({
+ antennas: getOrderedAntennas(state),
+});
+
+class Antennas extends ImmutablePureComponent {
+
+ static propTypes = {
+ params: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ antennas: ImmutablePropTypes.list,
+ intl: PropTypes.object.isRequired,
+ multiColumn: PropTypes.bool,
+ };
+
+ UNSAFE_componentWillMount () {
+ this.props.dispatch(fetchAntennas());
+ }
+
+ render () {
+ const { intl, antennas, multiColumn } = this.props;
+
+ if (!antennas) {
+ return (
+
+
+
+ );
+ }
+
+ const emptyMessage = ;
+
+ return (
+
+
+
+
+
+ }
+ bindToDocument={!multiColumn}
+ >
+ {antennas.map(antenna =>
+ ,
+ )}
+
+
+
+ {intl.formatMessage(messages.heading)}
+
+
+
+ );
+ }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(Antennas));
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx
index 852ea7b4d8..e03caa23a2 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.jsx
+++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx
@@ -11,6 +11,7 @@ import {
EmbedModal,
ListEditor,
ListAdder,
+ AntennaEditor,
AntennaAdder,
CompareHistoryModal,
FilterModal,
@@ -46,6 +47,7 @@ export const MODAL_COMPONENTS = {
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,
'LIST_EDITOR': ListEditor,
+ 'ANTENNA_EDITOR': AntennaEditor,
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
'LIST_ADDER': ListAdder,
'ANTENNA_ADDER': AntennaAdder,
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
index 7ec39e489e..f680683cae 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
@@ -27,6 +27,7 @@ const messages = defineMessages({
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
+ antennas: { id: 'navigation_bar.antennas', defaultMessage: 'Antennas' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
@@ -109,6 +110,7 @@ class NavigationPanel extends Component {
{signedIn && (
<>
+
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index b511605da3..39ae5956c4 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -60,6 +60,8 @@ import {
Mutes,
PinnedStatuses,
Lists,
+ Antennas,
+ AntennaSetting,
Directory,
Explore,
ReactionDeck,
@@ -207,6 +209,7 @@ class SwitchingColumnsArea extends PureComponent {
+
@@ -248,6 +251,7 @@ class SwitchingColumnsArea extends PureComponent {
+
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 4cee87ef45..85e612a541 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -42,6 +42,10 @@ export function Lists () {
return import(/* webpackChunkName: "features/lists" */'../../lists');
}
+export function Antennas () {
+ return import(/* webpackChunkName: "features/antennas" */'../../antennas');
+}
+
export function Status () {
return import(/* webpackChunkName: "features/status" */'../../status');
}
@@ -158,6 +162,14 @@ export function AntennaAdder () {
return import(/*webpackChunkName: "features/antenna_adder" */'../../antenna_adder');
}
+export function AntennaEditor () {
+ return import(/*webpackChunkName: "features/antenna_editor" */'../../antenna_editor');
+}
+
+export function AntennaSetting () {
+ return import(/*webpackChunkName: "features/antenna_setting" */'../../antenna_setting');
+}
+
export function Tesseract () {
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
}
diff --git a/app/javascript/mastodon/reducers/antenna_adder.js b/app/javascript/mastodon/reducers/antenna_adder.js
index 8448d6984f..947574fdc2 100644
--- a/app/javascript/mastodon/reducers/antenna_adder.js
+++ b/app/javascript/mastodon/reducers/antenna_adder.js
@@ -6,8 +6,8 @@ import {
ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST,
ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS,
ANTENNA_ADDER_ANTENNAS_FETCH_FAIL,
- ANTENNA_EDITOR_ADD_ACCOUNT_SUCCESS,
- ANTENNA_EDITOR_REMOVE_ACCOUNT_SUCCESS,
+ ANTENNA_EDITOR_ADD_SUCCESS,
+ ANTENNA_EDITOR_REMOVE_SUCCESS,
} from '../actions/antennas';
const initialState = ImmutableMap({
@@ -38,9 +38,9 @@ export default function antennaAdderReducer(state = initialState, action) {
map.set('loaded', true);
map.set('items', ImmutableList(action.antennas.map(item => item.id)));
}));
- case ANTENNA_EDITOR_ADD_ACCOUNT_SUCCESS:
+ case ANTENNA_EDITOR_ADD_SUCCESS:
return state.updateIn(['antennas', 'items'], antenna => antenna.unshift(action.antennaId));
- case ANTENNA_EDITOR_REMOVE_ACCOUNT_SUCCESS:
+ case ANTENNA_EDITOR_REMOVE_SUCCESS:
return state.updateIn(['antennas', 'items'], antenna => antenna.filterNot(item => item === action.antennaId));
default:
return state;
diff --git a/app/javascript/mastodon/reducers/antenna_editor.js b/app/javascript/mastodon/reducers/antenna_editor.js
index c1de305514..86c0f187f7 100644
--- a/app/javascript/mastodon/reducers/antenna_editor.js
+++ b/app/javascript/mastodon/reducers/antenna_editor.js
@@ -1,9 +1,23 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import {
+ ANTENNA_CREATE_REQUEST,
+ ANTENNA_CREATE_FAIL,
+ ANTENNA_CREATE_SUCCESS,
+ ANTENNA_UPDATE_REQUEST,
+ ANTENNA_UPDATE_FAIL,
+ ANTENNA_UPDATE_SUCCESS,
+ ANTENNA_EDITOR_RESET,
+ ANTENNA_EDITOR_SETUP,
+ ANTENNA_EDITOR_TITLE_CHANGE,
ANTENNA_ACCOUNTS_FETCH_REQUEST,
ANTENNA_ACCOUNTS_FETCH_SUCCESS,
ANTENNA_ACCOUNTS_FETCH_FAIL,
+ ANTENNA_EDITOR_SUGGESTIONS_READY,
+ ANTENNA_EDITOR_SUGGESTIONS_CLEAR,
+ ANTENNA_EDITOR_SUGGESTIONS_CHANGE,
+ ANTENNA_EDITOR_ADD_SUCCESS,
+ ANTENNA_EDITOR_REMOVE_SUCCESS,
} from '../actions/antennas';
const initialState = ImmutableMap({
@@ -11,16 +25,51 @@ const initialState = ImmutableMap({
isSubmitting: false,
isChanged: false,
title: '',
+ accountsCount: 0,
accounts: ImmutableMap({
items: ImmutableList(),
loaded: false,
isLoading: false,
}),
+
+ suggestions: ImmutableMap({
+ value: '',
+ items: ImmutableList(),
+ }),
});
export default function antennaEditorReducer(state = initialState, action) {
switch(action.type) {
+ case ANTENNA_EDITOR_RESET:
+ return initialState;
+ case ANTENNA_EDITOR_SETUP:
+ return state.withMutations(map => {
+ map.set('antennaId', action.antenna.get('id'));
+ map.set('title', action.antenna.get('title'));
+ map.set('accountsCount', action.antenna.get('accounts_count'));
+ map.set('isSubmitting', false);
+ });
+ case ANTENNA_EDITOR_TITLE_CHANGE:
+ return state.withMutations(map => {
+ map.set('title', action.value);
+ map.set('isChanged', true);
+ });
+ case ANTENNA_CREATE_REQUEST:
+ case ANTENNA_UPDATE_REQUEST:
+ return state.withMutations(map => {
+ map.set('isSubmitting', true);
+ map.set('isChanged', false);
+ });
+ case ANTENNA_CREATE_FAIL:
+ case ANTENNA_UPDATE_FAIL:
+ return state.set('isSubmitting', false);
+ case ANTENNA_CREATE_SUCCESS:
+ case ANTENNA_UPDATE_SUCCESS:
+ return state.withMutations(map => {
+ map.set('isSubmitting', false);
+ map.set('antennaId', action.antenna.id);
+ });
case ANTENNA_ACCOUNTS_FETCH_REQUEST:
return state.setIn(['accounts', 'isLoading'], true);
case ANTENNA_ACCOUNTS_FETCH_FAIL:
@@ -31,6 +80,19 @@ export default function antennaEditorReducer(state = initialState, action) {
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:
+ return state.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id)));
+ case ANTENNA_EDITOR_SUGGESTIONS_CLEAR:
+ return state.update('suggestions', suggestions => suggestions.withMutations(map => {
+ map.set('items', ImmutableList());
+ map.set('value', '');
+ }));
+ case ANTENNA_EDITOR_ADD_SUCCESS:
+ 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));
default:
return state;
}
diff --git a/app/javascript/mastodon/reducers/antennas.js b/app/javascript/mastodon/reducers/antennas.js
index 1909fc96c0..9ccf52512a 100644
--- a/app/javascript/mastodon/reducers/antennas.js
+++ b/app/javascript/mastodon/reducers/antennas.js
@@ -1,7 +1,14 @@
import { Map as ImmutableMap, fromJS } from 'immutable';
import {
+ ANTENNA_FETCH_SUCCESS,
+ ANTENNA_FETCH_FAIL,
ANTENNAS_FETCH_SUCCESS,
+ ANTENNA_CREATE_SUCCESS,
+ ANTENNA_UPDATE_SUCCESS,
+ ANTENNA_DELETE_SUCCESS,
+ ANTENNA_EDITOR_ADD_SUCCESS,
+ ANTENNA_EDITOR_REMOVE_SUCCESS,
} from '../actions/antennas';
const initialState = ImmutableMap();
@@ -18,8 +25,19 @@ const normalizeAntennas = (state, antennas) => {
export default function antennas(state = initialState, action) {
switch(action.type) {
+ case ANTENNA_FETCH_SUCCESS:
+ case ANTENNA_CREATE_SUCCESS:
+ case ANTENNA_UPDATE_SUCCESS:
+ return normalizeAntenna(state, action.antenna);
case ANTENNAS_FETCH_SUCCESS:
return normalizeAntennas(state, action.antennas);
+ case ANTENNA_DELETE_SUCCESS:
+ case ANTENNA_FETCH_FAIL:
+ return state.set(action.id, false);
+ case ANTENNA_EDITOR_ADD_SUCCESS:
+ return state.setIn([action.antennaId, 'accounts_count'], state.getIn([action.antennaId, 'accounts_count']) + 1);
+ case ANTENNA_EDITOR_REMOVE_SUCCESS:
+ return state.setIn([action.antennaId, 'accounts_count'], state.getIn([action.antennaId, 'accounts_count']) - 1);
default:
return state;
}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 47c86709c7..f1b3a6ae9c 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -7364,6 +7364,15 @@ noscript {
}
}
+.antenna-setting {
+ margin: 8px 16px;
+
+ h3 {
+ font-size: 16px;
+ margin: 24px 0 8px;
+ }
+}
+
.reaction_deck_container {
&__row {
display: flex;
diff --git a/app/models/antenna.rb b/app/models/antenna.rb
index 38bf04b7b4..33ccf817a2 100644
--- a/app/models/antenna.rb
+++ b/app/models/antenna.rb
@@ -33,6 +33,7 @@ class Antenna < ApplicationRecord
has_many :antenna_domains, inverse_of: :antenna, dependent: :destroy
has_many :antenna_tags, inverse_of: :antenna, dependent: :destroy
has_many :antenna_accounts, inverse_of: :antenna, dependent: :destroy
+ has_many :accounts, through: :antenna_accounts
belongs_to :account
belongs_to :list, optional: true
diff --git a/app/serializers/rest/antenna_serializer.rb b/app/serializers/rest/antenna_serializer.rb
index 4a4a148669..7f1edd48b9 100644
--- a/app/serializers/rest/antenna_serializer.rb
+++ b/app/serializers/rest/antenna_serializer.rb
@@ -1,9 +1,35 @@
# frozen_string_literal: true
class REST::AntennaSerializer < ActiveModel::Serializer
- attributes :id, :title
+ attributes :id, :title, :stl, :with_media_only, :ignore_reblog, :accounts_count, :domains_count, :tags_count, :keywords_count
+
+ class ListSerializer < ActiveModel::Serializer
+ attributes :id, :title
+
+ def id
+ object.id.to_s
+ end
+ end
+
+ has_one :list, serializer: ListSerializer, optional: true
def id
object.id.to_s
end
+
+ def accounts_count
+ object.antenna_accounts.count
+ end
+
+ def domains_count
+ object.antenna_domains.count
+ end
+
+ def tags_count
+ object.antenna_tags.count
+ end
+
+ def keywords_count
+ object.keywords&.size || 0
+ end
end
diff --git a/config/routes.rb b/config/routes.rb
index 7a99a27c92..5042f7b165 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -16,6 +16,7 @@ Rails.application.routes.draw do
/public/remote
/conversations
/lists/(*any)
+ /antennasw/(*any)
/notifications
/favourites
/emoji_reactions