kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'rm-pl-admin' into 'main'
Admin refactors See merge request soapbox-pub/soapbox!3166environments/review-main-yi2y9f/deployments/4927
commit
5157b7cd76
|
@ -3,7 +3,6 @@ import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||||
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
import { importFetchedAccount, importFetchedAccounts, importFetchedStatuses } from 'soapbox/actions/importer';
|
||||||
import { accountIdsToAccts } from 'soapbox/selectors';
|
import { accountIdsToAccts } from 'soapbox/selectors';
|
||||||
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
import { filterBadges, getTagDiff } from 'soapbox/utils/badges';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
|
@ -71,16 +70,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_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS';
|
||||||
const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL';
|
const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_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';
|
|
||||||
|
|
||||||
const ADMIN_USER_INDEX_FETCH_FAIL = 'ADMIN_USER_INDEX_FETCH_FAIL';
|
|
||||||
const ADMIN_USER_INDEX_FETCH_REQUEST = 'ADMIN_USER_INDEX_FETCH_REQUEST';
|
|
||||||
const ADMIN_USER_INDEX_FETCH_SUCCESS = 'ADMIN_USER_INDEX_FETCH_SUCCESS';
|
|
||||||
|
|
||||||
const ADMIN_USER_INDEX_QUERY_SET = 'ADMIN_USER_INDEX_QUERY_SET';
|
|
||||||
|
|
||||||
const fetchConfig = () =>
|
const fetchConfig = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
|
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
|
||||||
|
@ -118,100 +107,52 @@ const updateSoapboxConfig = (data: Record<string, any>) =>
|
||||||
return dispatch(updateConfig(params));
|
return dispatch(updateConfig(params));
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchMastodonReports = (params: Record<string, any>) =>
|
function fetchReports(params: Record<string, any> = {}) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
return async (dispatch: AppDispatch, getState: () => RootState): Promise<void> => {
|
||||||
api(getState)
|
dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params });
|
||||||
.get('/api/v1/admin/reports', { params })
|
|
||||||
.then(({ data: reports }) => {
|
try {
|
||||||
|
const { data: reports } = await api(getState).get('/api/v1/admin/reports', { params });
|
||||||
reports.forEach((report: APIEntity) => {
|
reports.forEach((report: APIEntity) => {
|
||||||
dispatch(importFetchedAccount(report.account?.account));
|
dispatch(importFetchedAccount(report.account?.account));
|
||||||
dispatch(importFetchedAccount(report.target_account?.account));
|
dispatch(importFetchedAccount(report.target_account?.account));
|
||||||
dispatch(importFetchedStatuses(report.statuses));
|
dispatch(importFetchedStatuses(report.statuses));
|
||||||
});
|
});
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
|
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
|
||||||
}).catch(error => {
|
} catch (error) {
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params });
|
dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params });
|
||||||
});
|
|
||||||
|
|
||||||
const fetchPleromaReports = (params: Record<string, any>) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
api(getState)
|
|
||||||
.get('/api/v1/pleroma/admin/reports', { params })
|
|
||||||
.then(({ data: { reports } }) => {
|
|
||||||
reports.forEach((report: APIEntity) => {
|
|
||||||
dispatch(importFetchedAccount(report.account));
|
|
||||||
dispatch(importFetchedAccount(report.actor));
|
|
||||||
dispatch(importFetchedStatuses(report.statuses));
|
|
||||||
});
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_FAIL, error, params });
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchReports = (params: Record<string, any> = {}) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_REPORTS_FETCH_REQUEST, params });
|
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
|
||||||
return dispatch(fetchMastodonReports(params));
|
|
||||||
} else {
|
|
||||||
const { resolved } = params;
|
|
||||||
|
|
||||||
return dispatch(fetchPleromaReports({
|
|
||||||
state: resolved === false ? 'open' : (resolved ? 'resolved' : null),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const patchMastodonReports = (reports: { id: string; state: string }[]) =>
|
function patchReports(ids: string[], reportState: string) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
return (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
Promise.all(reports.map(({ id, state }) => api(getState)
|
|
||||||
.post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`)
|
|
||||||
.then(() => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports });
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
|
|
||||||
const patchPleromaReports = (reports: { id: string; state: string }[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
api(getState)
|
|
||||||
.patch('/api/v1/pleroma/admin/reports', { reports })
|
|
||||||
.then(() => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports });
|
|
||||||
});
|
|
||||||
|
|
||||||
const patchReports = (ids: string[], reportState: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
const reports = ids.map(id => ({ id, state: reportState }));
|
const reports = ids.map(id => ({ id, state: reportState }));
|
||||||
|
|
||||||
dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports });
|
dispatch({ type: ADMIN_REPORTS_PATCH_REQUEST, reports });
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
return Promise.all(
|
||||||
return dispatch(patchMastodonReports(reports));
|
reports.map(async ({ id, state }) => {
|
||||||
} else {
|
try {
|
||||||
return dispatch(patchPleromaReports(reports));
|
await api(getState).post(`/api/v1/admin/reports/${id}/${state === 'resolved' ? 'reopen' : 'resolve'}`);
|
||||||
|
dispatch({ type: ADMIN_REPORTS_PATCH_SUCCESS, reports });
|
||||||
|
} catch (error) {
|
||||||
|
dispatch({ type: ADMIN_REPORTS_PATCH_FAIL, error, reports });
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const closeReports = (ids: string[]) =>
|
function closeReports(ids: string[]) {
|
||||||
patchReports(ids, 'closed');
|
return patchReports(ids, 'closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchUsers(filters: string[] = [], page = 1, query?: string | null, pageSize = 50, url?: string | null) {
|
||||||
|
return async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize });
|
||||||
|
|
||||||
const fetchMastodonUsers = (filters: string[], page: number, query: string | null | undefined, pageSize: number, next?: string | null) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const params: Record<string, any> = {
|
const params: Record<string, any> = {
|
||||||
username: query,
|
username: query,
|
||||||
};
|
};
|
||||||
|
@ -220,106 +161,49 @@ const fetchMastodonUsers = (filters: string[], page: number, query: string | nul
|
||||||
if (filters.includes('active')) params.active = true;
|
if (filters.includes('active')) params.active = true;
|
||||||
if (filters.includes('need_approval')) params.pending = true;
|
if (filters.includes('need_approval')) params.pending = true;
|
||||||
|
|
||||||
return api(getState)
|
try {
|
||||||
.get(next || '/api/v1/admin/accounts', { params })
|
const { data: accounts, ...response } = await api(getState).get(url || '/api/v1/admin/accounts', { params });
|
||||||
.then(({ data: accounts, ...response }) => {
|
const next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next')?.uri;
|
||||||
const next = getLinks(response as AxiosResponse<any, any>).refs.find(link => link.rel === 'next');
|
|
||||||
|
|
||||||
const count = next
|
|
||||||
? page * pageSize + 1
|
|
||||||
: (page - 1) * pageSize + accounts.length;
|
|
||||||
|
|
||||||
dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account)));
|
dispatch(importFetchedAccounts(accounts.map(({ account }: APIEntity) => account)));
|
||||||
dispatch(fetchRelationships(accounts.map((account: APIEntity) => account.id)));
|
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 });
|
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, accounts, pageSize, filters, page, next });
|
||||||
return { users: accounts, count, pageSize, next: next?.uri || false };
|
return { accounts, next };
|
||||||
}).catch(error =>
|
} catch (error) {
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }),
|
return dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize });
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchPleromaUsers = (filters: string[], page: number, query?: string | null, pageSize?: number) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const params: Record<string, any> = { filters: filters.join(), page, page_size: pageSize };
|
|
||||||
if (query) params.query = query;
|
|
||||||
|
|
||||||
return api(getState)
|
|
||||||
.get('/api/v1/pleroma/admin/users', { params })
|
|
||||||
.then(({ data: { users, count, page_size: pageSize } }) => {
|
|
||||||
dispatch(fetchRelationships(users.map((user: APIEntity) => user.id)));
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users, count, pageSize, filters, page });
|
|
||||||
return { users, count, pageSize };
|
|
||||||
}).catch(error =>
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, filters, page, pageSize }),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchUsers = (filters: string[] = [], page = 1, query?: string | null, pageSize = 50, next?: string | null) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_FETCH_REQUEST, filters, page, pageSize });
|
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
|
||||||
return dispatch(fetchMastodonUsers(filters, page, query, pageSize, next));
|
|
||||||
} else {
|
|
||||||
return dispatch(fetchPleromaUsers(filters, page, query, pageSize));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const revokeName = (accountId: string, reportId?: string) =>
|
function revokeName(accountId: string, reportId?: string) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
return (_dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
api(getState)
|
const params = {
|
||||||
.post(`/api/v1/admin/accounts/${accountId}/action`, {
|
|
||||||
type: 'revoke_name',
|
type: 'revoke_name',
|
||||||
report_id: reportId,
|
report_id: reportId,
|
||||||
});
|
};
|
||||||
|
|
||||||
const deactivateMastodonUsers = (accountIds: string[], reportId?: string) =>
|
return api(getState).post(`/api/v1/admin/accounts/${accountId}/action`, params);
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
};
|
||||||
Promise.all(accountIds.map(accountId => {
|
}
|
||||||
api(getState)
|
|
||||||
.post(`/api/v1/admin/accounts/${accountId}/action`, {
|
function deactivateUsers(accountIds: string[], reportId?: string) {
|
||||||
|
return (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
return Promise.all(
|
||||||
|
accountIds.map(async (accountId) => {
|
||||||
|
const params = {
|
||||||
type: 'disable',
|
type: 'disable',
|
||||||
report_id: reportId,
|
report_id: reportId,
|
||||||
})
|
};
|
||||||
.then(() => {
|
try {
|
||||||
|
await api(getState).post(`/api/v1/admin/accounts/${accountId}/action`, params);
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, accountIds: [accountId] });
|
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, accountIds: [accountId] });
|
||||||
}).catch(error => {
|
} catch (error) {
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds: [accountId] });
|
dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds: [accountId] });
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
const deactivatePleromaUsers = (accountIds: string[]) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = accountIdsToAccts(getState(), accountIds);
|
|
||||||
return api(getState)
|
|
||||||
.patch('/api/v1/pleroma/admin/users/deactivate', { nicknames })
|
|
||||||
.then(({ data: { users } }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_SUCCESS, users, accountIds });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_FAIL, error, accountIds });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deactivateUsers = (accountIds: string[], reportId?: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_DEACTIVATE_REQUEST, accountIds });
|
|
||||||
|
|
||||||
if (features.mastodonAdmin) {
|
|
||||||
return dispatch(deactivateMastodonUsers(accountIds, reportId));
|
|
||||||
} else {
|
|
||||||
return dispatch(deactivatePleromaUsers(accountIds));
|
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const deleteUser = (accountId: string) =>
|
const deleteUser = (accountId: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
@ -334,69 +218,31 @@ const deleteUser = (accountId: string) =>
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveMastodonUser = (accountId: string) =>
|
function approveUser(accountId: string) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
return async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
api(getState)
|
|
||||||
.post(`/api/v1/admin/accounts/${accountId}/approve`)
|
|
||||||
.then(({ data: user }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, user, accountId });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountId });
|
|
||||||
});
|
|
||||||
|
|
||||||
const approvePleromaUser = (accountId: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const nicknames = accountIdsToAccts(getState(), [accountId]);
|
|
||||||
return api(getState)
|
|
||||||
.patch('/api/v1/pleroma/admin/users/approve', { nicknames })
|
|
||||||
.then(({ data: { users } }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, user: users[0], accountId });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountId });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const rejectMastodonUser = (accountId: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
|
||||||
api(getState)
|
|
||||||
.post(`/api/v1/admin/accounts/${accountId}/reject`)
|
|
||||||
.then(({ data: user }) => {
|
|
||||||
dispatch({ type: ADMIN_USERS_REJECT_SUCCESS, user, accountId });
|
|
||||||
}).catch(error => {
|
|
||||||
dispatch({ type: ADMIN_USERS_REJECT_FAIL, error, accountId });
|
|
||||||
});
|
|
||||||
|
|
||||||
const approveUser = (accountId: string) =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountId });
|
dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, accountId });
|
||||||
|
try {
|
||||||
if (features.mastodonAdmin) {
|
const { data: user } = await api(getState)
|
||||||
return dispatch(approveMastodonUser(accountId));
|
.post(`/api/v1/admin/accounts/${accountId}/approve`);
|
||||||
} else {
|
dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, user, accountId });
|
||||||
return dispatch(approvePleromaUser(accountId));
|
} catch (error) {
|
||||||
|
dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, accountId });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const rejectUser = (accountId: string) =>
|
function rejectUser(accountId: string) {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
return async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
const instance = state.instance;
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USERS_REJECT_REQUEST, accountId });
|
dispatch({ type: ADMIN_USERS_REJECT_REQUEST, accountId });
|
||||||
|
try {
|
||||||
if (features.mastodonAdmin) {
|
const { data: user } = await api(getState)
|
||||||
return dispatch(rejectMastodonUser(accountId));
|
.post(`/api/v1/admin/accounts/${accountId}/reject`);
|
||||||
} else {
|
dispatch({ type: ADMIN_USERS_REJECT_SUCCESS, user, accountId });
|
||||||
return dispatch(deleteUser(accountId));
|
} catch (error) {
|
||||||
|
dispatch({ type: ADMIN_USERS_REJECT_FAIL, error, accountId });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const deleteStatus = (id: string) =>
|
const deleteStatus = (id: string) =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
@ -531,51 +377,6 @@ const setRole = (accountId: string, role: 'user' | 'moderator' | 'admin') =>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setUserIndexQuery = (query: string) => ({ type: ADMIN_USER_INDEX_QUERY_SET, query });
|
|
||||||
|
|
||||||
const fetchUserIndex = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const { filters, page, query, pageSize, isLoading } = getState().admin_user_index;
|
|
||||||
|
|
||||||
if (isLoading) return;
|
|
||||||
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_REQUEST });
|
|
||||||
|
|
||||||
dispatch(fetchUsers(filters.toJS() as string[], 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 });
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_FETCH_FAIL });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const expandUserIndex = () =>
|
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
const { filters, page, query, pageSize, isLoading, next, loaded } = getState().admin_user_index;
|
|
||||||
|
|
||||||
if (!loaded || isLoading) return;
|
|
||||||
|
|
||||||
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({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
|
||||||
} else {
|
|
||||||
const { users, count, next } = (data);
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_SUCCESS, users, count, next });
|
|
||||||
}
|
|
||||||
}).catch(() => {
|
|
||||||
dispatch({ type: ADMIN_USER_INDEX_EXPAND_FAIL });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ADMIN_CONFIG_FETCH_REQUEST,
|
ADMIN_CONFIG_FETCH_REQUEST,
|
||||||
ADMIN_CONFIG_FETCH_SUCCESS,
|
ADMIN_CONFIG_FETCH_SUCCESS,
|
||||||
|
@ -622,13 +423,6 @@ export {
|
||||||
ADMIN_REMOVE_PERMISSION_GROUP_REQUEST,
|
ADMIN_REMOVE_PERMISSION_GROUP_REQUEST,
|
||||||
ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS,
|
ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS,
|
||||||
ADMIN_REMOVE_PERMISSION_GROUP_FAIL,
|
ADMIN_REMOVE_PERMISSION_GROUP_FAIL,
|
||||||
ADMIN_USER_INDEX_EXPAND_FAIL,
|
|
||||||
ADMIN_USER_INDEX_EXPAND_REQUEST,
|
|
||||||
ADMIN_USER_INDEX_EXPAND_SUCCESS,
|
|
||||||
ADMIN_USER_INDEX_FETCH_FAIL,
|
|
||||||
ADMIN_USER_INDEX_FETCH_REQUEST,
|
|
||||||
ADMIN_USER_INDEX_FETCH_SUCCESS,
|
|
||||||
ADMIN_USER_INDEX_QUERY_SET,
|
|
||||||
fetchConfig,
|
fetchConfig,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
updateSoapboxConfig,
|
updateSoapboxConfig,
|
||||||
|
@ -652,7 +446,4 @@ export {
|
||||||
promoteToModerator,
|
promoteToModerator,
|
||||||
demoteToUser,
|
demoteToUser,
|
||||||
setRole,
|
setRole,
|
||||||
setUserIndexQuery,
|
|
||||||
fetchUserIndex,
|
|
||||||
expandUserIndex,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { HTTPError } from './HTTPError';
|
import { HTTPError } from './HTTPError';
|
||||||
|
|
||||||
interface Opts {
|
interface Opts {
|
||||||
searchParams?: Record<string, string | number | boolean>;
|
searchParams?: URLSearchParams | Record<string, string | number | boolean>;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,9 @@ export class MastodonClient {
|
||||||
const url = new URL(path, this.baseUrl);
|
const url = new URL(path, this.baseUrl);
|
||||||
|
|
||||||
if (opts.searchParams) {
|
if (opts.searchParams) {
|
||||||
const params = Object
|
const params = opts.searchParams instanceof URLSearchParams
|
||||||
|
? opts.searchParams
|
||||||
|
: Object
|
||||||
.entries(opts.searchParams)
|
.entries(opts.searchParams)
|
||||||
.map(([key, value]) => ([key, String(value)]));
|
.map(([key, value]) => ([key, String(value)]));
|
||||||
|
|
||||||
|
|
|
@ -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 };
|
||||||
|
}
|
|
@ -1,16 +1,13 @@
|
||||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
import React from 'react';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
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 { Widget } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
|
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 {
|
interface ILatestAccountsPanel {
|
||||||
|
@ -20,18 +17,8 @@ interface ILatestAccountsPanel {
|
||||||
const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const accountIds = useAppSelector<ImmutableOrderedSet<string>>((state) => state.admin.get('latestUsers').take(limit));
|
|
||||||
|
|
||||||
const [total, setTotal] = useState(accountIds.size);
|
const { accounts } = useAdminAccounts(['local', 'active'], limit);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetchUsers(['local', 'active'], 1, null, limit))
|
|
||||||
.then((value) => {
|
|
||||||
setTotal((value as { count: number }).count);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAction = () => {
|
const handleAction = () => {
|
||||||
history.push('/soapbox/admin/users');
|
history.push('/soapbox/admin/users');
|
||||||
|
@ -41,10 +28,9 @@ const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
||||||
<Widget
|
<Widget
|
||||||
title={intl.formatMessage(messages.title)}
|
title={intl.formatMessage(messages.title)}
|
||||||
onActionClick={handleAction}
|
onActionClick={handleAction}
|
||||||
actionTitle={intl.formatMessage(messages.expand, { count: total })}
|
|
||||||
>
|
>
|
||||||
{accountIds.take(limit).map((account) => (
|
{accounts.slice(0, limit).map(account => (
|
||||||
<AccountContainer key={account} id={account} withRelationship={false} withDate />
|
<Account key={account.id} account={account} withRelationship={false} withDate />
|
||||||
))}
|
))}
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import debounce from 'lodash/debounce';
|
import React from 'react';
|
||||||
import React, { useCallback, useEffect } from 'react';
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
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 ScrollableList from 'soapbox/components/scrollable-list';
|
||||||
import { Column, Input } from 'soapbox/components/ui';
|
import { Column } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.admin.users', defaultMessage: 'Users' },
|
heading: { id: 'column.admin.users', defaultMessage: 'Users' },
|
||||||
|
@ -15,51 +13,30 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const UserIndex: React.FC = () => {
|
const UserIndex: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const { isLoading, items, total, query, next } = useAppSelector((state) => state.admin_user_index);
|
const { accounts, isLoading, hasNextPage, fetchNextPage } = useAdminAccounts(['local']);
|
||||||
|
|
||||||
const handleLoadMore = () => {
|
const handleLoadMore = () => {
|
||||||
if (!isLoading) dispatch(expandUserIndex());
|
if (!isLoading) {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateQuery = useCallback(debounce(() => {
|
|
||||||
dispatch(fetchUserIndex());
|
|
||||||
}, 900, { leading: true }), []);
|
|
||||||
|
|
||||||
const handleQueryChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
|
||||||
dispatch(setUserIndexQuery(e.target.value));
|
|
||||||
updateQuery();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateQuery();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const hasMore = items.count() < total && !!next;
|
|
||||||
|
|
||||||
const showLoading = isLoading && items.isEmpty();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.heading)}>
|
<Column label={intl.formatMessage(messages.heading)}>
|
||||||
<Input
|
|
||||||
value={query}
|
|
||||||
onChange={handleQueryChange}
|
|
||||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
|
||||||
/>
|
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='user-index'
|
scrollKey='user-index'
|
||||||
hasMore={hasMore}
|
hasMore={hasNextPage}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
showLoading={showLoading}
|
showLoading={isLoading}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
emptyMessage={intl.formatMessage(messages.empty)}
|
emptyMessage={intl.formatMessage(messages.empty)}
|
||||||
className='mt-4'
|
className='mt-4'
|
||||||
itemClassName='pb-4'
|
itemClassName='pb-4'
|
||||||
>
|
>
|
||||||
{items.map(id =>
|
{accounts.map((account) =>
|
||||||
<AccountContainer key={id} id={id} withDate />,
|
<Account key={account.id} account={account} withDate />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -145,7 +145,6 @@
|
||||||
"admin.edit_rule.fields.text_placeholder": "Instance rule text",
|
"admin.edit_rule.fields.text_placeholder": "Instance rule text",
|
||||||
"admin.edit_rule.save": "Save",
|
"admin.edit_rule.save": "Save",
|
||||||
"admin.edit_rule.updated": "Rule edited",
|
"admin.edit_rule.updated": "Rule edited",
|
||||||
"admin.latest_accounts_panel.more": "Click to see {count, plural, one {# account} other {# accounts}}",
|
|
||||||
"admin.latest_accounts_panel.title": "Latest Accounts",
|
"admin.latest_accounts_panel.title": "Latest Accounts",
|
||||||
"admin.moderation_log.empty_message": "You have not performed any moderation actions yet. When you do, a history will be shown here.",
|
"admin.moderation_log.empty_message": "You have not performed any moderation actions yet. When you do, a history will be shown here.",
|
||||||
"admin.relays.add.fail": "Failed to follow the instance relay",
|
"admin.relays.add.fail": "Failed to follow the instance relay",
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
import { Set as ImmutableSet, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ADMIN_USER_INDEX_EXPAND_FAIL,
|
|
||||||
ADMIN_USER_INDEX_EXPAND_REQUEST,
|
|
||||||
ADMIN_USER_INDEX_EXPAND_SUCCESS,
|
|
||||||
ADMIN_USER_INDEX_FETCH_FAIL,
|
|
||||||
ADMIN_USER_INDEX_FETCH_REQUEST,
|
|
||||||
ADMIN_USER_INDEX_FETCH_SUCCESS,
|
|
||||||
ADMIN_USER_INDEX_QUERY_SET,
|
|
||||||
} 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<string>(),
|
|
||||||
filters: ImmutableSet(['local', 'active']),
|
|
||||||
total: Infinity,
|
|
||||||
pageSize: 50,
|
|
||||||
page: -1,
|
|
||||||
query: '',
|
|
||||||
next: null as string | null,
|
|
||||||
});
|
|
||||||
|
|
||||||
type State = ReturnType<typeof ReducerRecord>;
|
|
||||||
|
|
||||||
export default function admin_user_index(state: State = ReducerRecord(), action: AnyAction): State {
|
|
||||||
switch (action.type) {
|
|
||||||
case ADMIN_USER_INDEX_QUERY_SET:
|
|
||||||
return state.set('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);
|
|
||||||
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);
|
|
||||||
case ADMIN_USER_INDEX_FETCH_FAIL:
|
|
||||||
case ADMIN_USER_INDEX_EXPAND_FAIL:
|
|
||||||
return state
|
|
||||||
.set('isLoading', false);
|
|
||||||
case ADMIN_USER_INDEX_EXPAND_REQUEST:
|
|
||||||
return state
|
|
||||||
.set('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);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ import entities from 'soapbox/entity-store/reducer';
|
||||||
|
|
||||||
import accounts_meta from './accounts-meta';
|
import accounts_meta from './accounts-meta';
|
||||||
import admin from './admin';
|
import admin from './admin';
|
||||||
import admin_user_index from './admin-user-index';
|
|
||||||
import aliases from './aliases';
|
import aliases from './aliases';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import backups from './backups';
|
import backups from './backups';
|
||||||
|
@ -60,7 +59,6 @@ import user_lists from './user-lists';
|
||||||
const reducers = {
|
const reducers = {
|
||||||
accounts_meta,
|
accounts_meta,
|
||||||
admin,
|
admin,
|
||||||
admin_user_index,
|
|
||||||
aliases,
|
aliases,
|
||||||
auth,
|
auth,
|
||||||
backups,
|
backups,
|
||||||
|
|
Ładowanie…
Reference in New Issue