diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index 4530ec705..0aabbb42b 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -59,6 +59,10 @@ export const ACCOUNT_SEARCH_REQUEST = 'ACCOUNT_SEARCH_REQUEST'; export const ACCOUNT_SEARCH_SUCCESS = 'ACCOUNT_SEARCH_SUCCESS'; export const ACCOUNT_SEARCH_FAIL = 'ACCOUNT_SEARCH_FAIL'; +export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST'; +export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS'; +export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL'; + export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST'; export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS'; export const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL'; @@ -961,3 +965,17 @@ export function accountSearch(params, cancelToken) { }); }; } + +export function accountLookup(acct, cancelToken) { + return (dispatch, getState) => { + dispatch({ type: ACCOUNT_LOOKUP_REQUEST, acct }); + return api(getState).get('/api/v1/accounts/lookup', { params: { acct }, cancelToken }).then(({ data: account }) => { + if (account && account.id) dispatch(importFetchedAccount(account)); + dispatch({ type: ACCOUNT_LOOKUP_SUCCESS, account }); + return account; + }).catch(error => { + dispatch({ type: ACCOUNT_LOOKUP_FAIL }); + throw error; + }); + }; +} diff --git a/app/soapbox/features/auth_login/components/registration_form.js b/app/soapbox/features/auth_login/components/registration_form.js index f0d27c297..99baa6cde 100644 --- a/app/soapbox/features/auth_login/components/registration_form.js +++ b/app/soapbox/features/auth_login/components/registration_form.js @@ -5,6 +5,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; import { Link } from 'react-router-dom'; +import { CancelToken } from 'axios'; +import { debounce } from 'lodash'; import ShowablePassword from 'soapbox/components/showable_password'; import { SimpleForm, @@ -20,6 +22,7 @@ import { v4 as uuidv4 } from 'uuid'; import { getSettings } from 'soapbox/actions/settings'; import { openModal } from 'soapbox/actions/modal'; import { getFeatures } from 'soapbox/utils/features'; +import { accountLookup } from 'soapbox/actions/accounts'; const messages = defineMessages({ username: { id: 'registration.fields.username_placeholder', defaultMessage: 'Username' }, @@ -39,6 +42,7 @@ const mapStateToProps = (state, props) => ({ needsConfirmation: state.getIn(['instance', 'pleroma', 'metadata', 'account_activation_required']), needsApproval: state.getIn(['instance', 'approval_required']), supportsEmailList: getFeatures(state.get('instance')).emailList, + supportsAccountLookup: getFeatures(state.get('instance')).accountLookup, }); export default @connect(mapStateToProps) @@ -52,6 +56,7 @@ class RegistrationForm extends ImmutablePureComponent { needsConfirmation: PropTypes.bool, needsApproval: PropTypes.bool, supportsEmailList: PropTypes.bool, + supportsAccountLookup: PropTypes.bool, inviteToken: PropTypes.string, } @@ -64,10 +69,19 @@ class RegistrationForm extends ImmutablePureComponent { submissionLoading: false, params: ImmutableMap(), captchaIdempotencyKey: uuidv4(), + usernameUnavailable: false, passwordConfirmation: '', passwordMismatch: false, } + source = CancelToken.source(); + + refreshCancelToken = () => { + this.source.cancel(); + this.source = CancelToken.source(); + return this.source; + } + setParams = map => { this.setState({ params: this.state.params.merge(ImmutableMap(map)) }); } @@ -76,6 +90,14 @@ class RegistrationForm extends ImmutablePureComponent { this.setParams({ [e.target.name]: e.target.value }); } + onUsernameChange = e => { + this.setParams({ username: e.target.value }); + this.setState({ usernameUnavailable: false }); + this.source.cancel(); + + this.usernameAvailable(e.target.value); + } + onCheckboxChange = e => { this.setParams({ [e.target.name]: e.target.checked }); } @@ -145,6 +167,25 @@ class RegistrationForm extends ImmutablePureComponent { return params.get('password', '') === passwordConfirmation; } + usernameAvailable = debounce(username => { + const { dispatch, supportsAccountLookup } = this.props; + + if (!supportsAccountLookup) return; + + const source = this.refreshCancelToken(); + + dispatch(accountLookup(username, source.token)) + .then(account => { + this.setState({ usernameUnavailable: !!account }); + }) + .catch((error) => { + if (error.response && error.response.status === 404) { + this.setState({ usernameUnavailable: false }); + } + }); + + }, 1000, { trailing: true }); + onSubmit = e => { const { dispatch, inviteToken } = this.props; @@ -196,7 +237,7 @@ class RegistrationForm extends ImmutablePureComponent { render() { const { instance, intl, supportsEmailList } = this.props; - const { params, passwordConfirmation, passwordMismatch } = this.state; + const { params, usernameUnavailable, passwordConfirmation, passwordMismatch } = this.state; const isLoading = this.state.captchaLoading || this.state.submissionLoading; return ( @@ -204,6 +245,11 @@ class RegistrationForm extends ImmutablePureComponent {