From 23aa11dfe3dc81f07599afaebeb83cff0875f417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 14 Feb 2022 21:00:41 +0100 Subject: [PATCH 1/3] Use new API for account aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/aliases.js | 152 ++++++++++++------ .../features/aliases/components/account.js | 22 +-- app/soapbox/features/aliases/index.js | 23 ++- app/soapbox/reducers/aliases.js | 8 + app/soapbox/utils/features.js | 1 + 5 files changed, 147 insertions(+), 59 deletions(-) diff --git a/app/soapbox/actions/aliases.js b/app/soapbox/actions/aliases.js index cdeb7208c..ec9093965 100644 --- a/app/soapbox/actions/aliases.js +++ b/app/soapbox/actions/aliases.js @@ -1,14 +1,19 @@ import { defineMessages } from 'react-intl'; import { isLoggedIn } from 'soapbox/utils/auth'; +import { getFeatures } from 'soapbox/utils/features'; import api from '../api'; import { showAlertForError } from './alerts'; -import { importFetchedAccount, importFetchedAccounts } from './importer'; -import { ME_PATCH_SUCCESS } from './me'; +import { importFetchedAccounts } from './importer'; +import { patchMeSuccess } from './me'; import snackbar from './snackbar'; +export const ALIASES_FETCH_REQUEST = 'ALIASES_FETCH_REQUEST'; +export const ALIASES_FETCH_SUCCESS = 'ALIASES_FETCH_SUCCESS'; +export const ALIASES_FETCH_FAIL = 'ALIASES_FETCH_FAIL'; + export const ALIASES_SUGGESTIONS_CHANGE = 'ALIASES_SUGGESTIONS_CHANGE'; export const ALIASES_SUGGESTIONS_READY = 'ALIASES_SUGGESTIONS_READY'; export const ALIASES_SUGGESTIONS_CLEAR = 'ALIASES_SUGGESTIONS_CLEAR'; @@ -26,6 +31,38 @@ const messages = defineMessages({ removeSuccess: { id: 'aliases.success.remove', defaultMessage: 'Account alias removed successfully' }, }); +export const fetchAliases = (dispatch, getState) => { + if (!isLoggedIn(getState)) return; + const state = getState(); + + const instance = state.get('instance'); + const features = getFeatures(instance); + + if (!features.accountMoving) return; + + dispatch(fetchAliasesRequest()); + + api(getState).get('/api/pleroma/aliases') + .then(response => { + dispatch(fetchAliasesSuccess(response.data.aliases)); + }) + .catch(err => dispatch(fetchAliasesFail(err))); +}; + +export const fetchAliasesRequest = () => ({ + type: ALIASES_FETCH_REQUEST, +}); + +export const fetchAliasesSuccess = aliases => ({ + type: ALIASES_FETCH_SUCCESS, + value: aliases, +}); + +export const fetchAliasesFail = error => ({ + type: ALIASES_FETCH_FAIL, + error, +}); + export const fetchAliasesSuggestions = q => (dispatch, getState) => { if (!isLoggedIn(getState)) return; @@ -56,80 +93,103 @@ export const changeAliasesSuggestions = value => ({ value, }); -export const addToAliases = (intl, apId) => (dispatch, getState) => { +export const addToAliases = (intl, account) => (dispatch, getState) => { if (!isLoggedIn(getState)) return; const state = getState(); - const me = state.get('me'); - const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']); + const instance = state.get('instance'); + const features = getFeatures(instance); - dispatch(addToAliasesRequest(apId)); + if (!features.accountMoving) { + const me = state.get('me'); + const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']); - api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, apId] }) - .then((response => { + dispatch(addToAliasesRequest()); + + api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.getIn(['pleroma', 'ap_id'])] }) + .then((response => { + dispatch(snackbar.success(intl.formatMessage(messages.createSuccess))); + dispatch(addToAliasesSuccess()); + dispatch(patchMeSuccess(response.data)); + })) + .catch(err => dispatch(addToAliasesFail(err))); + + return; + } + + dispatch(addToAliasesRequest()); + + api(getState).put('/api/pleroma/aliases', { + alias: account.get('acct'), + }) + .then(response => { dispatch(snackbar.success(intl.formatMessage(messages.createSuccess))); - dispatch(addToAliasesSuccess(response.data)); - })) - .catch(err => dispatch(addToAliasesFail(err))); + dispatch(addToAliasesSuccess); + dispatch(fetchAliases); + }) + .catch(err => dispatch(fetchAliasesFail(err))); }; -export const addToAliasesRequest = (apId) => ({ +export const addToAliasesRequest = () => ({ type: ALIASES_ADD_REQUEST, - apId, }); -export const addToAliasesSuccess = me => dispatch => { - dispatch(importFetchedAccount(me)); - dispatch({ - type: ME_PATCH_SUCCESS, - me, - }); - dispatch({ - type: ALIASES_ADD_SUCCESS, - }); -}; +export const addToAliasesSuccess = () => ({ + type: ALIASES_ADD_SUCCESS, +}); -export const addToAliasesFail = (apId, error) => ({ +export const addToAliasesFail = error => ({ type: ALIASES_ADD_FAIL, - apId, error, }); -export const removeFromAliases = (intl, apId) => (dispatch, getState) => { +export const removeFromAliases = (intl, account) => (dispatch, getState) => { if (!isLoggedIn(getState)) return; const state = getState(); - const me = state.get('me'); - const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']); + const instance = state.get('instance'); + const features = getFeatures(instance); - dispatch(removeFromAliasesRequest(apId)); + if (!features.accountMoving) { + const me = state.get('me'); + const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']); - api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter(id => id !== apId) }) + dispatch(removeFromAliasesRequest()); + + api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: alsoKnownAs.filter(id => id !== account) }) + .then(response => { + dispatch(snackbar.success(intl.formatMessage(messages.removeSuccess))); + dispatch(removeFromAliasesSuccess); + }) + .catch(err => dispatch(removeFromAliasesFail(err))); + + return; + } + + dispatch(addToAliasesRequest()); + + api(getState).delete('/api/pleroma/aliases', { + data: { + alias: account, + }, + }) .then(response => { dispatch(snackbar.success(intl.formatMessage(messages.removeSuccess))); - dispatch(removeFromAliasesSuccess(response.data)); + dispatch(removeFromAliasesSuccess); + dispatch(fetchAliases); }) - .catch(err => dispatch(removeFromAliasesFail(apId, err))); + .catch(err => dispatch(fetchAliasesFail(err))); }; -export const removeFromAliasesRequest = (apId) => ({ +export const removeFromAliasesRequest = () => ({ type: ALIASES_REMOVE_REQUEST, - apId, }); -export const removeFromAliasesSuccess = me => dispatch => { - dispatch(importFetchedAccount(me)); - dispatch({ - type: ME_PATCH_SUCCESS, - me, - }); - dispatch({ - type: ALIASES_REMOVE_SUCCESS, - }); -}; +export const removeFromAliasesSuccess = () => ({ + type: ALIASES_REMOVE_SUCCESS, +}); -export const removeFromAliasesFail = (apId, error) => ({ +export const removeFromAliasesFail = error => ({ type: ALIASES_REMOVE_FAIL, - apId, error, }); diff --git a/app/soapbox/features/aliases/components/account.js b/app/soapbox/features/aliases/components/account.js index 3e4c7f936..fd0abd21b 100644 --- a/app/soapbox/features/aliases/components/account.js +++ b/app/soapbox/features/aliases/components/account.js @@ -5,11 +5,12 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; -import { addToAliases } from '../../../actions/aliases'; -import Avatar from '../../../components/avatar'; -import DisplayName from '../../../components/display_name'; -import IconButton from '../../../components/icon_button'; -import { makeGetAccount } from '../../../selectors'; +import { addToAliases } from 'soapbox/actions/aliases'; +import Avatar from 'soapbox/components/avatar'; +import DisplayName from 'soapbox/components/display_name'; +import IconButton from 'soapbox/components/icon_button'; +import { makeGetAccount } from 'soapbox/selectors'; +import { getFeatures } from 'soapbox/utils/features'; const messages = defineMessages({ add: { id: 'aliases.account.add', defaultMessage: 'Create alias' }, @@ -18,17 +19,20 @@ const messages = defineMessages({ const makeMapStateToProps = () => { const getAccount = makeGetAccount(); - const mapStateToProps = (state, { accountId, added }) => { + const mapStateToProps = (state, { accountId, added, aliases }) => { const me = state.get('me'); - const ownAccount = getAccount(state, me); + + const instance = state.get('instance'); + const features = getFeatures(instance); const account = getAccount(state, accountId); const apId = account.getIn(['pleroma', 'ap_id']); + const name = features.accountMoving ? account.get('acct') : apId; return { account, apId, - added: typeof added === 'undefined' ? ownAccount.getIn(['pleroma', 'also_known_as']).includes(apId) : added, + added: typeof added === 'undefined' ? aliases.includes(name) : added, me, }; }; @@ -56,7 +60,7 @@ class Account extends ImmutablePureComponent { added: false, }; - handleOnAdd = () => this.props.onAdd(this.props.intl, this.props.apId); + handleOnAdd = () => this.props.onAdd(this.props.intl, this.props.account); render() { const { account, accountId, intl, added, me } = this.props; diff --git a/app/soapbox/features/aliases/index.js b/app/soapbox/features/aliases/index.js index a2f844d98..c9bd35749 100644 --- a/app/soapbox/features/aliases/index.js +++ b/app/soapbox/features/aliases/index.js @@ -1,13 +1,15 @@ +import { List as ImmutableList } from 'immutable'; import React from 'react'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; +import { fetchAliases, removeFromAliases } from 'soapbox/actions/aliases'; import Icon from 'soapbox/components/icon'; +import ScrollableList from 'soapbox/components/scrollable_list'; import { makeGetAccount } from 'soapbox/selectors'; +import { getFeatures } from 'soapbox/utils/features'; -import { removeFromAliases } from '../../actions/aliases'; -import ScrollableList from '../../components/scrollable_list'; import Column from '../ui/components/column'; import ColumnSubheading from '../ui/components/column_subheading'; @@ -30,8 +32,16 @@ const makeMapStateToProps = () => { const me = state.get('me'); const account = getAccount(state, me); + const instance = state.get('instance'); + const features = getFeatures(instance); + + let aliases; + + if (features.accountMoving) aliases = state.getIn(['aliases', 'aliases', 'items'], ImmutableList()); + else aliases = account.getIn(['pleroma', 'also_known_as']); + return { - aliases: account.getIn(['pleroma', 'also_known_as']), + aliases, searchAccountIds: state.getIn(['aliases', 'suggestions', 'items']), loaded: state.getIn(['aliases', 'suggestions', 'loaded']), }; @@ -44,6 +54,11 @@ export default @connect(makeMapStateToProps) @injectIntl class Aliases extends ImmutablePureComponent { + componentDidMount = e => { + const { dispatch } = this.props; + dispatch(fetchAliases); + } + handleFilterDelete = e => { const { dispatch, intl } = this.props; dispatch(removeFromAliases(intl, e.currentTarget.dataset.value)); @@ -65,7 +80,7 @@ class Aliases extends ImmutablePureComponent { ) : (
- {searchAccountIds.map(accountId => )} + {searchAccountIds.map(accountId => )}
) } diff --git a/app/soapbox/reducers/aliases.js b/app/soapbox/reducers/aliases.js index 7c6394ee9..158e91158 100644 --- a/app/soapbox/reducers/aliases.js +++ b/app/soapbox/reducers/aliases.js @@ -4,9 +4,14 @@ import { ALIASES_SUGGESTIONS_READY, ALIASES_SUGGESTIONS_CLEAR, ALIASES_SUGGESTIONS_CHANGE, + ALIASES_FETCH_SUCCESS, } from '../actions/aliases'; const initialState = ImmutableMap({ + aliases: ImmutableMap({ + loaded: false, + items: ImmutableList(), + }), suggestions: ImmutableMap({ value: '', loaded: false, @@ -16,6 +21,9 @@ const initialState = ImmutableMap({ export default function aliasesReducer(state = initialState, action) { switch(action.type) { + case ALIASES_FETCH_SUCCESS: + return state + .setIn(['aliases', 'items'], action.value); case ALIASES_SUGGESTIONS_CHANGE: return state .setIn(['suggestions', 'value'], action.value) diff --git a/app/soapbox/utils/features.js b/app/soapbox/utils/features.js index bba076d3b..54355e005 100644 --- a/app/soapbox/utils/features.js +++ b/app/soapbox/utils/features.js @@ -88,6 +88,7 @@ export const getFeatures = createSelector([instance => instance], instance => { ]), birthdays: v.software === PLEROMA && gte(v.version, '2.4.50'), ethereumLogin: v.software === MITRA, + accountMoving: v.software === PLEROMA && gte(v.version, '2.4.50'), }; }); From f75ffeadd8d3d467ef2a683fc7823b9d0bf188d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 14 Feb 2022 21:35:35 +0100 Subject: [PATCH 2/3] Account migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/security.js | 20 +++ app/soapbox/features/migration/index.js | 115 ++++++++++++++++++ app/soapbox/features/ui/index.js | 2 + .../features/ui/util/async-components.js | 4 + app/styles/components/buttons.scss | 2 +- 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 app/soapbox/features/migration/index.js diff --git a/app/soapbox/actions/security.js b/app/soapbox/actions/security.js index 4cc957992..254acbdfb 100644 --- a/app/soapbox/actions/security.js +++ b/app/soapbox/actions/security.js @@ -35,6 +35,10 @@ export const DELETE_ACCOUNT_REQUEST = 'DELETE_ACCOUNT_REQUEST'; export const DELETE_ACCOUNT_SUCCESS = 'DELETE_ACCOUNT_SUCCESS'; export const DELETE_ACCOUNT_FAIL = 'DELETE_ACCOUNT_FAIL'; +export const MOVE_ACCOUNT_REQUEST = 'MOVE_ACCOUNT_REQUEST'; +export const MOVE_ACCOUNT_SUCCESS = 'MOVE_ACCOUNT_SUCCESS'; +export const MOVE_ACCOUNT_FAIL = 'MOVE_ACCOUNT_FAIL'; + export function fetchOAuthTokens() { return (dispatch, getState) => { dispatch({ type: FETCH_TOKENS_REQUEST }); @@ -124,3 +128,19 @@ export function deleteAccount(intl, password) { }); }; } + +export function moveAccount(targetAccount, password) { + return (dispatch, getState) => { + dispatch({ type: MOVE_ACCOUNT_REQUEST }); + return api(getState).post('/api/pleroma/move_account', { + password, + target_account: targetAccount, + }).then(response => { + if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure + dispatch({ type: MOVE_ACCOUNT_SUCCESS, response }); + }).catch(error => { + dispatch({ type: MOVE_ACCOUNT_FAIL, error, skipAlert: true }); + throw error; + }); + }; +} diff --git a/app/soapbox/features/migration/index.js b/app/soapbox/features/migration/index.js new file mode 100644 index 000000000..a999ca030 --- /dev/null +++ b/app/soapbox/features/migration/index.js @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; + +import { moveAccount } from 'soapbox/actions/security'; +import snackbar from 'soapbox/actions/snackbar'; +import ShowablePassword from 'soapbox/components/showable_password'; +import { FieldsGroup, SimpleForm, TextInput } from 'soapbox/features/forms'; +import Column from 'soapbox/features/ui/components/column'; + +const messages = defineMessages({ + heading: { id: 'column.migration', defaultMessage: 'Account migration' }, + submit: { id: 'migration.submit', defaultMessage: 'Move followers' }, + moveAccountSuccess: { id: 'migration.move_account.success', defaultMessage: 'Account successfully moved.' }, + moveAccountFail: { id: 'migration.move_account.fail', defaultMessage: 'Account migration failed.' }, + acctFieldLabel: { id: 'migration.fields.acct.label', defaultMessage: 'Handle of the new account' }, + acctFieldPlaceholder: { id: 'migration.fields.acct.placeholder', defaultMessage: 'username@domain' }, + currentPasswordFieldLabel: { id: 'migration.fields.confirm_password.label', defaultMessage: 'Current password' }, +}); + +export default @connect() +@injectIntl +class Migration extends ImmutablePureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + state = { + targetAccount: '', + password: '', + isLoading: false, + } + + handleInputChange = e => { + this.setState({ [e.target.name]: e.target.value }); + } + + clearForm = () => { + this.setState({ targetAccount: '', password: '' }); + } + + handleSubmit = e => { + const { targetAccount, password } = this.state; + const { dispatch, intl } = this.props; + this.setState({ isLoading: true }); + return dispatch(moveAccount(targetAccount, password)).then(() => { + this.clearForm(); + dispatch(snackbar.success(intl.formatMessage(messages.moveAccountSuccess))); + }).catch(error => { + dispatch(snackbar.error(intl.formatMessage(messages.moveAccountFail))); + }).then(() => { + this.setState({ isLoading: false }); + }); + } + + render() { + const { intl } = this.props; + + return ( + + +
+ +

+ + + + ), + }} + /> +

+ + +
+ +
+
+
+
+
+ ); + } + +} diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 1e0ed2d51..34534c506 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -104,6 +104,7 @@ import { UserIndex, FederationRestrictions, Aliases, + Migration, FollowRecommendations, Directory, SidebarMenu, @@ -314,6 +315,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index 341c59047..5f44a2801 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -414,6 +414,10 @@ export function Aliases() { return import(/* webpackChunkName: "features/aliases" */'../../aliases'); } +export function Migration() { + return import(/* webpackChunkName: "features/migration" */'../../migration'); +} + export function ScheduleForm() { return import(/* webpackChunkName: "features/compose" */'../../compose/components/schedule_form'); } diff --git a/app/styles/components/buttons.scss b/app/styles/components/buttons.scss index 51157bf31..677ae8e28 100644 --- a/app/styles/components/buttons.scss +++ b/app/styles/components/buttons.scss @@ -67,7 +67,7 @@ button { &:disabled, &.disabled { - background-color: var(--brand-color--med); + opacity: 0.2; cursor: default; } From 48a57cc9986dbb4c5799ca8ad92c5b791956005c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 14 Feb 2022 21:37:12 +0100 Subject: [PATCH 3/3] Fix aliases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/aliases.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/soapbox/actions/aliases.js b/app/soapbox/actions/aliases.js index ec9093965..e28817681 100644 --- a/app/soapbox/actions/aliases.js +++ b/app/soapbox/actions/aliases.js @@ -109,7 +109,7 @@ export const addToAliases = (intl, account) => (dispatch, getState) => { api(getState).patch('/api/v1/accounts/update_credentials', { also_known_as: [...alsoKnownAs, account.getIn(['pleroma', 'ap_id'])] }) .then((response => { dispatch(snackbar.success(intl.formatMessage(messages.createSuccess))); - dispatch(addToAliasesSuccess()); + dispatch(addToAliasesSuccess); dispatch(patchMeSuccess(response.data)); })) .catch(err => dispatch(addToAliasesFail(err))); @@ -160,6 +160,7 @@ export const removeFromAliases = (intl, account) => (dispatch, getState) => { .then(response => { dispatch(snackbar.success(intl.formatMessage(messages.removeSuccess))); dispatch(removeFromAliasesSuccess); + dispatch(patchMeSuccess(response.data)); }) .catch(err => dispatch(removeFromAliasesFail(err)));