From d2b3d37724a999d4cc78c46157593267e29d184e Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Tue, 18 Jun 2024 18:18:00 +0200
Subject: [feature/frontend] Reports frontend v2 (#3022)

* use apiutil + paging in admin processor+handlers

* we're making it happen

* fix little whoopsie

* styling for report list

* don't youuuu forget about meee don't don't don't don't

* last bits

* sanitize content before showing in report statuses

* update report docs
---
 web/source/settings/components/status.tsx | 242 ++++++++++++++++++++++++++++++
 1 file changed, 242 insertions(+)
 create mode 100644 web/source/settings/components/status.tsx

(limited to 'web/source/settings/components/status.tsx')

diff --git a/web/source/settings/components/status.tsx b/web/source/settings/components/status.tsx
new file mode 100644
index 000000000..56b061d39
--- /dev/null
+++ b/web/source/settings/components/status.tsx
@@ -0,0 +1,242 @@
+/*
+	GoToSocial
+	Copyright (C) GoToSocial Authors admin@gotosocial.org
+	SPDX-License-Identifier: AGPL-3.0-or-later
+
+	This program is free software: you can redistribute it and/or modify
+	it under the terms of the GNU Affero General Public License as published by
+	the Free Software Foundation, either version 3 of the License, or
+	(at your option) any later version.
+
+	This program is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+	GNU Affero General Public License for more details.
+
+	You should have received a copy of the GNU Affero General Public License
+	along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import React from "react";
+import { useVerifyCredentialsQuery } from "../lib/query/oauth";
+import { MediaAttachment, Status as StatusType } from "../lib/types/status";
+import sanitize from "sanitize-html";
+
+export function FakeStatus({ children }) {
+	const { data: account = {
+		avatar: "/assets/default_avatars/GoToSocial_icon1.png",
+		display_name: "",
+		username: ""
+	} } = useVerifyCredentialsQuery();
+
+	return (
+		<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">
+						{children}
+					</div>
+				</div>
+			</section>
+		</article>
+	);
+}
+
+export function Status({ status }: { status: StatusType }) {
+	return (
+		<article
+			className="status expanded"
+			id={status.id}
+			role="region"
+		>
+			<StatusHeader status={status} />
+			<StatusBody status={status} />
+			<StatusFooter status={status} />
+			<a
+				href={status.url}
+				target="_blank"
+				className="status-link"
+				data-nosnippet
+				title="Open this status (opens in new tab)"
+			>
+				Open this status (opens in new tab)
+			</a>
+		</article>
+	);
+}
+
+function StatusHeader({ status }: { status: StatusType }) {
+	const author = status.account;
+	
+	return (
+		<header className="status-header">
+			<address>
+				<a
+					href={author.url}
+					rel="author"
+					title="Open profile"
+					target="_blank"
+				>
+					<img
+						className="avatar"
+						aria-hidden="true"
+						src={author.avatar}
+						alt={`Avatar for ${author.username}`}
+						title={`Avatar for ${author.username}`}
+					/>
+					<div className="author-strap">
+						<span className="displayname text-cutoff">{author.display_name}</span>
+						<span className="sr-only">,</span>
+						<span className="username text-cutoff">@{author.acct}</span>
+					</div>
+					<span className="sr-only">(open profile)</span>
+				</a>
+			</address>
+		</header>
+	);
+}
+
+function StatusBody({ status }: { status: StatusType }) {
+	let content: string;
+	if (status.content.length === 0) {
+		content = "[no content set]";
+	} else {
+		// HTML has already been through
+		// the instance sanitizer by now,
+		// but do it again just in case.
+		content = sanitize(status.content);
+	}
+
+	return (
+		<div className="status-body">
+			<details className="text-spoiler">
+				<summary>
+					<span
+						className="spoiler-text"
+						lang={status.language}
+					>
+						{ status.spoiler_text
+							? status.spoiler_text + " "
+							: "[no content warning set] "
+						}
+					</span>
+					<span
+						className="button"
+						role="button"
+						tabIndex={0}
+						aria-label="Toggle content visibility"
+					>
+						Toggle content visibility
+					</span>
+				</summary>
+				<div
+					className="text"
+					dangerouslySetInnerHTML={{__html: content}}
+				/>
+			</details>
+			<StatusMedia status={status} />
+		</div>
+	);
+}
+
+function StatusMedia({ status }: { status: StatusType }) {
+	if (status.media_attachments.length === 0) {
+		return null;
+	}
+
+	const count = status.media_attachments.length;
+	const aria_label = count === 1 ? "1 attachment" : `${count} attachments`;
+	const oddOrEven = count % 2 === 0 ? "even" : "odd";
+	const single = count === 1 ? " single" : "";
+
+	return (
+		<div
+			className={`media ${oddOrEven}${single}`}
+			role="group"
+			aria-label={aria_label}
+		>
+			{ status.media_attachments.map((media) => {
+				return (
+					<StatusMediaEntry
+						key={media.id}
+						media={media}
+					/>
+				);
+			})}
+		</div>
+	);
+}
+
+function StatusMediaEntry({ media }: { media: MediaAttachment }) {
+	return (
+		<div className="media-wrapper">
+			<details className="image-spoiler media-spoiler">
+				<summary>
+					<div className="show sensitive button" aria-hidden="true">Show media</div>
+					<span className="eye button" role="button" tabIndex={0} aria-label="Toggle show media">
+						<i className="hide fa fa-fw fa-eye-slash" aria-hidden="true"></i>
+						<i className="show fa fa-fw fa-eye" aria-hidden="true"></i>
+					</span>
+					<img
+						src={media.preview_url}
+						loading="lazy"
+						alt={media.description}
+						title={media.description}
+						width={media.meta.small.width}
+						height={media.meta.small.height}
+					/>
+				</summary>
+				<a
+					href={media.url}
+					target="_blank"
+				>
+					<img
+						src={media.url}
+						loading="lazy"
+						alt={media.description}
+						width={media.meta.original.width}
+						height={media.meta.original.height}
+					/>
+				</a>
+			</details>
+		</div>
+	);
+}
+
+function StatusFooter({ status }: { status: StatusType }) {
+	return (
+		<aside className="status-info" aria-hidden="true">    
+			<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={status.created_at}>
+								{ new Date(status.created_at).toLocaleString() }
+							</time>
+						</dd>
+					</div>
+				</div>
+				<div className="stats-item language">
+					<dt className="sr-only">Language</dt>
+					<dd>{status.language}</dd>
+				</div>
+			</dl>
+		</aside>
+	);
+}
-- 
cgit v1.2.3

