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> -	); -} | 
