diff options
Diffstat (limited to 'web/source/settings/admin')
30 files changed, 0 insertions, 3659 deletions
diff --git a/web/source/settings/admin/accounts/detail/actions.tsx b/web/source/settings/admin/accounts/detail/actions.tsx deleted file mode 100644 index 75ab8db6e..000000000 --- a/web/source/settings/admin/accounts/detail/actions.tsx +++ /dev/null @@ -1,89 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; - -import { useActionAccountMutation } from "../../../lib/query"; - -import MutationButton from "../../../components/form/mutation-button"; - -import useFormSubmit from "../../../lib/form/submit"; -import { - useValue, - useTextInput, - useBoolInput, -} from "../../../lib/form"; - -import { Checkbox, TextInput } from "../../../components/form/inputs"; -import { AdminAccount } from "../../../lib/types/account"; - -export interface AccountActionsProps { - account: AdminAccount, -} - -export function AccountActions({ account }: AccountActionsProps) { - const form = { - id: useValue("id", account.id), - reason: useTextInput("text") - }; - - const reallySuspend = useBoolInput("reallySuspend"); - const [accountAction, result] = useFormSubmit(form, useActionAccountMutation()); - - return ( - <form - onSubmit={accountAction} - aria-labelledby="account-moderation-actions" - > - <h3 id="account-moderation-actions">Account Moderation Actions</h3> - <div> - Currently only the "suspend" action is implemented.<br/> - Suspending an account will delete it from your server, and remove all of its media, posts, relationships, etc.<br/> - If the suspended account is local, suspending will also send out a "delete" message to other servers, requesting them to remove its data from their instance as well.<br/> - <b>Account suspension cannot be reversed.</b> - </div> - <TextInput - field={form.reason} - placeholder="Reason for this action" - /> - <div className="action-buttons"> - {/* <MutationButton - label="Disable" - name="disable" - result={result} - /> - <MutationButton - label="Silence" - name="silence" - result={result} - /> */} - <MutationButton - disabled={account.suspended || reallySuspend.value === undefined || reallySuspend.value === false} - label="Suspend" - name="suspend" - result={result} - /> - <Checkbox - label="Really suspend" - field={reallySuspend} - ></Checkbox> - </div> - </form> - ); -} diff --git a/web/source/settings/admin/accounts/detail/handlesignup.tsx b/web/source/settings/admin/accounts/detail/handlesignup.tsx deleted file mode 100644 index a61145a22..000000000 --- a/web/source/settings/admin/accounts/detail/handlesignup.tsx +++ /dev/null @@ -1,118 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; -import { useLocation } from "wouter"; - -import { useHandleSignupMutation } from "../../../lib/query"; - -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, - accountsBaseUrl: string, -} - -export function HandleSignup({account, accountsBaseUrl}: 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(accountsBaseUrl); - } - } - }); - - return ( - <form - onSubmit={handleSignup} - aria-labelledby="account-handle-signup" - > - <h3 id="account-handle-signup">Handle Account Sign-Up</h3> - <Select - field={form.approveOrReject} - label="Approve or Reject" - options={ - <> - <option value="approve">Approve</option> - <option value="reject">Reject</option> - </> - } - > - </Select> - { form.approveOrReject.value === "reject" && - // Only show form fields relevant - // to "reject" if rejecting. - // On "approve" these fields will - // be ignored anyway. - <> - <TextInput - field={form.privateComment} - label="(Optional) private comment on why sign-up was rejected (shown to other admins only)" - /> - <Checkbox - field={form.sendEmail} - label="Send email to applicant" - /> - <TextInput - field={form.message} - label={"(Optional) message to include in email to applicant, if send email is checked"} - /> - </> } - <MutationButton - disabled={false} - label={form.approveOrReject.value === "approve" ? "Approve" : "Reject"} - result={result} - /> - </form> - ); -} diff --git a/web/source/settings/admin/accounts/detail/index.tsx b/web/source/settings/admin/accounts/detail/index.tsx deleted file mode 100644 index 79eb493de..000000000 --- a/web/source/settings/admin/accounts/detail/index.tsx +++ /dev/null @@ -1,179 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; -import { useRoute, Redirect } from "wouter"; - -import { useGetAccountQuery } from "../../../lib/query"; - -import FormWithData from "../../../lib/form/form-with-data"; - -import { useBaseUrl } from "../../../lib/navigation/util"; -import FakeProfile from "../../../components/fake-profile"; - -import { AdminAccount } from "../../../lib/types/account"; -import { HandleSignup } from "./handlesignup"; -import { AccountActions } from "./actions"; -import BackButton from "../../../components/back-button"; - -export default function AccountDetail() { - // /settings/admin/accounts - const accountsBaseUrl = useBaseUrl(); - - let [_match, params] = useRoute(`${accountsBaseUrl}/:accountId`); - - if (params?.accountId == undefined) { - return <Redirect to={accountsBaseUrl} />; - } else { - return ( - <div className="account-detail"> - <h1 className="text-cutoff"> - <BackButton to={accountsBaseUrl} /> Account Details - </h1> - <FormWithData - dataQuery={useGetAccountQuery} - queryArg={params.accountId} - DataForm={AccountDetailForm} - {...{accountsBaseUrl}} - /> - </div> - ); - } -} - -interface AccountDetailFormProps { - accountsBaseUrl: string, - data: AdminAccount, -} - -function AccountDetailForm({ data: adminAcct, accountsBaseUrl }: AccountDetailFormProps) { - let yesOrNo = (b: boolean) => { - return b ? "yes" : "no"; - }; - - let 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 ( - <> - <FakeProfile {...adminAcct.account} /> - <h3>General Account Details</h3> - { adminAcct.suspended && - <div className="info"> - <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i> - <b>Account is suspended.</b> - </div> - } - <dl className="info-list"> - { !local && - <div className="info-list-entry"> - <dt>Domain</dt> - <dd>{adminAcct.domain}</dd> - </div>} - <div className="info-list-entry"> - <dt>Created</dt> - <dd><time dateTime={adminAcct.created_at}>{created}</time></dd> - </div> - <div className="info-list-entry"> - <dt>Last posted</dt> - <dd>{lastPosted}</dd> - </div> - <div className="info-list-entry"> - <dt>Suspended</dt> - <dd>{yesOrNo(adminAcct.suspended)}</dd> - </div> - <div className="info-list-entry"> - <dt>Silenced</dt> - <dd>{yesOrNo(adminAcct.silenced)}</dd> - </div> - <div className="info-list-entry"> - <dt>Statuses</dt> - <dd>{adminAcct.account.statuses_count}</dd> - </div> - <div className="info-list-entry"> - <dt>Followers</dt> - <dd>{adminAcct.account.followers_count}</dd> - </div> - <div className="info-list-entry"> - <dt>Following</dt> - <dd>{adminAcct.account.following_count}</dd> - </div> - </dl> - { local && - // Only show local account details - // if this is a local account! - <> - <h3>Local Account Details</h3> - { !adminAcct.approved && - <div className="info"> - <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i> - <b>Account is pending.</b> - </div> - } - { !adminAcct.confirmed && - <div className="info"> - <i className="fa fa-fw fa-info-circle" aria-hidden="true"></i> - <b>Account email not yet confirmed.</b> - </div> - } - <dl className="info-list"> - <div className="info-list-entry"> - <dt>Email</dt> - <dd>{adminAcct.email} {<b>{adminAcct.confirmed ? "(confirmed)" : "(not confirmed)"}</b> }</dd> - </div> - <div className="info-list-entry"> - <dt>Disabled</dt> - <dd>{yesOrNo(adminAcct.disabled)}</dd> - </div> - <div className="info-list-entry"> - <dt>Approved</dt> - <dd>{yesOrNo(adminAcct.approved)}</dd> - </div> - <div className="info-list-entry"> - <dt>Sign-Up Reason</dt> - <dd>{adminAcct.invite_request ?? <i>none provided</i>}</dd> - </div> - { (adminAcct.ip && adminAcct.ip !== "0.0.0.0") && - <div className="info-list-entry"> - <dt>Sign-Up IP</dt> - <dd>{adminAcct.ip}</dd> - </div> } - { adminAcct.locale && - <div className="info-list-entry"> - <dt>Locale</dt> - <dd>{adminAcct.locale}</dd> - </div> } - </dl> - </> } - { local && !adminAcct.approved - ? - <HandleSignup - account={adminAcct} - accountsBaseUrl={accountsBaseUrl} - /> - : - <AccountActions account={adminAcct} /> - } - </> - ); -} diff --git a/web/source/settings/admin/accounts/index.tsx b/web/source/settings/admin/accounts/index.tsx deleted file mode 100644 index 3c69f7406..000000000 --- a/web/source/settings/admin/accounts/index.tsx +++ /dev/null @@ -1,49 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; -import { Switch, Route } from "wouter"; - -import AccountDetail from "./detail"; -import { AccountSearchForm } from "./search"; - -export default function Accounts({ baseUrl }) { - return ( - <Switch> - <Route path={`${baseUrl}/:accountId`}> - <AccountDetail /> - </Route> - <AccountOverview /> - </Switch> - ); -} - -function AccountOverview({ }) { - return ( - <div className="accounts-view"> - <h1>Accounts Overview</h1> - <span> - You can perform actions on an account by clicking - its name in a report, or by searching for the account - using the form below and clicking on its name. - </span> - <AccountSearchForm /> - </div> - ); -} diff --git a/web/source/settings/admin/accounts/pending/index.tsx b/web/source/settings/admin/accounts/pending/index.tsx deleted file mode 100644 index 459472147..000000000 --- a/web/source/settings/admin/accounts/pending/index.tsx +++ /dev/null @@ -1,40 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; -import { useSearchAccountsQuery } from "../../../lib/query"; -import { AccountList } from "../../../components/account-list"; - -export default function AccountsPending() { - const searchRes = useSearchAccountsQuery({status: "pending"}); - - return ( - <div className="accounts-view"> - <h1>Pending Accounts</h1> - <AccountList - isLoading={searchRes.isLoading} - isSuccess={searchRes.isSuccess} - data={searchRes.data} - isError={searchRes.isError} - error={searchRes.error} - emptyMessage="No pending account sign-ups." - /> - </div> - ); -} diff --git a/web/source/settings/admin/accounts/search/index.tsx b/web/source/settings/admin/accounts/search/index.tsx deleted file mode 100644 index 560bbb76a..000000000 --- a/web/source/settings/admin/accounts/search/index.tsx +++ /dev/null @@ -1,125 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; - -import { useLazySearchAccountsQuery } from "../../../lib/query"; -import { useTextInput } from "../../../lib/form"; - -import { AccountList } from "../../../components/account-list"; -import { SearchAccountParams } from "../../../lib/types/account"; -import { Select, TextInput } from "../../../components/form/inputs"; -import MutationButton from "../../../components/form/mutation-button"; - -export function AccountSearchForm() { - const [searchAcct, searchRes] = useLazySearchAccountsQuery(); - - 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"), - }; - - function submitSearch(e) { - e.preventDefault(); - - // Parse query parameters. - const entries = Object.entries(form).map(([k, v]) => { - // Take only defined form fields. - if (v.value === undefined || v.value.length === 0) { - return null; - } - return [[k, v.value]]; - }).flatMap(kv => { - // Remove any nulls. - return kv || []; - }); - - const params: SearchAccountParams = Object.fromEntries(entries); - searchAcct(params); - } - - return ( - <> - <form onSubmit={submitSearch}> - <TextInput - field={form.username} - label={"(Optional) username (without leading '@' symbol)"} - placeholder="someone" - /> - <TextInput - field={form.by_domain} - label={"(Optional) domain"} - placeholder="example.org" - /> - <Select - field={form.origin} - label="Account origin" - options={ - <> - <option value="">Local or remote</option> - <option value="local">Local only</option> - <option value="remote">Remote only</option> - </> - } - ></Select> - <TextInput - field={form.email} - label={"(Optional) email address (local accounts only)"} - placeholder={"someone@example.org"} - /> - <TextInput - field={form.ip} - label={"(Optional) IP address (local accounts only)"} - placeholder={"198.51.100.0"} - /> - <Select - field={form.status} - label="Account status" - options={ - <> - <option value="">Any</option> - <option value="pending">Pending only</option> - <option value="disabled">Disabled only</option> - <option value="suspended">Suspended only</option> - </> - } - ></Select> - <MutationButton - disabled={false} - label={"Search"} - result={searchRes} - /> - </form> - <AccountList - isLoading={searchRes.isLoading} - isSuccess={searchRes.isSuccess} - data={searchRes.data} - isError={searchRes.isError} - error={searchRes.error} - emptyMessage="No accounts found that match your query" - /> - </> - ); -} diff --git a/web/source/settings/admin/actions/keys/expireremote.tsx b/web/source/settings/admin/actions/keys/expireremote.tsx deleted file mode 100644 index 3b5da2836..000000000 --- a/web/source/settings/admin/actions/keys/expireremote.tsx +++ /dev/null @@ -1,63 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; - -import { useInstanceKeysExpireMutation } from "../../../lib/query"; - -import { useTextInput } from "../../../lib/form"; -import { TextInput } from "../../../components/form/inputs"; - -import MutationButton from "../../../components/form/mutation-button"; - -export default function ExpireRemote({}) { - const domainField = useTextInput("domain"); - - const [expire, expireResult] = useInstanceKeysExpireMutation(); - - function submitExpire(e) { - e.preventDefault(); - expire(domainField.value); - } - - return ( - <form onSubmit={submitExpire}> - <h2>Expire remote instance keys</h2> - <p> - Mark all public keys from the given remote instance as expired.<br/><br/> - This is useful in cases where the remote domain has had to rotate their keys for whatever - reason (security issue, data leak, routine safety procedure, etc), and your instance can no - longer communicate with theirs properly using cached keys. A key marked as expired in this way - will be lazily refetched next time a request is made to your instance signed by the owner of that - key. - </p> - <TextInput - field={domainField} - label="Domain" - type="string" - placeholder="example.org" - /> - <MutationButton - disabled={false} - label="Expire keys" - result={expireResult} - /> - </form> - ); -} diff --git a/web/source/settings/admin/actions/keys/index.tsx b/web/source/settings/admin/actions/keys/index.tsx deleted file mode 100644 index 74bfd36ee..000000000 --- a/web/source/settings/admin/actions/keys/index.tsx +++ /dev/null @@ -1,30 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; -import ExpireRemote from "./expireremote"; - -export default function Keys() { - return ( - <> - <h1>Key Actions</h1> - <ExpireRemote /> - </> - ); -} diff --git a/web/source/settings/admin/actions/media/cleanup.tsx b/web/source/settings/admin/actions/media/cleanup.tsx deleted file mode 100644 index fd3ca1f41..000000000 --- a/web/source/settings/admin/actions/media/cleanup.tsx +++ /dev/null @@ -1,61 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; - -import { useMediaCleanupMutation } from "../../../lib/query"; - -import { useTextInput } from "../../../lib/form"; -import { TextInput } from "../../../components/form/inputs"; - -import MutationButton from "../../../components/form/mutation-button"; - -export default function Cleanup({}) { - const daysField = useTextInput("days", { defaultValue: "30" }); - - const [mediaCleanup, mediaCleanupResult] = useMediaCleanupMutation(); - - function submitCleanup(e) { - e.preventDefault(); - mediaCleanup(daysField.value); - } - - return ( - <form onSubmit={submitCleanup}> - <h2>Cleanup</h2> - <p> - Clean up remote media older than the specified number of days. - If the remote instance is still online they will be refetched when needed. - Also cleans up unused headers and avatars from the media cache. - </p> - <TextInput - field={daysField} - label="Days" - type="number" - min="0" - placeholder="30" - /> - <MutationButton - disabled={false} - label="Remove old media" - result={mediaCleanupResult} - /> - </form> - ); -} diff --git a/web/source/settings/admin/actions/media/index.tsx b/web/source/settings/admin/actions/media/index.tsx deleted file mode 100644 index b3b805986..000000000 --- a/web/source/settings/admin/actions/media/index.tsx +++ /dev/null @@ -1,30 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; -import Cleanup from "./cleanup"; - -export default function Media() { - return ( - <> - <h1>Media Actions</h1> - <Cleanup /> - </> - ); -} diff --git a/web/source/settings/admin/domain-permissions/detail.tsx b/web/source/settings/admin/domain-permissions/detail.tsx deleted file mode 100644 index f74802666..000000000 --- a/web/source/settings/admin/domain-permissions/detail.tsx +++ /dev/null @@ -1,254 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; - -import { useMemo } from "react"; -import { useLocation } from "wouter"; - -import { useTextInput, useBoolInput } from "../../lib/form"; - -import useFormSubmit from "../../lib/form/submit"; - -import { TextInput, Checkbox, TextArea } from "../../components/form/inputs"; - -import Loading from "../../components/loading"; -import BackButton from "../../components/back-button"; -import MutationButton from "../../components/form/mutation-button"; - -import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../lib/query/admin/domain-permissions/get"; -import { useAddDomainAllowMutation, useAddDomainBlockMutation, useRemoveDomainAllowMutation, useRemoveDomainBlockMutation } from "../../lib/query/admin/domain-permissions/update"; -import { DomainPerm, PermType } from "../../lib/types/domain-permission"; -import { NoArg } from "../../lib/types/query"; -import { Error } from "../../components/error"; - -export interface DomainPermDetailProps { - baseUrl: string; - permType: PermType; - domain: string; -} - -export default function DomainPermDetail({ baseUrl, permType, domain }: DomainPermDetailProps) { - const { data: domainBlocks = {}, isLoading: isLoadingDomainBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" }); - const { data: domainAllows = {}, isLoading: isLoadingDomainAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" }); - - let isLoading; - switch (permType) { - case "block": - isLoading = isLoadingDomainBlocks; - break; - case "allow": - isLoading = isLoadingDomainAllows; - break; - default: - throw "perm type unknown"; - } - - if (domain == "view") { - // Retrieve domain from form field submission. - domain = (new URL(document.location.toString())).searchParams.get("domain")?? "unknown"; - } - - if (domain == "unknown") { - throw "unknown domain"; - } - - // Normalize / decode domain (it may be URL-encoded). - domain = decodeURIComponent(domain); - - // Check if we already have a perm of the desired type for this domain. - const existingPerm: DomainPerm | undefined = useMemo(() => { - if (permType == "block") { - return domainBlocks[domain]; - } else { - return domainAllows[domain]; - } - }, [domainBlocks, domainAllows, domain, permType]); - - let infoContent: React.JSX.Element; - - if (isLoading) { - infoContent = <Loading />; - } else if (existingPerm == undefined) { - infoContent = <span>No stored {permType} yet, you can add one below:</span>; - } else { - infoContent = ( - <div className="info"> - <i className="fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i> - <b>Editing domain permissions isn't implemented yet, <a href="https://github.com/superseriousbusiness/gotosocial/issues/1198" target="_blank" rel="noopener noreferrer">check here for progress</a></b> - </div> - ); - } - - return ( - <div> - <h1 className="text-cutoff"><BackButton to={baseUrl} /> Domain {permType} for: <span title={domain}>{domain}</span></h1> - {infoContent} - <DomainPermForm - defaultDomain={domain} - perm={existingPerm} - permType={permType} - baseUrl={baseUrl} - /> - </div> - ); -} - -interface DomainPermFormProps { - defaultDomain: string; - perm?: DomainPerm; - permType: PermType; - baseUrl: string; -} - -function DomainPermForm({ defaultDomain, perm, permType, baseUrl }: DomainPermFormProps) { - const isExistingPerm = perm !== undefined; - const disabledForm = isExistingPerm - ? { - disabled: true, - title: "Domain permissions currently cannot be edited." - } - : { - disabled: false, - title: "", - }; - - const form = { - domain: useTextInput("domain", { source: perm, defaultValue: defaultDomain }), - obfuscate: useBoolInput("obfuscate", { source: perm }), - commentPrivate: useTextInput("private_comment", { source: perm }), - commentPublic: useTextInput("public_comment", { source: perm }) - }; - - // Check which perm type we're meant to be handling - // here, and use appropriate mutations and results. - // We can't call these hooks conditionally because - // react is like "weh" (mood), but we can decide - // which ones to use conditionally. - const [ addBlock, addBlockResult ] = useAddDomainBlockMutation(); - const [ removeBlock, removeBlockResult] = useRemoveDomainBlockMutation({ fixedCacheKey: perm?.id }); - const [ addAllow, addAllowResult ] = useAddDomainAllowMutation(); - const [ removeAllow, removeAllowResult ] = useRemoveDomainAllowMutation({ fixedCacheKey: perm?.id }); - - const [ - addTrigger, - addResult, - removeTrigger, - removeResult, - ] = useMemo(() => { - return permType == "block" - ? [ - addBlock, - addBlockResult, - removeBlock, - removeBlockResult, - ] - : [ - addAllow, - addAllowResult, - removeAllow, - removeAllowResult, - ]; - }, [permType, - addBlock, addBlockResult, removeBlock, removeBlockResult, - addAllow, addAllowResult, removeAllow, removeAllowResult, - ]); - - // Use appropriate submission params for this permType. - const [submitForm, submitFormResult] = useFormSubmit(form, [addTrigger, addResult], { changedOnly: false }); - - // Uppercase first letter of given permType. - const permTypeUpper = useMemo(() => { - return permType.charAt(0).toUpperCase() + permType.slice(1); - }, [permType]); - - const [location, setLocation] = useLocation(); - - function verifyUrlThenSubmit(e) { - // Adding a new domain permissions happens on a url like - // "/settings/admin/domain-permissions/:permType/domain.com", - // but if domain input changes, that doesn't match anymore - // and causes issues later on so, before submitting the form, - // silently change url, and THEN submit. - let correctUrl = `${baseUrl}/${form.domain.value}`; - if (location != correctUrl) { - setLocation(correctUrl); - } - return submitForm(e); - } - - return ( - <form onSubmit={verifyUrlThenSubmit}> - <TextInput - field={form.domain} - label="Domain" - placeholder="example.com" - {...disabledForm} - /> - - <Checkbox - field={form.obfuscate} - label="Obfuscate domain in public lists" - {...disabledForm} - /> - - <TextArea - field={form.commentPrivate} - label="Private comment" - rows={3} - {...disabledForm} - /> - - <TextArea - field={form.commentPublic} - label="Public comment" - rows={3} - {...disabledForm} - /> - - <div className="action-buttons row"> - <MutationButton - label={permTypeUpper} - result={submitFormResult} - showError={false} - {...disabledForm} - /> - - { - isExistingPerm && - <MutationButton - type="button" - onClick={() => removeTrigger(perm.id?? "")} - label="Remove" - result={removeResult} - className="button danger" - showError={false} - disabled={!isExistingPerm} - /> - } - </div> - - <> - {addResult.error && <Error error={addResult.error} />} - {removeResult.error && <Error error={removeResult.error} />} - </> - - </form> - ); -} diff --git a/web/source/settings/admin/domain-permissions/export-format-table.jsx b/web/source/settings/admin/domain-permissions/export-format-table.jsx deleted file mode 100644 index 7fcffa348..000000000 --- a/web/source/settings/admin/domain-permissions/export-format-table.jsx +++ /dev/null @@ -1,65 +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 <http://www.gnu.org/licenses/>. -*/ - -const React = require("react"); - -module.exports = function ExportFormatTable() { - return ( - <div className="export-format-table-wrapper without-border"> - <table className="export-format-table"> - <thead> - <tr> - <th rowSpan={2} /> - <th colSpan={2}>Includes</th> - <th colSpan={2}>Importable by</th> - </tr> - <tr> - <th>Domain</th> - <th>Public comment</th> - <th>GoToSocial</th> - <th>Mastodon</th> - </tr> - </thead> - <tbody> - <Format name="Text" info={[true, false, true, false]} /> - <Format name="JSON" info={[true, true, true, false]} /> - <Format name="CSV" info={[true, true, true, true]} /> - </tbody> - </table> - </div> - ); -}; - -function Format({ name, info }) { - return ( - <tr> - <td><b>{name}</b></td> - {info.map((b, key) => <td key={key} className="bool">{bool(b)}</td>)} - </tr> - ); -} - -function bool(val) { - return ( - <> - <i className={`fa fa-${val ? "check" : "times"}`} aria-hidden="true"></i> - <span className="sr-only">{val ? "Yes" : "No"}</span> - </> - ); -}
\ No newline at end of file diff --git a/web/source/settings/admin/domain-permissions/form.tsx b/web/source/settings/admin/domain-permissions/form.tsx deleted file mode 100644 index 57502d6d9..000000000 --- a/web/source/settings/admin/domain-permissions/form.tsx +++ /dev/null @@ -1,153 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; - -import { useEffect } from "react"; - -import { useExportDomainListMutation } from "../../lib/query/admin/domain-permissions/export"; -import useFormSubmit from "../../lib/form/submit"; - -import { - RadioGroup, - TextArea, - Select, -} from "../../components/form/inputs"; - -import MutationButton from "../../components/form/mutation-button"; - -import { Error } from "../../components/error"; -import ExportFormatTable from "./export-format-table"; - -import type { - FormSubmitFunction, - FormSubmitResult, - RadioFormInputHook, - TextFormInputHook, -} from "../../lib/form/types"; - -export interface ImportExportFormProps { - form: { - domains: TextFormInputHook; - exportType: TextFormInputHook; - permType: RadioFormInputHook; - }; - submitParse: FormSubmitFunction; - parseResult: FormSubmitResult; -} - -export default function ImportExportForm({ form, submitParse, parseResult }: ImportExportFormProps) { - const [submitExport, exportResult] = useFormSubmit(form, useExportDomainListMutation()); - - function fileChanged(e) { - const reader = new FileReader(); - reader.onload = function (read) { - const res = read.target?.result; - if (typeof res === "string") { - form.domains.value = res; - submitParse(); - } - }; - reader.readAsText(e.target.files[0]); - } - - useEffect(() => { - if (exportResult.isSuccess) { - form.domains.setter(exportResult.data); - } - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [exportResult]); - - return ( - <> - <h1>Import / Export domain permissions</h1> - <p>This page can be used to import and export lists of domain permissions.</p> - <p>Exports can be done in various formats, with varying functionality and support in other software.</p> - <p>Imports will automatically detect what format is being processed.</p> - <ExportFormatTable /> - <div className="import-export"> - <TextArea - field={form.domains} - label="Domains" - placeholder={`google.com\nfacebook.com`} - rows={8} - /> - - <RadioGroup - field={form.permType} - /> - - <div className="button-grid"> - <MutationButton - label="Import" - type="button" - onClick={() => submitParse()} - result={parseResult} - showError={false} - disabled={form.permType.value === undefined || form.permType.value.length === 0} - /> - <label className={`button with-icon${form.permType.value === undefined || form.permType.value.length === 0 ? " disabled" : ""}`}> - <i className="fa fa-fw " aria-hidden="true" /> - Import file - <input - type="file" - className="hidden" - onChange={fileChanged} - accept="application/json,text/plain,text/csv" - disabled={form.permType.value === undefined || form.permType.value.length === 0} - /> - </label> - <b /> {/* grid filler */} - <MutationButton - label="Export" - type="button" - onClick={() => submitExport("export")} - result={exportResult} showError={false} - disabled={form.permType.value === undefined || form.permType.value.length === 0} - /> - <MutationButton - label="Export to file" - wrapperClassName="export-file-button" - type="button" - onClick={() => submitExport("export-file")} - result={exportResult} - showError={false} - disabled={form.permType.value === undefined || form.permType.value.length === 0} - /> - <div className="export-file"> - <span> - as - </span> - <Select - field={form.exportType} - options={<> - <option value="plain">Text</option> - <option value="json">JSON</option> - <option value="csv">CSV</option> - </>} - /> - </div> - </div> - - {parseResult.error && <Error error={parseResult.error} />} - {exportResult.error && <Error error={exportResult.error} />} - </div> - </> - ); -} diff --git a/web/source/settings/admin/domain-permissions/import-export.tsx b/web/source/settings/admin/domain-permissions/import-export.tsx deleted file mode 100644 index 871bca131..000000000 --- a/web/source/settings/admin/domain-permissions/import-export.tsx +++ /dev/null @@ -1,90 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; - -import { Switch, Route, Redirect, useLocation } from "wouter"; - -import { useProcessDomainPermissionsMutation } from "../../lib/query/admin/domain-permissions/process"; - -import { useTextInput, useRadioInput } from "../../lib/form"; - -import useFormSubmit from "../../lib/form/submit"; - -import { ProcessImport } from "./process"; -import ImportExportForm from "./form"; - -export default function ImportExport({ baseUrl }) { - const form = { - domains: useTextInput("domains"), - exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true }), - permType: useRadioInput("permType", { - options: { - block: "Domain blocks", - allow: "Domain allows", - } - }) - }; - - const [submitParse, parseResult] = useFormSubmit(form, useProcessDomainPermissionsMutation(), { changedOnly: false }); - - const [_location, setLocation] = useLocation(); - - return ( - <Switch> - <Route path={`${baseUrl}/process`}> - { - parseResult.isSuccess - ? ( - <> - <h1> - <span - className="button" - onClick={() => { - parseResult.reset(); - setLocation(baseUrl); - }} - > - < back - </span> - Confirm import of domain {form.permType.value}s: - </h1> - <ProcessImport - list={parseResult.data} - permType={form.permType} - /> - </> - ) - : <Redirect to={baseUrl} /> - } - </Route> - <Route> - { - parseResult.isSuccess - ? <Redirect to={`${baseUrl}/process`} /> - : <ImportExportForm - form={form} - submitParse={submitParse} - parseResult={parseResult} - /> - } - </Route> - </Switch> - ); -} diff --git a/web/source/settings/admin/domain-permissions/index.tsx b/web/source/settings/admin/domain-permissions/index.tsx deleted file mode 100644 index 7d790cfc8..000000000 --- a/web/source/settings/admin/domain-permissions/index.tsx +++ /dev/null @@ -1,49 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; -import { Switch, Route } from "wouter"; - -import DomainPermissionsOverview from "./overview"; -import { PermType } from "../../lib/types/domain-permission"; -import DomainPermDetail from "./detail"; - -export default function DomainPermissions({ baseUrl }: { baseUrl: string }) { - return ( - <Switch> - <Route path="/settings/admin/domain-permissions/:permType/:domain"> - {params => ( - <DomainPermDetail - permType={params.permType as PermType} - baseUrl={baseUrl} - domain={params.domain} - /> - )} - </Route> - <Route path="/settings/admin/domain-permissions/:permType"> - {params => ( - <DomainPermissionsOverview - permType={params.permType as PermType} - baseUrl={baseUrl} - /> - )} - </Route> - </Switch> - ); -} diff --git a/web/source/settings/admin/domain-permissions/overview.tsx b/web/source/settings/admin/domain-permissions/overview.tsx deleted file mode 100644 index bdfd214bc..000000000 --- a/web/source/settings/admin/domain-permissions/overview.tsx +++ /dev/null @@ -1,198 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; - -import { useMemo } from "react"; -import { Link, useLocation } from "wouter"; -import { matchSorter } from "match-sorter"; - -import { useTextInput } from "../../lib/form"; - -import { TextInput } from "../../components/form/inputs"; - -import Loading from "../../components/loading"; -import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../lib/query/admin/domain-permissions/get"; -import type { MappedDomainPerms, PermType } from "../../lib/types/domain-permission"; -import { NoArg } from "../../lib/types/query"; - -export interface DomainPermissionsOverviewProps { - // Params injected by - // the wouter router. - permType: PermType; - baseUrl: string, -} - -export default function DomainPermissionsOverview({ permType, baseUrl }: DomainPermissionsOverviewProps) { - if (permType !== "block" && permType !== "allow") { - throw "unrecognized perm type " + permType; - } - - // Uppercase first letter of given permType. - const permTypeUpper = useMemo(() => { - return permType.charAt(0).toUpperCase() + permType.slice(1); - }, [permType]); - - // Fetch / wait for desired perms to load. - const { data: blocks, isLoading: isLoadingBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" }); - const { data: allows, isLoading: isLoadingAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" }); - - let data: MappedDomainPerms | undefined; - let isLoading: boolean; - - if (permType == "block") { - data = blocks; - isLoading = isLoadingBlocks; - } else { - data = allows; - isLoading = isLoadingAllows; - } - - if (isLoading || data === undefined) { - return <Loading />; - } - - return ( - <div> - <h1>Domain {permTypeUpper}s</h1> - { permType == "block" ? <BlockHelperText/> : <AllowHelperText/> } - <DomainPermsList - data={data} - baseUrl={baseUrl} - permType={permType} - permTypeUpper={permTypeUpper} - /> - <Link to="/settings/admin/domain-permissions/import-export"> - <a>Or use the bulk import/export interface</a> - </Link> - </div> - ); -} - -interface DomainPermsListProps { - data: MappedDomainPerms; - baseUrl: string; - permType: PermType; - permTypeUpper: string; -} - -function DomainPermsList({ data, baseUrl, permType, permTypeUpper }: DomainPermsListProps) { - // Format perms into a list. - const perms = useMemo(() => { - return Object.values(data); - }, [data]); - - const [_location, setLocation] = useLocation(); - const filterField = useTextInput("filter"); - - function filterFormSubmit(e) { - e.preventDefault(); - setLocation(`${baseUrl}/${filter}`); - } - - const filter = filterField.value ?? ""; - const filteredPerms = useMemo(() => { - return matchSorter(perms, filter, { keys: ["domain"] }); - }, [perms, filter]); - const filtered = perms.length - filteredPerms.length; - - const filterInfo = ( - <span> - {perms.length} {permType}ed domain{perms.length != 1 ? "s" : ""} {filtered > 0 && `(${filtered} filtered by search)`} - </span> - ); - - const entries = filteredPerms.map((entry) => { - return ( - <Link key={entry.domain} to={`${baseUrl}/${entry.domain}`}> - <a className="entry nounderline"> - <span id="domain">{entry.domain}</span> - <span id="date">{new Date(entry.created_at ?? "").toLocaleString()}</span> - </a> - </Link> - ); - }); - - return ( - <div className="domain-permissions-list"> - <form className="filter" role="search" onSubmit={filterFormSubmit}> - <TextInput - field={filterField} - placeholder="example.org" - label={`Search or add domain ${permType}`} - /> - <Link to={`${baseUrl}/${filter}`}> - <a className="button">{permTypeUpper} {filter}</a> - </Link> - </form> - <div> - {filterInfo} - <div className="list"> - <div className="entries scrolling"> - {entries} - </div> - </div> - </div> - </div> - ); -} - -function BlockHelperText() { - return ( - <p> - Blocking a domain blocks interaction between your instance, and all current and future accounts on - instance(s) running on the blocked domain. Stored content will be removed, and no more data is sent to - the remote server. This extends to all subdomains as well, so blocking 'example.com' also blocks 'social.example.com'. - <br/> - <a - href="https://docs.gotosocial.org/en/latest/admin/domain_blocks/" - target="_blank" - className="docslink" - rel="noreferrer" - > - Learn more about domain blocks (opens in a new tab) - </a> - <br/> - </p> - ); -} - -function AllowHelperText() { - return ( - <p> - Allowing a domain explicitly allows instance(s) running on that domain to interact with your instance. - If you're running in allowlist mode, this is how you "allow" instances through. - If you're running in blocklist mode (the default federation mode), you can use explicit domain allows - to override domain blocks. In blocklist mode, explicitly allowed instances will be able to interact with - your instance regardless of any domain blocks in place. This extends to all subdomains as well, so allowing - 'example.com' also allows 'social.example.com'. This is useful when you're importing a block list but - there are some domains on the list you don't want to block: just create an explicit allow for those domains - before importing the list. - <br/> - <a - href="https://docs.gotosocial.org/en/latest/admin/federation_modes/" - target="_blank" - className="docslink" - rel="noreferrer" - > - Learn more about federation modes (opens in a new tab) - </a> - </p> - ); -} diff --git a/web/source/settings/admin/domain-permissions/process.tsx b/web/source/settings/admin/domain-permissions/process.tsx deleted file mode 100644 index bb9411b9d..000000000 --- a/web/source/settings/admin/domain-permissions/process.tsx +++ /dev/null @@ -1,402 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; - -import { memo, useMemo, useCallback, useEffect } from "react"; - -import { isValidDomainPermission, hasBetterScope } from "../../lib/util/domain-permission"; - -import { - useTextInput, - useBoolInput, - useRadioInput, - useCheckListInput, -} from "../../lib/form"; - -import { - Select, - TextArea, - RadioGroup, - Checkbox, - TextInput, -} from "../../components/form/inputs"; - -import useFormSubmit from "../../lib/form/submit"; - -import CheckList from "../../components/check-list"; -import MutationButton from "../../components/form/mutation-button"; -import FormWithData from "../../lib/form/form-with-data"; - -import { useImportDomainPermsMutation } from "../../lib/query/admin/domain-permissions/import"; -import { - useDomainAllowsQuery, - useDomainBlocksQuery -} from "../../lib/query/admin/domain-permissions/get"; - -import type { DomainPerm, MappedDomainPerms } from "../../lib/types/domain-permission"; -import type { ChecklistInputHook, RadioFormInputHook } from "../../lib/form/types"; - -export interface ProcessImportProps { - list: DomainPerm[], - permType: RadioFormInputHook, -} - -export const ProcessImport = memo( - function ProcessImport({ list, permType }: ProcessImportProps) { - return ( - <div className="without-border"> - <FormWithData - dataQuery={permType.value == "allow" - ? useDomainAllowsQuery - : useDomainBlocksQuery - } - DataForm={ImportList} - {...{ list, permType }} - /> - </div> - ); - } -); - -export interface ImportListProps { - list: Array<DomainPerm>, - data: MappedDomainPerms, - permType: RadioFormInputHook, -} - -function ImportList({ list, data: domainPerms, permType }: ImportListProps) { - const hasComment = useMemo(() => { - let hasPublic = false; - let hasPrivate = false; - - list.some((entry) => { - if (entry.public_comment) { - hasPublic = true; - } - - if (entry.private_comment) { - hasPrivate = true; - } - - return hasPublic && hasPrivate; - }); - - if (hasPublic && hasPrivate) { - return { both: true }; - } else if (hasPublic) { - return { type: "public_comment" }; - } else if (hasPrivate) { - return { type: "private_comment" }; - } else { - return {}; - } - }, [list]); - - const showComment = useTextInput("showComment", { defaultValue: hasComment.type ?? "public_comment" }); - - const form = { - domains: useCheckListInput("domains", { entries: list }), // DomainPerm is actually also a Checkable. - obfuscate: useBoolInput("obfuscate"), - privateComment: useTextInput("private_comment", { - defaultValue: `Imported on ${new Date().toLocaleString()}` - }), - privateCommentBehavior: useRadioInput("private_comment_behavior", { - defaultValue: "append", - options: { - append: "Append to", - replace: "Replace" - } - }), - publicComment: useTextInput("public_comment"), - publicCommentBehavior: useRadioInput("public_comment_behavior", { - defaultValue: "append", - options: { - append: "Append to", - replace: "Replace" - } - }), - permType: permType, - }; - - const [importDomains, importResult] = useFormSubmit(form, useImportDomainPermsMutation(), { changedOnly: false }); - - return ( - <> - <form - onSubmit={importDomains} - className="domain-perm-import-list" - > - <span>{list.length} domain{list.length != 1 ? "s" : ""} in this list</span> - - {hasComment.both && - <Select field={showComment} options={ - <> - <option value="public_comment">Show public comments</option> - <option value="private_comment">Show private comments</option> - </> - } /> - } - - <div className="checkbox-list-wrapper"> - <DomainCheckList - field={form.domains} - domainPerms={domainPerms} - commentType={showComment.value as "public_comment" | "private_comment"} - permType={form.permType} - /> - </div> - - <TextArea - field={form.privateComment} - label="Private comment" - rows={3} - /> - <RadioGroup - field={form.privateCommentBehavior} - label="imported private comment" - /> - - <TextArea - field={form.publicComment} - label="Public comment" - rows={3} - /> - <RadioGroup - field={form.publicCommentBehavior} - label="imported public comment" - /> - - <Checkbox - field={form.obfuscate} - label="Obfuscate domains in public lists" - /> - - <MutationButton - label="Import" - disabled={false} - result={importResult} - /> - </form> - </> - ); -} - -interface DomainCheckListProps { - field: ChecklistInputHook, - domainPerms: MappedDomainPerms, - commentType: "public_comment" | "private_comment", - permType: RadioFormInputHook, -} - -function DomainCheckList({ field, domainPerms, commentType, permType }: DomainCheckListProps) { - const getExtraProps = useCallback((entry: DomainPerm) => { - return { - comment: entry[commentType], - alreadyExists: entry.domain in domainPerms, - permType: permType, - }; - }, [domainPerms, commentType, permType]); - - const entriesWithSuggestions = useMemo(() => { - const fieldValue = (field.value ?? {}) as { [k: string]: DomainPerm; }; - return Object.values(fieldValue).filter((entry) => entry.suggest); - }, [field.value]); - - return ( - <> - <CheckList - field={field as ChecklistInputHook} - header={<> - <b>Domain</b> - <b> - {commentType == "public_comment" && "Public comment"} - {commentType == "private_comment" && "Private comment"} - </b> - </>} - EntryComponent={DomainEntry} - getExtraProps={getExtraProps} - /> - <UpdateHint - entries={entriesWithSuggestions} - updateEntry={field.onChange} - updateMultiple={field.updateMultiple} - /> - </> - ); -} - -interface UpdateHintProps { - entries, - updateEntry, - updateMultiple, -} - -const UpdateHint = memo( - function UpdateHint({ entries, updateEntry, updateMultiple }: UpdateHintProps) { - if (entries.length == 0) { - return null; - } - - function changeAll() { - updateMultiple( - entries.map((entry) => [entry.key, { domain: entry.suggest, suggest: null }]) - ); - } - - return ( - <div className="update-hints"> - <p> - {entries.length} {entries.length == 1 ? "entry uses" : "entries use"} a specific subdomain, - which you might want to change to the main domain, as that includes all it's (future) subdomains. - </p> - <div className="hints"> - {entries.map((entry) => ( - <UpdateableEntry key={entry.key} entry={entry} updateEntry={updateEntry} /> - ))} - </div> - {entries.length > 0 && <a onClick={changeAll}>change all</a>} - </div> - ); - } -); - -interface UpdateableEntryProps { - entry, - updateEntry, -} - -const UpdateableEntry = memo( - function UpdateableEntry({ entry, updateEntry }: UpdateableEntryProps) { - return ( - <> - <span className="text-cutoff">{entry.domain}</span> - <i className="fa fa-long-arrow-right" aria-hidden="true"></i> - <span>{entry.suggest}</span> - <a role="button" onClick={() => - updateEntry(entry.key, { domain: entry.suggest, suggest: null }) - }>change</a> - </> - ); - } -); - -function domainValidationError(isValid) { - return isValid ? "" : "Invalid domain"; -} - -interface DomainEntryProps { - entry; - onChange; - extraProps: { - alreadyExists: boolean; - comment: string; - permType: RadioFormInputHook; - }; -} - -function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment, permType } }: DomainEntryProps) { - const domainField = useTextInput("domain", { - defaultValue: entry.domain, - showValidation: entry.checked, - initValidation: domainValidationError(entry.valid), - validator: (value) => domainValidationError(isValidDomainPermission(value)) - }); - - useEffect(() => { - if (entry.valid != domainField.valid) { - onChange({ valid: domainField.valid }); - } - }, [onChange, entry.valid, domainField.valid]); - - useEffect(() => { - if (entry.domain != domainField.value) { - domainField.setter(entry.domain); - } - // domainField.setter is enough, eslint wants domainField - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [entry.domain, domainField.setter]); - - useEffect(() => { - onChange({ suggest: hasBetterScope(domainField.value ?? "") }); - // only need this update if it's the entry.checked that updated, not onChange - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [domainField.value]); - - function clickIcon(e) { - if (entry.suggest) { - e.stopPropagation(); - e.preventDefault(); - domainField.setter(entry.suggest); - onChange({ domain: entry.suggest, checked: true }); - } - } - - return ( - <> - <div className="domain-input"> - <TextInput - field={domainField} - onChange={(e) => { - domainField.onChange(e); - onChange({ domain: e.target.value, checked: true }); - }} - /> - <span id="icon" onClick={clickIcon}> - <DomainEntryIcon - alreadyExists={alreadyExists} - suggestion={entry.suggest} - permTypeString={permType.value?? ""} - /> - </span> - </div> - <p>{comment}</p> - </> - ); -} - -interface DomainEntryIconProps { - alreadyExists: boolean; - suggestion: string; - permTypeString: string; -} - -function DomainEntryIcon({ alreadyExists, suggestion, permTypeString }: DomainEntryIconProps) { - let icon; - let text; - - if (suggestion) { - icon = "fa-info-circle suggest-changes"; - text = `Entry targets a specific subdomain, consider changing it to '${suggestion}'.`; - } else if (alreadyExists) { - icon = "fa-history permission-already-exists"; - text = `Domain ${permTypeString} already exists.`; - } - - if (!icon) { - return null; - } - - return ( - <> - <i className={`fa fa-fw ${icon}`} aria-hidden="true" title={text}></i> - <span className="sr-only">{text}</span> - </> - ); -}
\ No newline at end of file diff --git a/web/source/settings/admin/emoji/category-select.jsx b/web/source/settings/admin/emoji/category-select.jsx deleted file mode 100644 index e5cf29939..000000000 --- a/web/source/settings/admin/emoji/category-select.jsx +++ /dev/null @@ -1,96 +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 <http://www.gnu.org/licenses/>. -*/ - -const React = require("react"); -const splitFilterN = require("split-filter-n"); -const syncpipe = require('syncpipe'); -const { matchSorter } = require("match-sorter"); - -const ComboBox = require("../../components/combo-box"); -const { useListEmojiQuery } = require("../../lib/query/admin/custom-emoji"); - -function useEmojiByCategory(emoji) { - // split all emoji over an object keyed by the category names (or Unsorted) - return React.useMemo(() => splitFilterN( - emoji, - [], - (entry) => entry.category ?? "Unsorted" - ), [emoji]); -} - -function CategorySelect({ field, children }) { - const { value, setIsNew } = field; - - const { - data: emoji = [], - isLoading, - isSuccess, - error - } = useListEmojiQuery({ filter: "domain:local" }); - - const emojiByCategory = useEmojiByCategory(emoji); - - const categories = React.useMemo(() => new Set(Object.keys(emojiByCategory)), [emojiByCategory]); - - // data used by the ComboBox element to select an emoji category - const categoryItems = React.useMemo(() => { - return syncpipe(emojiByCategory, [ - (_) => Object.keys(_), // just emoji category names - (_) => matchSorter(_, value, { threshold: matchSorter.rankings.NO_MATCH }), // sorted by complex algorithm - (_) => _.map((categoryName) => [ // map to input value, and selectable element with icon - categoryName, - <> - <img src={emojiByCategory[categoryName][0].static_url} aria-hidden="true"></img> - {categoryName} - </> - ]) - ]); - }, [emojiByCategory, value]); - - React.useEffect(() => { - if (value != undefined && isSuccess && value.trim().length > 0) { - setIsNew(!categories.has(value.trim())); - } - }, [categories, value, isSuccess, setIsNew]); - - if (error) { // fall back to plain text input, but this would almost certainly have caused a bigger error message elsewhere - return ( - <> - <input type="text" placeholder="e.g., reactions" onChange={(e) => { field.value = e.target.value; }} />; - </> - ); - } else if (isLoading) { - return <input type="text" value="Loading categories..." disabled={true} />; - } - - return ( - <ComboBox - field={field} - items={categoryItems} - label="Category" - placeholder="e.g., reactions" - children={children} - /> - ); -} - -module.exports = { - useEmojiByCategory, - CategorySelect -};
\ No newline at end of file diff --git a/web/source/settings/admin/emoji/local/detail.js b/web/source/settings/admin/emoji/local/detail.js deleted file mode 100644 index a78e3e499..000000000 --- a/web/source/settings/admin/emoji/local/detail.js +++ /dev/null @@ -1,146 +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 <http://www.gnu.org/licenses/>. -*/ - -import React, { useEffect } from "react"; -import { useRoute, Link, Redirect } from "wouter"; - -import { useComboBoxInput, useFileInput, useValue } from "../../../lib/form"; -import { CategorySelect } from "../category-select"; - -import useFormSubmit from "../../../lib/form/submit"; -import { useBaseUrl } from "../../../lib/navigation/util"; - -import FakeToot from "../../../components/fake-toot"; -import FormWithData from "../../../lib/form/form-with-data"; -import Loading from "../../../components/loading"; -import { FileInput } from "../../../components/form/inputs"; -import MutationButton from "../../../components/form/mutation-button"; -import { Error } from "../../../components/error"; - -import { useGetEmojiQuery, useEditEmojiMutation, useDeleteEmojiMutation } from "../../../lib/query/admin/custom-emoji"; - -export default function EmojiDetailRoute({ }) { - const baseUrl = useBaseUrl(); - let [_match, params] = useRoute(`${baseUrl}/:emojiId`); - if (params?.emojiId == undefined) { - return <Redirect to={baseUrl} />; - } else { - return ( - <div className="emoji-detail"> - <Link to={baseUrl}><a>< go back</a></Link> - <FormWithData dataQuery={useGetEmojiQuery} queryArg={params.emojiId} DataForm={EmojiDetailForm} /> - </div> - ); - } -} - -function EmojiDetailForm({ data: emoji }) { - const baseUrl = useBaseUrl(); - const form = { - id: useValue("id", emoji.id), - category: useComboBoxInput("category", { source: emoji }), - image: useFileInput("image", { - withPreview: true, - maxSize: 50 * 1024 // TODO: get from instance api - }) - }; - - const [modifyEmoji, result] = useFormSubmit(form, useEditEmojiMutation()); - - // Automatic submitting of category change - useEffect(() => { - if ( - form.category.hasChanged() && - !form.category.state.open && - !form.category.isNew) { - modifyEmoji(); - } - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [form.category.hasChanged(), form.category.isNew, form.category.state.open]); - - const [deleteEmoji, deleteResult] = useDeleteEmojiMutation(); - - if (deleteResult.isSuccess) { - return <Redirect to={baseUrl} />; - } - - return ( - <> - <div className="emoji-header"> - <img src={emoji.url} alt={emoji.shortcode} title={emoji.shortcode} /> - <div> - <h2>{emoji.shortcode}</h2> - <MutationButton - label="Delete" - type="button" - onClick={() => deleteEmoji(emoji.id)} - className="danger" - showError={false} - result={deleteResult} - /> - </div> - </div> - - <form onSubmit={modifyEmoji} className="left-border"> - <h2>Modify this emoji {result.isLoading && <Loading />}</h2> - - <div className="update-category"> - <CategorySelect - field={form.category} - > - <MutationButton - name="create-category" - label="Create" - result={result} - showError={false} - style={{ visibility: (form.category.isNew ? "initial" : "hidden") }} - /> - </CategorySelect> - </div> - - <div className="update-image"> - <FileInput - field={form.image} - label="Image" - accept="image/png,image/gif" - /> - - <MutationButton - name="image" - label="Replace image" - showError={false} - result={result} - /> - - <FakeToot> - Look at this new custom emoji <img - className="emoji" - src={form.image.previewURL ?? emoji.url} - title={`:${emoji.shortcode}:`} - alt={emoji.shortcode} - /> isn't it cool? - </FakeToot> - - {result.error && <Error error={result.error} />} - {deleteResult.error && <Error error={deleteResult.error} />} - </div> - </form> - </> - ); -}
\ No newline at end of file diff --git a/web/source/settings/admin/emoji/local/index.tsx b/web/source/settings/admin/emoji/local/index.tsx deleted file mode 100644 index 74a891f3e..000000000 --- a/web/source/settings/admin/emoji/local/index.tsx +++ /dev/null @@ -1,35 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; -import { Switch, Route } from "wouter"; - -import EmojiOverview from "./overview"; -import EmojiDetail from "./detail"; - -export default function CustomEmoji({ baseUrl }) { - return ( - <Switch> - <Route path={`${baseUrl}/:emojiId`}> - <EmojiDetail /> - </Route> - <EmojiOverview /> - </Switch> - ); -} diff --git a/web/source/settings/admin/emoji/local/new-emoji.tsx b/web/source/settings/admin/emoji/local/new-emoji.tsx deleted file mode 100644 index c6a203765..000000000 --- a/web/source/settings/admin/emoji/local/new-emoji.tsx +++ /dev/null @@ -1,116 +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 <http://www.gnu.org/licenses/>. -*/ - -import React, { useMemo, useEffect } from "react"; - -import { useFileInput, useComboBoxInput } from "../../../lib/form"; -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 MutationButton from "../../../components/form/mutation-button"; -import { useAddEmojiMutation } from "../../../lib/query/admin/custom-emoji"; -import { useInstanceV1Query } from "../../../lib/query"; - -export default function NewEmojiForm() { - const shortcode = useShortcode(); - - const { data: instance } = useInstanceV1Query(); - const emojiMaxSize = useMemo(() => { - return instance?.configuration?.emojis?.emoji_size_limit ?? 50 * 1024; - }, [instance]); - - const image = useFileInput("image", { - withPreview: true, - maxSize: emojiMaxSize - }); - - const category = useComboBoxInput("category"); - - const [submitForm, result] = useFormSubmit({ - shortcode, image, category - }, useAddEmojiMutation()); - - useEffect(() => { - if (shortcode.value === undefined || shortcode.value.length == 0) { - if (image.value != undefined) { - let [name, _ext] = image.value.name.split("."); - shortcode.setter(name); - } - } - - /* We explicitly don't want to have 'shortcode' as a dependency here - because we only want to change the shortcode to the filename if the field is empty - at the moment the file is selected, not some time after when the field is emptied - */ - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [image.value]); - - let emojiOrShortcode; - - if (image.previewValue != undefined) { - emojiOrShortcode = <img - className="emoji" - src={image.previewValue} - title={`:${shortcode.value}:`} - alt={shortcode.value} - />; - } else if (shortcode.value !== undefined && shortcode.value.length > 0) { - emojiOrShortcode = `:${shortcode.value}:`; - } else { - emojiOrShortcode = `:your_emoji_here:`; - } - - return ( - <div> - <h2>Add new custom emoji</h2> - - <FakeToot> - Look at this new custom emoji {emojiOrShortcode} isn't it cool? - </FakeToot> - - <form onSubmit={submitForm} className="form-flex"> - <FileInput - field={image} - accept="image/png,image/gif,image/webp" - /> - - <TextInput - field={shortcode} - label="Shortcode, must be unique among the instance's local emoji" - /> - - <CategorySelect - field={category} - children={[]} - /> - - <MutationButton - disabled={image.previewValue === undefined} - label="Upload emoji" - result={result} - /> - </form> - </div> - ); -}
\ No newline at end of file diff --git a/web/source/settings/admin/emoji/local/overview.js b/web/source/settings/admin/emoji/local/overview.js deleted file mode 100644 index 45bfd614d..000000000 --- a/web/source/settings/admin/emoji/local/overview.js +++ /dev/null @@ -1,153 +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 <http://www.gnu.org/licenses/>. -*/ - -const React = require("react"); -const { Link } = require("wouter"); -const syncpipe = require("syncpipe"); -const { matchSorter } = require("match-sorter"); - -const NewEmojiForm = require("./new-emoji").default; -const { useTextInput } = require("../../../lib/form"); - -const { useEmojiByCategory } = require("../category-select"); -const { useBaseUrl } = require("../../../lib/navigation/util"); - -const Loading = require("../../../components/loading"); -const { Error } = require("../../../components/error"); -const { TextInput } = require("../../../components/form/inputs"); -const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji"); - -module.exports = function EmojiOverview({ }) { - const { - data: emoji = [], - isLoading, - isError, - error - } = useListEmojiQuery({ filter: "domain:local" }); - - let content = null; - - if (isLoading) { - content = <Loading />; - } else if (isError) { - content = <Error error={error} />; - } else { - content = ( - <> - <EmojiList emoji={emoji} /> - <NewEmojiForm emoji={emoji} /> - </> - ); - } - - return ( - <> - <h1>Local Custom Emoji</h1> - <p> - To use custom emoji in your toots they have to be 'local' to the instance. - You can either upload them here directly, or copy from those already - present on other (known) instances through the <Link to={`./remote`}>Remote Emoji</Link> page. - </p> - <p> - <strong>Be warned!</strong> If you upload more than about 300-400 custom emojis in - total on your instance, this may lead to rate-limiting issues for users and clients - if they try to load all the emoji images at once (which is what many clients do). - </p> - {content} - </> - ); -}; - -function EmojiList({ emoji }) { - const filterField = useTextInput("filter"); - const filter = filterField.value; - - const emojiByCategory = useEmojiByCategory(emoji); - - /* Filter emoji based on shortcode match with user input, hiding empty categories */ - const { filteredEmoji, hidden } = React.useMemo(() => { - let hidden = emoji.length; - const filteredEmoji = syncpipe(emojiByCategory, [ - (_) => Object.entries(emojiByCategory), - (_) => _.map(([category, entries]) => { - let filteredEntries = matchSorter(entries, filter, { keys: ["shortcode"] }); - if (filteredEntries.length == 0) { - return null; - } else { - hidden -= filteredEntries.length; - return [category, filteredEntries]; - } - }), - (_) => _.filter((value) => value !== null) - ]); - - return { filteredEmoji, hidden }; - }, [filter, emojiByCategory, emoji.length]); - - return ( - <div> - <h2>Overview</h2> - {emoji.length > 0 - ? <span>{emoji.length} custom emoji {hidden > 0 && `(${hidden} filtered)`}</span> - : <span>No custom emoji yet, you can add one below.</span> - } - <div className="list emoji-list"> - <div className="header"> - <TextInput - field={filterField} - name="emoji-shortcode" - placeholder="Search" - /> - </div> - <div className="entries scrolling"> - {filteredEmoji.length > 0 - ? ( - <div className="entries scrolling"> - {filteredEmoji.map(([category, entries]) => { - return <EmojiCategory key={category} category={category} entries={entries} />; - })} - </div> - ) - : <div className="entry">No local emoji matched your filter.</div> - } - </div> - </div> - </div> - ); -} - -function EmojiCategory({ category, entries }) { - const baseUrl = useBaseUrl(); - return ( - <div className="entry"> - <b>{category}</b> - <div className="emoji-group"> - {entries.map((e) => { - return ( - <Link key={e.id} to={`${baseUrl}/${e.id}`}> - <a> - <img src={e.url} alt={e.shortcode} title={`:${e.shortcode}:`} /> - </a> - </Link> - ); - })} - </div> - </div> - ); -}
\ No newline at end of file diff --git a/web/source/settings/admin/emoji/local/use-shortcode.js b/web/source/settings/admin/emoji/local/use-shortcode.js deleted file mode 100644 index 67255860f..000000000 --- a/web/source/settings/admin/emoji/local/use-shortcode.js +++ /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 <http://www.gnu.org/licenses/>. -*/ - -const React = require("react"); - -const { useTextInput } = require("../../../lib/form"); -const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji"); - -const shortcodeRegex = /^\w{2,30}$/; - -module.exports = function useShortcode() { - const { data: emoji = [] } = useListEmojiQuery({ - filter: "domain:local" - }); - - const emojiCodes = React.useMemo(() => { - return new Set(emoji.map((e) => e.shortcode)); - }, [emoji]); - - return useTextInput("shortcode", { - validator: function validateShortcode(code) { - // technically invalid, but hacky fix to prevent validation error on page load - if (code == "") { return ""; } - - if (emojiCodes.has(code)) { - return "Shortcode already in use"; - } - - if (code.length < 2 || code.length > 30) { - return "Shortcode must be between 2 and 30 characters"; - } - - if (!shortcodeRegex.test(code)) { - return "Shortcode must only contain letters, numbers, and underscores"; - } - - return ""; - } - }); -};
\ No newline at end of file diff --git a/web/source/settings/admin/emoji/remote/index.tsx b/web/source/settings/admin/emoji/remote/index.tsx deleted file mode 100644 index d9c786be2..000000000 --- a/web/source/settings/admin/emoji/remote/index.tsx +++ /dev/null @@ -1,54 +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 <http://www.gnu.org/licenses/>. -*/ - -import React, { useMemo } from "react"; - -import ParseFromToot from "./parse-from-toot"; - -import Loading from "../../../components/loading"; -import { Error } from "../../../components/error"; -import { useListEmojiQuery } from "../../../lib/query/admin/custom-emoji"; - -export default function RemoteEmoji() { - // local emoji are queried for shortcode collision detection - const { - data: emoji = [], - isLoading, - error - } = useListEmojiQuery({ filter: "domain:local" }); - - const emojiCodes = useMemo(() => { - return new Set(emoji.map((e) => e.shortcode)); - }, [emoji]); - - return ( - <> - <h1>Custom Emoji (remote)</h1> - {error && - <Error error={error} /> - } - {isLoading - ? <Loading /> - : <> - <ParseFromToot emojiCodes={emojiCodes} /> - </> - } - </> - ); -} diff --git a/web/source/settings/admin/emoji/remote/parse-from-toot.tsx b/web/source/settings/admin/emoji/remote/parse-from-toot.tsx deleted file mode 100644 index df1c221ba..000000000 --- a/web/source/settings/admin/emoji/remote/parse-from-toot.tsx +++ /dev/null @@ -1,235 +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 <http://www.gnu.org/licenses/>. -*/ - -import React, { useCallback, useEffect } from "react"; - -import { useTextInput, useComboBoxInput, useCheckListInput } from "../../../lib/form"; - -import useFormSubmit from "../../../lib/form/submit"; - -import CheckList from "../../../components/check-list"; -import { CategorySelect } from '../category-select'; - -import { TextInput } from "../../../components/form/inputs"; -import MutationButton from "../../../components/form/mutation-button"; -import { Error } from "../../../components/error"; -import { useSearchItemForEmojiMutation, usePatchRemoteEmojisMutation } from "../../../lib/query/admin/custom-emoji"; - -export default function ParseFromToot({ emojiCodes }) { - const [searchStatus, result] = useSearchItemForEmojiMutation(); - const urlField = useTextInput("url"); - - function submitSearch(e) { - e.preventDefault(); - if (urlField.value !== undefined && urlField.value.trim().length != 0) { - searchStatus(urlField.value); - } - } - - return ( - <div className="parse-emoji"> - <h2>Steal this look</h2> - <form onSubmit={submitSearch}> - <div className="form-field text"> - <label htmlFor="url"> - Link to a toot: - </label> - <div className="row"> - <input - type="text" - id="url" - name="url" - onChange={urlField.onChange} - value={urlField.value} - /> - <button disabled={result.isLoading}> - <i className={[ - "fa fa-fw", - (result.isLoading - ? "fa-refresh fa-spin" - : "fa-search") - ].join(" ")} aria-hidden="true" title="Search" /> - <span className="sr-only">Search</span> - </button> - </div> - </div> - </form> - <SearchResult result={result} localEmojiCodes={emojiCodes} /> - </div> - ); -} - -function SearchResult({ result, localEmojiCodes }) { - const { error, data, isSuccess, isError } = result; - - if (!(isSuccess || isError)) { - return null; - } - - if (error == "NONE_FOUND") { - return "No results found"; - } else if (error == "LOCAL_INSTANCE") { - return <b>This is a local user/toot, all referenced emoji are already on your instance</b>; - } else if (error != undefined) { - return <Error error={result.error} />; - } - - if (data.list.length == 0) { - return <b>This {data.type == "statuses" ? "toot" : "account"} doesn't use any custom emoji</b>; - } - - return ( - <CopyEmojiForm - localEmojiCodes={localEmojiCodes} - type={data.type} - emojiList={data.list} - /> - ); -} - -function CopyEmojiForm({ localEmojiCodes, type, emojiList }) { - const form = { - selectedEmoji: useCheckListInput("selectedEmoji", { - entries: emojiList, - uniqueKey: "id" - }), - category: useComboBoxInput("category") - }; - - const [formSubmit, result] = useFormSubmit( - form, - usePatchRemoteEmojisMutation(), - { - changedOnly: false, - onFinish: ({ data }) => { - if (data) { - // uncheck all successfully processed emoji - const processed = data.map((emoji) => { - return [emoji.id, { checked: false }]; - }); - form.selectedEmoji.updateMultiple(processed); - } - } - } - ); - - const buttonsInactive = form.selectedEmoji.someSelected - ? { - disabled: false, - title: "" - } - : { - disabled: true, - title: "No emoji selected, cannot perform any actions" - }; - - const checkListExtraProps = useCallback(() => ({ localEmojiCodes }), [localEmojiCodes]); - - return ( - <div className="parsed"> - <span>This {type == "statuses" ? "toot" : "account"} uses the following custom emoji, select the ones you want to copy/disable:</span> - <form onSubmit={formSubmit}> - <CheckList - field={form.selectedEmoji} - header={<></>} - EntryComponent={EmojiEntry} - getExtraProps={checkListExtraProps} - /> - - <CategorySelect - field={form.category} - children={[]} - /> - - <div className="action-buttons row"> - <MutationButton - name="copy" - label="Copy to local emoji" - result={result} - showError={false} - {...buttonsInactive} - /> - <MutationButton - name="disable" - label="Disable" - result={result} - className="button danger" - showError={false} - {...buttonsInactive} - /> - </div> - {result.error && ( - Array.isArray(result.error) - ? <ErrorList errors={result.error} /> - : <Error error={result.error} /> - )} - </form> - </div> - ); -} - -function ErrorList({ errors }) { - return ( - <div className="error"> - One or multiple emoji failed to process: - {errors.map(([shortcode, err]) => ( - <div key={shortcode}> - <b>{shortcode}:</b> {err} - </div> - ))} - </div> - ); -} - -function EmojiEntry({ entry: emoji, onChange, extraProps: { localEmojiCodes } }) { - const shortcodeField = useTextInput("shortcode", { - defaultValue: emoji.shortcode, - validator: function validateShortcode(code) { - return (emoji.checked && localEmojiCodes.has(code)) - ? "Shortcode already in use" - : ""; - } - }); - - useEffect(() => { - if (emoji.valid != shortcodeField.valid) { - onChange({ valid: shortcodeField.valid }); - } - }, [onChange, emoji.valid, shortcodeField.valid]); - - useEffect(() => { - shortcodeField.validate(); - // only need this update if it's the emoji.checked that updated, not shortcodeField - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [emoji.checked]); - - return ( - <> - <img className="emoji" src={emoji.url} title={emoji.shortcode} /> - - <TextInput - field={shortcodeField} - onChange={(e) => { - shortcodeField.onChange(e); - onChange({ shortcode: e.target.value, checked: true }); - }} - /> - </> - ); -}
\ No newline at end of file diff --git a/web/source/settings/admin/reports/detail.tsx b/web/source/settings/admin/reports/detail.tsx deleted file mode 100644 index 94268dc1f..000000000 --- a/web/source/settings/admin/reports/detail.tsx +++ /dev/null @@ -1,252 +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 <http://www.gnu.org/licenses/>. -*/ - -import React, { useState } from "react"; -import { useRoute, Redirect } from "wouter"; - -import FormWithData from "../../lib/form/form-with-data"; -import BackButton from "../../components/back-button"; - -import { useValue, useTextInput } from "../../lib/form"; -import useFormSubmit from "../../lib/form/submit"; - -import { TextArea } from "../../components/form/inputs"; - -import MutationButton from "../../components/form/mutation-button"; -import Username from "./username"; -import { useBaseUrl } from "../../lib/navigation/util"; -import { useGetReportQuery, useResolveReportMutation } from "../../lib/query/admin/reports"; - -export default function ReportDetail({ }) { - const baseUrl = useBaseUrl(); - let [_match, params] = useRoute(`${baseUrl}/:reportId`); - if (params?.reportId == undefined) { - return <Redirect to={baseUrl} />; - } else { - return ( - <div className="report-detail"> - <h1> - <BackButton to={baseUrl} /> Report Details - </h1> - <FormWithData - dataQuery={useGetReportQuery} - queryArg={params.reportId} - DataForm={ReportDetailForm} - /> - </div> - ); - } -} - -function ReportDetailForm({ data: report }) { - const from = report.account; - const target = report.target_account; - - return ( - <div className="report detail"> - <div className="usernames"> - <Username user={from} /> reported <Username user={target} /> - </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> - </div> - - {!report.action_taken && <ReportActionForm report={report} />} - - { - 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> - } - </div> - ); -} - -function ReportActionForm({ report }) { - const form = { - id: useValue("id", report.id), - comment: useTextInput("action_taken_comment") - }; - - const [submit, result] = useFormSubmit(form, useResolveReportMutation(), { changedOnly: false }); - - return ( - <form onSubmit={submit} className="info-block"> - <h3>Resolving this report</h3> - <p> - 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> - <TextArea - field={form.comment} - label="Comment" - /> - <MutationButton - disabled={false} - label="Resolve" - result={result} - /> - </form> - ); -} - -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); - } - - 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> - ); -} diff --git a/web/source/settings/admin/reports/index.tsx b/web/source/settings/admin/reports/index.tsx deleted file mode 100644 index 052d72761..000000000 --- a/web/source/settings/admin/reports/index.tsx +++ /dev/null @@ -1,103 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; -import { Link, Switch, Route } from "wouter"; - -import FormWithData from "../../lib/form/form-with-data"; - -import ReportDetail from "./detail"; -import Username from "./username"; -import { useBaseUrl } from "../../lib/navigation/util"; -import { useListReportsQuery } from "../../lib/query/admin/reports"; - -export default function Reports({ baseUrl }) { - return ( - <div className="reports"> - <Switch> - <Route path={`${baseUrl}/:reportId`}> - <ReportDetail /> - </Route> - <ReportOverview /> - </Switch> - </div> - ); -} - -function ReportOverview({ }) { - return ( - <> - <h1>Reports</h1> - <div> - <p> - Here you can view and resolve reports made to your instance, originating from local and remote users. - </p> - </div> - <FormWithData - dataQuery={useListReportsQuery} - DataForm={ReportsList} - /> - </> - ); -} - -function ReportsList({ data: reports }) { - return ( - <div className="list"> - {reports.map((report) => ( - <ReportEntry key={report.id} report={report} /> - ))} - </div> - ); -} - -function ReportEntry({ report }) { - const baseUrl = useBaseUrl(); - const from = report.account; - const target = report.target_account; - - let comment = report.comment.length > 200 - ? report.comment.slice(0, 200) + "..." - : report.comment; - - return ( - <Link to={`${baseUrl}/${report.id}`}> - <a className={`report entry${report.action_taken ? " resolved" : ""}`}> - <div className="byline"> - <div className="usernames"> - <Username user={from} link={false} /> reported <Username user={target} link={false} /> - </div> - <h3 className="report-status"> - {report.action_taken ? "Resolved" : "Open"} - </h3> - </div> - <div className="details"> - <b>Created: </b> - <span>{new Date(report.created_at).toLocaleString()}</span> - - <b>Reason: </b> - {comment.length > 0 - ? <p>{comment}</p> - : <i className="no-comment">none provided</i> - } - </div> - </a> - </Link> - ); -} diff --git a/web/source/settings/admin/reports/username.tsx b/web/source/settings/admin/reports/username.tsx deleted file mode 100644 index 6fba0b804..000000000 --- a/web/source/settings/admin/reports/username.tsx +++ /dev/null @@ -1,54 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; -import { Link } from "wouter"; - -export default function Username({ user, link = true }) { - let className = "user"; - let isLocal = user.domain == null; - - if (user.suspended) { - className += " suspended"; - } - - if (isLocal) { - className += " local"; - } - - let icon = isLocal - ? { fa: "fa-home", info: "Local user" } - : { fa: "fa-external-link-square", info: "Remote user" }; - - let Element: any = "div"; - let href: any = null; - - if (link) { - Element = Link; - href = `/settings/admin/accounts/${user.id}`; - } - - return ( - <Element className={className} to={href}> - <span className="acct">@{user.account.acct}</span> - <i className={`fa fa-fw ${icon.fa}`} aria-hidden="true" title={icon.info} /> - <span className="sr-only">{icon.info}</span> - </Element> - ); -} diff --git a/web/source/settings/admin/settings/index.tsx b/web/source/settings/admin/settings/index.tsx deleted file mode 100644 index 69fbfd4ca..000000000 --- a/web/source/settings/admin/settings/index.tsx +++ /dev/null @@ -1,190 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; - -import { useTextInput, useFileInput } from "../../lib/form"; - -const useFormSubmit = require("../../lib/form/submit").default; - -import { TextInput, TextArea, FileInput } from "../../components/form/inputs"; - -const FormWithData = require("../../lib/form/form-with-data").default; -import MutationButton from "../../components/form/mutation-button"; - -import { useInstanceV1Query } from "../../lib/query"; -import { useUpdateInstanceMutation } from "../../lib/query/admin"; -import { InstanceV1 } from "../../lib/types/instance"; - -export default function AdminSettings() { - return ( - <FormWithData - dataQuery={useInstanceV1Query} - DataForm={AdminSettingsForm} - /> - ); -} - -interface AdminSettingsFormProps{ - data: InstanceV1; -} - -function AdminSettingsForm({ data: instance }: AdminSettingsFormProps) { - const titleLimit = 40; - const shortDescLimit = 500; - const descLimit = 5000; - const termsLimit = 5000; - - const form = { - title: useTextInput("title", { - source: instance, - validator: (val: string) => val.length <= titleLimit ? "" : `Instance title is ${val.length} characters; must be ${titleLimit} characters or less` - }), - thumbnail: useFileInput("thumbnail", { withPreview: true }), - thumbnailDesc: useTextInput("thumbnail_description", { source: instance }), - shortDesc: useTextInput("short_description", { - source: instance, - // Select "raw" text version of parsed field for editing. - valueSelector: (s: InstanceV1) => s.short_description_text, - validator: (val: string) => val.length <= shortDescLimit ? "" : `Instance short description is ${val.length} characters; must be ${shortDescLimit} characters or less` - }), - description: useTextInput("description", { - source: instance, - // Select "raw" text version of parsed field for editing. - valueSelector: (s: InstanceV1) => s.description_text, - validator: (val: string) => val.length <= descLimit ? "" : `Instance description is ${val.length} characters; must be ${descLimit} characters or less` - }), - terms: useTextInput("terms", { - source: instance, - // Select "raw" text version of parsed field for editing. - valueSelector: (s: InstanceV1) => s.terms_text, - validator: (val: string) => val.length <= termsLimit ? "" : `Instance terms and conditions is ${val.length} characters; must be ${termsLimit} characters or less` - }), - contactUser: useTextInput("contact_username", { source: instance, valueSelector: (s) => s.contact_account?.username }), - contactEmail: useTextInput("contact_email", { source: instance, valueSelector: (s) => s.email }) - }; - - const [submitForm, result] = useFormSubmit(form, useUpdateInstanceMutation()); - - return ( - <form onSubmit={submitForm}> - <h1>Instance Settings</h1> - - <div className="form-section-docs"> - <h3>Appearance</h3> - <a - href="https://docs.gotosocial.org/en/latest/admin/settings/#instance-appearance" - target="_blank" - className="docslink" - rel="noreferrer" - > - Learn more about these settings (opens in a new tab) - </a> - </div> - - <TextInput - field={form.title} - label={`Instance title (max ${titleLimit} characters)`} - placeholder="My GoToSocial instance" - /> - - <div className="file-upload" aria-labelledby="avatar"> - <strong id="avatar">Instance avatar (1:1 images look best)</strong> - <div className="file-upload-with-preview"> - <img - className="preview avatar" - src={form.thumbnail.previewValue ?? instance?.thumbnail} - alt={form.thumbnailDesc.value ?? (instance?.thumbnail ? `Thumbnail image for the instance` : "No instance thumbnail image set")} - /> - <div className="file-input-with-image-description"> - <FileInput - field={form.thumbnail} - accept="image/png, image/jpeg, image/webp, image/gif" - /> - <TextInput - field={form.thumbnailDesc} - label="Avatar image description" - placeholder="A cute drawing of a smiling sloth." - /> - </div> - </div> - - </div> - - <div className="form-section-docs"> - <h3>Descriptors</h3> - <a - href="https://docs.gotosocial.org/en/latest/admin/settings/#instance-descriptors" - target="_blank" - className="docslink" - rel="noreferrer" - > - Learn more about these settings (opens in a new tab) - </a> - </div> - - <TextArea - field={form.shortDesc} - label={`Short description (markdown accepted, max ${shortDescLimit} characters)`} - placeholder="A small testing instance for the GoToSocial alpha software." - rows={6} - /> - - <TextArea - field={form.description} - label={`Full description (markdown accepted, max ${descLimit} characters)`} - placeholder="A small testing instance for the GoToSocial alpha software. Just trying it out, my main instance is https://example.com" - rows={6} - /> - - <TextArea - field={form.terms} - label={`Terms & Conditions (markdown accepted, max ${termsLimit} characters)`} - placeholder="Terms and conditions of using this instance, data policy, imprint, GDPR stuff, yadda yadda." - rows={6} - /> - - <div className="form-section-docs"> - <h3>Contact info</h3> - <a - href="https://docs.gotosocial.org/en/latest/admin/settings/#instance-contact-info" - target="_blank" - className="docslink" - rel="noreferrer" - > - Learn more about these settings (opens in a new tab) - </a> - </div> - - <TextInput - field={form.contactUser} - label="Contact user (local account username)" - placeholder="admin" - /> - - <TextInput - field={form.contactEmail} - label="Contact email" - placeholder="admin@example.com" - /> - - <MutationButton label="Save" result={result} disabled={false} /> - </form> - ); -}
\ No newline at end of file diff --git a/web/source/settings/admin/settings/rules.tsx b/web/source/settings/admin/settings/rules.tsx deleted file mode 100644 index e5e4d17c5..000000000 --- a/web/source/settings/admin/settings/rules.tsx +++ /dev/null @@ -1,174 +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 <http://www.gnu.org/licenses/>. -*/ - -import React from "react"; -import { Switch, Route, Link, Redirect, useRoute } from "wouter"; - -import { useInstanceRulesQuery, useAddInstanceRuleMutation, useUpdateInstanceRuleMutation, useDeleteInstanceRuleMutation } from "../../lib/query"; -import FormWithData from "../../lib/form/form-with-data"; -import { useBaseUrl } from "../../lib/navigation/util"; - -import { useValue, useTextInput } from "../../lib/form"; -import useFormSubmit from "../../lib/form/submit"; - -import { TextArea } from "../../components/form/inputs"; -import MutationButton from "../../components/form/mutation-button"; -import { Error } from "../../components/error"; - -export default function InstanceRulesData({ baseUrl }) { - return ( - <FormWithData - dataQuery={useInstanceRulesQuery} - DataForm={InstanceRules} - {...{baseUrl}} - /> - ); -} - -function InstanceRules({ baseUrl, data: rules }) { - return ( - <Switch> - <Route path={`${baseUrl}/:ruleId`}> - <InstanceRuleDetail rules={rules} /> - </Route> - <Route> - <div> - <h1>Instance Rules</h1> - <div> - <p> - The rules for your instance are listed on the about page, and can be selected when submitting reports. - </p> - </div> - <InstanceRuleList rules={rules} /> - </div> - </Route> - </Switch> - ); -} - -function InstanceRuleList({ rules }) { - const newRule = useTextInput("text", {}); - - const [submitForm, result] = useFormSubmit({ newRule }, useAddInstanceRuleMutation(), { - changedOnly: true, - onFinish: () => newRule.reset() - }); - - return ( - <> - <form onSubmit={submitForm} className="new-rule"> - <ol className="instance-rules"> - {Object.values(rules).map((rule: any) => ( - <InstanceRule key={rule.id} rule={rule} /> - ))} - </ol> - <TextArea - field={newRule} - label="New instance rule" - /> - <MutationButton - disabled={newRule.value === undefined || newRule.value.length === 0} - label="Add rule" - result={result} - /> - </form> - </> - ); -} - -function InstanceRule({ rule }) { - const baseUrl = useBaseUrl(); - - return ( - <Link to={`${baseUrl}/${rule.id}`}> - <a className="rule"> - <li> - <h2>{rule.text} <i className="fa fa-pencil edit-icon" /></h2> - </li> - <span>{new Date(rule.created_at).toLocaleString()}</span> - </a> - </Link> - ); -} - -function InstanceRuleDetail({ rules }) { - const baseUrl = useBaseUrl(); - let [_match, params] = useRoute(`${baseUrl}/:ruleId`); - - if (params?.ruleId == undefined || rules[params.ruleId] == undefined) { - return <Redirect to={baseUrl} />; - } else { - return ( - <> - <Link to={baseUrl}><a>< go back</a></Link> - <InstanceRuleForm rule={rules[params.ruleId]} /> - </> - ); - } -} - -function InstanceRuleForm({ rule }) { - const baseUrl = useBaseUrl(); - const form = { - id: useValue("id", rule.id), - rule: useTextInput("text", { defaultValue: rule.text }) - }; - - const [submitForm, result] = useFormSubmit(form, useUpdateInstanceRuleMutation()); - - const [deleteRule, deleteResult] = useDeleteInstanceRuleMutation({ fixedCacheKey: rule.id }); - - if (result.isSuccess || deleteResult.isSuccess) { - return ( - <Redirect to={baseUrl} /> - ); - } - - return ( - <div className="rule-detail"> - <form onSubmit={submitForm}> - <TextArea - field={form.rule} - /> - - <div className="action-buttons row"> - <MutationButton - label="Save" - showError={false} - result={result} - disabled={!form.rule.hasChanged()} - /> - - <MutationButton - disabled={false} - type="button" - onClick={() => deleteRule(rule.id)} - label="Delete" - className="button danger" - showError={false} - result={deleteResult} - /> - </div> - - {result.error && <Error error={result.error} />} - {deleteResult.error && <Error error={deleteResult.error} />} - </form> - </div> - ); -} |