You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
6.9 KiB
278 lines
6.9 KiB
/* |
|
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, { useRef } from "react"; |
|
import { useVerifyCredentialsQuery } from "../lib/query/login"; |
|
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); |
|
} |
|
|
|
const detailsRef = useRef<HTMLDetailsElement>(null); |
|
const detailsOnClick = () => { |
|
detailsRef.current?.click(); |
|
}; |
|
|
|
const summaryRef = useRef<HTMLElement>(null); |
|
const summaryOnClick = () => { |
|
summaryRef.current?.click(); |
|
}; |
|
|
|
return ( |
|
<div className="status-body"> |
|
<details |
|
className="text-spoiler" |
|
ref={detailsRef} |
|
> |
|
<summary |
|
tabIndex={-1} |
|
ref={summaryRef} |
|
> |
|
<div |
|
className="spoiler-content" |
|
lang={status.language} |
|
> |
|
{ status.spoiler_text |
|
? status.spoiler_text + " " |
|
: "[no content warning set] " |
|
} |
|
</div> |
|
<span |
|
className="button" |
|
role="button" |
|
tabIndex={0} |
|
aria-label="Toggle content visibility" |
|
onClick={detailsOnClick} |
|
onKeyDown={e => e.key === "Enter" && summaryOnClick()} |
|
> |
|
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 }) { |
|
const detailsRef = useRef<HTMLDetailsElement>(null); |
|
const detailsOnClick = () => { |
|
detailsRef.current?.click(); |
|
}; |
|
|
|
const summaryRef = useRef<HTMLElement>(null); |
|
const summaryOnClick = () => { |
|
summaryRef.current?.click(); |
|
}; |
|
|
|
return ( |
|
<div className="media-wrapper"> |
|
<details className="image-spoiler media-spoiler"> |
|
<summary tabIndex={-1} ref={summaryRef}> |
|
<div |
|
className="show sensitive button" |
|
role="button" |
|
tabIndex={0} |
|
aria-hidden="true" |
|
onClick={detailsOnClick} |
|
onKeyDown={e => e.key === "Enter" && summaryOnClick()} |
|
> |
|
Show media |
|
</div> |
|
<span |
|
className="eye button" |
|
role="button" |
|
tabIndex={0} |
|
aria-label="Toggle show media" |
|
onClick={detailsOnClick} |
|
onKeyDown={e => e.key === "Enter" && summaryOnClick()} |
|
> |
|
<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> |
|
</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> |
|
); |
|
}
|
|
|