From c0f8c9d5e7146e78b3b88034732cc1a31775ad50 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 19 Oct 2024 13:23:08 -0500 Subject: [PATCH] Add useAdminAccount hook, refactor admin accounts in dashboard, refactor admin actions --- src/actions/admin.ts | 26 +++--- src/api/MastodonClient.ts | 10 +- src/api/hooks/admin/useAdminAccounts.ts | 29 ++++++ .../components/latest-accounts-panel.tsx | 26 ++---- src/features/admin/user-index.tsx | 47 +++------- src/reducers/admin-user-index.ts | 92 ++++++++++--------- 6 files changed, 114 insertions(+), 116 deletions(-) create mode 100644 src/api/hooks/admin/useAdminAccounts.ts diff --git a/src/actions/admin.ts b/src/actions/admin.ts index 8236b7a01..3a06273cb 100644 --- a/src/actions/admin.ts +++ b/src/actions/admin.ts @@ -173,16 +173,12 @@ function fetchUsers(filters: string[] = [], page = 1, query?: string | null, pag try { const { data: accounts, ...response } = await api(getState).get(url || '/api/v1/admin/accounts', { params }); - const next = getLinks(response as AxiosResponse).refs.find(link => link.rel === 'next'); - - const count = next - ? page * pageSize + 1 - : (page - 1) * pageSize + accounts.length; + const next = getLinks(response as AxiosResponse).refs.find(link => link.rel === 'next')?.uri; dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account))); dispatch(fetchRelationships(accounts.map((account_1: APIEntity) => account_1.id))); - dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users: accounts, count, pageSize, filters, page, next: next?.uri || false }); - return { users: accounts, count, pageSize, next: next?.uri || false }; + dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, accounts, pageSize, filters, page, next }); + return { accounts, next }; } catch (error) { return dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }); } @@ -401,13 +397,13 @@ const fetchUserIndex = () => dispatch({ type: ADMIN_USER_INDEX_FETCH_REQUEST }); - dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize)) + dispatch(fetchUsers([...filters], page + 1, query, pageSize)) .then((data: any) => { if (data.error) { dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL }); } else { - const { users, count, next } = (data); - dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, users, count, next }); + const { accounts, next } = data; + dispatch({ type: ADMIN_USER_INDEX_FETCH_SUCCESS, accounts, next }); } }).catch(() => { dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL }); @@ -422,13 +418,13 @@ const expandUserIndex = () => dispatch({ type: ADMIN_USER_INDEX_EXPAND_REQUEST }); - dispatch(fetchUsers(filters.toJS() as string[], page + 1, query, pageSize, next)) - .then((data: any) => { - if (data.error) { + dispatch(fetchUsers([...filters], page + 1, query, pageSize, next)) + .then((data) => { + if ('error' in data) { dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL }); } else { - const { users, count, next } = (data); - dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, users, count, next }); + const { accounts, next } = data; + dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, accounts, next }); } }).catch(() => { dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL }); diff --git a/src/api/MastodonClient.ts b/src/api/MastodonClient.ts index 0f74abe17..42263d517 100644 --- a/src/api/MastodonClient.ts +++ b/src/api/MastodonClient.ts @@ -1,7 +1,7 @@ import { HTTPError } from './HTTPError'; interface Opts { - searchParams?: Record; + searchParams?: URLSearchParams | Record; headers?: Record; signal?: AbortSignal; } @@ -51,9 +51,11 @@ export class MastodonClient { const url = new URL(path, this.baseUrl); if (opts.searchParams) { - const params = Object - .entries(opts.searchParams) - .map(([key, value]) => ([key, String(value)])); + const params = opts.searchParams instanceof URLSearchParams + ? opts.searchParams + : Object + .entries(opts.searchParams) + .map(([key, value]) => ([key, String(value)])); url.search = new URLSearchParams(params).toString(); } diff --git a/src/api/hooks/admin/useAdminAccounts.ts b/src/api/hooks/admin/useAdminAccounts.ts new file mode 100644 index 000000000..cb329ced6 --- /dev/null +++ b/src/api/hooks/admin/useAdminAccounts.ts @@ -0,0 +1,29 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { useApi } from 'soapbox/hooks'; +import { adminAccountSchema } from 'soapbox/schemas/admin-account'; + +type Filter = 'local' | 'remote' | 'active' | 'pending' | 'disabled' | 'silenced' | 'suspended' | 'sensitized'; + +/** https://docs.joinmastodon.org/methods/admin/accounts/#v1 */ +export function useAdminAccounts(filters: Filter[] = [], limit?: number) { + const api = useApi(); + + const searchParams = new URLSearchParams(); + + for (const filter of filters) { + searchParams.append(filter, 'true'); + } + + if (typeof limit === 'number') { + searchParams.append('limit', limit.toString()); + } + + const { entities, ...rest } = useEntities( + [Entities.ACCOUNTS, searchParams.toString()], + () => api.get('/api/v1/admin/accounts', { searchParams }), + { schema: adminAccountSchema.transform(({ account }) => account) }, + ); + + return { accounts: entities, ...rest }; +} \ No newline at end of file diff --git a/src/features/admin/components/latest-accounts-panel.tsx b/src/features/admin/components/latest-accounts-panel.tsx index 0d3ce94c9..c179afaf3 100644 --- a/src/features/admin/components/latest-accounts-panel.tsx +++ b/src/features/admin/components/latest-accounts-panel.tsx @@ -1,16 +1,13 @@ -import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; -import { fetchUsers } from 'soapbox/actions/admin'; +import { useAdminAccounts } from 'soapbox/api/hooks/admin/useAdminAccounts'; +import Account from 'soapbox/components/account'; import { Widget } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account-container'; -import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; const messages = defineMessages({ title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' }, - expand: { id: 'admin.latest_accounts_panel.more', defaultMessage: 'Click to see {count, plural, one {# account} other {# accounts}}' }, }); interface ILatestAccountsPanel { @@ -20,18 +17,8 @@ interface ILatestAccountsPanel { const LatestAccountsPanel: React.FC = ({ limit = 5 }) => { const intl = useIntl(); const history = useHistory(); - const dispatch = useAppDispatch(); - const accountIds = useAppSelector>((state) => state.admin.get('latestUsers').take(limit)); - const [total, setTotal] = useState(accountIds.size); - - useEffect(() => { - dispatch(fetchUsers(['local', 'active'], 1, null, limit)) - .then((value) => { - setTotal((value as { count: number }).count); - }) - .catch(() => {}); - }, []); + const { accounts } = useAdminAccounts(['local', 'active'], limit); const handleAction = () => { history.push('/soapbox/admin/users'); @@ -41,10 +28,9 @@ const LatestAccountsPanel: React.FC = ({ limit = 5 }) => { - {accountIds.take(limit).map((account) => ( - + {accounts.slice(0, limit).map(account => ( + ))} ); diff --git a/src/features/admin/user-index.tsx b/src/features/admin/user-index.tsx index 06dcde6e6..93d418d96 100644 --- a/src/features/admin/user-index.tsx +++ b/src/features/admin/user-index.tsx @@ -1,12 +1,10 @@ -import debounce from 'lodash/debounce'; -import React, { useCallback, useEffect } from 'react'; +import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { expandUserIndex, fetchUserIndex, setUserIndexQuery } from 'soapbox/actions/admin'; +import { useAdminAccounts } from 'soapbox/api/hooks/admin/useAdminAccounts'; +import Account from 'soapbox/components/account'; import ScrollableList from 'soapbox/components/scrollable-list'; -import { Column, Input } from 'soapbox/components/ui'; -import AccountContainer from 'soapbox/containers/account-container'; -import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { Column } from 'soapbox/components/ui'; const messages = defineMessages({ heading: { id: 'column.admin.users', defaultMessage: 'Users' }, @@ -15,51 +13,30 @@ const messages = defineMessages({ }); const UserIndex: React.FC = () => { - const dispatch = useAppDispatch(); const intl = useIntl(); - const { isLoading, items, total, query, next } = useAppSelector((state) => state.admin_user_index); + const { accounts, isLoading, hasNextPage, fetchNextPage } = useAdminAccounts(['local']); const handleLoadMore = () => { - if (!isLoading) dispatch(expandUserIndex()); + if (!isLoading) { + fetchNextPage(); + } }; - const updateQuery = useCallback(debounce(() => { - dispatch(fetchUserIndex()); - }, 900, { leading: true }), []); - - const handleQueryChange: React.ChangeEventHandler = e => { - dispatch(setUserIndexQuery(e.target.value)); - updateQuery(); - }; - - useEffect(() => { - updateQuery(); - }, []); - - const hasMore = items.count() < total && !!next; - - const showLoading = isLoading && items.isEmpty(); - return ( - - {items.map(id => - , + {accounts.map((account) => + , )} diff --git a/src/reducers/admin-user-index.ts b/src/reducers/admin-user-index.ts index 51bb162ff..b16a6f690 100644 --- a/src/reducers/admin-user-index.ts +++ b/src/reducers/admin-user-index.ts @@ -1,5 +1,3 @@ -import { Set as ImmutableSet, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable'; - import { ADMIN_USER_INDEX_EXPAND_FAIL, ADMIN_USER_INDEX_EXPAND_REQUEST, @@ -11,57 +9,67 @@ import { } from 'soapbox/actions/admin'; import type { AnyAction } from 'redux'; -import type { APIEntity } from 'soapbox/types/entities'; -const ReducerRecord = ImmutableRecord({ - isLoading: false, - loaded: false, - items: ImmutableOrderedSet(), - filters: ImmutableSet(['local', 'active']), - total: Infinity, - pageSize: 50, - page: -1, - query: '', - next: null as string | null, -}); +interface State { + isLoading: boolean; + loaded: boolean; + items: Set; + filters: Set; + pageSize: number; + page: number; + query: string; + next: string | null; +} -type State = ReturnType; +function createState(): State { + return { + isLoading: false, + loaded: false, + items: new Set(), + filters: new Set(['local', 'active']), + pageSize: 50, + page: -1, + query: '', + next: null, + }; +} -export default function admin_user_index(state: State = ReducerRecord(), action: AnyAction): State { +export default function admin_user_index(state: State = createState(), action: AnyAction): State { switch (action.type) { case ADMIN_USER_INDEX_QUERY_SET: - return state.set('query', action.query); + return { ...state, query: action.query }; case ADMIN_USER_INDEX_FETCH_REQUEST: - return state - .set('isLoading', true) - .set('loaded', true) - .set('items', ImmutableOrderedSet()) - .set('total', action.count) - .set('page', 0) - .set('next', null); + return { + ...state, + isLoading: true, + loaded: true, + items: new Set(), + page: 0, + next: null, + }; case ADMIN_USER_INDEX_FETCH_SUCCESS: - return state - .set('isLoading', false) - .set('loaded', true) - .set('items', ImmutableOrderedSet(action.users.map((user: APIEntity) => user.id))) - .set('total', action.count) - .set('page', 1) - .set('next', action.next); + return { + ...state, + isLoading: false, + loaded: true, + items: new Set(action.accounts.map((account: { id: string }) => account.id)), + page: 1, + next: action.next, + }; case ADMIN_USER_INDEX_FETCH_FAIL: case ADMIN_USER_INDEX_EXPAND_FAIL: - return state - .set('isLoading', false); + return { ...state, isLoading: false }; case ADMIN_USER_INDEX_EXPAND_REQUEST: - return state - .set('isLoading', true); + return { ...state, isLoading: true }; case ADMIN_USER_INDEX_EXPAND_SUCCESS: - return state - .set('isLoading', false) - .set('loaded', true) - .set('items', state.items.union(action.users.map((user: APIEntity) => user.id))) - .set('total', action.count) - .set('page', 1) - .set('next', action.next); + return { + ...state, + isLoading: false, + loaded: true, + items: new Set([...state.items, ...action.accounts.map((account: { id: string }) => account.id)]), + page: 1, + next: action.next, + }; default: return state; }