diff --git a/apps/frontend/components/chat/chat-video-lightbox.tsx b/apps/frontend/components/chat/chat-video-lightbox.tsx
new file mode 100644
index 0000000..ab41368
--- /dev/null
+++ b/apps/frontend/components/chat/chat-video-lightbox.tsx
@@ -0,0 +1,269 @@
+'use client';
+
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { createPortal } from 'react-dom';
+import { Loader2, Pause, Play, Volume2, VolumeX, X } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+
+function formatDuration(seconds: number) {
+ if (!Number.isFinite(seconds) || seconds < 0) return '0:00';
+ const whole = Math.floor(seconds);
+ const mins = Math.floor(whole / 60);
+ const secs = whole % 60;
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
+}
+
+export function ChatVideoLightbox({
+ open,
+ src,
+ fileName,
+ durationMs,
+ onClose
+}: {
+ open: boolean;
+ src: string;
+ fileName?: string;
+ durationMs?: number;
+ onClose: () => void;
+}) {
+ const videoRef = useRef
(null);
+ const hideControlsTimerRef = useRef | null>(null);
+ const [mounted, setMounted] = useState(false);
+ const [playing, setPlaying] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [muted, setMuted] = useState(false);
+ const [progress, setProgress] = useState(0);
+ const [duration, setDuration] = useState(durationMs ? durationMs / 1000 : 0);
+ const [controlsVisible, setControlsVisible] = useState(true);
+ const [scrubbing, setScrubbing] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ const revealControls = useCallback(() => {
+ setControlsVisible(true);
+ if (hideControlsTimerRef.current) clearTimeout(hideControlsTimerRef.current);
+ hideControlsTimerRef.current = setTimeout(() => {
+ if (playing) setControlsVisible(false);
+ }, 2800);
+ }, [playing]);
+
+ const togglePlayback = useCallback(() => {
+ const video = videoRef.current;
+ if (!video) return;
+ if (video.paused) {
+ void video.play();
+ } else {
+ video.pause();
+ }
+ revealControls();
+ }, [revealControls]);
+
+ useEffect(() => {
+ if (!open) return;
+ function handleKeyDown(event: KeyboardEvent) {
+ if (event.key === 'Escape') onClose();
+ if (event.key === ' ') {
+ event.preventDefault();
+ togglePlayback();
+ }
+ }
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [open, onClose, togglePlayback]);
+
+ useEffect(() => {
+ if (!open || typeof document === 'undefined') return;
+ const previousOverflow = document.body.style.overflow;
+ document.body.style.overflow = 'hidden';
+ return () => {
+ document.body.style.overflow = previousOverflow;
+ };
+ }, [open]);
+
+ useEffect(() => {
+ const video = videoRef.current;
+ if (!video || !open) return;
+
+ const onLoaded = () => {
+ if (Number.isFinite(video.duration) && video.duration > 0) {
+ setDuration(video.duration);
+ }
+ setLoading(false);
+ };
+ const onTimeUpdate = () => {
+ if (!scrubbing && video.duration) {
+ setProgress(video.currentTime / video.duration);
+ }
+ };
+ const onPlay = () => {
+ setPlaying(true);
+ revealControls();
+ };
+ const onPause = () => {
+ setPlaying(false);
+ setControlsVisible(true);
+ };
+ const onEnded = () => {
+ setPlaying(false);
+ setControlsVisible(true);
+ };
+
+ video.addEventListener('loadedmetadata', onLoaded);
+ video.addEventListener('timeupdate', onTimeUpdate);
+ video.addEventListener('play', onPlay);
+ video.addEventListener('pause', onPause);
+ video.addEventListener('ended', onEnded);
+
+ return () => {
+ video.removeEventListener('loadedmetadata', onLoaded);
+ video.removeEventListener('timeupdate', onTimeUpdate);
+ video.removeEventListener('play', onPlay);
+ video.removeEventListener('pause', onPause);
+ video.removeEventListener('ended', onEnded);
+ };
+ }, [open, revealControls, scrubbing, src]);
+
+ useEffect(() => {
+ if (!open) return;
+ const video = videoRef.current;
+ if (!video) return;
+ setLoading(true);
+ setProgress(0);
+ setPlaying(false);
+ video.src = src;
+ video.load();
+ void video.play().catch(() => undefined);
+ }, [open, src]);
+
+ useEffect(() => {
+ return () => {
+ if (hideControlsTimerRef.current) clearTimeout(hideControlsTimerRef.current);
+ };
+ }, []);
+
+ function seekTo(ratio: number) {
+ const video = videoRef.current;
+ if (!video || !video.duration) return;
+ const next = Math.min(Math.max(ratio, 0), 1);
+ video.currentTime = next * video.duration;
+ setProgress(next);
+ }
+
+ if (!open || !mounted) return null;
+
+ return createPortal(
+
+
+
+
{fileName?.trim() || 'Видео'}
+
Полноэкранный просмотр
+
+
+
+
+
+
+
+ {loading ? (
+
+
+
+ ) : null}
+
+ {!playing && !loading ? (
+
+ ) : null}
+
+
+
+
+
{
+ setScrubbing(true);
+ seekTo(Number(event.target.value) / 1000);
+ }}
+ onMouseUp={() => setScrubbing(false)}
+ onTouchEnd={() => setScrubbing(false)}
+ aria-label="Позиция воспроизведения"
+ />
+
+
+
+
+
+ {formatDuration(progress * duration)} / {formatDuration(duration)}
+
+
+
+
+
+
+
+
+
,
+ document.body
+ );
+}
diff --git a/apps/frontend/components/chat/chat-video-player.tsx b/apps/frontend/components/chat/chat-video-player.tsx
index fa8ead6..bf6f291 100644
--- a/apps/frontend/components/chat/chat-video-player.tsx
+++ b/apps/frontend/components/chat/chat-video-player.tsx
@@ -1,7 +1,8 @@
'use client';
import { useEffect, useRef, useState } from 'react';
-import { Loader2, Pause, Play } from 'lucide-react';
+import { Loader2, Maximize2, Pause, Play } from 'lucide-react';
+import { ChatVideoLightbox } from '@/components/chat/chat-video-lightbox';
import { cn } from '@/lib/utils';
function formatDuration(seconds: number) {
@@ -16,17 +17,20 @@ export function ChatVideoPlayer({
src,
durationMs,
fileName,
- variant = 'theirs'
+ variant = 'theirs',
+ enableFullscreen = true
}: {
src: string;
durationMs?: number;
fileName?: string;
variant?: 'mine' | 'theirs';
+ enableFullscreen?: boolean;
}) {
const videoRef = useRef(null);
const [playing, setPlaying] = useState(false);
const [loading, setLoading] = useState(true);
const [duration, setDuration] = useState(durationMs ? durationMs / 1000 : 0);
+ const [fullscreenOpen, setFullscreenOpen] = useState(false);
useEffect(() => {
const video = videoRef.current;
@@ -65,43 +69,74 @@ export function ChatVideoPlayer({
}
}
+ function openFullscreen(event: React.MouseEvent) {
+ event.stopPropagation();
+ videoRef.current?.pause();
+ setPlaying(false);
+ setFullscreenOpen(true);
+ }
+
return (
-
-
-