diff options
Diffstat (limited to 'web/source')
| -rw-r--r-- | web/source/package.json | 1 | ||||
| -rw-r--r-- | web/source/settings/admin/accounts/detail.jsx | 114 | ||||
| -rw-r--r-- | web/source/settings/admin/accounts/index.jsx | 140 | ||||
| -rw-r--r-- | web/source/settings/admin/reports/detail.jsx | 2 | ||||
| -rw-r--r-- | web/source/settings/admin/reports/index.jsx | 7 | ||||
| -rw-r--r-- | web/source/settings/admin/reports/username.jsx | 7 | ||||
| -rw-r--r-- | web/source/settings/components/error.jsx | 14 | ||||
| -rw-r--r-- | web/source/settings/components/fake-profile.jsx | 8 | ||||
| -rw-r--r-- | web/source/settings/index.js | 1 | ||||
| -rw-r--r-- | web/source/settings/lib/navigation/components.jsx | 72 | ||||
| -rw-r--r-- | web/source/settings/lib/query/admin/index.js | 26 | ||||
| -rw-r--r-- | web/source/settings/lib/query/base.js | 2 | ||||
| -rw-r--r-- | web/source/settings/style.css | 51 | ||||
| -rw-r--r-- | web/source/settings/user/profile.js | 2 | ||||
| -rw-r--r-- | web/source/yarn.lock | 7 | 
15 files changed, 418 insertions, 36 deletions
| diff --git a/web/source/package.json b/web/source/package.json index e1ca357b3..bd082b6e7 100644 --- a/web/source/package.json +++ b/web/source/package.json @@ -28,7 +28,6 @@      "psl": "^1.9.0",      "react": "^18.2.0",      "react-dom": "^18.2.0", -    "react-error-boundary": "^3.1.4",      "react-redux": "^8.0.4",      "redux": "^4.2.0",      "redux-persist": "^6.0.0", diff --git a/web/source/settings/admin/accounts/detail.jsx b/web/source/settings/admin/accounts/detail.jsx new file mode 100644 index 000000000..189c07e8d --- /dev/null +++ b/web/source/settings/admin/accounts/detail.jsx @@ -0,0 +1,114 @@ +/* +	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/>. +*/ + +"use strict"; + +const React = require("react"); +const { useRoute, Redirect } = require("wouter"); + +const query = require("../../lib/query"); + +const FormWithData = require("../../lib/form/form-with-data"); + +const { useBaseUrl } = require("../../lib/navigation/util"); +const FakeProfile = require("../../components/fake-profile"); +const MutationButton = require("../../components/form/mutation-button"); + +const useFormSubmit = require("../../lib/form/submit"); +const { useValue, useTextInput } = require("../../lib/form"); +const { TextInput } = require("../../components/form/inputs"); + +module.exports = function AccountDetail({ }) { +	const baseUrl = useBaseUrl(); + +	let [_match, params] = useRoute(`${baseUrl}/:accountId`); + +	if (params?.accountId == undefined) { +		return <Redirect to={baseUrl} />; +	} else { +		return ( +			<div className="account-detail"> +				<h1> +					Account Details +				</h1> +				<FormWithData +					dataQuery={query.useGetAccountQuery} +					queryArg={params.accountId} +					DataForm={AccountDetailForm} +				/> +			</div> +		); +	} +}; + +function AccountDetailForm({ data: account }) { +	let content; +	if (account.suspended) { +		content = ( +			<h2 className="error">Account is suspended.</h2> +		); +	} else { +		content = <ModifyAccount account={account} />; +	} + +	return ( +		<> +			<FakeProfile {...account} /> + +			{content} +		</> +	); +} + +function ModifyAccount({ account }) { +	const form = { +		id: useValue("id", account.id), +		reason: useTextInput("text", {}) +	}; + +	const [modifyAccount, result] = useFormSubmit(form, query.useActionAccountMutation()); + +	return ( +		<form onSubmit={modifyAccount}> +			<h2>Actions</h2> +			<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 +					label="Suspend" +					name="suspend" +					result={result} +				/> +			</div> +		</form> +	); +}
\ No newline at end of file diff --git a/web/source/settings/admin/accounts/index.jsx b/web/source/settings/admin/accounts/index.jsx new file mode 100644 index 000000000..1dea36fb5 --- /dev/null +++ b/web/source/settings/admin/accounts/index.jsx @@ -0,0 +1,140 @@ +/* +	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/>. +*/ + +"use strict"; + +const React = require("react"); +const { Switch, Route, Link } = require("wouter"); + +const query = require("../../lib/query"); +const { useTextInput } = require("../../lib/form"); + +const AccountDetail = require("./detail"); +const { useBaseUrl } = require("../../lib/navigation/util"); +const { Error } = require("../../components/error"); + +module.exports = function Accounts({ baseUrl }) { +	return ( +		<div className="accounts"> +			<Switch> +				<Route path={`${baseUrl}/:accountId`}> +					<AccountDetail /> +				</Route> +				<AccountOverview /> +			</Switch> +		</div> +	); +}; + +function AccountOverview({ }) { +	return ( +		<> +			<h1>Accounts</h1> +			<div> +				Pending <a href="https://github.com/superseriousbusiness/gotosocial/issues/581">#581</a>, +				there is currently no way to list accounts.<br /> +				You can perform actions on reported accounts by clicking their name in the report, or searching for a username below. +			</div> + +			<AccountSearchForm /> +		</> +	); +} + +function AccountSearchForm() { +	const [searchAccount, result] = query.useSearchAccountMutation(); + +	const [onAccountChange, _resetAccount, { account }] = useTextInput("account"); + +	function submitSearch(e) { +		e.preventDefault(); +		if (account.trim().length != 0) { +			searchAccount(account); +		} +	} + +	return ( +		<div className="account-search"> +			<form onSubmit={submitSearch}> +				<div className="form-field text"> +					<label htmlFor="url"> +						Account: +					</label> +					<div className="row"> +						<input +							type="text" +							id="account" +							name="account" +							onChange={onAccountChange} +							value={account} +						/> +						<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> +			<AccountList +				isSuccess={result.isSuccess} +				data={result.data} +				isError={result.isError} +				error={result.error} +			/> +		</div> +	); +} + +function AccountList({ isSuccess, data, isError, error }) { +	const baseUrl = useBaseUrl(); + +	if (!(isSuccess || isError)) { +		return null; +	} + +	if (error) { +		return <Error error={error} />; +	} + +	if (data.length == 0) { +		return <b>No accounts found that match your query</b>; +	} + +	return ( +		<> +			<h2>Results:</h2> +			<div className="list"> +				{data.map((acc) => ( +					<Link key={acc.acct} className="account entry" to={`${baseUrl}/${acc.id}`}> +						{acc.display_name?.length > 0 +							? acc.display_name +							: acc.username +						} +						<span id="username">(@{acc.acct})</span> +					</Link> +				))} +			</div> +		</> +	); +}
\ No newline at end of file diff --git a/web/source/settings/admin/reports/detail.jsx b/web/source/settings/admin/reports/detail.jsx index e9c40cb13..e85b345b0 100644 --- a/web/source/settings/admin/reports/detail.jsx +++ b/web/source/settings/admin/reports/detail.jsx @@ -165,7 +165,7 @@ function ReportedToot({ toot }) {  				}  			</section>  			<aside className="info"> -				<time datetime={toot.created_at}>{new Date(toot.created_at).toLocaleString()}</time> +				<time dateTime={toot.created_at}>{new Date(toot.created_at).toLocaleString()}</time>  			</aside>  		</article>  	); diff --git a/web/source/settings/admin/reports/index.jsx b/web/source/settings/admin/reports/index.jsx index b2b8b4295..62a71c3e3 100644 --- a/web/source/settings/admin/reports/index.jsx +++ b/web/source/settings/admin/reports/index.jsx @@ -48,13 +48,6 @@ function ReportOverview({ }) {  		<>  			<h1>Reports</h1>  			<div> -				<div className="info"> -					<i className="fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i> -					<p> -						<b>This interface is currently very limited</b>, only providing a basic overview. <br /> -						Work is in progress on a more full-fledged moderation experience. -					</p> -				</div>  				<p>  					Here you can view and resolve reports made to your instance, originating from local and remote users.  				</p> diff --git a/web/source/settings/admin/reports/username.jsx b/web/source/settings/admin/reports/username.jsx index 7dbabb491..eca3570d8 100644 --- a/web/source/settings/admin/reports/username.jsx +++ b/web/source/settings/admin/reports/username.jsx @@ -20,6 +20,7 @@  "use strict";  const React = require("react"); +const { Link } = require("wouter");  module.exports = function Username({ user, link = true }) {  	let className = "user"; @@ -41,12 +42,12 @@ module.exports = function Username({ user, link = true }) {  	let href = null;  	if (link) { -		Element = "a"; -		href = user.account.url; +		Element = Link; +		href = `/settings/admin/accounts/${user.id}`;  	}  	return ( -		<Element className={className} href={href} target="_blank" rel="noreferrer" > +		<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> diff --git a/web/source/settings/components/error.jsx b/web/source/settings/components/error.jsx index 7f718b3f5..283083481 100644 --- a/web/source/settings/components/error.jsx +++ b/web/source/settings/components/error.jsx @@ -31,12 +31,14 @@ function ErrorFallback({ error, resetErrorBoundary }) {  				<a href="https://matrix.to/#/#gotosocial-help:superseriousbusiness.org">Matrix support room</a>.  				<br />Include the details below:  			</p> -			<pre> -				{error.name}: {error.message} -			</pre> -			<pre> -				{error.stack} -			</pre> +			<div className="details"> +				<pre> +					{error.name}: {error.message} +				</pre> +				<pre> +					{error.stack} +				</pre> +			</div>  			<p>  				<button onClick={resetErrorBoundary}>Try again</button> or <a href="">refresh the page</a>  			</p> diff --git a/web/source/settings/components/fake-profile.jsx b/web/source/settings/components/fake-profile.jsx index 5f00cd6d8..04424a5ed 100644 --- a/web/source/settings/components/fake-profile.jsx +++ b/web/source/settings/components/fake-profile.jsx @@ -29,7 +29,7 @@ module.exports = function FakeProfile({ avatar, header, display_name, username,  					<img src={header} alt={header ? `header image for ${username}` : "None set"} />  				</div>  				<div className="basic-info" aria-hidden="true"> -					<a className="avatar" href="{{.account.Avatar}}"> +					<a className="avatar" href={avatar}>  						<img src={avatar} alt={avatar ? `avatar image for ${username}` : "None set"} />  					</a>  					<span className="displayname text-cutoff"> @@ -37,9 +37,9 @@ module.exports = function FakeProfile({ avatar, header, display_name, username,  						<span className="sr-only">.</span>  					</span>  					<span className="username text-cutoff">@{username}</span> -					{(role && role != "user") && -						<div className={`role ${role}`}> -							<span className="sr-only">Role: </span>{role} +					{(role && role.name != "user") && +						<div className={`role ${role.name}`}> +							<span className="sr-only">Role: </span>{role.name}  						</div>  					}  				</div> diff --git a/web/source/settings/index.js b/web/source/settings/index.js index 66848291b..8eb11e0aa 100644 --- a/web/source/settings/index.js +++ b/web/source/settings/index.js @@ -44,6 +44,7 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [  		permissions: ["admin"]  	}, [  		Item("Reports", { icon: "fa-flag", wildcard: true }, require("./admin/reports")), +		Item("Accounts", { icon: "fa-users", wildcard: true }, require("./admin/accounts")),  		Menu("Federation", { icon: "fa-hubzilla" }, [  			Item("Federation", { icon: "fa-hubzilla", url: "", wildcard: true }, require("./admin/federation")),  			Item("Import/Export", { icon: "fa-floppy-o", wildcard: true }, require("./admin/federation/import-export")), diff --git a/web/source/settings/lib/navigation/components.jsx b/web/source/settings/lib/navigation/components.jsx index 18e0cd76c..365ebff89 100644 --- a/web/source/settings/lib/navigation/components.jsx +++ b/web/source/settings/lib/navigation/components.jsx @@ -21,11 +21,8 @@  const React = require("react");  const { Link, Route, Redirect, Switch, useLocation, useRouter } = require("wouter"); -const { ErrorBoundary } = require("react-error-boundary");  const syncpipe = require("syncpipe"); -const { ErrorFallback } = require("../../components/error"); -  const {  	RoleContext,  	useHasPermission, @@ -72,8 +69,8 @@ function ViewRouter(routing, defaultRoute) {  				(_) => _.map((route) => {  					return (  						<Route path={route.routingUrl} key={route.key}> -							<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => { }}> -								{/* FIXME: implement onReset */} +							<ErrorBoundary> +								{/* FIXME: implement reset */}  								<BaseUrlContext.Provider value={route.url}>  									{route.view}  								</BaseUrlContext.Provider> @@ -134,6 +131,71 @@ function MenuComponent({ type, name, url, icon, permissions, links, level, child  	);  } +class ErrorBoundary extends React.Component { + +	constructor() { +		super(); +		this.state = {}; + +		this.resetErrorBoundary = () => { +			this.setState({}); +		}; +	} + +	static getDerivedStateFromError(error) { +		return { hadError: true, error }; +	} + +	componentDidCatch(_e, info) { +		this.setState({ +			...this.state, +			componentStack: info.componentStack +		}); +	} + +	render() { +		if (this.state.hadError) { +			return ( +				<ErrorFallback +					error={this.state.error} +					componentStack={this.state.componentStack} +					resetErrorBoundary={this.resetErrorBoundary} +				/> +			); +		} else { +			return this.props.children; +		} +	} +} + +function ErrorFallback({ error, componentStack, resetErrorBoundary }) { +	return ( +		<div className="error"> +			<p> +				{"An error occured, please report this on the "} +				<a href="https://github.com/superseriousbusiness/gotosocial/issues">GoToSocial issue tracker</a> +				{" or "} +				<a href="https://matrix.to/#/#gotosocial-help:superseriousbusiness.org">Matrix support room</a>. +				<br />Include the details below: +			</p> +			<div className="details"> +				<pre> +					{error.name}: {error.message} + +					{componentStack && [ +						"\n\nComponent trace:", +						componentStack +					]} +					{["\n\nError trace: ", error.stack]} +				</pre> +			</div> +			<p> +				<button onClick={resetErrorBoundary}>Try again</button> or <a href="">refresh the page</a> +			</p> +		</div> +	); +} +  module.exports = {  	Sidebar,  	ViewRouter, diff --git a/web/source/settings/lib/query/admin/index.js b/web/source/settings/lib/query/admin/index.js index 534ae962c..dd4a61b51 100644 --- a/web/source/settings/lib/query/admin/index.js +++ b/web/source/settings/lib/query/admin/index.js @@ -78,6 +78,32 @@ const endpoints = (build) => ({  			}  		})  	}), +	getAccount: build.query({ +		query: (id) => ({ +			url: `/api/v1/accounts/${id}` +		}), +		providesTags: (_, __, id) => [{ type: "Account", id }] +	}), +	actionAccount: build.mutation({ +		query: ({ id, action, reason }) => ({ +			method: "POST", +			url: `/api/v1/admin/accounts/${id}/action`, +			asForm: true, +			body: { +				type: action, +				text: reason +			} +		}), +		invalidatesTags: (_, __, { id }) => [{ type: "Account", id }] +	}), +	searchAccount: build.mutation({ +		query: (username) => ({ +			url: `/api/v2/search?q=${encodeURIComponent(username)}&resolve=true` +		}), +		transformResponse: (res) => { +			return res.accounts ?? []; +		} +	}),  	...require("./import-export")(build),  	...require("./custom-emoji")(build),  	...require("./reports")(build) diff --git a/web/source/settings/lib/query/base.js b/web/source/settings/lib/query/base.js index bee19bf18..f880853d2 100644 --- a/web/source/settings/lib/query/base.js +++ b/web/source/settings/lib/query/base.js @@ -73,7 +73,7 @@ function instanceBasedQuery(args, api, extraOptions) {  module.exports = createApi({  	reducerPath: "api",  	baseQuery: instanceBasedQuery, -	tagTypes: ["Auth", "Emoji", "Reports"], +	tagTypes: ["Auth", "Emoji", "Reports", "Account"],  	endpoints: (build) => ({  		instance: build.query({  			query: () => ({ diff --git a/web/source/settings/style.css b/web/source/settings/style.css index 3a3c14924..9392b76a5 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -61,6 +61,7 @@ header {  		background: $bg-accent;  		padding: 2rem;  		border-radius: $br; +		max-width: 100%;  		& > div, & > form {  			border-left: 0.2rem solid $border-accent; @@ -92,6 +93,10 @@ header {  				padding-left: 0;  			}  		} + +		& > .error { +			display: grid; /* prevents error overflowing */ +		}  	}  	.sidebar { @@ -250,11 +255,20 @@ input, select, textarea {  	font-weight: bold;  	padding: 0.5rem;  	white-space: pre-wrap; +	position: relative;  	a {  		color: $error-link;  	} +	.details { +		max-width: 100%; +		overflow: hidden; +		display: flex; +		flex-direction: column; +		gap: 0.5rem; +	} +  	pre {  		background: $bg;  		color: $fg; @@ -395,6 +409,7 @@ section.with-sidebar > div, section.with-sidebar > form {  .user-profile {  	.overview {  		display: grid; +		max-width: 60rem;  		grid-template-columns: 70% 30%;  		grid-template-rows: 100%;  		gap: 1rem; @@ -1062,6 +1077,42 @@ button.with-padding {  	}  } +.account-search { +	form { +		margin-bottom: 1rem; +	} + +	.list { +		margin: 0.5rem 0; + +		a { +			color: $fg; +			text-decoration: none; + +			#username { +				color: $link-fg; +				margin-left: 0.5em; +			} +		} +	} +} + +.account-detail { +	display: flex; +	flex-direction: column; +	gap: 1rem; + +	.profile { +		overflow: hidden; +		max-width: 60rem; +	} + +	.action-buttons { +		display: flex; +		gap: 0.5rem; +	} +} +  @media screen and (orientation: portrait) {  	.reports .report .byline {  		grid-template-columns: 1fr; diff --git a/web/source/settings/user/profile.js b/web/source/settings/user/profile.js index ae2da0003..4b39d1822 100644 --- a/web/source/settings/user/profile.js +++ b/web/source/settings/user/profile.js @@ -91,7 +91,7 @@ function UserProfileForm({ data: profile }) {  					header={form.header.previewValue ?? profile.header}  					display_name={form.displayName.value ?? profile.username}  					username={profile.username} -					role={profile.role.name} +					role={profile.role}  				/>  				<div className="files">  					<div> diff --git a/web/source/yarn.lock b/web/source/yarn.lock index 45fe5891c..7a06633eb 100644 --- a/web/source/yarn.lock +++ b/web/source/yarn.lock @@ -4605,13 +4605,6 @@ react-dom@^18.2.0:      loose-envify "^1.1.0"      scheduler "^0.23.0" -react-error-boundary@^3.1.4: -  version "3.1.4" -  resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0" -  integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA== -  dependencies: -    "@babel/runtime" "^7.12.5" -  react-is@^16.13.1, react-is@^16.7.0:    version "16.13.1"    resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" | 
