feat: add VideoViewer component for enhanced video playback experience; implement download functionality and viewer modal

This commit is contained in:
2026-04-24 08:38:22 +10:00
parent 35ccdc0a22
commit 7b69ed6df8
7 changed files with 192 additions and 128 deletions

View File

@@ -6,6 +6,10 @@ import {
Chip,
Icon,
Icons,
Modal,
Overlay,
OverlayBackdrop,
OverlayCenter,
Spinner,
Text,
Tooltip,
@@ -14,6 +18,7 @@ import {
} from 'folds';
import classNames from 'classnames';
import { BlurhashCanvas } from 'react-blurhash';
import FocusTrap from 'focus-trap-react';
import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import {
IThumbnailContent,
@@ -33,7 +38,14 @@ import {
import { getCurrentAccessToken } from '../../../utils/auth';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { validBlurHash } from '../../../utils/blurHash';
import { ModalWide } from '../../../styles/Modal.css';
import { stopPropagation } from '../../../utils/keyboard';
type RenderViewerProps = {
src: string;
alt: string;
requestClose: () => void;
};
type RenderVideoProps = {
title: string;
src: string;
@@ -41,6 +53,7 @@ type RenderVideoProps = {
onError: () => void;
autoPlay: boolean;
controls: boolean;
onExpand?: () => void;
};
type VideoContentProps = {
body: string;
@@ -52,6 +65,7 @@ type VideoContentProps = {
markedAsSpoiler?: boolean;
spoilerReason?: string;
renderThumbnail?: () => ReactNode;
renderViewer?: (props: RenderViewerProps) => ReactNode;
renderVideo: (props: RenderVideoProps) => ReactNode;
};
export const VideoContent = as<'div', VideoContentProps>(
@@ -67,6 +81,7 @@ export const VideoContent = as<'div', VideoContentProps>(
markedAsSpoiler,
spoilerReason,
renderThumbnail,
renderViewer,
renderVideo,
...props
},
@@ -78,6 +93,7 @@ export const VideoContent = as<'div', VideoContentProps>(
const [load, setLoad] = useState(false);
const [error, setError] = useState(false);
const [viewer, setViewer] = useState(false);
const [blurred, setBlurred] = useState(markedAsSpoiler ?? false);
const [srcState, loadSrc] = useAsyncCallback(
@@ -113,6 +129,28 @@ export const VideoContent = as<'div', VideoContentProps>(
return (
<Box className={classNames(css.RelativeBase, className)} {...props} ref={ref}>
{renderViewer && srcState.status === AsyncStatus.Success && (
<Overlay open={viewer} backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setViewer(false),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Modal className={ModalWide} size="500">
{renderViewer({
src: srcState.data,
alt: body,
requestClose: () => setViewer(false),
})}
</Modal>
</FocusTrap>
</OverlayCenter>
</Overlay>
)}
{typeof blurHash === 'string' && !load && (
<BlurhashCanvas
style={{ width: '100%', height: '100%' }}
@@ -154,6 +192,7 @@ export const VideoContent = as<'div', VideoContentProps>(
onError: handleError,
autoPlay: true,
controls: true,
onExpand: renderViewer ? () => setViewer(true) : undefined,
})}
</Box>
)}