diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index d6f111859..e9f864770 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -4238,7 +4238,7 @@ class PlApiClient { return this.#paginatedGet('/api/v1/admin/reports', { params }, adminReportSchema); } else { return this.#paginatedPleromaReports({ - state: params?.resolved === true ? 'resolved' : params?.resolved === false ? 'open' : undefined, + state: params?.resolved === true ? 'resolved' : 'open', page_size: params?.limit || 100, }); } @@ -4297,6 +4297,7 @@ class PlApiClient { * * Mark a report as resolved with no further action taken. * + * `action_taken_comment` param requires features{@link Features.mastodonAdminResolveReportWithComment}. * @param action_taken_comment Optional admin comment on the action taken in response to this report. Supported by GoToSocial only. * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#resolve} */ diff --git a/packages/pl-api/lib/features.ts b/packages/pl-api/lib/features.ts index f104bf6e3..bf2cc57e7 100644 --- a/packages/pl-api/lib/features.ts +++ b/packages/pl-api/lib/features.ts @@ -1210,6 +1210,8 @@ const getFeatures = (instance: Instance) => { v.software === PLEROMA && v.build === PL, ]), + mastodonAdminResolveReportWithComment: v.software === GOTOSOCIAL, + /** * @see POST /api/v1/admin/dimensions * @see POST /api/v1/admin/measures diff --git a/packages/pl-api/package.json b/packages/pl-api/package.json index 99d3f0650..b513a0369 100644 --- a/packages/pl-api/package.json +++ b/packages/pl-api/package.json @@ -1,6 +1,6 @@ { "name": "pl-api", - "version": "1.0.0-rc.86", + "version": "1.0.0-rc.87", "type": "module", "homepage": "https://codeberg.org/mkljczk/pl-fe/src/branch/develop/packages/pl-api", "repository": { diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index 3fb4a037b..880830123 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -105,7 +105,7 @@ "multiselect-react-dropdown": "^2.0.25", "mutative": "^1.1.0", "path-browserify": "^1.0.1", - "pl-api": "^1.0.0-rc.86", + "pl-api": "^1.0.0-rc.87", "postcss": "^8.5.3", "process": "^0.11.10", "punycode": "^2.1.1", diff --git a/packages/pl-fe/src/features/admin/components/dimension.tsx b/packages/pl-fe/src/features/admin/components/dimension.tsx index 9eb1c2d39..c2f9c5936 100644 --- a/packages/pl-fe/src/features/admin/components/dimension.tsx +++ b/packages/pl-fe/src/features/admin/components/dimension.tsx @@ -10,7 +10,7 @@ interface IDimension { dimension: AdminDimensionKey; startAt: string; endAt: string; - label: JSX.Element; + label?: JSX.Element; params: AdminGetDimensionsParams; } @@ -73,7 +73,9 @@ const Dimension: React.FC = ({ return (
- {label} + {label && ( + {label} + )} {content}
diff --git a/packages/pl-fe/src/features/admin/tabs/reports.tsx b/packages/pl-fe/src/features/admin/tabs/reports.tsx index cb13b9c6b..f9ad732a4 100644 --- a/packages/pl-fe/src/features/admin/tabs/reports.tsx +++ b/packages/pl-fe/src/features/admin/tabs/reports.tsx @@ -21,14 +21,14 @@ const Reports: React.FC = () => { const intl = useIntl(); const [params, setParams] = useSearchParams(); - const resolved = params.get('resolved') as any as boolean; + const resolved = params.get('resolved') as any as boolean || undefined; const accountId = params.get('account_id') || undefined; const targetAccountId = params.get('target_account_id') || undefined; const { account } = useAccount(accountId); const { account: targetAccount } = useAccount(targetAccountId); - const { data: reportIds = [], isPending } = useReports({ + const { data: reportIds = [], isPending, hasNextPage, fetchNextPage } = useReports({ resolved, account_id: accountId, target_account_id: targetAccountId, @@ -71,7 +71,9 @@ const Reports: React.FC = () => { isLoading={isPending} showLoading={isPending} emptyMessage={intl.formatMessage(messages.emptyMessage)} - listClassName='divide-y divide-solid divide-gray-200 dark:divide-gray-800' + hasMore={hasNextPage} + onLoadMore={fetchNextPage} + itemClassName='pt-4' > {reportIds.map(report => report && )} diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 59d0b30b0..fff1d467a 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -188,6 +188,17 @@ "admin.relays.new.url_placeholder": "Instance relay URL", "admin.relays.unfollow": "Unfollow", "admin.relays.url": "Instance URL:", + "admin.report.assigned_account": "Assigned moderator", + "admin.report.created_at": "Reported", + "admin.report.moderate": "Open account in moderation interface", + "admin.report.reopen": "Reopen report", + "admin.report.reported_by": "Reported by", + "admin.report.resolve": "Mark as resolved", + "admin.report.resolve.hint": "No action will be taken against the reported account, no strike recorded, and the report will be closed.", + "admin.report.resolved": "Resolved", + "admin.report.statuses": "Reported content", + "admin.report.target_account": "Reported account", + "admin.report.unresolved": "Unresolved", "admin.reports.account": "Reported by:", "admin.reports.actions.view_status": "View post", "admin.reports.comment": "Comment:", @@ -1489,6 +1500,7 @@ "reply_mentions.reply": "Replying to {accounts}", "reply_mentions.reply.hoverable": "Replying to {accounts}", "reply_mentions.reply_empty": "Replying to post", + "report.assign_to_self.success": "Assigned report to yourself.", "report.block": "Block {target}", "report.block_hint": "Do you also want to block this account?", "report.confirmation.content": "If we find that this {entity} is violating the {link} we will take further action on the matter.", @@ -1507,8 +1519,14 @@ "report.previous": "Previous", "report.reason.blankslate": "You have removed all statuses from being selected.", "report.reason.title": "Reason for reporting", + "report.reopen.success": "Report reopened.", + "report.resolve.comment.confirm": "Resolve report", + "report.resolve.comment.heading": "Add a comment", + "report.resolve.comment.placeholder": "You can include an optional comment while resolving this report. If the report was created by a local account, the comment will be sent to the user.", + "report.resolve.success": "Report marked as resolved.", "report.submit": "Submit", "report.target": "Reporting {target}", + "report.unassign.success": "Unassigned report.", "save": "Save", "schedule.post_time": "Post date/time", "schedule.remove": "Remove schedule", diff --git a/packages/pl-fe/src/pages/dashboard/report.tsx b/packages/pl-fe/src/pages/dashboard/report.tsx index 2c77323a9..030c73b25 100644 --- a/packages/pl-fe/src/pages/dashboard/report.tsx +++ b/packages/pl-fe/src/pages/dashboard/report.tsx @@ -1,26 +1,272 @@ -import React from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import React, { useCallback, useState } from 'react'; +import { defineMessages, FormattedDate, FormattedMessage, useIntl } from 'react-intl'; +import { Link } from 'react-router-dom'; +import ReactSwipeableViews from 'react-swipeable-views'; +import Account from 'pl-fe/components/account'; +import List, { ListItem } from 'pl-fe/components/list'; +import Card from 'pl-fe/components/ui/card'; import Column from 'pl-fe/components/ui/column'; -import { useReport } from 'pl-fe/queries/admin/use-reports'; +import HStack from 'pl-fe/components/ui/hstack'; +import Icon from 'pl-fe/components/ui/icon'; +import IconButton from 'pl-fe/components/ui/icon-button'; +import Stack from 'pl-fe/components/ui/stack'; +import Text from 'pl-fe/components/ui/text'; +import StatusContainer from 'pl-fe/containers/status-container'; +import ColumnLoading from 'pl-fe/features/ui/components/column-loading'; +import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; +import { useFeatures } from 'pl-fe/hooks/use-features'; +import { useReopenReport, useReport, useResolveReport, useSelfAssignReport, useUnassignReport } from 'pl-fe/queries/admin/use-reports'; +import { makeGetReport } from 'pl-fe/selectors'; +import { useModalsStore } from 'pl-fe/stores/modals'; +import toast from 'pl-fe/toast'; const messages = defineMessages({ columnHeading: { id: 'column.report', defaultMessage: 'Report #{id}' }, + reportAssign: { id: 'report.assign_to_self', defaultMessage: 'Assign to self' }, + reportUnassign: { id: 'report.unassign', defaultMessage: 'Unassign' }, + reportAssigned: { id: 'report.assign_to_self.success', defaultMessage: 'Assigned report to yourself.' }, + reportUnassigned: { id: 'report.unassign.success', defaultMessage: 'Unassigned report.' }, + reportResolved: { id: 'report.resolve.success', defaultMessage: 'Report marked as resolved.' }, + reportReopened: { id: 'report.reopen.success', defaultMessage: 'Report reopened.' }, + reportCommentHeading: { id: 'report.resolve.comment.heading', defaultMessage: 'Add a comment' }, + reportCommentPlaceholder: { id: 'report.resolve.comment.placeholder', defaultMessage: 'You can include an optional comment while resolving this report. If the report was created by a local account, the comment will be sent to the user.' }, + reportCommentConfirm: { id: 'report.resolve.comment.confirm', defaultMessage: 'Resolve report' }, }); type RouteParams = { reportId: string }; +interface IReportStatuses { + statusIds: Array; +} + +const ReportStatuses: React.FC = ({ statusIds }) => { + const [index, setIndex] = useState(0); + + const handleChangeIndex = (index: number) => { + setIndex(index % statusIds.length); + }; + + return ( +
+ {index !== 0 && ( +
+ +
+ )} + + {statusIds.map(statusId =>
)} +
+ {index !== statusIds.length - 1 && ( +
+ +
+ )} +
+ ); +}; + interface IReportPage { params: RouteParams; } -const ReportPage: React.FC = ({ params }) => { +const ReportPage: React.FC = (props) => { + const { reportId } = props.params; + + const features = useFeatures(); const intl = useIntl(); - const { data: report } = useReport(params.reportId); + const { data: minifiedReport } = useReport(reportId); + const { openModal } = useModalsStore(); + + const getReport = useCallback(makeGetReport(), []); + + const report = useAppSelector((state) => getReport(state, minifiedReport)); + + const { mutate: selfAssignReport } = useSelfAssignReport(reportId); + const { mutate: unassignReport } = useUnassignReport(reportId); + const { mutate: resolveReport } = useResolveReport(reportId); + const { mutate: reopenReport } = useReopenReport(reportId); + + const handleSelfAssignReport = () => { + selfAssignReport(undefined, { + onSuccess: () => { + toast.success(intl.formatMessage(messages.reportAssigned)); + }, + }); + }; + + const handleUnassignReport = () => { + unassignReport(undefined, { + onSuccess: () => { + toast.success(intl.formatMessage(messages.reportUnassigned)); + }, + }); + }; + + const handleResolveReport = () => { + const onConfirm = (actionTakenComment?: string) => { + resolveReport(actionTakenComment, { + onSuccess: () => { + toast.success(intl.formatMessage(messages.reportResolved)); + }, + }); + }; + + if (features.mastodonAdminResolveReportWithComment) { + openModal('TEXT_FIELD', { + heading: intl.formatMessage(messages.reportCommentHeading), + placeholder: intl.formatMessage(messages.reportCommentPlaceholder), + confirm: intl.formatMessage(messages.reportCommentConfirm), + onConfirm, + }); + } else { + onConfirm(); + } + }; + + const handleReopenReport = () => { + reopenReport(undefined, { + onSuccess: () => { + toast.success(intl.formatMessage(messages.reportReopened)); + }, + }); + }; + + if (!report) return ; return ( - - {report?.category} + + +
+ {report.target_account && ( + + + + + + + + + + + )} + + + + + + + + + + + + + + + + + + {features.mastodonAdmin && ( + + + + + + )} + +
+ + + + + + + +
+ + + + + + {report.account?.acct} + +
+ + + + + + {report.action_taken ? : } + +
+ + + + + {report.assigned_account ? ( + + + + @{report.assigned_account.acct} + + + + + ) : ( + + )} +
+
+ {report.status_ids?.length > 0 && ( + + + + + + + + + )} + + {report.action_taken ? ( + } + onClick={handleReopenReport} + /> + ) : ( + } + hint={} + onClick={handleResolveReport} + /> + )} + } + onClick={() => openModal('ACCOUNT_MODERATION', { accountId: report.target_account_id })} + /> +
); }; diff --git a/packages/pl-fe/src/queries/admin/use-reports.ts b/packages/pl-fe/src/queries/admin/use-reports.ts index b9df6df8d..a75e52f8c 100644 --- a/packages/pl-fe/src/queries/admin/use-reports.ts +++ b/packages/pl-fe/src/queries/admin/use-reports.ts @@ -8,7 +8,7 @@ import { makePaginatedResponseQuery } from '../utils/make-paginated-response-que import { makePaginatedResponseQueryOptions } from '../utils/make-paginated-response-query-options'; import { minifyAdminReport, minifyAdminReportList } from '../utils/minify-list'; -import type { AdminGetReportsParams, PaginationParams } from 'pl-api'; +import type { AdminGetReportsParams, AdminUpdateReportParams, PaginationParams } from 'pl-api'; const useReports = makePaginatedResponseQuery( (params: Omit) => ['admin', 'reportLists', params], @@ -41,6 +41,45 @@ const usePendingReportsCount = () => { }); }; +const useUpdateReport = (reportId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['admin', 'reports', reportId], + mutationFn: (params: AdminUpdateReportParams) => client.admin.reports.updateReport(reportId, params), + onSuccess: (report) => { + queryClient.setQueryData(['admin', 'reports', reportId], minifyAdminReport(report)); + }, + }); +}; + +const useSelfAssignReport = (reportId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['admin', 'reports', reportId], + mutationFn: () => client.admin.reports.assignReportToSelf(reportId), + onSuccess: (report) => { + queryClient.setQueryData(['admin', 'reports', reportId], minifyAdminReport(report)); + }, + }); +}; + +const useUnassignReport = (reportId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['admin', 'reports', reportId], + mutationFn: () => client.admin.reports.unassignReport(reportId), + onSuccess: (report) => { + queryClient.setQueryData(['admin', 'reports', reportId], minifyAdminReport(report)); + }, + }); +}; + const useResolveReport = (reportId: string) => { const client = useClient(); const queryClient = useQueryClient(); @@ -52,7 +91,7 @@ const useResolveReport = (reportId: string) => { queryClient.setQueryData(['admin', 'reports', reportId], minifyAdminReport(report)); queryClient.setQueriesData({ queryKey: ['admin', 'reportLists', { - resolved: false, + resolved: undefined, }], exact: false, }, filterById(reportId)); @@ -66,4 +105,29 @@ const useResolveReport = (reportId: string) => { }); }; -export { useReports, useReport, pendingReportsQuery, usePendingReportsCount, useResolveReport }; +const useReopenReport = (reportId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['admin', 'reports', reportId], + mutationFn: () => client.admin.reports.reopenReport(reportId), + onSuccess: (report) => { + queryClient.setQueryData(['admin', 'reports', reportId], minifyAdminReport(report)); + queryClient.setQueriesData({ + queryKey: ['admin', 'reportLists', { + resolved: true, + }], + exact: false, + }, filterById(reportId)); + queryClient.invalidateQueries({ + queryKey: ['admin', 'reportLists', { + resolved: undefined, + }], + exact: false, + }); + }, + }); +}; + +export { useReports, useReport, pendingReportsQuery, usePendingReportsCount, useUpdateReport, useSelfAssignReport, useUnassignReport, useResolveReport, useReopenReport }; diff --git a/packages/pl-fe/src/selectors/index.ts b/packages/pl-fe/src/selectors/index.ts index 7b741b946..0c5e32988 100644 --- a/packages/pl-fe/src/selectors/index.ts +++ b/packages/pl-fe/src/selectors/index.ts @@ -206,16 +206,18 @@ const makeGetReport = () => { (state: RootState, report?: ReturnType) => report, (state: RootState, report?: ReturnType) => selectAccount(state, report?.account_id || ''), (state: RootState, report?: ReturnType) => selectAccount(state, report?.target_account_id || ''), + (state: RootState, report?: ReturnType) => selectAccount(state, report?.assigned_account_id || ''), (state: RootState, report?: ReturnType) => report?.status_ids .map((statusId) => getStatus(state, { id: statusId })) .filter((status): status is SelectedStatus => status !== null), ], - (report, account, target_account, statuses = []) => { + (report, account, target_account, assigned_account, statuses = []) => { if (!report) return null; return { ...report, account, target_account, + assigned_account, statuses, }; }, diff --git a/packages/pl-fe/yarn.lock b/packages/pl-fe/yarn.lock index 70d9fc84c..8602f259c 100644 --- a/packages/pl-fe/yarn.lock +++ b/packages/pl-fe/yarn.lock @@ -6998,10 +6998,10 @@ pkg-dir@^4.1.0: dependencies: find-up "^4.0.0" -pl-api@^1.0.0-rc.86: - version "1.0.0-rc.86" - resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-1.0.0-rc.86.tgz#50a54d0264d4aca8926bbc65c4ec283712ee6806" - integrity sha512-IX/SpzfJniWMaEoJ7g8ecU01Y49HgGV8vYN0JJTT3YxB6Q64inTcU1DQVmjrlja8lBU3w1/4FnjtS2JMBsvNIg== +pl-api@^1.0.0-rc.87: + version "1.0.0-rc.87" + resolved "https://registry.yarnpkg.com/pl-api/-/pl-api-1.0.0-rc.87.tgz#e02573c61697d51d6df1ccc64a061173d18ce1dd" + integrity sha512-ng+7nBP5RiREmOjs9ot8SObvNc1lHRsEA7za8w6HDSGVFOf4VGBaYRRxO2dKLFkxk1ViRm2z5cWssN2mKk8xjQ== dependencies: blurhash "^2.0.5" http-link-header "^1.1.3"