mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 19:52:25 -05:00 
			
		
		
		
	[feature/Frogend] basic report admin interface (#1424)
* basic listing of reports * report detail overview, resolving * report detail styling tweaks * linter fixes
This commit is contained in:
		
					parent
					
						
							
								47daddc10c
							
						
					
				
			
			
				commit
				
					
						83b522a1b6
					
				
			
		
					 8 changed files with 575 additions and 2 deletions
				
			
		
							
								
								
									
										234
									
								
								web/source/settings/admin/reports/detail.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										234
									
								
								web/source/settings/admin/reports/detail.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,234 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	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 BackButton = require("../../components/back-button"); | ||||||
|  | 
 | ||||||
|  | const { useValue, useTextInput } = require("../../lib/form"); | ||||||
|  | const useFormSubmit = require("../../lib/form/submit"); | ||||||
|  | 
 | ||||||
|  | const { TextArea } = require("../../components/form/inputs"); | ||||||
|  | 
 | ||||||
|  | const MutationButton = require("../../components/form/mutation-button"); | ||||||
|  | const Username = require("./username"); | ||||||
|  | 
 | ||||||
|  | module.exports = function ReportDetail({ baseUrl }) { | ||||||
|  | 	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={query.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> | ||||||
|  | 				<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, query.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 label="Resolve" result={result} /> | ||||||
|  | 		</form> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function ReportedToot({ toot }) { | ||||||
|  | 	const account = toot.account; | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<div className="toot expanded"> | ||||||
|  | 			<div className="contentgrid"> | ||||||
|  | 				<span className="avatar"> | ||||||
|  | 					<img src={account.avatar} alt="" /> | ||||||
|  | 				</span> | ||||||
|  | 				<span className="displayname">{account.display_name.trim().length > 0 ? account.display_name : account.username}</span> | ||||||
|  | 				<span className="username">@{account.username}</span> | ||||||
|  | 				<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} /> | ||||||
|  | 				} | ||||||
|  | 			</div> | ||||||
|  | 			<div className="toot-info"> | ||||||
|  | 				<a | ||||||
|  | 					href={toot.url} | ||||||
|  | 					target="_blank" | ||||||
|  | 					rel="noreferrer" | ||||||
|  | 				>{new Date(toot.created_at).toLocaleString()}</a> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function TootCW({ note, content }) { | ||||||
|  | 	const [visible, setVisible] = React.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} | ||||||
|  | 							size={m.meta?.original} | ||||||
|  | 							type={m.type} | ||||||
|  | 						/> | ||||||
|  | 					</a> | ||||||
|  | 				</div> | ||||||
|  | 			))} | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | } | ||||||
							
								
								
									
										112
									
								
								web/source/settings/admin/reports/index.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								web/source/settings/admin/reports/index.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	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 { Link, Switch, Route } = require("wouter"); | ||||||
|  | 
 | ||||||
|  | const query = require("../../lib/query"); | ||||||
|  | 
 | ||||||
|  | const FormWithData = require("../../lib/form/form-with-data"); | ||||||
|  | 
 | ||||||
|  | const ReportDetail = require("./detail"); | ||||||
|  | const Username = require("./username"); | ||||||
|  | 
 | ||||||
|  | const baseUrl = "/settings/admin/reports"; | ||||||
|  | 
 | ||||||
