Add support to mark videos as spoilers (#2255)
Some checks failed
Deploy to Netlify (dev) / Deploy to Netlify (push) Has been cancelled

* Add support for MSC4193: Spoilers on Media

* Clarify variable names and wording

* Restore list atom

* Improve spoilered image UX with autoload off

* Use `aria-pressed` to indicate attachment spoiler state

* Improve spoiler button tooltip wording, keep reveal button from conflicting with load errors

* Make it possible to mark videos as spoilers

* Allow videos to be marked as spoilers when uploaded

* Apply requested changes

* Show a loading spinner on spoiled media when unblurred

---------

Co-authored-by: Ajay Bura <32841439+ajbura@users.noreply.github.com>
This commit is contained in:
Ginger 2025-09-24 23:41:35 -04:00 committed by GitHub
parent 867a47218a
commit b78f6f23b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 112 additions and 24 deletions

View file

@ -3,6 +3,7 @@ import {
Badge,
Box,
Button,
Chip,
Icon,
Icons,
Spinner,
@ -47,6 +48,8 @@ type VideoContentProps = {
info: IVideoInfo & IThumbnailContent;
encInfo?: EncryptedAttachmentInfo;
autoPlay?: boolean;
markedAsSpoiler?: boolean;
spoilerReason?: string;
renderThumbnail?: () => ReactNode;
renderVideo: (props: RenderVideoProps) => ReactNode;
};
@ -60,6 +63,8 @@ export const VideoContent = as<'div', VideoContentProps>(
info,
encInfo,
autoPlay,
markedAsSpoiler,
spoilerReason,
renderThumbnail,
renderVideo,
...props
@ -72,6 +77,7 @@ export const VideoContent = as<'div', VideoContentProps>(
const [load, setLoad] = useState(false);
const [error, setError] = useState(false);
const [blurred, setBlurred] = useState(markedAsSpoiler ?? false);
const [srcState, loadSrc] = useAsyncCallback(
useCallback(async () => {
@ -114,11 +120,15 @@ export const VideoContent = as<'div', VideoContentProps>(
/>
)}
{renderThumbnail && !load && (
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
<Box
className={classNames(css.AbsoluteContainer, blurred && css.Blur)}
alignItems="Center"
justifyContent="Center"
>
{renderThumbnail()}
</Box>
)}
{!autoPlay && srcState.status === AsyncStatus.Idle && (
{!autoPlay && !blurred && srcState.status === AsyncStatus.Idle && (
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
<Button
variant="Secondary"
@ -133,7 +143,7 @@ export const VideoContent = as<'div', VideoContentProps>(
</Box>
)}
{srcState.status === AsyncStatus.Success && (
<Box className={css.AbsoluteContainer}>
<Box className={classNames(css.AbsoluteContainer, blurred && css.Blur)}>
{renderVideo({
title: body,
src: srcState.data,
@ -144,8 +154,39 @@ export const VideoContent = as<'div', VideoContentProps>(
})}
</Box>
)}
{blurred && !error && srcState.status !== AsyncStatus.Error && (
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
<TooltipProvider
tooltip={
typeof spoilerReason === 'string' && (
<Tooltip variant="Secondary">
<Text>{spoilerReason}</Text>
</Tooltip>
)
}
position="Top"
align="Center"
>
{(triggerRef) => (
<Chip
ref={triggerRef}
variant="Secondary"
radii="Pill"
size="500"
outlined
onClick={() => {
setBlurred(false);
}}
>
<Text size="B300">Spoiler</Text>
</Chip>
)}
</TooltipProvider>
</Box>
)}
{(srcState.status === AsyncStatus.Loading || srcState.status === AsyncStatus.Success) &&
!load && (
!load &&
!blurred && (
<Box className={css.AbsoluteContainer} alignItems="Center" justifyContent="Center">
<Spinner variant="Secondary" />
</Box>