From d2b3d37724a999d4cc78c46157593267e29d184e Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:18:00 +0200 Subject: [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 --- web/source/package.json | 2 + web/source/settings/components/fake-profile.tsx | 52 ---- web/source/settings/components/fake-toot.tsx | 56 ---- web/source/settings/components/profile.tsx | 52 ++++ web/source/settings/components/status.tsx | 242 +++++++++++++++ web/source/settings/components/username.tsx | 2 +- .../settings/lib/query/admin/reports/index.ts | 52 +++- web/source/settings/lib/query/gts-api.ts | 2 +- web/source/settings/lib/types/report.ts | 25 +- web/source/settings/lib/types/status.ts | 83 +++++ web/source/settings/lib/util/index.ts | 43 +++ web/source/settings/style.css | 182 +++++------ .../settings/views/admin/emoji/local/detail.tsx | 6 +- .../settings/views/admin/emoji/local/new-emoji.tsx | 6 +- .../admin/http-header-permissions/overview.tsx | 2 +- .../views/moderation/accounts/detail/index.tsx | 4 +- .../views/moderation/accounts/detail/util.tsx | 43 --- .../views/moderation/accounts/search/index.tsx | 2 +- .../settings/views/moderation/reports/detail.tsx | 335 +++++++++++---------- .../settings/views/moderation/reports/overview.tsx | 97 ------ .../settings/views/moderation/reports/search.tsx | 252 ++++++++++++++++ web/source/settings/views/moderation/router.tsx | 5 +- web/source/settings/views/user/profile.tsx | 2 +- web/source/yarn.lock | 88 ++++++ 24 files changed, 1089 insertions(+), 546 deletions(-) delete mode 100644 web/source/settings/components/fake-profile.tsx delete mode 100644 web/source/settings/components/fake-toot.tsx create mode 100644 web/source/settings/components/profile.tsx create mode 100644 web/source/settings/components/status.tsx create mode 100644 web/source/settings/lib/types/status.ts create mode 100644 web/source/settings/lib/util/index.ts delete mode 100644 web/source/settings/views/moderation/accounts/detail/util.tsx delete mode 100644 web/source/settings/views/moderation/reports/overview.tsx create mode 100644 web/source/settings/views/moderation/reports/search.tsx (limited to 'web/source') diff --git a/web/source/package.json b/web/source/package.json index e90176308..bce3546d2 100644 --- a/web/source/package.json +++ b/web/source/package.json @@ -33,6 +33,7 @@ "react-redux": "^8.1.3", "redux": "^4.2.0", "redux-persist": "^6.0.0", + "sanitize-html": "^2.13.0", "skulk": "^0.0.8-fix", "wouter": "^3.1.0" }, @@ -49,6 +50,7 @@ "@types/parse-link-header": "^2.0.3", "@types/psl": "^1.1.1", "@types/react-dom": "^18.2.8", + "@types/sanitize-html": "^2.11.0", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", "autoprefixer": "^10.4.19", diff --git a/web/source/settings/components/fake-profile.tsx b/web/source/settings/components/fake-profile.tsx deleted file mode 100644 index 4a5157378..000000000 --- a/web/source/settings/components/fake-profile.tsx +++ /dev/null @@ -1,52 +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"; - -export default function FakeProfile({ avatar, header, display_name, username, role }) { - return ( // Keep in sync with web/template/profile.tmpl -
-
-
- {header -
- -
-
- ); -} diff --git a/web/source/settings/components/fake-toot.tsx b/web/source/settings/components/fake-toot.tsx deleted file mode 100644 index ad0c387a4..000000000 --- a/web/source/settings/components/fake-toot.tsx +++ /dev/null @@ -1,56 +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 { useVerifyCredentialsQuery } from "../lib/query/oauth"; - -export default function FakeToot({ children }) { - const { data: account = { - avatar: "/assets/default_avatars/GoToSocial_icon1.png", - display_name: "", - username: "" - } } = useVerifyCredentialsQuery(); - - return ( - - ); -} diff --git a/web/source/settings/components/profile.tsx b/web/source/settings/components/profile.tsx new file mode 100644 index 000000000..4a5157378 --- /dev/null +++ b/web/source/settings/components/profile.tsx @@ -0,0 +1,52 @@ +/* + 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"; + +export default function FakeProfile({ avatar, header, display_name, username, role }) { + return ( // Keep in sync with web/template/profile.tmpl +
+
+
+ {header +
+ +
+
+ ); +} diff --git a/web/source/settings/components/status.tsx b/web/source/settings/components/status.tsx new file mode 100644 index 000000000..56b061d39 --- /dev/null +++ b/web/source/settings/components/status.tsx @@ -0,0 +1,242 @@ +/* + 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 { useVerifyCredentialsQuery } from "../lib/query/oauth"; +import { MediaAttachment, Status as StatusType } from "../lib/types/status"; +import sanitize from "sanitize-html"; + +export function FakeStatus({ children }) { + const { data: account = { + avatar: "/assets/default_avatars/GoToSocial_icon1.png", + display_name: "", + username: "" + } } = useVerifyCredentialsQuery(); + + return ( + + ); +} + +export function Status({ status }: { status: StatusType }) { + return ( + + ); +} + +function StatusHeader({ status }: { status: StatusType }) { + const author = status.account; + + return ( +
+
+ +
+
+ ); +} + +function StatusBody({ status }: { status: StatusType }) { + let content: string; + if (status.content.length === 0) { + content = "[no content set]"; + } else { + // HTML has already been through + // the instance sanitizer by now, + // but do it again just in case. + content = sanitize(status.content); + } + + return ( +
+
+ + + { status.spoiler_text + ? status.spoiler_text + " " + : "[no content warning set] " + } + + + Toggle content visibility + + +
+
+ +
+ ); +} + +function StatusMedia({ status }: { status: StatusType }) { + if (status.media_attachments.length === 0) { + return null; + } + + const count = status.media_attachments.length; + const aria_label = count === 1 ? "1 attachment" : `${count} attachments`; + const oddOrEven = count % 2 === 0 ? "even" : "odd"; + const single = count === 1 ? " single" : ""; + + return ( +
+ { status.media_attachments.map((media) => { + return ( + + ); + })} +
+ ); +} + +function StatusMediaEntry({ media }: { media: MediaAttachment }) { + return ( +
+
+ + + + + + + {media.description} + + + {media.description} + +
+
+ ); +} + +function StatusFooter({ status }: { status: StatusType }) { + return ( + + ); +} diff --git a/web/source/settings/components/username.tsx b/web/source/settings/components/username.tsx index f7be1cd4a..56ba67c4f 100644 --- a/web/source/settings/components/username.tsx +++ b/web/source/settings/components/username.tsx @@ -60,7 +60,7 @@ export default function Username({ account, linkTo, backLocation, classNames }: ); if (linkTo) { - className += " spanlink"; + className += " pseudolink"; return ( ({ - listReports: build.query({ - query: (params) => ({ - url: "/api/v1/admin/reports", - params: { - // Override provided limit. - limit: 100, - ...params + searchReports: build.query({ + query: (form) => { + const params = new(URLSearchParams); + Object.entries(form).forEach(([k, v]) => { + if (v !== undefined) { + params.append(k, v); + } + }); + + let query = ""; + if (params.size !== 0) { + query = `?${params.toString()}`; } - }), - providesTags: [{ type: "Reports", id: "LIST" }] + + return { + url: `/api/v1/admin/reports${query}` + }; + }, + // Headers required for paging. + transformResponse: (apiResp: AdminReport[], meta) => { + const accounts = apiResp; + const linksStr = meta?.response?.headers.get("Link"); + const links = parse(linksStr); + return { accounts, links }; + }, + // Only provide LIST tag id since this model is not the + // same as getReport model (due to transformResponse). + providesTags: [{ type: "Report", id: "TRANSFORMED" }] }), getReport: build.query({ query: (id) => ({ url: `/api/v1/admin/reports/${id}` }), - providesTags: (_res, _error, id) => [{ type: "Reports", id }] + providesTags: (_result, _error, id) => [ + { type: 'Report', id } + ], }), resolveReport: build.mutation({ @@ -55,8 +77,8 @@ const extended = gtsApi.injectEndpoints({ }), invalidatesTags: (res) => res - ? [{ type: "Reports", id: "LIST" }, { type: "Reports", id: res.id }] - : [{ type: "Reports", id: "LIST" }] + ? [{ type: "Report", id: "LIST" }, { type: "Report", id: res.id }] + : [{ type: "Report", id: "LIST" }] }) }) }); @@ -64,7 +86,7 @@ const extended = gtsApi.injectEndpoints({ /** * List reports received on this instance, filtered using given parameters. */ -const useListReportsQuery = extended.useListReportsQuery; +const useLazySearchReportsQuery = extended.useLazySearchReportsQuery; /** * Get a single report by its ID. @@ -77,7 +99,7 @@ const useGetReportQuery = extended.useGetReportQuery; const useResolveReportMutation = extended.useResolveReportMutation; export { - useListReportsQuery, + useLazySearchReportsQuery, useGetReportQuery, useResolveReportMutation, }; diff --git a/web/source/settings/lib/query/gts-api.ts b/web/source/settings/lib/query/gts-api.ts index ef994e655..f96a55fda 100644 --- a/web/source/settings/lib/query/gts-api.ts +++ b/web/source/settings/lib/query/gts-api.ts @@ -136,7 +136,7 @@ export const gtsApi = createApi({ tagTypes: [ "Auth", "Emoji", - "Reports", + "Report", "Account", "InstanceRules", "HTTPHeaderAllows", diff --git a/web/source/settings/lib/types/report.ts b/web/source/settings/lib/types/report.ts index bb3d53c27..4ef694be6 100644 --- a/web/source/settings/lib/types/report.ts +++ b/web/source/settings/lib/types/report.ts @@ -17,6 +17,10 @@ along with this program. If not, see . */ +import { Links } from "parse-link-header"; +import { AdminAccount } from "./account"; +import { Status } from "./status"; + /** * Admin model of a report. Differs from the client * model, which contains less detailed information. @@ -56,29 +60,25 @@ export interface AdminReport { updated_at: string; /** * Account that created the report. - * TODO: model this properly. */ - account: Object; + account: AdminAccount; /** * Reported account. - * TODO: model this properly. */ - target_account: Object; + target_account: AdminAccount; /** * Admin account assigned to handle this report, if any. - * TODO: model this properly. */ - assigned_account?: Object; + assigned_account?: AdminAccount; /** * Admin account that has taken action on this report, if any. - * TODO: model this properly. */ - action_taken_by_account?: Object; + action_taken_by_account?: AdminAccount; /** * Statuses cited by this report, if any. * TODO: model this properly. */ - statuses: Object[]; + statuses: Status[]; /** * Rules broken according to the reporter, if any. * TODO: model this properly. @@ -108,7 +108,7 @@ export interface AdminReportResolveParams { /** * Parameters for GET to /api/v1/admin/reports. */ -export interface AdminReportListParams { +export interface AdminSearchReportParams { /** * If set, show only resolved (true) or only unresolved (false) reports. */ @@ -142,3 +142,8 @@ export interface AdminReportListParams { */ limit?: number; } + +export interface AdminSearchReportResp { + accounts: AdminReport[]; + links: Links | null; +} diff --git a/web/source/settings/lib/types/status.ts b/web/source/settings/lib/types/status.ts new file mode 100644 index 000000000..e46f4a6b7 --- /dev/null +++ b/web/source/settings/lib/types/status.ts @@ -0,0 +1,83 @@ +/* + 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 { Account } from "./account"; +import { CustomEmoji } from "./custom-emoji"; + +export interface Status { + id: string; + created_at: string; + in_reply_to_id: string | null; + in_reply_to_account_id: string | null; + sensitive: boolean; + spoiler_text: string; + visibility: string; + language: string; + uri: string; + url: string; + replies_count: number; + reblogs_count: number; + favourites_count: number; + favourited: boolean; + reblogged: boolean; + muted: boolean; + bookmarked: boolean; + pinned: boolean; + content: string, + reblog: Status | null, + account: Account, + media_attachments: MediaAttachment[], + mentions: []; + tags: []; + emojis: CustomEmoji[]; + card: null; + poll: null; +} + +export interface MediaAttachment { + id: string; + type: string; + url: string; + text_url: string; + preview_url: string; + remote_url: string | null; + preview_remote_url: string | null; + meta: MediaAttachmentMeta; + description: string; + blurhash: string; +} + +interface MediaAttachmentMeta { + original: { + width: number; + height: number; + size: string; + aspect: number; + }, + small: { + width: number; + height: number; + size: string; + aspect: number; + }, + focus: { + x: number; + y: number; + } +} diff --git a/web/source/settings/lib/util/index.ts b/web/source/settings/lib/util/index.ts new file mode 100644 index 000000000..d016f3398 --- /dev/null +++ b/web/source/settings/lib/util/index.ts @@ -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 "../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/style.css b/web/source/settings/style.css index d2420bdfc..cdae6b972 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -1045,62 +1045,62 @@ button.with-padding { } } -.reports { - p { - margin: 0; - } - +.reports-view { .report { display: flex; flex-direction: column; + flex-wrap: nowrap; gap: 0.5rem; - margin: 0.5rem 0; - - text-decoration: none; color: $fg; - - padding: 1rem; - - border: none; border-left: 0.3rem solid $border-accent; - .usernames { - line-height: 2rem; - } - - .byline { - display: grid; - grid-template-columns: 1fr auto; - gap: 0.5rem; + .username-lozenge { + display: flex; + flex-wrap: nowrap; + height: 100%; + align-items: center; + padding-top: 0; + padding-bottom: 0; - .report-status { - color: $border-accent; + .fa { + flex-shrink: 0; } } - .details { - display: grid; - grid-template-columns: auto 1fr; - gap: 0.2rem 0.5rem; - padding: 0.5rem; - - justify-items: start; + .report-byline { + max-width: fit-content; } - h3 { - margin: 0; + .info-list { + border: none; + + .info-list-entry { + background: none; + padding: 0; + + .report-target .username-lozenge { + color: $bg; + } + + .reported-by .username-lozenge { + color: $fg; + font-weight: initial; + border-radius: 0; + background: none; + } + } } &.resolved { - color: $fg-reduced; - border-left: 0.4rem solid $bg; + border-left: 0.3rem solid $list-entry-bg; - .byline .report-status { + .info-list, + .info-list .info-list-entry .reported-by .username-lozenge { color: $fg-reduced; } - - .user { - opacity: 0.8; + + &:hover { + border-color: $fg-accent; } } @@ -1109,72 +1109,42 @@ button.with-padding { padding: 0; } } +} - .report.detail { - display: flex; - flex-direction: column; - margin-top: 1rem; - gap: 1rem; - - .info-block { - padding: 0.5rem; - background: $gray2; - } - - .info { - display: block; - } - - .reported-toots { - margin-top: 0.5rem; +.report-detail { + .info-list { + + &.overview { + margin-top: 1rem; } - .toot .toot-info { - padding: 0.5rem; - background: $toot-info-bg; - - a { - color: $fg-reduced; - } + .username-lozenge { + display: flex; + flex-wrap: nowrap; + height: 100%; + align-items: center; + padding-top: 0; + padding-bottom: 0; + max-width: fit-content; - &:last-child { - border-bottom-left-radius: $br; - border-bottom-right-radius: $br; + .fa { + flex-shrink: 0; } } } -} - -.username-lozenge { - line-height: 1.3rem; - display: inline-block; - background: $fg-accent; - color: $bg; - border-radius: $br; - padding: 0.15rem; - font-weight: bold; - text-decoration: none; - - .acct { - word-break: break-all; - } - &.suspended { - background: $bg-accent; - color: $fg; - text-decoration: line-through; - } + .report-statuses { + width: min(100%, 50rem); - &.local { - background: $green1; + .thread { + display: flex; + flex-direction: column; + gap: 2rem; + padding: 0; + } } } -.spanlink { - cursor: pointer; - text-decoration: none; -} - .accounts-view { .pageable-list { .username-lozenge { @@ -1223,6 +1193,36 @@ button.with-padding { } } +.username-lozenge { + line-height: 1.3rem; + display: inline-block; + background: $fg-accent; + color: $bg; + border-radius: $br; + padding: 0.15rem; + font-weight: bold; + text-decoration: none; + + .acct { + word-break: break-all; + } + + &.suspended { + background: $bg-accent; + color: $fg; + text-decoration: line-through; + } + + &.local { + background: $green1; + } +} + +.pseudolink { + cursor: pointer; + text-decoration: none; +} + .info-list { border: 0.1rem solid $gray1; display: flex; diff --git a/web/source/settings/views/admin/emoji/local/detail.tsx b/web/source/settings/views/admin/emoji/local/detail.tsx index 2913b6c17..4126bbedc 100644 --- a/web/source/settings/views/admin/emoji/local/detail.tsx +++ b/web/source/settings/views/admin/emoji/local/detail.tsx @@ -22,7 +22,7 @@ import { Redirect, useParams } from "wouter"; import { useComboBoxInput, useFileInput, useValue } from "../../../../lib/form"; import useFormSubmit from "../../../../lib/form/submit"; import { useBaseUrl } from "../../../../lib/navigation/util"; -import FakeToot from "../../../../components/fake-toot"; +import { FakeStatus } from "../../../../components/status"; import FormWithData from "../../../../lib/form/form-with-data"; import Loading from "../../../../components/loading"; import { FileInput } from "../../../../components/form/inputs"; @@ -124,14 +124,14 @@ function EmojiDetailForm({ data: emoji }) { disabled={!form.image.value} /> - + Look at this new custom emoji {emoji.shortcode} isn't it cool? - + {result.error && } {deleteResult.error && } diff --git a/web/source/settings/views/admin/emoji/local/new-emoji.tsx b/web/source/settings/views/admin/emoji/local/new-emoji.tsx index 20f45f372..f2f5a56b1 100644 --- a/web/source/settings/views/admin/emoji/local/new-emoji.tsx +++ b/web/source/settings/views/admin/emoji/local/new-emoji.tsx @@ -23,7 +23,7 @@ import useShortcode from "./use-shortcode"; import useFormSubmit from "../../../../lib/form/submit"; import { TextInput, FileInput } from "../../../../components/form/inputs"; import { CategorySelect } from '../category-select'; -import FakeToot from "../../../../components/fake-toot"; +import { FakeStatus } from "../../../../components/status"; import MutationButton from "../../../../components/form/mutation-button"; import { useAddEmojiMutation } from "../../../../lib/query/admin/custom-emoji"; import { useInstanceV1Query } from "../../../../lib/query/gts-api"; @@ -103,9 +103,9 @@ export default function NewEmojiForm() {

Add new custom emoji

- + Look at this new custom emoji {emojiOrShortcode} isn't it cool? - +
{ // When clicking on a header perm, // go to the detail view for perm. diff --git a/web/source/settings/views/moderation/accounts/detail/index.tsx b/web/source/settings/views/moderation/accounts/detail/index.tsx index 830a894cb..958a3121b 100644 --- a/web/source/settings/views/moderation/accounts/detail/index.tsx +++ b/web/source/settings/views/moderation/accounts/detail/index.tsx @@ -21,13 +21,13 @@ import React from "react"; import { useGetAccountQuery } from "../../../../lib/query/admin"; import FormWithData from "../../../../lib/form/form-with-data"; -import FakeProfile from "../../../../components/fake-profile"; +import FakeProfile from "../../../../components/profile"; import { AdminAccount } from "../../../../lib/types/account"; 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"; +import { UseOurInstanceAccount, yesOrNo } from "../../../../lib/util"; export default function AccountDetail() { const params: { accountID: string } = useParams(); diff --git a/web/source/settings/views/moderation/accounts/detail/util.tsx b/web/source/settings/views/moderation/accounts/detail/util.tsx deleted file mode 100644 index b82d44a6e..000000000 --- a/web/source/settings/views/moderation/accounts/detail/util.tsx +++ /dev/null @@ -1,43 +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 { 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/search/index.tsx b/web/source/settings/views/moderation/accounts/search/index.tsx index 16e89ce43..f37e22a66 100644 --- a/web/source/settings/views/moderation/accounts/search/index.tsx +++ b/web/source/settings/views/moderation/accounts/search/index.tsx @@ -83,7 +83,7 @@ export function AccountSearchForm() { } // Location to return to when user clicks "back" on the account detail view. - const backLocation = location + (urlQueryParams ? `?${urlQueryParams}` : ""); + const backLocation = location + (urlQueryParams.size > 0 ? `?${urlQueryParams}` : ""); // Function to map an item to a list entry. function itemToEntry(account: AdminAccount): ReactNode { diff --git a/web/source/settings/views/moderation/reports/detail.tsx b/web/source/settings/views/moderation/reports/detail.tsx index ad8d69a47..7d6e542fb 100644 --- a/web/source/settings/views/moderation/reports/detail.tsx +++ b/web/source/settings/views/moderation/reports/detail.tsx @@ -17,8 +17,8 @@ along with this program. If not, see . */ -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 ( -
-

Report Details

+
+

Report Details

); } -function ReportDetailForm({ data: report }) { +function ReportDetailForm({ data: report }: { data: AdminReport }) { + const [ location ] = useLocation(); + const baseUrl = useBaseUrl(); + + return ( + <> + + + { report.action_taken + && + } + + { report.statuses && + + } + + { !report.action_taken && + + } + + ); +} + +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 ( -
-
- - <> reported - +
+
+
Reported account
+
+ +
+
+ +
+
Reported by
+
+ +
- {report.action_taken && -
-

Resolved by @{report.action_taken_by_account.account.acct}

- at {new Date(report.action_taken_at).toLocaleString()} -
- Comment: {report.action_taken_comment} -
- } +
+
Status
+
+ { report.action_taken + ? <>{status} + : {status} + } +
+
-
-

Report info:

-
- Created: - {new Date(report.created_at).toLocaleString()} +
+
Reason
+
+ { comment.length > 0 + ? <>{comment} + : none provided + } +
+
- Forwarded: {report.forwarded ? "Yes" : "No"} - Category: {report.category} +
+
Created
+
+ +
+
- Reason: - {report.comment.length > 0 - ?

{report.comment}

- : none provided - } +
+
Category
+
{ report.category }
+
-
+
+
Forwarded
+
{ yesOrNo(report.forwarded) }
+
+ ); +} - {!report.action_taken && } - - { - report.statuses.length > 0 && -
-

Reported toots ({report.statuses.length}):

-
- {report.statuses.map((status) => ( - - ))} -
+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 ( + <> +

Moderation History

+
+
+
Handled by
+
+ +
- } -
+ +
+
Handled
+
+ +
+
+ +
+
Comment
+
{ report.action_taken_comment ?? "none"}
+
+ + ); } @@ -118,13 +206,18 @@ function ReportActionForm({ report }) { const [submit, result] = useFormSubmit(form, useResolveReportMutation(), { changedOnly: false }); return ( - -

Resolving this report

-

+ +

Resolve this report

+ <> 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.
- This will be visible to the user that created the report! -

+ This is useful for providing an explanation about what action was + taken (if any) before the report was marked as resolved. +
+ + Any comment made here will be visible + to the user that created the report! + +