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 ( +
+
+
+
+ +
+ +
+ {button} +
+
+
+ ); + } + +} + +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 ( +
+ + +