feat: report page, take action on reports

This commit is contained in:
sam 2025-02-03 17:03:32 +01:00
parent a0ba712632
commit cacd3a30b7
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
14 changed files with 502 additions and 14 deletions

View file

@ -18,7 +18,7 @@ export const load = async ({ parent, fetch, cookies }) => {
const reports = await apiRequest<Report[]>("GET", "/moderation/reports", { fetch, cookies });
const staleReportCount = reports.filter(
(r) => idTimestamp(r.id).diffNow(["days"]).days >= 7,
(r) => idTimestamp(r.id).diffNow(["days"]).days <= -7,
).length;
return {

View file

@ -0,0 +1,23 @@
import { apiRequest } from "$api";
import type { Report } from "$api/models/moderation";
export const load = async ({ url, fetch, cookies }) => {
const before = url.searchParams.get("before");
const after = url.searchParams.get("after");
const byReporter = url.searchParams.get("by-reporter");
const byTarget = url.searchParams.get("by-target");
const includeClosed = url.searchParams.get("include-closed") === "true";
const params = new URLSearchParams();
if (before) params.set("before", before);
if (after) params.set("after", after);
if (byReporter) params.set("by-reporter", byReporter);
if (byTarget) params.set("by-target", byTarget);
if (includeClosed) params.set("include-closed", "true");
const reports = await apiRequest<Report[]>("GET", `/moderation/reports?${params.toString()}`, {
fetch,
cookies,
});
return { reports, url: url.toString(), byReporter, byTarget, before, after };
};

View file

@ -0,0 +1,125 @@
<script lang="ts">
import Link45deg from "svelte-bootstrap-icons/lib/Link45deg.svelte";
import Funnel from "svelte-bootstrap-icons/lib/Funnel.svelte";
import FunnelFill from "svelte-bootstrap-icons/lib/FunnelFill.svelte";
import type { PageData } from "./$types";
import { t } from "$lib/i18n";
import { DateTime } from "luxon";
import { idTimestamp } from "$lib";
type Props = { data: PageData };
let { data }: Props = $props();
const addReporter = (id: string | null) => {
const url = new URL(data.url);
if (id) url.searchParams.set("by-reporter", id);
else url.searchParams.delete("by-reporter");
return url.toString();
};
const addTarget = (id: string | null) => {
const url = new URL(data.url);
if (id) url.searchParams.set("by-target", id);
else url.searchParams.delete("by-target");
return url.toString();
};
const addBefore = (id: string) => {
const url = new URL(data.url);
url.searchParams.delete("after");
url.searchParams.set("before", id);
return url.toString();
};
const addAfter = (id: string) => {
const url = new URL(data.url);
url.searchParams.delete("before");
url.searchParams.set("after", id);
return url.toString();
};
</script>
<svelte:head>
<title>Reports • pronouns.cc</title>
</svelte:head>
<h2>Reports</h2>
<ul>
{#if data.byTarget}
<li>Filtering by target (<a href={addTarget(null)}>clear</a>)</li>
{/if}
{#if data.byReporter}
<li>Filtering by reporter (<a href={addReporter(null)}>clear</a>)</li>
{/if}
</ul>
{#if data.before}
<a href={addAfter(data.before)}>Show newer reports</a>
{/if}
<table class="table">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">User</th>
<th scope="col">Member</th>
<th scope="col">Reporter</th>
<th scope="col">Reason</th>
<th scope="col">Context?</th>
<th scope="col">Created at</th>
</tr>
</thead>
<tbody>
{#each data.reports as report (report.id)}
<tr>
<td>
<a href="/admin/reports/{report.id}">
<Link45deg />
<span class="visually-hidden">Open report</span>
</a>
</td>
<td>
<a href="/@{report.target_user.username}">@{report.target_user.username}</a>
(<a href={addTarget(report.target_user.id)}>
{#if data.byTarget === report.target_user.id}<FunnelFill />{:else}<Funnel />{/if}
</a>)
</td>
<td>
{#if report.target_member}
<a href="@/{report.target_user.username}/{report.target_member.name}">
{report.target_member.name}
</a>
{:else}
<em>(none)</em>
{/if}
</td>
<td>
<a href="/@{report.reporter.username}">{report.reporter.username}</a>
(<a href={addReporter(report.reporter.id)}>
{#if data.byReporter === report.reporter.id}<FunnelFill />{:else}<Funnel />{/if}
</a>)
</td>
<td><code>{report.reason}</code></td>
<td>
{#if report.context}
{$t("yes")}
{:else}
{$t("no")}
{/if}
</td>
<td>
{idTimestamp(report.id).toLocaleString(DateTime.DATETIME_SHORT)}
</td>
</tr>
{/each}
</tbody>
</table>
{#if data.reports.length === 100}
<a href={addBefore(data.reports[data.reports.length - 1].id)}>Show older reports</a>
{/if}

View file

@ -0,0 +1,13 @@
import { apiRequest } from "$api";
import type { ReportDetails } from "$api/models/moderation";
import { createModactions } from "$lib/actions/modaction";
export const load = async ({ params, fetch, cookies }) => {
const resp = await apiRequest<ReportDetails>("GET", `/moderation/reports/${params.id}`, {
fetch,
cookies,
});
return { report: resp.report, user: resp.user, member: resp.member };
};
export const actions = createModactions();

View file

@ -0,0 +1,78 @@
<script lang="ts">
import type { Member } from "$api/models/member";
import type { User } from "$api/models/user";
import ActionForm from "$components/admin/ActionForm.svelte";
import PartialProfileCard from "$components/admin/PartialProfileCard.svelte";
import ProfileHeader from "$components/profile/ProfileHeader.svelte";
import MemberCard from "$components/profile/user/MemberCard.svelte";
import { renderMarkdown } from "$lib/markdown";
import type { ActionData, PageData } from "./$types";
type Props = { data: PageData; form: ActionData };
let { data, form }: Props = $props();
let { report, user, member } = $derived(data);
</script>
<svelte:head>
<title>Report on @{user.username} • pronouns.cc</title>
</svelte:head>
<div class="row">
<div class="col-md">
<h3>Target user</h3>
<PartialProfileCard user={report.target_user} />
</div>
{#if report.target_member}
<div class="col-md">
<h3>Target member</h3>
<MemberCard
username={report.target_user.username}
member={report.target_member}
allPreferences={{}}
/>
</div>
{/if}
<div class="col-md">
<h3>Reporter</h3>
<PartialProfileCard user={report.reporter} />
</div>
</div>
<div class="row">
<div class="col-md">
<h3>Reason</h3>
<p><code>{report.reason}</code></p>
</div>
<div class="col-md">
<h3>Context</h3>
<p>
{#if report.context}
{@html renderMarkdown(report.context)}
{:else}
<em>(no context given)</em>
{/if}
</p>
</div>
</div>
<div class="row">
<h3>Take action</h3>
<ActionForm
userId={report.target_user.id}
reportId={report.id}
memberId={report.target_member?.id}
{form}
/>
</div>
{#if report.snapshot}
<h3>Profile at time of report</h3>
<hr />
{#if report.target_type === "USER"}
{@const snapshot = report.snapshot as User}
<ProfileHeader profile={snapshot} name="@{snapshot.username}" />
{:else}
{@const snapshot = report.snapshot as Member}
<ProfileHeader profile={snapshot} name="{snapshot.name} (@{snapshot.user.username})" />
{/if}
{/if}