kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'dashboard-reports' into dashboard
commit
290b139fbb
|
@ -122,6 +122,8 @@ const fetchMastodonReports = (params: Record<string, any>) =>
|
|||
reports.forEach((report: APIEntity) => {
|
||||
dispatch(importFetchedAccount(report.account?.account));
|
||||
dispatch(importFetchedAccount(report.target_account?.account));
|
||||
dispatch(importFetchedAccount(report.assigned_account?.account));
|
||||
dispatch(importFetchedAccount(report.action_taken_by_account?.account));
|
||||
dispatch(importFetchedStatuses(report.statuses));
|
||||
});
|
||||
dispatch({ type: ADMIN_REPORTS_FETCH_SUCCESS, reports, params });
|
||||
|
@ -539,7 +541,6 @@ const expandUserIndex = () =>
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
export {
|
||||
ADMIN_CONFIG_FETCH_REQUEST,
|
||||
ADMIN_CONFIG_FETCH_SUCCESS,
|
||||
|
|
|
@ -68,7 +68,7 @@ const importFetchedAccounts = (accounts: APIEntity[], args = { should_refetch: f
|
|||
const normalAccounts: APIEntity[] = [];
|
||||
|
||||
const processAccount = (account: APIEntity) => {
|
||||
if (!account.id) return;
|
||||
if (!account?.id) return;
|
||||
|
||||
if (should_refetch) {
|
||||
account.should_refetch = true;
|
||||
|
|
|
@ -160,7 +160,6 @@ const expandFollowedHashtagsFail = (error: unknown) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
|
||||
export {
|
||||
HASHTAG_FETCH_REQUEST,
|
||||
HASHTAG_FETCH_SUCCESS,
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||
import { useApi, useFeatures } from 'soapbox/hooks';
|
||||
import { type Report, reportSchema } from 'soapbox/schemas';
|
||||
|
||||
const useReports = () => {
|
||||
const api = useApi();
|
||||
const features = useFeatures();
|
||||
|
||||
const { entities: reports, ...result } = useEntities<Report>(
|
||||
[Entities.GROUPS, 'popular'],
|
||||
() => api.get('/api/v1/admin/reports'),
|
||||
{
|
||||
schema: reportSchema,
|
||||
enabled: features.groupsDiscovery,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
reports,
|
||||
};
|
||||
};
|
||||
|
||||
export { reports };
|
|
@ -91,7 +91,6 @@ const Announcements: React.FC = () => {
|
|||
|
||||
const { data: announcements, isLoading } = useAnnouncements();
|
||||
|
||||
|
||||
const handleCreateAnnouncement = () => {
|
||||
dispatch(openModal('EDIT_ANNOUNCEMENT'));
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { deleteStatusModal } from 'soapbox/actions/moderation';
|
||||
|
@ -6,9 +6,10 @@ import DropdownMenu from 'soapbox/components/dropdown-menu';
|
|||
import StatusContent from 'soapbox/components/status-content';
|
||||
import StatusMedia from 'soapbox/components/status-media';
|
||||
import { HStack, Stack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetStatus } from 'soapbox/selectors';
|
||||
|
||||
import type { AdminReport, Status } from 'soapbox/types/entities';
|
||||
import type { Report as ReportEntity } from 'soapbox/schemas';
|
||||
|
||||
const messages = defineMessages({
|
||||
viewStatus: { id: 'admin.reports.actions.view_status', defaultMessage: 'View post' },
|
||||
|
@ -16,14 +17,19 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface IReportStatus {
|
||||
status: Status;
|
||||
report?: AdminReport;
|
||||
statusId: string;
|
||||
report?: ReportEntity;
|
||||
}
|
||||
|
||||
const ReportStatus: React.FC<IReportStatus> = ({ status }) => {
|
||||
const ReportStatus: React.FC<IReportStatus> = ({ statusId }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getStatus = useCallback(makeGetStatus(), []);
|
||||
const status = useAppSelector(state => getStatus(state, { id: statusId }));
|
||||
|
||||
if (!status) return null;
|
||||
|
||||
const handleDeleteStatus = () => {
|
||||
dispatch(deleteStatusModal(intl, status.id));
|
||||
};
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { closeReports } from 'soapbox/actions/admin';
|
||||
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
|
||||
import { useAccount } from 'soapbox/api/hooks';
|
||||
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
||||
import { Accordion, Avatar, Button, Stack, HStack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetReport } from 'soapbox/selectors';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import ReportStatus from './report-status';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
import type { Account, AdminReport, Status } from 'soapbox/types/entities';
|
||||
import type { Account as AccountEntity, Report as ReportEntity } from 'soapbox/schemas';
|
||||
|
||||
const messages = defineMessages({
|
||||
reportClosed: { id: 'admin.reports.report_closed_message', defaultMessage: 'Report on @{name} was closed' },
|
||||
|
@ -30,16 +29,14 @@ const Report: React.FC<IReport> = ({ id }) => {
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getReport = useCallback(makeGetReport(), []);
|
||||
|
||||
const report = useAppSelector((state) => getReport(state, id) as AdminReport | undefined);
|
||||
const report = useAppSelector((state) => state.admin.reports.get(id) as ReportEntity | undefined);
|
||||
|
||||
const [accordionExpanded, setAccordionExpanded] = useState(false);
|
||||
|
||||
if (!report) return null;
|
||||
const { account } = useAccount(report?.account.id) as { account: AccountEntity };
|
||||
const { account: targetAccount } = useAccount(report?.target_account.id) as { account: AccountEntity };
|
||||
|
||||
const account = report.account as Account;
|
||||
const targetAccount = report.target_account as Account;
|
||||
if (!report) return null;
|
||||
|
||||
const makeMenu = () => {
|
||||
return [{
|
||||
|
@ -76,8 +73,8 @@ const Report: React.FC<IReport> = ({ id }) => {
|
|||
};
|
||||
|
||||
const menu = makeMenu();
|
||||
const statuses = report.statuses as ImmutableList<Status>;
|
||||
const statusCount = statuses.count();
|
||||
const statuses = report.statuses;
|
||||
const statusCount = statuses.length;
|
||||
const acct = targetAccount.acct as string;
|
||||
const reporterAcct = account.acct as string;
|
||||
|
||||
|
@ -113,7 +110,7 @@ const Report: React.FC<IReport> = ({ id }) => {
|
|||
<ReportStatus
|
||||
key={status.id}
|
||||
report={report}
|
||||
status={status}
|
||||
statusId={status.id}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
|
|
@ -270,7 +270,6 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
</FormGroup>
|
||||
)}
|
||||
|
||||
|
||||
{!features.nostrSignup && (
|
||||
<Input
|
||||
type='email'
|
||||
|
|
|
@ -9,7 +9,6 @@ import { useFeatures } from 'soapbox/hooks';
|
|||
|
||||
import NewFolderForm from './components/new-folder-form';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
});
|
||||
|
|
|
@ -13,7 +13,6 @@ import GroupMemberListItem from './components/group-member-list-item';
|
|||
|
||||
import type { Group } from 'soapbox/types/entities';
|
||||
|
||||
|
||||
interface IGroupMembers {
|
||||
params: { groupId: string };
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ const GroupTagTimeline: React.FC<IGroupTimeline> = (props) => {
|
|||
}
|
||||
}, [groupId, tag]);
|
||||
|
||||
|
||||
if (isLoading || !tag || !group) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import { render, screen } from 'soapbox/jest/test-helpers';
|
|||
|
||||
import Blankslate from './blankslate';
|
||||
|
||||
|
||||
describe('<Blankslate />', () => {
|
||||
describe('with string props', () => {
|
||||
it('should render correctly', () => {
|
||||
|
|
|
@ -20,7 +20,8 @@ import {
|
|||
ADMIN_USERS_APPROVE_REQUEST,
|
||||
ADMIN_USERS_APPROVE_SUCCESS,
|
||||
} from 'soapbox/actions/admin';
|
||||
import { normalizeAdminReport, normalizeAdminAccount } from 'soapbox/normalizers';
|
||||
import { normalizeAdminAccount } from 'soapbox/normalizers';
|
||||
import { reportSchema, Report } from 'soapbox/schemas';
|
||||
import { normalizeId } from 'soapbox/utils/normalizers';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
@ -28,7 +29,7 @@ import type { APIEntity } from 'soapbox/types/entities';
|
|||
import type { Config } from 'soapbox/utils/config-db';
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
reports: ImmutableMap<string, ReducerAdminReport>(),
|
||||
reports: ImmutableMap<string, Report>(),
|
||||
openReports: ImmutableOrderedSet<string>(),
|
||||
users: ImmutableMap<string, ReducerAdminAccount>(),
|
||||
latestUsers: ImmutableOrderedSet<string>(),
|
||||
|
@ -40,20 +41,11 @@ const ReducerRecord = ImmutableRecord({
|
|||
type State = ReturnType<typeof ReducerRecord>;
|
||||
|
||||
type AdminAccountRecord = ReturnType<typeof normalizeAdminAccount>;
|
||||
type AdminReportRecord = ReturnType<typeof normalizeAdminReport>;
|
||||
|
||||
export interface ReducerAdminAccount extends AdminAccountRecord {
|
||||
account: string | null;
|
||||
}
|
||||
|
||||
export interface ReducerAdminReport extends AdminReportRecord {
|
||||
account: string | null;
|
||||
target_account: string | null;
|
||||
action_taken_by_account: string | null;
|
||||
assigned_account: string | null;
|
||||
statuses: ImmutableList<string | null>;
|
||||
}
|
||||
|
||||
// Lol https://javascript.plainenglish.io/typescript-essentials-conditionally-filter-types-488705bfbf56
|
||||
type FilterConditionally<Source, Condition> = Pick<Source, {[K in keyof Source]: Source[K] extends Condition ? K : never}[keyof Source]>;
|
||||
|
||||
|
@ -139,26 +131,11 @@ function approveUsers(state: State, users: APIUser[]): State {
|
|||
});
|
||||
}
|
||||
|
||||
const minifyReport = (report: AdminReportRecord): ReducerAdminReport => {
|
||||
return report.mergeWith((o, n) => n || o, {
|
||||
account: normalizeId(report.getIn(['account', 'id'])),
|
||||
target_account: normalizeId(report.getIn(['target_account', 'id'])),
|
||||
action_taken_by_account: normalizeId(report.getIn(['action_taken_by_account', 'id'])),
|
||||
assigned_account: normalizeId(report.getIn(['assigned_account', 'id'])),
|
||||
statuses: report.get('statuses').map((status: any) => normalizeId(status.get('id'))),
|
||||
}) as ReducerAdminReport;
|
||||
};
|
||||
|
||||
const fixReport = (report: APIEntity): ReducerAdminReport => {
|
||||
return normalizeAdminReport(report).withMutations(report => {
|
||||
minifyReport(report);
|
||||
}) as ReducerAdminReport;
|
||||
};
|
||||
|
||||
function importReports(state: State, reports: APIEntity[]): State {
|
||||
return state.withMutations(state => {
|
||||
reports.forEach(report => {
|
||||
const normalizedReport = fixReport(report);
|
||||
console.log(reportSchema.safeParse(report));
|
||||
const normalizedReport = reportSchema.parse(report);
|
||||
if (!normalizedReport.action_taken) {
|
||||
state.update('openReports', orderedSet => orderedSet.add(report.id));
|
||||
}
|
||||
|
|
|
@ -11,27 +11,19 @@ import { relationshipSchema } from './relationship';
|
|||
import { coerceObject, contentSchema, filteredArray, makeCustomEmojiMap } from './utils';
|
||||
|
||||
import type { Resolve } from 'soapbox/utils/types';
|
||||
import { roleSchema } from './role';
|
||||
|
||||
const avatarMissing = require('soapbox/assets/images/avatar-missing.png');
|
||||
const headerMissing = require('soapbox/assets/images/header-missing.png');
|
||||
|
||||
const birthdaySchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
||||
|
||||
const hexSchema = z.string().regex(/^#[a-f0-9]{6}$/i);
|
||||
|
||||
const fieldSchema = z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
verified_at: z.string().datetime().nullable().catch(null),
|
||||
});
|
||||
|
||||
const roleSchema = z.object({
|
||||
id: z.string().catch(''),
|
||||
name: z.string().catch(''),
|
||||
color: hexSchema.catch(''),
|
||||
highlighted: z.boolean().catch(true),
|
||||
});
|
||||
|
||||
const baseAccountSchema = z.object({
|
||||
acct: z.string().catch(''),
|
||||
avatar: z.string().catch(avatarMissing),
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { accountSchema } from './account';
|
||||
import { roleSchema } from './role';
|
||||
import { filteredArray } from './utils';
|
||||
|
||||
import type { Resolve } from 'soapbox/utils/types';
|
||||
|
||||
const adminIpSchema = z.object({
|
||||
ip: z.string(),
|
||||
used_at: z.string().datetime(),
|
||||
});
|
||||
|
||||
const adminRoleSchema = z.preprocess((data: any) => {
|
||||
if (typeof data === 'string') {
|
||||
return { name: data };
|
||||
}
|
||||
|
||||
return data;
|
||||
}, roleSchema.extend({
|
||||
permissions: z.number().nullable().catch(null),
|
||||
}));
|
||||
|
||||
const adminAccountSchema = z.preprocess((data: any) => {
|
||||
if (!data.account) {
|
||||
return {
|
||||
...data,
|
||||
approved: data.is_approved,
|
||||
confirmed: data.is_confirmed,
|
||||
disabled: !data.is_active,
|
||||
invite_request: data.registration_reason,
|
||||
role: data.roles?.admin ? 'admin' : (data.roles?.moderator ? 'moderator' : null),
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}, z.object({
|
||||
account: accountSchema,
|
||||
id: z.string(),
|
||||
username: z.string().catch(''),
|
||||
domain: z.string().nullable().catch(null),
|
||||
created_at: z.string().datetime().catch(new Date().toUTCString()),
|
||||
email: z.string().catch(''),
|
||||
ip: z.string().nullable().catch(null),
|
||||
ips: filteredArray(adminIpSchema),
|
||||
locale: z.string().catch(''),
|
||||
invite_request: z.string().nullable().catch(null),
|
||||
role: adminRoleSchema.nullable().catch(null),
|
||||
confirmed: z.boolean().catch(false),
|
||||
approved: z.boolean().catch(false),
|
||||
disabled: z.boolean().catch(false),
|
||||
silenced: z.boolean().catch(false),
|
||||
suspended: z.boolean().catch(false),
|
||||
created_by_application_id: z.string().nullable().catch(null),
|
||||
invited_by_account_id: z.string().nullable().catch(null),
|
||||
}));
|
||||
|
||||
type AdminAccount = Resolve<z.infer<typeof adminAccountSchema>>;
|
||||
|
||||
export { adminAccountSchema, type AdminAccount };
|
|
@ -1,6 +1,7 @@
|
|||
export { accountSchema, type Account } from './account';
|
||||
export { announcementSchema, adminAnnouncementSchema, type Announcement, type AdminAnnouncement } from './announcement';
|
||||
export { announcementReactionSchema, type AnnouncementReaction } from './announcement-reaction';
|
||||
export { adminAccountSchema, type AdminAccount } from './admin-account';
|
||||
export { attachmentSchema, type Attachment } from './attachment';
|
||||
export { bookmarkFolderSchema, type BookmarkFolder } from './bookmark-folder';
|
||||
export { cardSchema, type Card } from './card';
|
||||
|
@ -20,6 +21,8 @@ export { patronUserSchema, type PatronUser } from './patron';
|
|||
export { pollSchema, type Poll, type PollOption } from './poll';
|
||||
export { relationshipSchema, type Relationship } from './relationship';
|
||||
export { relaySchema, type Relay } from './relay';
|
||||
export { reportSchema, type Report } from './report';
|
||||
export { roleSchema, type Role } from './role';
|
||||
export { ruleSchema, adminRuleSchema, type Rule, type AdminRule } from './rule';
|
||||
export { statusSchema, type Status } from './status';
|
||||
export { tagSchema, type Tag } from './tag';
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { adminAccountSchema } from './admin-account';
|
||||
import { ruleSchema } from './rule';
|
||||
import { statusSchema } from './status';
|
||||
import { filteredArray } from './utils';
|
||||
|
||||
import type { Resolve } from 'soapbox/utils/types';
|
||||
|
||||
const reportSchema = z.preprocess((data: any) => {
|
||||
if (data.actor) {
|
||||
return {
|
||||
...data,
|
||||
target_account: data.account,
|
||||
account: data.actor,
|
||||
action_taken: data.state !== 'open',
|
||||
comment: data.content,
|
||||
updated_at: data.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}, z.object({
|
||||
account: adminAccountSchema,
|
||||
action_taken: z.boolean().catch(false),
|
||||
action_taken_at: z.string().datetime().nullable().catch(null),
|
||||
action_taken_by_account: adminAccountSchema.nullable().catch(null),
|
||||
assigned_account: adminAccountSchema.nullable().catch(null),
|
||||
category: z.enum(['spam', 'violation', 'other']).catch('other'),
|
||||
comment: z.string().catch(''),
|
||||
created_at: z.string().datetime().catch(new Date().toUTCString()),
|
||||
forwarded: z.boolean().catch(false),
|
||||
id: z.string(),
|
||||
rules: filteredArray(ruleSchema),
|
||||
statuses: filteredArray(statusSchema),
|
||||
target_account: adminAccountSchema,
|
||||
updated_at: z.string().datetime().catch(new Date().toUTCString()),
|
||||
}));
|
||||
|
||||
type Report = Resolve<z.infer<typeof reportSchema>>;
|
||||
|
||||
export { reportSchema, type Report };
|
|
@ -0,0 +1,16 @@
|
|||
import z from 'zod';
|
||||
|
||||
import type { Resolve } from 'soapbox/utils/types';
|
||||
|
||||
const hexSchema = z.string().regex(/^#[a-f0-9]{6}$/i);
|
||||
|
||||
const roleSchema = z.object({
|
||||
id: z.string().catch(''),
|
||||
name: z.string().catch(''),
|
||||
color: hexSchema.catch(''),
|
||||
highlighted: z.boolean().catch(true),
|
||||
});
|
||||
|
||||
type Role = Resolve<z.infer<typeof roleSchema>>;
|
||||
|
||||
export { roleSchema, type Role };
|
|
@ -3,7 +3,6 @@ import {
|
|||
List as ImmutableList,
|
||||
OrderedSet as ImmutableOrderedSet,
|
||||
Record as ImmutableRecord,
|
||||
fromJS,
|
||||
} from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
|
@ -233,34 +232,6 @@ export const makeGetChat = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export const makeGetReport = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
return createSelector(
|
||||
[
|
||||
(state: RootState, id: string) => state.admin.reports.get(id),
|
||||
(state: RootState, id: string) => selectAccount(state, state.admin.reports.get(id)?.account || ''),
|
||||
(state: RootState, id: string) => selectAccount(state, state.admin.reports.get(id)?.target_account || ''),
|
||||
(state: RootState, id: string) => ImmutableList(fromJS(state.admin.reports.get(id)?.statuses)).map(
|
||||
statusId => state.statuses.get(normalizeId(statusId)))
|
||||
.filter((s: any) => s)
|
||||
.map((s: any) => getStatus(state, s.toJS())),
|
||||
],
|
||||
|
||||
(report, account, targetAccount, statuses) => {
|
||||
if (!report) return null;
|
||||
return report.withMutations((report) => {
|
||||
// @ts-ignore
|
||||
report.set('account', account);
|
||||
// @ts-ignore
|
||||
report.set('target_account', targetAccount);
|
||||
// @ts-ignore
|
||||
report.set('statuses', statuses);
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const getAuthUserIds = createSelector([
|
||||
(state: RootState) => state.auth.users,
|
||||
], authUsers => {
|
||||
|
@ -318,7 +289,6 @@ const getRemoteInstanceFederation = (state: RootState, host: string): HostFedera
|
|||
) as HostFederation;
|
||||
};
|
||||
|
||||
|
||||
export const makeGetHosts = () => {
|
||||
return createSelector([getSimplePolicy], (simplePolicy) => {
|
||||
const { accept, reject_deletes, report_removal, ...rest } = simplePolicy;
|
||||
|
|
|
@ -59,7 +59,6 @@ function immutableizeStore<T, S extends Record<string, T | undefined>>(state: S)
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
immutableizeStore,
|
||||
immutableizeEntity,
|
||||
|
|
Ładowanie…
Reference in New Issue