[feature/frontend] Reports frontend v2 (#3022)

* use apiutil + paging in admin processor+handlers

* we're making it happen

* fix little whoopsie

* styling for report list

* don't youuuu forget about meee don't don't don't don't

* last bits

* sanitize content before showing in report statuses

* update report docs
This commit is contained in:
tobi 2024-06-18 18:18:00 +02:00 committed by GitHub
commit d2b3d37724
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 1389 additions and 726 deletions

View file

@ -17,8 +17,8 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useState } from "react";
import { useParams } from "wouter";
import React from "react";
import { useLocation, useParams } from "wouter";
import FormWithData from "../../../lib/form/form-with-data";
import BackButton from "../../../components/back-button";
import { useValue, useTextInput } from "../../../lib/form";
@ -28,84 +28,172 @@ import MutationButton from "../../../components/form/mutation-button";
import Username from "../../../components/username";
import { useGetReportQuery, useResolveReportMutation } from "../../../lib/query/admin/reports";
import { useBaseUrl } from "../../../lib/navigation/util";
import { AdminReport } from "../../../lib/types/report";
import { yesOrNo } from "../../../lib/util";
import { Status } from "../../../components/status";
export default function ReportDetail({ }) {
const params: { reportId: string } = useParams();
const baseUrl = useBaseUrl();
const params = useParams();
const backLocation: String = history.state?.backLocation ?? `~${baseUrl}`;
return (
<div className="reports">
<h1><BackButton to={`~${baseUrl}`}/> Report Details</h1>
<div className="report-detail">
<h1><BackButton to={backLocation}/> Report Details</h1>
<FormWithData
dataQuery={useGetReportQuery}
queryArg={params.reportId}
DataForm={ReportDetailForm}
{...{ backLocation: backLocation }}
/>
</div>
);
}
function ReportDetailForm({ data: report }) {
function ReportDetailForm({ data: report }: { data: AdminReport }) {
const [ location ] = useLocation();
const baseUrl = useBaseUrl();
return (
<>
<ReportBasicInfo
report={report}
baseUrl={baseUrl}
location={location}
/>
{ report.action_taken
&& <ReportHistory
report={report}
baseUrl={baseUrl}
location={location}
/>
}
{ report.statuses &&
<ReportStatuses report={report} />
}
{ !report.action_taken &&
<ReportActionForm report={report} />
}
</>
);
}
interface ReportSectionProps {
report: AdminReport;
baseUrl: string;
location: string;
}
function ReportBasicInfo({ report, baseUrl, location }: ReportSectionProps) {
const from = report.account;
const target = report.target_account;
const comment = report.comment;
const status = report.action_taken ? "Resolved" : "Unresolved";
const created = new Date(report.created_at).toLocaleString();
return (
<div className="report detail">
<div className="usernames">
<Username
account={from}
linkTo={`~/settings/moderation/accounts/${from.id}`}
backLocation={`~/settings/moderation/reports/${report.id}`}
/>
<> reported </>
<Username
account={target}
linkTo={`~/settings/moderation/accounts/${target.id}`}
backLocation={`~/settings/moderation/reports/${report.id}`}
/>
<dl className="info-list overview">
<div className="info-list-entry">
<dt>Reported account</dt>
<dd>
<Username
account={target}
linkTo={`~/settings/moderation/accounts/${target.id}`}
backLocation={`~${baseUrl}${location}`}
/>
</dd>
</div>
<div className="info-list-entry">
<dt>Reported by</dt>
<dd>
<Username
account={from}
linkTo={`~/settings/moderation/accounts/${from.id}`}
backLocation={`~${baseUrl}${location}`}
/>
</dd>
</div>
{report.action_taken &&
<div className="info">
<h3>Resolved by @{report.action_taken_by_account.account.acct}</h3>
<span className="timestamp">at {new Date(report.action_taken_at).toLocaleString()}</span>
<br />
<b>Comment: </b><span>{report.action_taken_comment}</span>
</div>
}
<div className="info-block">
<h3>Report info:</h3>
<div className="details">
<b>Created: </b>
<span>{new Date(report.created_at).toLocaleString()}</span>
<b>Forwarded: </b> <span>{report.forwarded ? "Yes" : "No"}</span>
<b>Category: </b> <span>{report.category}</span>
<b>Reason: </b>
{report.comment.length > 0
? <p>{report.comment}</p>
: <i className="no-comment">none provided</i>
<div className="info-list-entry">
<dt>Status</dt>
<dd>
{ report.action_taken
? <>{status}</>
: <b>{status}</b>
}
</div>
</dd>
</div>
{!report.action_taken && <ReportActionForm report={report} />}
<div className="info-list-entry">
<dt>Reason</dt>
<dd>
{ comment.length > 0
? <>{comment}</>
: <i>none provided</i>
}
</dd>
</div>
{
report.statuses.length > 0 &&
<div className="info-block">
<h3>Reported toots ({report.statuses.length}):</h3>
<div className="reported-toots">
{report.statuses.map((status) => (
<ReportedToot key={status.id} toot={status} />
))}
</div>
<div className="info-list-entry">
<dt>Created</dt>
<dd>
<time dateTime={report.created_at}>{created}</time>
</dd>
</div>
<div className="info-list-entry">
<dt>Category</dt>
<dd>{ report.category }</dd>
</div>
<div className="info-list-entry">
<dt>Forwarded</dt>
<dd>{ yesOrNo(report.forwarded) }</dd>
</div>
</dl>
);
}
function ReportHistory({ report, baseUrl, location }: ReportSectionProps) {
const handled_by = report.action_taken_by_account;
if (!handled_by) {
throw "report handled by action_taken_by_account undefined";
}
const handled = report.action_taken_at ? new Date(report.action_taken_at).toLocaleString() : "never";
return (
<>
<h3>Moderation History</h3>
<dl className="info-list">
<div className="info-list-entry">
<dt>Handled by</dt>
<dd>
<Username
account={handled_by}
linkTo={`~/settings/moderation/accounts/${handled_by.id}`}
backLocation={`~${baseUrl}${location}`}
/>
</dd>
</div>
}
</div>
<div className="info-list-entry">
<dt>Handled</dt>
<dd>
<time dateTime={report.action_taken_at}>{handled}</time>
</dd>
</div>
<div className="info-list-entry">
<dt>Comment</dt>
<dd>{ report.action_taken_comment ?? "none"}</dd>
</div>
</dl>
</>
);
}
@ -118,13 +206,18 @@ function ReportActionForm({ report }) {
const [submit, result] = useFormSubmit(form, useResolveReportMutation(), { changedOnly: false });
return (
<form onSubmit={submit} className="info-block">
<h3>Resolving this report</h3>
<p>
<form onSubmit={submit}>
<h3>Resolve this report</h3>
<>
An optional comment can be included while resolving this report.
Useful for providing an explanation about what action was taken (if any) before the report was marked as resolved.<br />
<b>This will be visible to the user that created the report!</b>
</p>
This is useful for providing an explanation about what action was
taken (if any) before the report was marked as resolved.
<br />
<b>
Any comment made here will be visible
to the user that created the report!
</b>
</>
<TextArea
field={form.comment}
label="Comment"
@ -138,116 +231,24 @@ function ReportActionForm({ report }) {
);
}
function ReportedToot({ toot }) {
const account = toot.account;
return (
<article className="status expanded">
<header className="status-header">
<address>
<a style={{margin: 0}}>
<img className="avatar" src={account.avatar} alt="" />
<dl className="author-strap">
<dt className="sr-only">Display name</dt>
<dd className="displayname text-cutoff">
{account.display_name.trim().length > 0 ? account.display_name : account.username}
</dd>
<dt className="sr-only">Username</dt>
<dd className="username text-cutoff">@{account.username}</dd>
</dl>
</a>
</address>
</header>
<section className="status-body">
<div className="text">
<div className="content">
{toot.spoiler_text?.length > 0
? <TootCW content={toot.content} note={toot.spoiler_text} />
: toot.content
}
</div>
</div>
{toot.media_attachments?.length > 0 &&
<TootMedia media={toot.media_attachments} sensitive={toot.sensitive} />
}
</section>
<aside className="status-info">
<dl className="status-stats">
<div className="stats-grouping">
<div className="stats-item published-at text-cutoff">
<dt className="sr-only">Published</dt>
<dd>
<time dateTime={toot.created_at}>{new Date(toot.created_at).toLocaleString()}</time>
</dd>
</div>
</div>
</dl>
</aside>
</article>
);
}
function TootCW({ note, content }) {
const [visible, setVisible] = useState(false);
function toggleVisible() {
setVisible(!visible);
function ReportStatuses({ report }: { report: AdminReport }) {
if (report.statuses.length === 0) {
return null;
}
return (
<>
<div className="spoiler">
<span>{note}</span>
<label className="button spoiler-label" onClick={toggleVisible}>Show {visible ? "less" : "more"}</label>
</div>
{visible && content}
</>
);
}
function TootMedia({ media, sensitive }) {
let classes = (media.length % 2 == 0) ? "even" : "odd";
if (media.length == 1) {
classes += " single";
}
return (
<div className={`media photoswipe-gallery ${classes}`}>
{media.map((m) => (
<div key={m.id} className="media-wrapper">
{sensitive && <>
<input id={`sensitiveMedia-${m.id}`} type="checkbox" className="sensitive-checkbox hidden" />
<div className="sensitive">
<div className="open">
<label htmlFor={`sensitiveMedia-${m.id}`} className="button" role="button" tabIndex={0}>
<i className="fa fa-eye-slash" title="Hide sensitive media"></i>
</label>
</div>
<div className="closed" title={m.description}>
<label htmlFor={`sensitiveMedia-${m.id}`} className="button" role="button" tabIndex={0}>
Show sensitive media
</label>
</div>
</div>
</>}
<a
href={m.url}
title={m.description}
target="_blank"
rel="noreferrer"
data-cropped="true"
data-pswp-width={`${m.meta?.original.width}px`}
data-pswp-height={`${m.meta?.original.height}px`}
>
<img
alt={m.description}
src={m.url}
// thumb={m.preview_url}
sizes={m.meta?.original}
/>
</a>
</div>
))}
<div className="report-statuses">
<h3>Reported Statuses</h3>
<ul className="thread">
{ report.statuses.map((status) => {
return (
<Status
key={status.id}
status={status}
/>
);
})}
</ul>
</div>
);
}