|  | module.exports = function Reports() { | ||||||
|  | 	return ( | ||||||
|  | 		<div className="reports"> | ||||||
|  | 			<Switch> | ||||||
|  | 				<Route path={`${baseUrl}/:reportId`}> | ||||||
|  | 					<ReportDetail baseUrl={baseUrl} /> | ||||||
|  | 				</Route> | ||||||
|  | 				<ReportOverview baseUrl={baseUrl} /> | ||||||
|  | 			</Switch> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function ReportOverview({ _baseUrl }) { | ||||||
|  | 	return ( | ||||||
|  | 		<> | ||||||
|  | 			<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> | ||||||
|  | 			</div> | ||||||
|  | 			<FormWithData | ||||||
|  | 				dataQuery={query.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 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="users"> | ||||||
|  | 						<Username user={from} link={false} /> reported <Username user={target} link={false} /> | ||||||
|  | 					</div> | ||||||
|  | 					<h3 className="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> | ||||||
|  | 	); | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								web/source/settings/admin/reports/username.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								web/source/settings/admin/reports/username.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	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"); | ||||||
|  | 
 | ||||||
|  | module.exports = 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 = "span"; | ||||||
|  | 	let href = null; | ||||||
|  | 
 | ||||||
|  | 	if (link) { | ||||||
|  | 		Element = "a"; | ||||||
|  | 		href = user.account.url; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return ( | ||||||
|  | 		<Element className={className} href={href} target="_blank" rel="noreferrer" > | ||||||
|  | 			@{user.account.acct} | ||||||
|  | 			<i className={`fa fa-fw ${icon.fa}`} aria-hidden="true" title={icon.info} /> | ||||||
|  | 			<span className="sr-only">{icon.info}</span> | ||||||
|  | 		</Element> | ||||||
|  | 	); | ||||||
|  | }; | ||||||
|  | @ -43,6 +43,7 @@ const nav = { | ||||||
| 		"Instance Settings": require("./admin/settings.js"), | 		"Instance Settings": require("./admin/settings.js"), | ||||||
| 		"Actions": require("./admin/actions"), | 		"Actions": require("./admin/actions"), | ||||||
| 		"Federation": require("./admin/federation"), | 		"Federation": require("./admin/federation"), | ||||||
|  | 		"Reports": require("./admin/reports") | ||||||
| 	}, | 	}, | ||||||
| 	"Custom Emoji": { | 	"Custom Emoji": { | ||||||
| 		adminOnly: true, | 		adminOnly: true, | ||||||
|  |  | ||||||
|  | @ -78,7 +78,8 @@ const endpoints = (build) => ({ | ||||||
| 		}) | 		}) | ||||||
| 	}), | 	}), | ||||||
| 	...require("./import-export")(build), | 	...require("./import-export")(build), | ||||||
| 	...require("./custom-emoji")(build) | 	...require("./custom-emoji")(build), | ||||||
|  | 	...require("./reports")(build) | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| module.exports = base.injectEndpoints({ endpoints }); | module.exports = base.injectEndpoints({ endpoints }); | ||||||
							
								
								
									
										52
									
								
								web/source/settings/lib/query/admin/reports.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								web/source/settings/lib/query/admin/reports.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,52 @@ | ||||||
