diff options
Diffstat (limited to 'web/source/settings/components/status.tsx')
-rw-r--r-- | web/source/settings/components/status.tsx | 242 |
1 files changed, 242 insertions, 0 deletions
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> + ); +} |