diff --git a/app/soapbox/actions/admin.ts b/app/soapbox/actions/admin.ts index d17716be9..613615140 100644 --- a/app/soapbox/actions/admin.ts +++ b/app/soapbox/actions/admin.ts @@ -2,7 +2,7 @@ import { defineMessages } from 'react-intl'; import { fetchRelationships } from 'soapbox/actions/accounts'; import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer'; -import { selectAccount } from 'soapbox/selectors'; +import { accountIdsToAccts } from 'soapbox/selectors'; import toast from 'soapbox/toast'; import { filterBadges, getTagDiff } from 'soapbox/utils/badges'; import { getFeatures } from 'soapbox/utils/features'; @@ -75,14 +75,6 @@ const ADMIN_REMOVE_PERMISSION_GROUP_REQUEST = 'ADMIN_REMOVE_PERMISSION_GROUP_REQ const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS'; const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL'; -const ADMIN_USERS_SUGGEST_REQUEST = 'ADMIN_USERS_SUGGEST_REQUEST'; -const ADMIN_USERS_SUGGEST_SUCCESS = 'ADMIN_USERS_SUGGEST_SUCCESS'; -const ADMIN_USERS_SUGGEST_FAIL = 'ADMIN_USERS_SUGGEST_FAIL'; - -const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST'; -const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS'; -const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL'; - const ADMIN_USER_INDEX_EXPAND_FAIL = 'ADMIN_USER_INDEX_EXPAND_FAIL'; const ADMIN_USER_INDEX_EXPAND_REQUEST = 'ADMIN_USER_INDEX_EXPAND_REQUEST'; const ADMIN_USER_INDEX_EXPAND_SUCCESS = 'ADMIN_USER_INDEX_EXPAND_SUCCESS'; @@ -122,8 +114,6 @@ const messages = defineMessages({ announcementUpdateSuccess: { id: 'admin.edit_announcement.updated', defaultMessage: 'Announcement edited' }, }); -const nicknamesFromIds = (getState: () => RootState, ids: string[]) => ids.map((id) => selectAccount(getState(), id)!.acct); - const fetchConfig = () => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST }); @@ -330,7 +320,7 @@ const deactivateMastodonUsers = (accountIds: string[], reportId?: string) => const deactivatePleromaUsers = (accountIds: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); return api(getState) .patch('/api/v1/pleroma/admin/users/deactivate', { nicknames }) .then(({ data: { users } }) => { @@ -358,7 +348,7 @@ const deactivateUsers = (accountIds: string[], reportId?: string) => const deleteUsers = (accountIds: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); dispatch({ type: ADMIN_USERS_DELETE_REQUEST, accountIds }); return api(getState) .delete('/api/v1/pleroma/admin/users', { data: { nicknames } }) @@ -383,7 +373,7 @@ const approveMastodonUsers = (accountIds: string[]) => const approvePleromaUsers = (accountIds: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); return api(getState) .patch('/api/v1/pleroma/admin/users/approve', { nicknames }) .then(({ data: { users } }) => { @@ -448,7 +438,7 @@ const fetchModerationLog = (params?: Record) => const tagUsers = (accountIds: string[], tags: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); dispatch({ type: ADMIN_USERS_TAG_REQUEST, accountIds, tags }); return api(getState) .put('/api/v1/pleroma/admin/users/tag', { nicknames, tags }) @@ -461,7 +451,7 @@ const tagUsers = (accountIds: string[], tags: string[]) => const untagUsers = (accountIds: string[], tags: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); // Legacy: allow removing legacy 'donor' tags. if (tags.includes('badge:donor')) { @@ -496,17 +486,9 @@ const setBadges = (accountId: string, oldTags: string[], newTags: string[]) => return dispatch(setTags(accountId, oldBadges, newBadges)); }; -const verifyUser = (accountId: string) => - (dispatch: AppDispatch) => - dispatch(tagUsers([accountId], ['verified'])); - -const unverifyUser = (accountId: string) => - (dispatch: AppDispatch) => - dispatch(untagUsers([accountId], ['verified'])); - const addPermission = (accountIds: string[], permissionGroup: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup }); return api(getState) .post(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames }) @@ -519,7 +501,7 @@ const addPermission = (accountIds: string[], permissionGroup: string) => const removePermission = (accountIds: string[], permissionGroup: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); + const nicknames = accountIdsToAccts(getState(), accountIds); dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup }); return api(getState) .delete(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { data: { nicknames } }) @@ -563,32 +545,6 @@ const setRole = (accountId: string, role: 'user' | 'moderator' | 'admin') => } }; -const suggestUsers = (accountIds: string[]) => - (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); - dispatch({ type: ADMIN_USERS_SUGGEST_REQUEST, accountIds }); - return api(getState) - .patch('/api/v1/pleroma/admin/users/suggest', { nicknames }) - .then(({ data: { users } }) => { - dispatch({ type: ADMIN_USERS_SUGGEST_SUCCESS, users, accountIds }); - }).catch(error => { - dispatch({ type: ADMIN_USERS_SUGGEST_FAIL, error, accountIds }); - }); - }; - -const unsuggestUsers = (accountIds: string[]) => - (dispatch: AppDispatch, getState: () => RootState) => { - const nicknames = nicknamesFromIds(getState, accountIds); - dispatch({ type: ADMIN_USERS_UNSUGGEST_REQUEST, accountIds }); - return api(getState) - .patch('/api/v1/pleroma/admin/users/unsuggest', { nicknames }) - .then(({ data: { users } }) => { - dispatch({ type: ADMIN_USERS_UNSUGGEST_SUCCESS, users, accountIds }); - }).catch(error => { - dispatch({ type: ADMIN_USERS_UNSUGGEST_FAIL, error, accountIds }); - }); - }; - const setUserIndexQuery = (query: string) => ({ type: ADMIN_USER_INDEX_QUERY_SET, query }); const fetchUserIndex = () => @@ -766,12 +722,6 @@ export { ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS, ADMIN_REMOVE_PERMISSION_GROUP_FAIL, - ADMIN_USERS_SUGGEST_REQUEST, - ADMIN_USERS_SUGGEST_SUCCESS, - ADMIN_USERS_SUGGEST_FAIL, - ADMIN_USERS_UNSUGGEST_REQUEST, - ADMIN_USERS_UNSUGGEST_SUCCESS, - ADMIN_USERS_UNSUGGEST_FAIL, ADMIN_USER_INDEX_EXPAND_FAIL, ADMIN_USER_INDEX_EXPAND_REQUEST, ADMIN_USER_INDEX_EXPAND_SUCCESS, @@ -812,16 +762,12 @@ export { untagUsers, setTags, setBadges, - verifyUser, - unverifyUser, addPermission, removePermission, promoteToAdmin, promoteToModerator, demoteToUser, setRole, - suggestUsers, - unsuggestUsers, setUserIndexQuery, fetchUserIndex, expandUserIndex, diff --git a/app/soapbox/api/hooks/admin/index.ts b/app/soapbox/api/hooks/admin/index.ts new file mode 100644 index 000000000..ef4dc082d --- /dev/null +++ b/app/soapbox/api/hooks/admin/index.ts @@ -0,0 +1,2 @@ +export { useSuggest } from './useSuggest'; +export { useVerify } from './useVerify'; \ No newline at end of file diff --git a/app/soapbox/api/hooks/admin/useSuggest.ts b/app/soapbox/api/hooks/admin/useSuggest.ts new file mode 100644 index 000000000..b20bc5308 --- /dev/null +++ b/app/soapbox/api/hooks/admin/useSuggest.ts @@ -0,0 +1,58 @@ +import { useTransaction } from 'soapbox/entity-store/hooks'; +import { EntityCallbacks } from 'soapbox/entity-store/hooks/types'; +import { useApi, useGetState } from 'soapbox/hooks'; +import { accountIdsToAccts } from 'soapbox/selectors'; + +import type { Account } from 'soapbox/schemas'; + +function useSuggest() { + const api = useApi(); + const getState = useGetState(); + const { transaction } = useTransaction(); + + function suggestEffect(accountIds: string[], suggested: boolean) { + const updater = (account: Account): Account => { + if (account.pleroma) { + account.pleroma.is_suggested = suggested; + } + return account; + }; + + transaction({ + Accounts: accountIds.reduce Account>>( + (result, id) => ({ ...result, [id]: updater }), + {}), + }); + } + + async function suggest(accountIds: string[], callbacks?: EntityCallbacks) { + const accts = accountIdsToAccts(getState(), accountIds); + suggestEffect(accountIds, true); + try { + await api.patch('/api/v1/pleroma/admin/users/suggest', { nicknames: accts }); + callbacks?.onSuccess?.(); + } catch (e) { + callbacks?.onError?.(e); + suggestEffect(accountIds, false); + } + } + + async function unsuggest(accountIds: string[], callbacks?: EntityCallbacks) { + const accts = accountIdsToAccts(getState(), accountIds); + suggestEffect(accountIds, false); + try { + await api.patch('/api/v1/pleroma/admin/users/unsuggest', { nicknames: accts }); + callbacks?.onSuccess?.(); + } catch (e) { + callbacks?.onError?.(e); + suggestEffect(accountIds, true); + } + } + + return { + suggest, + unsuggest, + }; +} + +export { useSuggest }; \ No newline at end of file diff --git a/app/soapbox/api/hooks/admin/useVerify.ts b/app/soapbox/api/hooks/admin/useVerify.ts new file mode 100644 index 000000000..090e1bc43 --- /dev/null +++ b/app/soapbox/api/hooks/admin/useVerify.ts @@ -0,0 +1,63 @@ +import { useTransaction } from 'soapbox/entity-store/hooks'; +import { EntityCallbacks } from 'soapbox/entity-store/hooks/types'; +import { useApi, useGetState } from 'soapbox/hooks'; +import { accountIdsToAccts } from 'soapbox/selectors'; + +import type { Account } from 'soapbox/schemas'; + +function useVerify() { + const api = useApi(); + const getState = useGetState(); + const { transaction } = useTransaction(); + + function verifyEffect(accountIds: string[], verified: boolean) { + const updater = (account: Account): Account => { + if (account.pleroma) { + const tags = account.pleroma.tags.filter((tag) => tag !== 'verified'); + if (verified) { + tags.push('verified'); + } + account.pleroma.tags = tags; + } + account.verified = verified; + return account; + }; + + transaction({ + Accounts: accountIds.reduce Account>>( + (result, id) => ({ ...result, [id]: updater }), + {}), + }); + } + + async function verify(accountIds: string[], callbacks?: EntityCallbacks) { + const accts = accountIdsToAccts(getState(), accountIds); + verifyEffect(accountIds, true); + try { + await api.put('/api/v1/pleroma/admin/users/tag', { nicknames: accts, tags: ['verified'] }); + callbacks?.onSuccess?.(); + } catch (e) { + callbacks?.onError?.(e); + verifyEffect(accountIds, false); + } + } + + async function unverify(accountIds: string[], callbacks?: EntityCallbacks) { + const accts = accountIdsToAccts(getState(), accountIds); + verifyEffect(accountIds, false); + try { + await api.delete('/api/v1/pleroma/admin/users/tag', { data: { nicknames: accts, tags: ['verified'] } }); + callbacks?.onSuccess?.(); + } catch (e) { + callbacks?.onError?.(e); + verifyEffect(accountIds, true); + } + } + + return { + verify, + unverify, + }; +} + +export { useVerify }; \ No newline at end of file diff --git a/app/soapbox/entity-store/hooks/useEntityLookup.ts b/app/soapbox/entity-store/hooks/useEntityLookup.ts index 29cf85244..1a8a11eda 100644 --- a/app/soapbox/entity-store/hooks/useEntityLookup.ts +++ b/app/soapbox/entity-store/hooks/useEntityLookup.ts @@ -3,9 +3,9 @@ import { useEffect, useState } from 'react'; import { z } from 'zod'; import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks'; -import { type RootState } from 'soapbox/store'; import { importEntities } from '../actions'; +import { findEntity } from '../selectors'; import { Entity } from '../types'; import { EntityFn } from './types'; @@ -58,16 +58,4 @@ function useEntityLookup( }; } -function findEntity( - state: RootState, - entityType: string, - lookupFn: LookupFn, -) { - const cache = state.entities[entityType]; - - if (cache) { - return (Object.values(cache.store) as TEntity[]).find(lookupFn); - } -} - export { useEntityLookup }; \ No newline at end of file diff --git a/app/soapbox/entity-store/selectors.ts b/app/soapbox/entity-store/selectors.ts index ac5f3feff..d1017c5b6 100644 --- a/app/soapbox/entity-store/selectors.ts +++ b/app/soapbox/entity-store/selectors.ts @@ -44,10 +44,24 @@ function selectEntities(state: RootState, path: Entities ) : []; } +/** Find an entity using a finder function. */ +function findEntity( + state: RootState, + entityType: string, + lookupFn: (entity: TEntity) => boolean, +) { + const cache = state.entities[entityType]; + + if (cache) { + return (Object.values(cache.store) as TEntity[]).find(lookupFn); + } +} + export { selectCache, selectList, selectListState, useListState, selectEntities, + findEntity, }; \ No newline at end of file diff --git a/app/soapbox/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx b/app/soapbox/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx index 016759205..2d5958f2d 100644 --- a/app/soapbox/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx +++ b/app/soapbox/features/ui/components/modals/account-moderation-modal/account-moderation-modal.tsx @@ -1,15 +1,10 @@ import React, { ChangeEventHandler, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { - verifyUser, - unverifyUser, - suggestUsers, - unsuggestUsers, - setBadges as saveBadges, -} from 'soapbox/actions/admin'; +import { setBadges as saveBadges } from 'soapbox/actions/admin'; import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation'; import { useAccount } from 'soapbox/api/hooks'; +import { useSuggest, useVerify } from 'soapbox/api/hooks/admin'; import Account from 'soapbox/components/account'; import List, { ListItem } from 'soapbox/components/list'; import MissingIndicator from 'soapbox/components/missing-indicator'; @@ -45,6 +40,8 @@ const AccountModerationModal: React.FC = ({ onClose, ac const intl = useIntl(); const dispatch = useAppDispatch(); + const { suggest, unsuggest } = useSuggest(); + const { verify, unverify } = useVerify(); const { account: ownAccount } = useOwnAccount(); const features = useFeatures(); const { account } = useAccount(accountId); @@ -70,22 +67,22 @@ const AccountModerationModal: React.FC = ({ onClose, ac const { checked } = e.target; const message = checked ? messages.userVerified : messages.userUnverified; - const action = checked ? verifyUser : unverifyUser; + const action = checked ? verify : unverify; - dispatch(action(account.id)) - .then(() => toast.success(intl.formatMessage(message, { acct: account.acct }))) - .catch(() => {}); + action([account.id], { + onSuccess: () => toast.success(intl.formatMessage(message, { acct: account.acct })), + }); }; const handleSuggestedChange: ChangeEventHandler = (e) => { const { checked } = e.target; const message = checked ? messages.userSuggested : messages.userUnsuggested; - const action = checked ? suggestUsers : unsuggestUsers; + const action = checked ? suggest : unsuggest; - dispatch(action([account.id])) - .then(() => toast.success(intl.formatMessage(message, { acct: account.acct }))) - .catch(() => {}); + action([account.id], { + onSuccess: () => toast.success(intl.formatMessage(message, { acct: account.acct })), + }); }; const handleDeactivate = () => { diff --git a/app/soapbox/reducers/__tests__/auth.test.ts b/app/soapbox/reducers/__tests__/auth.test.ts index 9f5726f77..d461b3ddd 100644 --- a/app/soapbox/reducers/__tests__/auth.test.ts +++ b/app/soapbox/reducers/__tests__/auth.test.ts @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { Map as ImmutableMap } from 'immutable'; import { AUTH_APP_CREATED, @@ -300,7 +300,7 @@ describe('auth reducer', () => { it('sets the value of `me`', () => { const action = { type: SWITCH_ACCOUNT, - account: fromJS({ url: 'https://gleasonator.com/users/benis' }), + account: { url: 'https://gleasonator.com/users/benis' }, }; const result = reducer(undefined, action); diff --git a/app/soapbox/reducers/accounts.ts b/app/soapbox/reducers/accounts.ts index 39176e731..5587e0a73 100644 --- a/app/soapbox/reducers/accounts.ts +++ b/app/soapbox/reducers/accounts.ts @@ -23,10 +23,6 @@ import { ADMIN_USERS_DELETE_FAIL, ADMIN_USERS_DEACTIVATE_REQUEST, ADMIN_USERS_DEACTIVATE_FAIL, - ADMIN_USERS_SUGGEST_REQUEST, - ADMIN_USERS_SUGGEST_FAIL, - ADMIN_USERS_UNSUGGEST_REQUEST, - ADMIN_USERS_UNSUGGEST_FAIL, } from 'soapbox/actions/admin'; import { CHATS_FETCH_SUCCESS, CHATS_EXPAND_SUCCESS, CHAT_FETCH_SUCCESS } from 'soapbox/actions/chats'; import { @@ -234,14 +230,6 @@ const importAdminUsers = (state: State, adminUsers: Array>): }); }; -const setSuggested = (state: State, accountIds: Array, isSuggested: boolean): State => { - return state.withMutations(state => { - accountIds.forEach(id => { - state.setIn([id, 'pleroma', 'is_suggested'], isSuggested); - }); - }); -}; - export default function accounts(state: State = initialState, action: AnyAction): State { switch (action.type) { case ACCOUNT_IMPORT: @@ -280,12 +268,6 @@ export default function accounts(state: State = initialState, action: AnyAction) return setActive(state, action.accountIds, true); case ADMIN_USERS_FETCH_SUCCESS: return importAdminUsers(state, action.users); - case ADMIN_USERS_SUGGEST_REQUEST: - case ADMIN_USERS_UNSUGGEST_FAIL: - return setSuggested(state, action.accountIds, true); - case ADMIN_USERS_UNSUGGEST_REQUEST: - case ADMIN_USERS_SUGGEST_FAIL: - return setSuggested(state, action.accountIds, false); default: return state; } diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts index 4bbf90a42..1f844bc27 100644 --- a/app/soapbox/selectors/index.ts +++ b/app/soapbox/selectors/index.ts @@ -33,6 +33,8 @@ export function selectOwnAccount(state: RootState) { } } +export const accountIdsToAccts = (state: RootState, ids: string[]) => ids.map((id) => selectAccount(state, id)!.acct); + const getAccountBase = (state: RootState, id: string) => state.entities[Entities.ACCOUNTS]?.store[id] as Account | undefined; const getAccountRelationship = (state: RootState, id: string) => state.relationships.get(id);