From 725a21b02721f92ed0420ed3f807ee921de77992 Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Wed, 1 May 2024 15:11:22 +0200
Subject: [feature] Page through accounts as moderator (#2881)
* [feature] Page through accounts as moderator
* aaaaa
* use COLLATE "C" for Postgres to ensure same ordering as SQLite
* fix typo, test paging up
* don't show moderation / info for our instance acct
---
.../views/moderation/accounts/detail/actions.tsx | 126 +++++++++++++++--
.../moderation/accounts/detail/handlesignup.tsx | 114 ---------------
.../views/moderation/accounts/detail/index.tsx | 157 +++++++++++++--------
.../views/moderation/accounts/detail/util.tsx | 43 ++++++
.../settings/views/moderation/accounts/index.tsx | 4 +-
.../views/moderation/accounts/pending/index.tsx | 28 +++-
.../views/moderation/accounts/search/index.tsx | 80 ++++++++---
web/source/settings/views/moderation/menu.tsx | 8 +-
.../settings/views/moderation/reports/detail.tsx | 12 +-
.../settings/views/moderation/reports/overview.tsx | 4 +-
.../settings/views/moderation/reports/username.tsx | 66 ---------
web/source/settings/views/moderation/router.tsx | 10 +-
12 files changed, 360 insertions(+), 292 deletions(-)
delete mode 100644 web/source/settings/views/moderation/accounts/detail/handlesignup.tsx
create mode 100644 web/source/settings/views/moderation/accounts/detail/util.tsx
delete mode 100644 web/source/settings/views/moderation/reports/username.tsx
(limited to 'web/source/settings/views/moderation')
diff --git a/web/source/settings/views/moderation/accounts/detail/actions.tsx b/web/source/settings/views/moderation/accounts/detail/actions.tsx
index 212bb4089..4132b778a 100644
--- a/web/source/settings/views/moderation/accounts/detail/actions.tsx
+++ b/web/source/settings/views/moderation/accounts/detail/actions.tsx
@@ -19,7 +19,7 @@
import React from "react";
-import { useActionAccountMutation } from "../../../../lib/query/admin";
+import { useActionAccountMutation, useHandleSignupMutation } from "../../../../lib/query/admin";
import MutationButton from "../../../../components/form/mutation-button";
import useFormSubmit from "../../../../lib/form/submit";
import {
@@ -27,22 +27,50 @@ import {
useTextInput,
useBoolInput,
} from "../../../../lib/form";
-import { Checkbox, TextInput } from "../../../../components/form/inputs";
+import { Checkbox, Select, TextInput } from "../../../../components/form/inputs";
import { AdminAccount } from "../../../../lib/types/account";
+import { useLocation } from "wouter";
export interface AccountActionsProps {
account: AdminAccount,
+ backLocation: string,
}
-export function AccountActions({ account }: AccountActionsProps) {
+export function AccountActions({ account, backLocation }: AccountActionsProps) {
+ const local = !account.domain;
+
+ // Available actions differ depending
+ // on the account's current status.
+ switch (true) {
+ case account.suspended:
+ // Can't do anything with
+ // suspended accounts currently.
+ return null;
+ case local && !account.approved:
+ // Unapproved local account sign-up,
+ // only show HandleSignup form.
+ return (
+
+ );
+ default:
+ // Normal local or remote account, show
+ // full range of moderation options.
+ return ;
+ }
+}
+
+function ModerateAccount({ account }: { account: AdminAccount }) {
const form = {
id: useValue("id", account.id),
reason: useTextInput("text")
};
-
+
const reallySuspend = useBoolInput("reallySuspend");
const [accountAction, result] = useFormSubmit(form, useActionAccountMutation());
-
+
return (
- {/*
- */}
);
}
+
+function HandleSignup({ account, backLocation }: { account: AdminAccount, backLocation: string }) {
+ const form = {
+ id: useValue("id", account.id),
+ approveOrReject: useTextInput("approve_or_reject", { defaultValue: "approve" }),
+ privateComment: useTextInput("private_comment"),
+ message: useTextInput("message"),
+ sendEmail: useBoolInput("send_email"),
+ };
+
+ const [_location, setLocation] = useLocation();
+
+ const [handleSignup, result] = useFormSubmit(form, useHandleSignupMutation(), {
+ changedOnly: false,
+ // After submitting the form, redirect back to
+ // /settings/admin/accounts if rejecting, since
+ // account will no longer be available at
+ // /settings/admin/accounts/:accountID endpoint.
+ onFinish: (res) => {
+ if (form.approveOrReject.value === "approve") {
+ // An approve request:
+ // stay on this page and
+ // serve updated details.
+ return;
+ }
+
+ if (res.data) {
+ // "reject" successful,
+ // redirect to accounts page.
+ setLocation(backLocation);
+ }
+ }
+ });
+
+ return (
+
+ );
+}
diff --git a/web/source/settings/views/moderation/accounts/detail/handlesignup.tsx b/web/source/settings/views/moderation/accounts/detail/handlesignup.tsx
deleted file mode 100644
index 59fa8bc65..000000000
--- a/web/source/settings/views/moderation/accounts/detail/handlesignup.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-import React from "react";
-import { useLocation } from "wouter";
-import { useHandleSignupMutation } from "../../../../lib/query/admin";
-import MutationButton from "../../../../components/form/mutation-button";
-import useFormSubmit from "../../../../lib/form/submit";
-import {
- useValue,
- useTextInput,
- useBoolInput,
-} from "../../../../lib/form";
-import { Checkbox, Select, TextInput } from "../../../../components/form/inputs";
-import { AdminAccount } from "../../../../lib/types/account";
-
-export interface HandleSignupProps {
- account: AdminAccount,
- backLocation: string,
-}
-
-export function HandleSignup({account, backLocation}: HandleSignupProps) {
- const form = {
- id: useValue("id", account.id),
- approveOrReject: useTextInput("approve_or_reject", { defaultValue: "approve" }),
- privateComment: useTextInput("private_comment"),
- message: useTextInput("message"),
- sendEmail: useBoolInput("send_email"),
- };
-
- const [_location, setLocation] = useLocation();
-
- const [handleSignup, result] = useFormSubmit(form, useHandleSignupMutation(), {
- changedOnly: false,
- // After submitting the form, redirect back to
- // /settings/admin/accounts if rejecting, since
- // account will no longer be available at
- // /settings/admin/accounts/:accountID endpoint.
- onFinish: (res) => {
- if (form.approveOrReject.value === "approve") {
- // An approve request:
- // stay on this page and
- // serve updated details.
- return;
- }
-
- if (res.data) {
- // "reject" successful,
- // redirect to accounts page.
- setLocation(backLocation);
- }
- }
- });
-
- return (
-
- );
-}
diff --git a/web/source/settings/views/moderation/accounts/detail/index.tsx b/web/source/settings/views/moderation/accounts/detail/index.tsx
index f34bc7481..830a894cb 100644
--- a/web/source/settings/views/moderation/accounts/detail/index.tsx
+++ b/web/source/settings/views/moderation/accounts/detail/index.tsx
@@ -23,51 +23,89 @@ import { useGetAccountQuery } from "../../../../lib/query/admin";
import FormWithData from "../../../../lib/form/form-with-data";
import FakeProfile from "../../../../components/fake-profile";
import { AdminAccount } from "../../../../lib/types/account";
-import { HandleSignup } from "./handlesignup";
import { AccountActions } from "./actions";
import { useParams } from "wouter";
+import { useBaseUrl } from "../../../../lib/navigation/util";
+import BackButton from "../../../../components/back-button";
+import { UseOurInstanceAccount, yesOrNo } from "./util";
export default function AccountDetail() {
const params: { accountID: string } = useParams();
-
+ const baseUrl = useBaseUrl();
+ const backLocation: String = history.state?.backLocation ?? `~${baseUrl}`;
+
return (
+
+
+ This is the service account for your instance; you
+ cannot perform moderation actions on this account.
+
+
+ >
+ );
+ }
+
+ const local = !adminAcct.domain;
+ return (
+ <>
+
+
+ {
+ // Only show local account details
+ // if this is a local account!
+ local &&
+ }
+
+ >
+ );
+}
- let created = new Date(adminAcct.created_at).toDateString();
+function GeneralAccountDetails({ adminAcct } : { adminAcct: AdminAccount }) {
+ const local = !adminAcct.domain;
+ const created = new Date(adminAcct.created_at).toDateString();
+
let lastPosted = "never";
if (adminAcct.account.last_status_at) {
lastPosted = new Date(adminAcct.account.last_status_at).toDateString();
}
- const local = !adminAcct.domain;
return (
<>
-
+ >
);
}
diff --git a/web/source/settings/views/moderation/accounts/detail/util.tsx b/web/source/settings/views/moderation/accounts/detail/util.tsx
new file mode 100644
index 000000000..b82d44a6e
--- /dev/null
+++ b/web/source/settings/views/moderation/accounts/detail/util.tsx
@@ -0,0 +1,43 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+import { useMemo } from "react";
+
+import { AdminAccount } from "../../../../lib/types/account";
+import { store } from "../../../../redux/store";
+
+export function yesOrNo(b: boolean): string {
+ return b ? "yes" : "no";
+}
+
+export function UseOurInstanceAccount(account: AdminAccount): boolean {
+ // Pull our own URL out of storage so we can
+ // tell if account is our instance account.
+ const ourDomain = useMemo(() => {
+ const instanceUrlStr = store.getState().oauth.instanceUrl;
+ if (!instanceUrlStr) {
+ return "";
+ }
+
+ const instanceUrl = new URL(instanceUrlStr);
+ return instanceUrl.host;
+ }, []);
+
+ return !account.domain && account.username == ourDomain;
+}
diff --git a/web/source/settings/views/moderation/accounts/index.tsx b/web/source/settings/views/moderation/accounts/index.tsx
index 79ba2c674..946ed323d 100644
--- a/web/source/settings/views/moderation/accounts/index.tsx
+++ b/web/source/settings/views/moderation/accounts/index.tsx
@@ -20,10 +20,10 @@
import React from "react";
import { AccountSearchForm } from "./search";
-export default function AccountsOverview({ }) {
+export default function AccountsSearch({ }) {
return (
-
Accounts Overview
+
Accounts Search
You can perform actions on an account by clicking
its name in a report, or by searching for the account
diff --git a/web/source/settings/views/moderation/accounts/pending/index.tsx b/web/source/settings/views/moderation/accounts/pending/index.tsx
index d5a32f09b..b72de52bf 100644
--- a/web/source/settings/views/moderation/accounts/pending/index.tsx
+++ b/web/source/settings/views/moderation/accounts/pending/index.tsx
@@ -17,20 +17,40 @@
along with this program. If not, see .
*/
-import React from "react";
+import React, { ReactNode } from "react";
import { useSearchAccountsQuery } from "../../../../lib/query/admin";
-import { AccountList } from "../../../../components/account-list";
+import { PageableList } from "../../../../components/pageable-list";
+import { useLocation } from "wouter";
+import Username from "../../../../components/username";
+import { AdminAccount } from "../../../../lib/types/account";
export default function AccountsPending() {
+ const [ location, _setLocation ] = useLocation();
const searchRes = useSearchAccountsQuery({status: "pending"});
+ // Function to map an item to a list entry.
+ function itemToEntry(account: AdminAccount): ReactNode {
+ const acc = account.account;
+ return (
+
+ );
+ }
+
return (
Pending Accounts
- .
*/
-import React from "react";
+import React, { ReactNode, useEffect, useMemo } from "react";
import { useLazySearchAccountsQuery } from "../../../../lib/query/admin";
import { useTextInput } from "../../../../lib/form";
-import { AccountList } from "../../../../components/account-list";
-import { SearchAccountParams } from "../../../../lib/types/account";
+import { PageableList } from "../../../../components/pageable-list";
import { Select, TextInput } from "../../../../components/form/inputs";
import MutationButton from "../../../../components/form/mutation-button";
+import { useLocation, useSearch } from "wouter";
+import { AdminAccount } from "../../../../lib/types/account";
+import Username from "../../../../components/username";
export function AccountSearchForm() {
+ const [ location, setLocation ] = useLocation();
+ const search = useSearch();
+ const urlQueryParams = useMemo(() => new URLSearchParams(search), [search]);
+ const [ searchAcct, searchRes ] = useLazySearchAccountsQuery();
+
+ // Populate search form using values from
+ // urlQueryParams, to allow paging.
const form = {
- origin: useTextInput("origin"),
- status: useTextInput("status"),
- permissions: useTextInput("permissions"),
- username: useTextInput("username"),
- display_name: useTextInput("display_name"),
- by_domain: useTextInput("by_domain"),
- email: useTextInput("email"),
- ip: useTextInput("ip"),
+ origin: useTextInput("origin", { defaultValue: urlQueryParams.get("origin") ?? ""}),
+ status: useTextInput("status", { defaultValue: urlQueryParams.get("status") ?? ""}),
+ permissions: useTextInput("permissions", { defaultValue: urlQueryParams.get("permissions") ?? ""}),
+ username: useTextInput("username", { defaultValue: urlQueryParams.get("username") ?? ""}),
+ display_name: useTextInput("display_name", { defaultValue: urlQueryParams.get("display_name") ?? ""}),
+ by_domain: useTextInput("by_domain", { defaultValue: urlQueryParams.get("by_domain") ?? ""}),
+ email: useTextInput("email", { defaultValue: urlQueryParams.get("email") ?? ""}),
+ ip: useTextInput("ip", { defaultValue: urlQueryParams.get("ip") ?? ""}),
+ limit: useTextInput("limit", { defaultValue: urlQueryParams.get("limit") ?? "50"})
};
- function submitSearch(e) {
+ // On mount, if urlQueryParams were provided,
+ // trigger the search. For example, if page
+ // was accessed at /search?origin=local&limit=20,
+ // then run a search with origin=local and
+ // limit=20 and immediately render the results.
+ useEffect(() => {
+ if (urlQueryParams.size > 0) {
+ searchAcct(Object.fromEntries(urlQueryParams), true);
+ }
+ }, [urlQueryParams, searchAcct]);
+
+ // Rather than triggering the search directly,
+ // the "submit" button changes the location
+ // based on form field params, and lets the
+ // useEffect hook above actually do the search.
+ function submitQuery(e) {
e.preventDefault();
// Parse query parameters.
@@ -52,16 +77,32 @@ export function AccountSearchForm() {
// Remove any nulls.
return kv || [];
});
- const params: SearchAccountParams = Object.fromEntries(entries);
- searchAcct(params);
+
+ const searchParams = new URLSearchParams(entries);
+ setLocation(location + "?" + searchParams.toString());
}
- const [ searchAcct, searchRes ] = useLazySearchAccountsQuery();
+ // Location to return to when user clicks "back" on the account detail view.
+ const backLocation = location + (urlQueryParams ? `?${urlQueryParams}` : "");
+
+ // Function to map an item to a list entry.
+ function itemToEntry(account: AdminAccount): ReactNode {
+ const acc = account.account;
+ return (
+
+ );
+ }
return (
<>
-
>
);
diff --git a/web/source/settings/views/moderation/menu.tsx b/web/source/settings/views/moderation/menu.tsx
index 4f01e0798..9488b8c30 100644
--- a/web/source/settings/views/moderation/menu.tsx
+++ b/web/source/settings/views/moderation/menu.tsx
@@ -28,7 +28,7 @@ import { useHasPermission } from "../../lib/navigation/util";
/**
* - /settings/moderation/reports/overview
* - /settings/moderation/reports/:reportId
- * - /settings/moderation/accounts/overview
+ * - /settings/moderation/accounts/search
* - /settings/moderation/accounts/pending
* - /settings/moderation/accounts/:accountID
* - /settings/moderation/domain-permissions/:permType
@@ -76,12 +76,12 @@ function ModerationAccountsMenu() {