|  | /* | ||||||
|  | 	GoToSocial | ||||||
|  | 	Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  | 	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"; | ||||||
|  | 
 | ||||||
|  | module.exports = (build) => ({ | ||||||
|  | 	listReports: build.query({ | ||||||
|  | 		query: (params = {}) => ({ | ||||||
|  | 			url: "/api/v1/admin/reports", | ||||||
|  | 			params: { | ||||||
|  | 				limit: 100, | ||||||
|  | 				...params | ||||||
|  | 			} | ||||||
|  | 		}), | ||||||
|  | 		providesTags: ["Reports"] | ||||||
|  | 	}), | ||||||
|  | 
 | ||||||
|  | 	getReport: build.query({ | ||||||
|  | 		query: (id) => ({ | ||||||
|  | 			url: `/api/v1/admin/reports/${id}` | ||||||
|  | 		}), | ||||||
|  | 		providesTags: (res, error, id) => [{ type: "Reports", id }] | ||||||
|  | 	}), | ||||||
|  | 
 | ||||||
|  | 	resolveReport: build.mutation({ | ||||||
|  | 		query: (formData) => ({ | ||||||
|  | 			url: `/api/v1/admin/reports/${formData.id}/resolve`, | ||||||
|  | 			method: "POST", | ||||||
|  | 			asForm: true, | ||||||
|  | 			body: formData | ||||||
|  | 		}), | ||||||
|  | 		invalidatesTags: (res) => | ||||||
|  | 			res | ||||||
|  | 				? [{ type: "Reports", id: "LIST" }, { type: "Reports", id: res.id }] | ||||||
|  | 				: [{ type: "Reports", id: "LIST" }] | ||||||
|  | 	}) | ||||||
|  | }); | ||||||
|  | @ -72,7 +72,7 @@ function instanceBasedQuery(args, api, extraOptions) { | ||||||
| module.exports = createApi({ | module.exports = createApi({ | ||||||
| 	reducerPath: "api", | 	reducerPath: "api", | ||||||
| 	baseQuery: instanceBasedQuery, | 	baseQuery: instanceBasedQuery, | ||||||
| 	tagTypes: ["Auth", "Emoji"], | 	tagTypes: ["Auth", "Emoji", "Reports"], | ||||||
| 	endpoints: (build) => ({ | 	endpoints: (build) => ({ | ||||||
| 		instance: build.query({ | 		instance: build.query({ | ||||||
| 			query: () => ({ | 			query: () => ({ | ||||||
|  |  | ||||||
|  | @ -663,6 +663,10 @@ span.form-info { | ||||||
| 	a { | 	a { | ||||||
| 		color: $info-link; | 		color: $info-link; | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	p { | ||||||
|  | 		margin-top: 0; | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| button.with-icon { | button.with-icon { | ||||||
|  | @ -805,6 +809,121 @@ button.with-padding { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .reports { | ||||||
|  | 	p { | ||||||
|  | 		margin: 0; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.report { | ||||||
|  | 		display: flex; | ||||||
|  | 		flex-direction: column; | ||||||
|  | 		gap: 0.5rem; | ||||||
|  | 		margin: 0.5rem 0; | ||||||
|  | 
 | ||||||
|  | 		text-decoration: none; | ||||||
|  | 		color: $fg; | ||||||
|  | 
 | ||||||
|  | 		padding: 1rem; | ||||||
|  | 
 | ||||||
|  | 		border: none; | ||||||
|  | 		border-left: 0.3rem solid $border-accent; | ||||||
|  | 
 | ||||||
|  | 		.byline { | ||||||
|  | 			display: grid; | ||||||
|  | 			grid-template-columns: 1fr auto; | ||||||
|  | 
 | ||||||
|  | 			.status { | ||||||
|  | 				color: $border-accent; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.details { | ||||||
|  | 			display: grid; | ||||||
|  | 			grid-template-columns: auto 1fr; | ||||||
|  | 			gap: 0.2rem 0.5rem; | ||||||
|  | 			padding: 0.5rem; | ||||||
|  | 
 | ||||||
|  | 			justify-items: start; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		h3 { | ||||||
|  | 			margin: 0; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		&.resolved { | ||||||
|  | 			color: $fg-reduced; | ||||||
|  | 			border-left: 0.4rem solid $bg; | ||||||
|  | 
 | ||||||
|  | 			.byline .status { | ||||||
|  | 				color: $fg-reduced; | ||||||
|  | 			} | ||||||
|  | 			 | ||||||
|  | 			.user { | ||||||
|  | 				opacity: 0.8; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		&.detail { | ||||||
|  | 			border: none; | ||||||
|  | 			padding: 0; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.report.detail { | ||||||
|  | 		display: flex; | ||||||
|  | 		flex-direction: column; | ||||||
|  | 		margin-top: 1rem; | ||||||
|  | 		gap: 1rem; | ||||||
|  | 
 | ||||||
|  | 		.info-block { | ||||||
|  | 			padding: 0.5rem; | ||||||
|  | 			background: $gray2; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.info { | ||||||
|  | 			display: block; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.reported-toots { | ||||||
|  | 			margin-top: 0.5rem; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.toot .toot-info { | ||||||
|  | 			padding: 0.5rem; | ||||||
|  | 			background: $toot-info-bg; | ||||||
|  | 
 | ||||||
|  | 			a { | ||||||
|  | 				color: $fg-reduced; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			&:last-child { | ||||||
|  | 				border-bottom-left-radius: $br; | ||||||
|  | 				border-bottom-right-radius: $br; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.user { | ||||||
|  | 		background: $fg-accent; | ||||||
|  | 		color: $bg; | ||||||
|  | 		border-radius: $br; | ||||||
|  | 		padding: 0.1rem 0.2rem; | ||||||
|  | 		margin: 0 0.1rem; | ||||||
|  | 		font-weight: bold; | ||||||
|  | 		text-decoration: none; | ||||||
|  | 
 | ||||||
|  | 		&.suspended { | ||||||
|  | 			background: $bg-accent; | ||||||
|  | 			color: $fg; | ||||||
|  | 			text-decoration: line-through; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		&.local { | ||||||
|  | 			background: $green1; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| [role="button"] { | [role="button"] { | ||||||
| 	cursor: pointer; | 	cursor: pointer; | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue