feat: add telegram-style resource stream
This commit is contained in:
118
src/components/messageStream/overlays/VideoPlayer.tsx
Normal file
118
src/components/messageStream/overlays/VideoPlayer.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type PropsWithChildren,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { X } from "lucide-react";
|
||||
import type { Attachment } from "../../../types/post";
|
||||
|
||||
type PlayerState = {
|
||||
attachment: Attachment;
|
||||
currentTime: number;
|
||||
} | null;
|
||||
|
||||
type Ctx = {
|
||||
openVideo: (attachment: Attachment, currentTime?: number) => void;
|
||||
closeVideo: () => void;
|
||||
};
|
||||
|
||||
const VideoPlayerContext = createContext<Ctx | null>(null);
|
||||
|
||||
export function useVideoPlayer(): Ctx {
|
||||
const ctx = useContext(VideoPlayerContext);
|
||||
if (!ctx)
|
||||
throw new Error("useVideoPlayer must be used inside VideoPlayerProvider");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function VideoPlayerProvider({ children }: PropsWithChildren) {
|
||||
const [state, setState] = useState<PlayerState>(null);
|
||||
|
||||
const openVideo = useCallback(
|
||||
(attachment: Attachment, currentTime = 0) =>
|
||||
setState({ attachment, currentTime }),
|
||||
[],
|
||||
);
|
||||
const closeVideo = useCallback(() => setState(null), []);
|
||||
|
||||
return (
|
||||
<VideoPlayerContext.Provider value={{ openVideo, closeVideo }}>
|
||||
{children}
|
||||
{state ? (
|
||||
<PlayerView
|
||||
attachment={state.attachment}
|
||||
startAt={state.currentTime}
|
||||
onClose={closeVideo}
|
||||
/>
|
||||
) : null}
|
||||
</VideoPlayerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function PlayerView({
|
||||
attachment,
|
||||
startAt,
|
||||
onClose,
|
||||
}: {
|
||||
attachment: Attachment;
|
||||
startAt: number;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
const prevOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKey);
|
||||
document.body.style.overflow = prevOverflow;
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
const v = videoRef.current;
|
||||
if (!v) return;
|
||||
if (startAt > 0) v.currentTime = startAt;
|
||||
v.play().catch(() => {});
|
||||
}, [startAt]);
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/95"
|
||||
onClick={onClose}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
className="absolute right-4 top-4 z-10 flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={attachment.url}
|
||||
poster={attachment.posterUrl}
|
||||
controls
|
||||
playsInline
|
||||
className="max-h-[92vh] max-w-[96vw] outline-none"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user