| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | /* | 
					
						
							|  |  |  | 	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/>.
 | 
					
						
							|  |  |  | */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | import React, { useState } from "react"; | 
					
						
							| 
									
										
										
										
											2025-03-17 15:06:17 +01:00
										 |  |  | import { useVerifyCredentialsQuery } from "../lib/query/login"; | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | import { MediaAttachment, Status as StatusType } from "../lib/types/status"; | 
					
						
							|  |  |  | import sanitize from "sanitize-html"; | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | import BlurhashCanvas from "./blurhash"; | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | export function FakeStatus({ children }) { | 
					
						
							|  |  |  | 	const { data: account = { | 
					
						
							| 
									
										
										
										
											2024-07-20 15:02:22 +02:00
										 |  |  | 		avatar: "/assets/default_avatars/GoToSocial_icon1.webp", | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | 		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); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | 	const [ detailsOpen, setDetailsOpen ] = useState(false); | 
					
						
							| 
									
										
										
										
											2025-04-09 14:14:20 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | 	return ( | 
					
						
							|  |  |  | 		<div className="status-body"> | 
					
						
							| 
									
										
										
										
											2025-04-09 14:14:20 +02:00
										 |  |  | 			<details | 
					
						
							|  |  |  | 				className="text-spoiler" | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | 				open={detailsOpen} | 
					
						
							| 
									
										
										
										
											2025-04-09 14:14:20 +02:00
										 |  |  | 			> | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | 				<summary tabIndex={-1}> | 
					
						
							| 
									
										
										
										
											2025-03-07 15:04:34 +01:00
										 |  |  | 					<div | 
					
						
							|  |  |  | 						className="spoiler-content" | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | 						lang={status.language} | 
					
						
							|  |  |  | 					> | 
					
						
							|  |  |  | 						{ status.spoiler_text | 
					
						
							|  |  |  | 							? status.spoiler_text + " " | 
					
						
							|  |  |  | 							: "[no content warning set] " | 
					
						
							|  |  |  | 						} | 
					
						
							| 
									
										
										
										
											2025-03-07 15:04:34 +01:00
										 |  |  | 					</div> | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | 					<span | 
					
						
							|  |  |  | 						className="button" | 
					
						
							|  |  |  | 						role="button" | 
					
						
							|  |  |  | 						tabIndex={0} | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | 						aria-label={detailsOpen ? "Hide content" : "Show content"} | 
					
						
							|  |  |  | 						onClick={(e) => { | 
					
						
							|  |  |  | 							e.preventDefault(); | 
					
						
							|  |  |  | 							setDetailsOpen(!detailsOpen); | 
					
						
							|  |  |  | 						}} | 
					
						
							|  |  |  | 						onKeyDown={(e) => { | 
					
						
							|  |  |  | 							if (e.key === "Enter") { | 
					
						
							|  |  |  | 								e.preventDefault(); | 
					
						
							|  |  |  | 								setDetailsOpen(!detailsOpen); | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 						}} | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | 					> | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | 						{detailsOpen ? "Hide content" : "Show content"} | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | 					</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 }) { | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | 	const [ detailsOpen, setDetailsOpen ] = useState(false); | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | 	return ( | 
					
						
							|  |  |  | 		<div className="media-wrapper"> | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | 			<details | 
					
						
							|  |  |  | 				className="image-spoiler media-spoiler" | 
					
						
							|  |  |  | 				open={detailsOpen} | 
					
						
							|  |  |  | 			> | 
					
						
							|  |  |  | 				<summary tabIndex={-1}> | 
					
						
							| 
									
										
										
										
											2025-04-09 14:14:20 +02:00
										 |  |  | 					<div | 
					
						
							|  |  |  | 						className="show sensitive button" | 
					
						
							|  |  |  | 						role="button" | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | 						tabIndex={-1} | 
					
						
							| 
									
										
										
										
											2025-04-09 14:14:20 +02:00
										 |  |  | 						aria-hidden="true" | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | 						onClick={(e) => { | 
					
						
							|  |  |  | 							e.preventDefault(); | 
					
						
							|  |  |  | 							setDetailsOpen(!detailsOpen); | 
					
						
							|  |  |  | 						}} | 
					
						
							|  |  |  | 						onKeyDown={(e) => { | 
					
						
							|  |  |  | 							if (e.key === "Enter") { | 
					
						
							|  |  |  | 								e.preventDefault(); | 
					
						
							|  |  |  | 								setDetailsOpen(!detailsOpen); | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 						}} | 
					
						
							| 
									
										
										
										
											2025-04-09 14:14:20 +02:00
										 |  |  | 					> | 
					
						
							|  |  |  | 						Show media | 
					
						
							|  |  |  | 					</div> | 
					
						
							|  |  |  | 					<span | 
					
						
							|  |  |  | 						className="eye button" | 
					
						
							|  |  |  | 						role="button" | 
					
						
							|  |  |  | 						tabIndex={0} | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | 						aria-label={detailsOpen ? "Hide media" : "Show media"} | 
					
						
							|  |  |  | 						onClick={(e) => { | 
					
						
							|  |  |  | 							e.preventDefault(); | 
					
						
							|  |  |  | 							setDetailsOpen(!detailsOpen); | 
					
						
							|  |  |  | 						}} | 
					
						
							|  |  |  | 						onKeyDown={(e) => { | 
					
						
							|  |  |  | 							if (e.key === "Enter") { | 
					
						
							|  |  |  | 								e.preventDefault(); | 
					
						
							|  |  |  | 								setDetailsOpen(!detailsOpen); | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 						}} | 
					
						
							| 
									
										
										
										
											2025-04-09 14:14:20 +02:00
										 |  |  | 					> | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | 						<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> | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | 					<div className="blurhash-container"> | 
					
						
							|  |  |  | 						<BlurhashCanvas media={media} /> | 
					
						
							|  |  |  | 					</div> | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | 				</summary> | 
					
						
							|  |  |  | 				<a | 
					
						
							| 
									
										
										
										
											2025-04-14 15:12:21 +02:00
										 |  |  | 					className="photoswipe-slide" | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | 					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 ( | 
					
						
							| 
									
										
										
										
											2024-08-24 11:49:37 +02:00
										 |  |  | 		<aside className="status-info"> | 
					
						
							| 
									
										
										
										
											2024-06-18 18:18:00 +02:00
										 |  |  | 			<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> | 
					
						
							|  |  |  | 	); | 
					
						
							|  |  |  | } |