GoToSocial/web/source/settings/components/status.tsx

243 lines
6.2 KiB
TypeScript

/*
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.webp",
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">
<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>
);
}