From 80a682f120fccee7c657ca045347ebc626af76ec Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 13 Jul 2021 15:16:31 -0500 Subject: [PATCH 1/9] Admin: add UserIndex to view a list of registered users --- app/soapbox/actions/accounts.js | 12 +++- app/soapbox/actions/admin.js | 6 +- .../features/account/components/header.js | 5 +- app/soapbox/features/admin/user_index.js | 71 +++++++++++++++++++ app/soapbox/features/ui/index.js | 2 + .../features/ui/util/async-components.js | 4 ++ app/soapbox/reducers/accounts.js | 63 ++++++++++++++++ app/soapbox/reducers/admin.js | 5 +- 8 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 app/soapbox/features/admin/user_index.js diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index bd2b199ba..39a7a2ceb 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -130,7 +130,9 @@ export function fetchAccount(id) { return (dispatch, getState) => { dispatch(fetchRelationships([id])); - if (getState().getIn(['accounts', id], null) !== null) { + const account = getState().getIn(['accounts', id]); + + if (account && !account.get('dirty')) { return; } @@ -156,7 +158,15 @@ export function fetchAccount(id) { export function fetchAccountByUsername(username) { return (dispatch, getState) => { + const account = getState().get('accounts').find(account => account.get('acct') === username); + + if (account) { + dispatch(fetchAccount(account.get('id'))); + return; + } + api(getState).get(`/api/v1/accounts/${username}`).then(response => { + dispatch(fetchRelationships([response.data.id])); dispatch(importFetchedAccount(response.data)); }).then(() => { dispatch(fetchAccountSuccess()); diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index 6a93b1682..96f7e5e12 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -1,5 +1,6 @@ import api from '../api'; import { importFetchedAccount, importFetchedStatuses } from 'soapbox/actions/importer'; +import { fetchRelationships } from 'soapbox/actions/accounts'; export const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST'; export const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS'; @@ -129,8 +130,9 @@ export function fetchUsers(params) { dispatch({ type: ADMIN_USERS_FETCH_REQUEST, params }); return api(getState) .get('/api/pleroma/admin/users', { params }) - .then(({ data }) => { - dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, data, params }); + .then(({ data: { users, count, page_size: pageSize } }) => { + dispatch(fetchRelationships(users.map(user => user.id))); + dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users, count, pageSize, params }); }).catch(error => { dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, params }); }); diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 6aebd5e28..537990134 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -291,7 +291,8 @@ class Header extends ImmutablePureComponent { const info = this.makeInfo(); const menu = this.makeMenu(); - const headerMissing = (account.get('header').indexOf('/headers/original/missing.png') > -1); + const header = account.get('header', ''); + const headerMissing = !header || ['/images/banner.png', '/headers/original/missing.png'].some(path => header.endsWith(path)); const avatarSize = isSmallScreen ? 90 : 200; const deactivated = !account.getIn(['pleroma', 'is_active'], true); @@ -306,7 +307,7 @@ class Header extends ImmutablePureComponent { - + {header && }
diff --git a/app/soapbox/features/admin/user_index.js b/app/soapbox/features/admin/user_index.js new file mode 100644 index 000000000..9501c3ceb --- /dev/null +++ b/app/soapbox/features/admin/user_index.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { debounce } from 'lodash'; +import LoadingIndicator from 'soapbox/components/loading_indicator'; +import { fetchUsers } from 'soapbox/actions/admin'; +import { FormattedMessage } from 'react-intl'; +import AccountContainer from 'soapbox/containers/account_container'; +import Column from 'soapbox/features/ui/components/column'; +import ScrollableList from 'soapbox/components/scrollable_list'; + +const mapStateToProps = state => { + return { + accountIds: state.getIn(['admin', 'usersList']), + hasMore: false, + }; +}; + +export default @connect(mapStateToProps) +class UserIndex extends ImmutablePureComponent { + + static propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + accountIds: ImmutablePropTypes.orderedSet, + hasMore: PropTypes.bool, + diffCount: PropTypes.number, + isAccount: PropTypes.bool, + unavailable: PropTypes.bool, + }; + + componentDidMount() { + this.props.dispatch(fetchUsers({ filters: 'local,active' })); + } + + handleLoadMore = debounce(() => { + // if (this.props.accountId && this.props.accountId !== -1) { + // this.props.dispatch(expandFollowers(this.props.accountId)); + // } + }, 300, { leading: true }); + + render() { + const { accountIds, hasMore } = this.props; + + if (!accountIds) { + return ( + + + + ); + } + + return ( + + } + > + {accountIds.map(id => + , + )} + + + ); + } + +} diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index a140cd0a7..8b1204ed7 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -95,6 +95,7 @@ import { ModerationLog, CryptoDonate, ScheduledStatuses, + UserIndex, } from './util/async-components'; // Dummy import, to make sure that ends up in the application bundle. @@ -265,6 +266,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 9d1fcae1d..c1bb39301 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -241,3 +241,7 @@ export function CryptoDonate() { export function ScheduledStatuses() { return import(/* webpackChunkName: "features/scheduled_statuses" */'../../scheduled_statuses'); } + +export function UserIndex() { + return import(/* webpackChunkName: "features/admin/user_index" */'../../admin/user_index'); +} diff --git a/app/soapbox/reducers/accounts.js b/app/soapbox/reducers/accounts.js index 7bab5aad4..0ea392263 100644 --- a/app/soapbox/reducers/accounts.js +++ b/app/soapbox/reducers/accounts.js @@ -13,6 +13,7 @@ import { } from 'immutable'; import { normalizePleromaUserFields } from 'soapbox/utils/pleroma'; import { + ADMIN_USERS_FETCH_SUCCESS, ADMIN_USERS_TAG_REQUEST, ADMIN_USERS_TAG_FAIL, ADMIN_USERS_UNTAG_REQUEST, @@ -121,6 +122,66 @@ const removePermission = (state, accountIds, permissionGroup) => { }); }; +const buildAccount = adminUser => fromJS({ + id: adminUser.get('id'), + username: adminUser.get('nickname').split('@')[0], + acct: adminUser.get('nickname'), + display_name: adminUser.get('display_name'), + display_name_html: adminUser.get('display_name'), + note: '', + url: adminUser.get('url'), + avatar: adminUser.get('avatar'), + avatar_static: adminUser.get('avatar'), + header: '', + header_static: '', + emojis: [], + fields: [], + pleroma: { + is_active: adminUser.get('is_active'), + is_confirmed: adminUser.get('is_confirmed'), + is_admin: adminUser.getIn(['roles', 'admin']), + is_moderator: adminUser.getIn(['roles', 'moderator']), + }, + source: { + pleroma: { + actor_type: adminUser.get('actor_type'), + }, + }, + dirty: true, +}); + +const mergeAdminUser = (account, adminUser) => { + return account.withMutations(account => { + account.set('display_name', adminUser.get('display_name')); + account.set('avatar', adminUser.get('avatar')); + account.set('avatar_static', adminUser.get('avatar')); + account.setIn(['pleroma', 'is_active'], adminUser.get('is_active')); + account.setIn(['pleroma', 'is_admin'], adminUser.getIn(['roles', 'admin'])); + account.setIn(['pleroma', 'is_moderator'], adminUser.getIn(['roles', 'moderator'])); + account.setIn(['pleroma', 'is_confirmed'], adminUser.get('is_confirmed')); + account.set('dirty', true); + }); +}; + +const importAdminUser = (state, adminUser) => { + const id = adminUser.get('id'); + const account = state.get(id); + + if (!account) { + return state.set(id, buildAccount(adminUser)); + } else { + return state.set(id, mergeAdminUser(account, adminUser)); + } +}; + +const importAdminUsers = (state, adminUsers) => { + return state.withMutations(state => { + fromJS(adminUsers).forEach(adminUser => { + importAdminUser(state, adminUser); + }); + }); +}; + export default function accounts(state = initialState, action) { switch(action.type) { case ACCOUNT_IMPORT: @@ -150,6 +211,8 @@ export default function accounts(state = initialState, action) { return removePermission(state, action.accountIds, action.permissionGroup); case ADMIN_USERS_DELETE_REQUEST: return setDeactivated(state, action.nicknames); + case ADMIN_USERS_FETCH_SUCCESS: + return importAdminUsers(state, action.users); default: return state; } diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js index 960df70f6..f8a654c74 100644 --- a/app/soapbox/reducers/admin.js +++ b/app/soapbox/reducers/admin.js @@ -21,6 +21,7 @@ const initialState = ImmutableMap({ reports: ImmutableMap(), openReports: ImmutableOrderedSet(), users: ImmutableMap(), + usersList: ImmutableOrderedSet(), awaitingApproval: ImmutableOrderedSet(), configs: ImmutableList(), needsReboot: false, @@ -28,6 +29,8 @@ const initialState = ImmutableMap({ function importUsers(state, users) { return state.withMutations(state => { + const ids = users.map(user => user.id); + state.update('usersList', ImmutableOrderedSet(), items => items.union(ids)); users.forEach(user => { user = normalizePleromaUserFields(user); if (!user.is_approved) { @@ -94,7 +97,7 @@ export default function admin(state = initialState, action) { case ADMIN_REPORTS_PATCH_SUCCESS: return handleReportDiffs(state, action.reports); case ADMIN_USERS_FETCH_SUCCESS: - return importUsers(state, action.data.users); + return importUsers(state, action.users); case ADMIN_USERS_DELETE_REQUEST: case ADMIN_USERS_DELETE_SUCCESS: return deleteUsers(state, action.nicknames); From 30439240454e81b74edb0de0809d7a298e2b2e76 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 13 Jul 2021 17:01:31 -0500 Subject: [PATCH 2/9] Refactor UserIndex to use its own state instead of Redux --- app/soapbox/actions/admin.js | 1 + app/soapbox/features/admin/index.js | 5 +- app/soapbox/features/admin/user_index.js | 79 +++++++++++++++--------- app/soapbox/reducers/admin.js | 3 - 4 files changed, 54 insertions(+), 34 deletions(-) diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index 96f7e5e12..e65b840ce 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -133,6 +133,7 @@ export function fetchUsers(params) { .then(({ data: { users, count, page_size: pageSize } }) => { dispatch(fetchRelationships(users.map(user => user.id))); dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users, count, pageSize, params }); + return { users, count, pageSize }; }).catch(error => { dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, params }); }); diff --git a/app/soapbox/features/admin/index.js b/app/soapbox/features/admin/index.js index 0ed82bc66..ac6c1204a 100644 --- a/app/soapbox/features/admin/index.js +++ b/app/soapbox/features/admin/index.js @@ -1,5 +1,6 @@ import React from 'react'; import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; +import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; @@ -92,14 +93,14 @@ class Dashboard extends ImmutablePureComponent {
}
- +
-
+
{retention &&
diff --git a/app/soapbox/features/admin/user_index.js b/app/soapbox/features/admin/user_index.js index 9501c3ceb..f14aef848 100644 --- a/app/soapbox/features/admin/user_index.js +++ b/app/soapbox/features/admin/user_index.js @@ -2,61 +2,82 @@ import React from 'react'; import { connect } from 'react-redux'; import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import { debounce } from 'lodash'; -import LoadingIndicator from 'soapbox/components/loading_indicator'; import { fetchUsers } from 'soapbox/actions/admin'; import { FormattedMessage } from 'react-intl'; import AccountContainer from 'soapbox/containers/account_container'; import Column from 'soapbox/features/ui/components/column'; import ScrollableList from 'soapbox/components/scrollable_list'; +import { Set as ImmutableSet, OrderedSet as ImmutableOrderedSet, is } from 'immutable'; -const mapStateToProps = state => { - return { - accountIds: state.getIn(['admin', 'usersList']), - hasMore: false, - }; -}; - -export default @connect(mapStateToProps) +export default @connect() class UserIndex extends ImmutablePureComponent { static propTypes = { - params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, - accountIds: ImmutablePropTypes.orderedSet, - hasMore: PropTypes.bool, - diffCount: PropTypes.number, - isAccount: PropTypes.bool, - unavailable: PropTypes.bool, }; + state = { + isLoading: true, + filters: ImmutableSet(['local', 'active']), + accountIds: ImmutableOrderedSet(), + total: Infinity, + page: 0, + } + + clearState = () => { + this.setState({ + isLoading: true, + page: 0, + }); + } + + fetchNextPage = () => { + const nextPage = this.state.page + 1; + const filters = this.state.filters.toJS().join(); + + this.props.dispatch(fetchUsers({ filters, page: nextPage })) + .then(({ users, count }) => { + const newIds = users.map(user => user.id); + + this.setState({ + isLoading: false, + accountIds: this.state.accountIds.union(newIds), + total: count, + page: nextPage, + }); + }) + .catch(() => {}); + } + componentDidMount() { - this.props.dispatch(fetchUsers({ filters: 'local,active' })); + this.fetchNextPage(); + } + + componentDidUpdate(prevProps, prevState) { + if (!is(this.state.filters, prevState.filters) || !is(this.state.q, prevState.q)) { + this.clearState(); + this.fetchNextPage(); + } } handleLoadMore = debounce(() => { - // if (this.props.accountId && this.props.accountId !== -1) { - // this.props.dispatch(expandFollowers(this.props.accountId)); - // } - }, 300, { leading: true }); + this.fetchNextPage(); + }, 2000, { leading: true }); render() { - const { accountIds, hasMore } = this.props; + const { accountIds, isLoading } = this.state; + const hasMore = accountIds.count() < this.state.total; - if (!accountIds) { - return ( - - - - ); - } + const showLoading = isLoading && accountIds.isEmpty(); return ( } > diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js index f8a654c74..58e8e354e 100644 --- a/app/soapbox/reducers/admin.js +++ b/app/soapbox/reducers/admin.js @@ -21,7 +21,6 @@ const initialState = ImmutableMap({ reports: ImmutableMap(), openReports: ImmutableOrderedSet(), users: ImmutableMap(), - usersList: ImmutableOrderedSet(), awaitingApproval: ImmutableOrderedSet(), configs: ImmutableList(), needsReboot: false, @@ -29,8 +28,6 @@ const initialState = ImmutableMap({ function importUsers(state, users) { return state.withMutations(state => { - const ids = users.map(user => user.id); - state.update('usersList', ImmutableOrderedSet(), items => items.union(ids)); users.forEach(user => { user = normalizePleromaUserFields(user); if (!user.is_approved) { From 732fba73f580c50b57da9ce6673ea16deeea3f14 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 13 Jul 2021 17:27:11 -0500 Subject: [PATCH 3/9] Admin: refactor fetchUsers() action --- app/soapbox/actions/admin.js | 10 ++++++---- app/soapbox/features/admin/awaiting_approval.js | 3 +-- app/soapbox/features/admin/user_index.js | 4 ++-- app/soapbox/features/ui/index.js | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index e65b840ce..1b48efa94 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -125,17 +125,19 @@ export function closeReports(ids) { return patchReports(ids, 'closed'); } -export function fetchUsers(params) { +export function fetchUsers(filters = [], page = 1, pageSize = 50) { return (dispatch, getState) => { - dispatch({ type: ADMIN_USERS_FETCH_REQUEST, params }); + const params = { filters: filters.join(), page, page_size: pageSize }; + + dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize }); return api(getState) .get('/api/pleroma/admin/users', { params }) .then(({ data: { users, count, page_size: pageSize } }) => { dispatch(fetchRelationships(users.map(user => user.id))); - dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users, count, pageSize, params }); + dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users, count, pageSize, filters, page }); return { users, count, pageSize }; }).catch(error => { - dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, params }); + dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }); }); }; } diff --git a/app/soapbox/features/admin/awaiting_approval.js b/app/soapbox/features/admin/awaiting_approval.js index 7706eb4a8..4070c8a35 100644 --- a/app/soapbox/features/admin/awaiting_approval.js +++ b/app/soapbox/features/admin/awaiting_approval.js @@ -39,8 +39,7 @@ class AwaitingApproval extends ImmutablePureComponent { componentDidMount() { const { dispatch } = this.props; - const params = { page: 1, filters: 'local,need_approval' }; - dispatch(fetchUsers(params)) + dispatch(fetchUsers(['local', 'need_approval'])) .then(() => this.setState({ isLoading: false })) .catch(() => {}); } diff --git a/app/soapbox/features/admin/user_index.js b/app/soapbox/features/admin/user_index.js index f14aef848..7d8fc4d3d 100644 --- a/app/soapbox/features/admin/user_index.js +++ b/app/soapbox/features/admin/user_index.js @@ -34,9 +34,9 @@ class UserIndex extends ImmutablePureComponent { fetchNextPage = () => { const nextPage = this.state.page + 1; - const filters = this.state.filters.toJS().join(); + const filters = this.state.filters.toJS(); - this.props.dispatch(fetchUsers({ filters, page: nextPage })) + this.props.dispatch(fetchUsers(filters, nextPage)) .then(({ users, count }) => { const newIds = users.map(user => user.id); diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 8b1204ed7..fd522c07f 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -430,7 +430,7 @@ class UI extends React.PureComponent { if (isStaff(account)) { this.props.dispatch(fetchReports({ state: 'open' })); - this.props.dispatch(fetchUsers({ page: 1, filters: 'local,need_approval' })); + this.props.dispatch(fetchUsers(['local', 'need_approval'])); } if (isAdmin(account)) { From a89ea524a9cb551b5ef11f455c59729f364da496 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 13 Jul 2021 17:59:55 -0500 Subject: [PATCH 4/9] Refactor AwaitingApproval --- .../features/admin/awaiting_approval.js | 62 ++++----------- .../admin/components/unapproved_account.js | 77 +++++++++++++++++++ app/soapbox/features/admin/user_index.js | 4 +- app/soapbox/reducers/admin.js | 7 +- app/soapbox/selectors/index.js | 5 +- 5 files changed, 104 insertions(+), 51 deletions(-) create mode 100644 app/soapbox/features/admin/components/unapproved_account.js diff --git a/app/soapbox/features/admin/awaiting_approval.js b/app/soapbox/features/admin/awaiting_approval.js index 4070c8a35..0d95fa685 100644 --- a/app/soapbox/features/admin/awaiting_approval.js +++ b/app/soapbox/features/admin/awaiting_approval.js @@ -5,24 +5,18 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from '../ui/components/column'; -import IconButton from 'soapbox/components/icon_button'; import ScrollableList from 'soapbox/components/scrollable_list'; -import { fetchUsers, deleteUsers, approveUsers } from 'soapbox/actions/admin'; -import snackbar from 'soapbox/actions/snackbar'; +import UnapprovedAccount from './components/unapproved_account'; +import { fetchUsers } from 'soapbox/actions/admin'; const messages = defineMessages({ heading: { id: 'column.admin.awaiting_approval', defaultMessage: 'Awaiting Approval' }, emptyMessage: { id: 'admin.awaiting_approval.empty_message', defaultMessage: 'There is nobody waiting for approval. When a new user signs up, you can review them here.' }, - approved: { id: 'admin.awaiting_approval.approved_message', defaultMessage: '{acct} was approved!' }, - rejected: { id: 'admin.awaiting_approval.rejected_message', defaultMessage: '{acct} was rejected.' }, }); -const mapStateToProps = state => { - const nicknames = state.getIn(['admin', 'awaitingApproval']); - return { - users: nicknames.toList().map(nickname => state.getIn(['admin', 'users', nickname])), - }; -}; +const mapStateToProps = state => ({ + accountIds: state.getIn(['admin', 'awaitingApproval']), +}); export default @connect(mapStateToProps) @injectIntl @@ -30,7 +24,7 @@ class AwaitingApproval extends ImmutablePureComponent { static propTypes = { intl: PropTypes.object.isRequired, - users: ImmutablePropTypes.list.isRequired, + accountIds: ImmutablePropTypes.orderedSet.isRequired, }; state = { @@ -44,45 +38,21 @@ class AwaitingApproval extends ImmutablePureComponent { .catch(() => {}); } - handleApprove = nickname => { - const { dispatch, intl } = this.props; - return e => { - dispatch(approveUsers([nickname])).then(() => { - const message = intl.formatMessage(messages.approved, { acct: `@${nickname}` }); - dispatch(snackbar.success(message)); - }).catch(() => {}); - }; - } - - handleReject = nickname => { - const { dispatch, intl } = this.props; - return e => { - dispatch(deleteUsers([nickname])).then(() => { - const message = intl.formatMessage(messages.rejected, { acct: `@${nickname}` }); - dispatch(snackbar.info(message)); - }).catch(() => {}); - }; - } - render() { - const { intl, users } = this.props; + const { intl, accountIds } = this.props; const { isLoading } = this.state; - const showLoading = isLoading && users.count() === 0; + const showLoading = isLoading && accountIds.count() === 0; return ( - - {users.map((user, i) => ( -
-
-
@{user.get('nickname')}
-
{user.get('registration_reason')}
-
-
- - -
-
+ + {accountIds.map(id => ( + ))}
diff --git a/app/soapbox/features/admin/components/unapproved_account.js b/app/soapbox/features/admin/components/unapproved_account.js new file mode 100644 index 000000000..578a55c0c --- /dev/null +++ b/app/soapbox/features/admin/components/unapproved_account.js @@ -0,0 +1,77 @@ +import React from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import IconButton from 'soapbox/components/icon_button'; +import { deleteUsers, approveUsers } from 'soapbox/actions/admin'; +import { makeGetAccount } from 'soapbox/selectors'; +import snackbar from 'soapbox/actions/snackbar'; + +const messages = defineMessages({ + approved: { id: 'admin.awaiting_approval.approved_message', defaultMessage: '{acct} was approved!' }, + rejected: { id: 'admin.awaiting_approval.rejected_message', defaultMessage: '{acct} was rejected.' }, +}); + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, { accountId }) => { + return { + account: getAccount(state, accountId), + }; + }; + + return mapStateToProps; +}; + +export default @connect(makeMapStateToProps) +@injectIntl +class UnapprovedAccount extends ImmutablePureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + account: ImmutablePropTypes.map.isRequired, + }; + + handleApprove = () => { + const { dispatch, intl, account } = this.props; + + dispatch(approveUsers([account.get('id')])) + .then(() => { + const message = intl.formatMessage(messages.approved, { acct: `@${account.get('acct')}` }); + dispatch(snackbar.success(message)); + }) + .catch(() => {}); + } + + handleReject = () => { + const { dispatch, intl, account } = this.props; + + dispatch(deleteUsers([account.get('id')])) + .then(() => { + const message = intl.formatMessage(messages.rejected, { acct: `@${account.get('acct')}` }); + dispatch(snackbar.info(message)); + }) + .catch(() => {}); + } + + render() { + const { account } = this.props; + + return ( +
+
+
@{account.get('acct')}
+
{account.getIn(['pleroma', 'admin', 'registration_reason'])}
+
+
+ + +
+
+ ); + } + +} diff --git a/app/soapbox/features/admin/user_index.js b/app/soapbox/features/admin/user_index.js index 7d8fc4d3d..4820ba815 100644 --- a/app/soapbox/features/admin/user_index.js +++ b/app/soapbox/features/admin/user_index.js @@ -33,8 +33,8 @@ class UserIndex extends ImmutablePureComponent { } fetchNextPage = () => { - const nextPage = this.state.page + 1; - const filters = this.state.filters.toJS(); + const { filters, page } = this.state; + const nextPage = page + 1; this.props.dispatch(fetchUsers(filters, nextPage)) .then(({ users, count }) => { diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js index 58e8e354e..ebd009ed9 100644 --- a/app/soapbox/reducers/admin.js +++ b/app/soapbox/reducers/admin.js @@ -31,9 +31,12 @@ function importUsers(state, users) { users.forEach(user => { user = normalizePleromaUserFields(user); if (!user.is_approved) { - state.update('awaitingApproval', orderedSet => orderedSet.add(user.nickname)); + state.update('awaitingApproval', orderedSet => orderedSet.add(user.id)); } - state.setIn(['users', user.nickname], fromJS(user)); + state.setIn(['users', user.id], ImmutableMap({ + email: user.email, + registration_reason: user.registration_reason, + })); }); }); } diff --git a/app/soapbox/selectors/index.js b/app/soapbox/selectors/index.js index 0615db229..9b222921e 100644 --- a/app/soapbox/selectors/index.js +++ b/app/soapbox/selectors/index.js @@ -5,6 +5,7 @@ const getAccountBase = (state, id) => state.getIn(['accounts', id], null const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null); const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null); const getAccountMoved = (state, id) => state.getIn(['accounts', state.getIn(['accounts', id, 'moved'])]); +const getAccountAdminData = (state, id) => state.getIn(['admin', 'users', id]); const getAccountPatron = (state, id) => { const url = state.getIn(['accounts', id, 'url']); return state.getIn(['patron', 'accounts', url]); @@ -16,8 +17,9 @@ export const makeGetAccount = () => { getAccountCounters, getAccountRelationship, getAccountMoved, + getAccountAdminData, getAccountPatron, - ], (base, counters, relationship, moved, patron) => { + ], (base, counters, relationship, moved, admin, patron) => { if (base === null) { return null; } @@ -26,6 +28,7 @@ export const makeGetAccount = () => { map.set('relationship', relationship); map.set('moved', moved); map.set('patron', patron); + map.setIn(['pleroma', 'admin'], admin); }); }); }; From 2f6cd35f38a1bbf2f985f73971d664eba52ddfaa Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 13 Jul 2021 18:11:11 -0500 Subject: [PATCH 5/9] Admin: refactor actions to use accountIds instead of nicknames --- app/soapbox/actions/admin.js | 27 +++++++++++++++------------ app/soapbox/actions/moderation.js | 4 ++-- app/soapbox/reducers/accounts.js | 26 ++++++++++++++------------ app/soapbox/reducers/admin.js | 12 ++++++------ 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index 1b48efa94..43119708d 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -142,41 +142,44 @@ export function fetchUsers(filters = [], page = 1, pageSize = 50) { }; } -export function deactivateUsers(nicknames) { +export function deactivateUsers(accountIds) { return (dispatch, getState) => { - dispatch({ type: ADMIN_USERS_DEACTIVATE_REQUEST, nicknames }); + const nicknames = nicknamesFromIds(getState, accountIds); + dispatch({ type: ADMIN_USERS_DEACTIVATE_REQUEST, accountIds }); return api(getState) .patch('/api/pleroma/admin/users/deactivate', { nicknames }) .then(({ data: { users } }) => { - dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, users, nicknames }); + dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, users, accountIds }); }).catch(error => { - dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, nicknames }); + dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds }); }); }; } -export function deleteUsers(nicknames) { +export function deleteUsers(accountIds) { return (dispatch, getState) => { - dispatch({ type: ADMIN_USERS_DELETE_REQUEST, nicknames }); + const nicknames = nicknamesFromIds(getState, accountIds); + dispatch({ type: ADMIN_USERS_DELETE_REQUEST, accountIds }); return api(getState) .delete('/api/pleroma/admin/users', { data: { nicknames } }) .then(({ data: nicknames }) => { - dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames }); + dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames, accountIds }); }).catch(error => { - dispatch({ type: ADMIN_USERS_DELETE_FAIL, error, nicknames }); + dispatch({ type: ADMIN_USERS_DELETE_FAIL, error, accountIds }); }); }; } -export function approveUsers(nicknames) { +export function approveUsers(accountIds) { return (dispatch, getState) => { - dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, nicknames }); + const nicknames = nicknamesFromIds(getState, accountIds); + dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountIds }); return api(getState) .patch('/api/pleroma/admin/users/approve', { nicknames }) .then(({ data: { users } }) => { - dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users, nicknames }); + dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users, accountIds }); }).catch(error => { - dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, nicknames }); + dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountIds }); }); }; } diff --git a/app/soapbox/actions/moderation.js b/app/soapbox/actions/moderation.js index cce860de8..cb84803bd 100644 --- a/app/soapbox/actions/moderation.js +++ b/app/soapbox/actions/moderation.js @@ -36,7 +36,7 @@ export function deactivateUserModal(intl, accountId, afterConfirm = () => {}) { message: intl.formatMessage(messages.deactivateUserPrompt, { acct }), confirm: intl.formatMessage(messages.deactivateUserConfirm, { name }), onConfirm: () => { - dispatch(deactivateUsers([acct])).then(() => { + dispatch(deactivateUsers([accountId])).then(() => { const message = intl.formatMessage(messages.userDeactivated, { acct }); dispatch(snackbar.success(message)); afterConfirm(); @@ -74,7 +74,7 @@ export function deleteUserModal(intl, accountId, afterConfirm = () => {}) { confirm, checkbox, onConfirm: () => { - dispatch(deleteUsers([acct])).then(() => { + dispatch(deleteUsers([accountId])).then(() => { const message = intl.formatMessage(messages.userDeleted, { acct }); dispatch(fetchAccountByUsername(acct)); dispatch(snackbar.success(message)); diff --git a/app/soapbox/reducers/accounts.js b/app/soapbox/reducers/accounts.js index 0ea392263..0ce12d28e 100644 --- a/app/soapbox/reducers/accounts.js +++ b/app/soapbox/reducers/accounts.js @@ -23,7 +23,12 @@ import { ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, ADMIN_REMOVE_PERMISSION_GROUP_FAIL, } from 'soapbox/actions/admin'; -import { ADMIN_USERS_DELETE_REQUEST } from 'soapbox/actions/admin'; +import { + ADMIN_USERS_DELETE_REQUEST, + ADMIN_USERS_DELETE_FAIL, + ADMIN_USERS_DEACTIVATE_REQUEST, + ADMIN_USERS_DEACTIVATE_FAIL, +} from 'soapbox/actions/admin'; const initialState = ImmutableMap(); @@ -80,17 +85,10 @@ const removeTags = (state, accountIds, tags) => { }); }; -const nicknamesToIds = (state, nicknames) => { - return nicknames.map(nickname => { - return state.find(account => account.get('acct') === nickname, null, ImmutableMap()).get('id'); - }); -}; - -const setDeactivated = (state, nicknames) => { - const ids = nicknamesToIds(state, nicknames); +const setActive = (state, accountIds, active) => { return state.withMutations(state => { - ids.forEach(id => { - state.setIn([id, 'pleroma', 'is_active'], false); + accountIds.forEach(id => { + state.setIn([id, 'pleroma', 'is_active'], active); }); }); }; @@ -210,7 +208,11 @@ export default function accounts(state = initialState, action) { case ADMIN_ADD_PERMISSION_GROUP_FAIL: return removePermission(state, action.accountIds, action.permissionGroup); case ADMIN_USERS_DELETE_REQUEST: - return setDeactivated(state, action.nicknames); + case ADMIN_USERS_DEACTIVATE_REQUEST: + return setActive(state, action.accountIds, false); + case ADMIN_USERS_DELETE_FAIL: + case ADMIN_USERS_DEACTIVATE_FAIL: + return setActive(state, action.accountIds, true); case ADMIN_USERS_FETCH_SUCCESS: return importAdminUsers(state, action.users); default: diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js index ebd009ed9..f7cb33fb6 100644 --- a/app/soapbox/reducers/admin.js +++ b/app/soapbox/reducers/admin.js @@ -41,11 +41,11 @@ function importUsers(state, users) { }); } -function deleteUsers(state, nicknames) { +function deleteUsers(state, accountIds) { return state.withMutations(state => { - nicknames.forEach(nickname => { - state.update('awaitingApproval', orderedSet => orderedSet.delete(nickname)); - state.deleteIn(['users', nickname]); + accountIds.forEach(id => { + state.update('awaitingApproval', orderedSet => orderedSet.delete(id)); + state.deleteIn(['users', id]); }); }); } @@ -100,9 +100,9 @@ export default function admin(state = initialState, action) { return importUsers(state, action.users); case ADMIN_USERS_DELETE_REQUEST: case ADMIN_USERS_DELETE_SUCCESS: - return deleteUsers(state, action.nicknames); + return deleteUsers(state, action.accountIds); case ADMIN_USERS_APPROVE_REQUEST: - return state.update('awaitingApproval', set => set.subtract(action.nicknames)); + return state.update('awaitingApproval', set => set.subtract(action.accountIds)); case ADMIN_USERS_APPROVE_SUCCESS: return approveUsers(state, action.users); default: From fa91defea0d7371247339f5e0babf46dac8663c3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 13 Jul 2021 18:35:37 -0500 Subject: [PATCH 6/9] Rename 'dirty' to 'should_refetch' --- app/soapbox/actions/accounts.js | 2 +- app/soapbox/reducers/accounts.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index 39a7a2ceb..5908f01f1 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -132,7 +132,7 @@ export function fetchAccount(id) { const account = getState().getIn(['accounts', id]); - if (account && !account.get('dirty')) { + if (account && !account.get('should_refetch')) { return; } diff --git a/app/soapbox/reducers/accounts.js b/app/soapbox/reducers/accounts.js index 0ce12d28e..bb21547fc 100644 --- a/app/soapbox/reducers/accounts.js +++ b/app/soapbox/reducers/accounts.js @@ -145,7 +145,7 @@ const buildAccount = adminUser => fromJS({ actor_type: adminUser.get('actor_type'), }, }, - dirty: true, + should_refetch: true, }); const mergeAdminUser = (account, adminUser) => { @@ -157,7 +157,6 @@ const mergeAdminUser = (account, adminUser) => { account.setIn(['pleroma', 'is_admin'], adminUser.getIn(['roles', 'admin'])); account.setIn(['pleroma', 'is_moderator'], adminUser.getIn(['roles', 'moderator'])); account.setIn(['pleroma', 'is_confirmed'], adminUser.get('is_confirmed')); - account.set('dirty', true); }); }; From 695e64cb1f71bf00b4bb2aa4f28f89d3ad608733 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 13 Jul 2021 18:41:38 -0500 Subject: [PATCH 7/9] Ingest tags from AdminAPI --- app/soapbox/reducers/accounts.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/soapbox/reducers/accounts.js b/app/soapbox/reducers/accounts.js index bb21547fc..761356507 100644 --- a/app/soapbox/reducers/accounts.js +++ b/app/soapbox/reducers/accounts.js @@ -139,6 +139,7 @@ const buildAccount = adminUser => fromJS({ is_confirmed: adminUser.get('is_confirmed'), is_admin: adminUser.getIn(['roles', 'admin']), is_moderator: adminUser.getIn(['roles', 'moderator']), + tags: adminUser.get('tags'), }, source: { pleroma: { @@ -157,6 +158,7 @@ const mergeAdminUser = (account, adminUser) => { account.setIn(['pleroma', 'is_admin'], adminUser.getIn(['roles', 'admin'])); account.setIn(['pleroma', 'is_moderator'], adminUser.getIn(['roles', 'moderator'])); account.setIn(['pleroma', 'is_confirmed'], adminUser.get('is_confirmed')); + account.setIn(['pleroma', 'tags'], adminUser.get('tags')); }); }; From 105961b3e75718436d36e0513558fb81d086a078 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 13 Jul 2021 18:43:31 -0500 Subject: [PATCH 8/9] Admin: link status count to local timeline --- app/soapbox/features/admin/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/admin/index.js b/app/soapbox/features/admin/index.js index ac6c1204a..0c2d90781 100644 --- a/app/soapbox/features/admin/index.js +++ b/app/soapbox/features/admin/index.js @@ -113,14 +113,14 @@ class Dashboard extends ImmutablePureComponent {
}
- +
-
+
From aae031f5d0fac06e4c8ae9de81af4a63c1844e92 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 13 Jul 2021 18:49:41 -0500 Subject: [PATCH 9/9] UserIndex: configurable pageSize --- app/soapbox/features/admin/user_index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/admin/user_index.js b/app/soapbox/features/admin/user_index.js index 4820ba815..71a173822 100644 --- a/app/soapbox/features/admin/user_index.js +++ b/app/soapbox/features/admin/user_index.js @@ -22,6 +22,7 @@ class UserIndex extends ImmutablePureComponent { filters: ImmutableSet(['local', 'active']), accountIds: ImmutableOrderedSet(), total: Infinity, + pageSize: 50, page: 0, } @@ -33,10 +34,10 @@ class UserIndex extends ImmutablePureComponent { } fetchNextPage = () => { - const { filters, page } = this.state; + const { filters, page, pageSize } = this.state; const nextPage = page + 1; - this.props.dispatch(fetchUsers(filters, nextPage)) + this.props.dispatch(fetchUsers(filters, nextPage, pageSize)) .then(({ users, count }) => { const newIds = users.map(user => user.id); @@ -55,7 +56,9 @@ class UserIndex extends ImmutablePureComponent { } componentDidUpdate(prevProps, prevState) { - if (!is(this.state.filters, prevState.filters) || !is(this.state.q, prevState.q)) { + const { filters, q } = this.state; + + if (!is(filters, prevState.filters) || !is(q, prevState.q)) { this.clearState(); this.fetchNextPage(); }