From 80a682f120fccee7c657ca045347ebc626af76ec Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 13 Jul 2021 15:16:31 -0500 Subject: [PATCH] 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);