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

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { ReactNode, useEffect } from 'react';
import { Box, Chip, Icon, IconButton, Icons, Text, color, config, toRem } from 'folds';
import { UploadCard, UploadCardError, UploadCardProgress } from './UploadCard';
import { UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
@ -13,8 +13,54 @@ import {
import { useObjectURL } from '../../hooks/useObjectURL';
import { useMediaConfig } from '../../hooks/useMediaConfig';
type ImagePreviewProps = { fileItem: TUploadItem; onSpoiler: (marked: boolean) => void };
function ImagePreview({ fileItem, onSpoiler }: ImagePreviewProps) {
type PreviewImageProps = {
fileItem: TUploadItem;
};
function PreviewImage({ fileItem }: PreviewImageProps) {
const { originalFile, metadata } = fileItem;
const fileUrl = useObjectURL(originalFile);
return (
<img
style={{
objectFit: 'contain',
width: '100%',
height: toRem(152),
filter: metadata.markedAsSpoiler ? 'blur(44px)' : undefined,
}}
alt={originalFile.name}
src={fileUrl}
/>
);
}
type PreviewVideoProps = {
fileItem: TUploadItem;
};
function PreviewVideo({ fileItem }: PreviewVideoProps) {
const { originalFile, metadata } = fileItem;
const fileUrl = useObjectURL(originalFile);
return (
// eslint-disable-next-line jsx-a11y/media-has-caption
<video
style={{
objectFit: 'contain',
width: '100%',
height: toRem(152),
filter: metadata.markedAsSpoiler ? 'blur(44px)' : undefined,
}}
src={fileUrl}
/>
);
}
type MediaPreviewProps = {
fileItem: TUploadItem;
onSpoiler: (marked: boolean) => void;
children: ReactNode;
};
function MediaPreview({ fileItem, onSpoiler, children }: MediaPreviewProps) {
const { originalFile, metadata } = fileItem;
const fileUrl = useObjectURL(originalFile);
@ -27,16 +73,7 @@ function ImagePreview({ fileItem, onSpoiler }: ImagePreviewProps) {
position: 'relative',
}}
>
<img
style={{
objectFit: 'contain',
width: '100%',
height: toRem(152),
filter: fileItem.metadata.markedAsSpoiler ? 'blur(44px)' : undefined,
}}
src={fileUrl}
alt={originalFile.name}
/>
{children}
<Box
justifyContent="End"
style={{
@ -136,7 +173,14 @@ export function UploadCardRenderer({
bottom={
<>
{fileItem.originalFile.type.startsWith('image') && (
<ImagePreview fileItem={fileItem} onSpoiler={handleSpoiler} />
<MediaPreview fileItem={fileItem} onSpoiler={handleSpoiler}>
<PreviewImage fileItem={fileItem} />
</MediaPreview>
)}
{fileItem.originalFile.type.startsWith('video') && (
<MediaPreview fileItem={fileItem} onSpoiler={handleSpoiler}>
<PreviewVideo fileItem={fileItem} />
</MediaPreview>
)}
{upload.status === UploadStatus.Idle && !fileSizeExceeded && (
<UploadCardProgress sentBytes={0} totalBytes={file.size} />