This commit is contained in:
Халимов Рустам
2026-03-11 01:25:19 +03:00
parent 3ab9cdbb2a
commit a0ccdb052f

View File

@@ -1535,141 +1535,137 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
</div>
</>
)}
{/* === Video layout (connected, has video) === */}
{showVideoArea ? (
<div
ref={videoContainerRef}
className="relative bg-black w-full"
style={{ aspectRatio: '16 / 9' }}
>
<div className="absolute inset-0 bg-black flex items-center justify-center">
{/* Unified Remote video — always rendered if area is shown to keep stream alive & prevent ref-swapping issues */}
<video
ref={remoteVideoRef}
autoPlay
playsInline
className={`absolute inset-0 w-full h-full object-contain bg-black transition-opacity duration-300 ${hasRemoteVideo ? 'opacity-100' : 'opacity-0'}`}
onContextMenu={(e) => { e.preventDefault(); setShowVolumeSlider(true); }}
/>
{/* Placeholder shown when video track is muted or not yet arrived */}
{/* === Main Content Area === */}
<div className="flex-1 flex flex-col relative overflow-hidden">
{/*
CRITICAL: Single stable remote video element.
We keep it always mounted so WebRTC srcObject is never lost
during transitions between voice and video modes.
*/}
<video
ref={remoteVideoRef}
autoPlay
playsInline
className={`
bg-black transition-all duration-500
${showVideoArea
? 'w-full h-full object-contain'
: 'w-0 h-0 opacity-0 absolute pointer-events-none'
}
`}
onContextMenu={(e) => { e.preventDefault(); setShowVolumeSlider(true); }}
/>
{showVideoArea ? (
<div
ref={videoContainerRef}
className="absolute inset-0 flex items-center justify-center bg-black"
>
{/* Placeholder shown when video track is not yet arrived or muted */}
{!hasRemoteVideo && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-zinc-900/50 backdrop-blur-sm z-10">
<VideoOff size={48} className="text-zinc-500 mb-2" />
<span className="text-sm text-zinc-500">{displayName}</span>
</div>
)}
</div>
{/* Local video PIP (bottom-right) */}
{hasLocalVideo && (
<div className="absolute bottom-3 right-3 w-48 rounded-xl overflow-hidden border-2 border-white/20 shadow-lg bg-black z-10"
style={{ aspectRatio: '16 / 9' }}
>
<video
ref={localVideoRef}
autoPlay
playsInline
muted
className="w-full h-full object-contain bg-black"
/>
{/* Local video PIP (bottom-right) */}
{hasLocalVideo && (
<div className="absolute bottom-3 right-3 w-48 rounded-xl overflow-hidden border-2 border-white/20 shadow-lg bg-black z-20"
style={{ aspectRatio: '16 / 9' }}
>
<video
ref={localVideoRef}
autoPlay
playsInline
muted
className="w-full h-full object-contain bg-black"
/>
</div>
)}
{/* Duration badge */}
<div className="absolute top-4 left-1/2 -translate-x-1/2 px-4 py-1 rounded-full bg-black/50 backdrop-blur-sm z-20">
<span className="text-sm text-white font-mono">{formatDuration(duration)}</span>
</div>
)}
{/* Duration badge */}
<div className="absolute top-4 left-1/2 -translate-x-1/2 px-4 py-1 rounded-full bg-black/50 backdrop-blur-sm z-10">
<span className="text-sm text-white font-mono">{formatDuration(duration)}</span>
{/* Top-right buttons */}
<div className="absolute top-4 right-4 flex items-center gap-2 z-20">
<button
onClick={() => setIsMinimized(true)}
className="w-8 h-8 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white/70 hover:text-white transition-colors"
>
<Minimize2 size={16} />
</button>
<button
onClick={toggleFullscreen}
className="w-8 h-8 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white/70 hover:text-white transition-colors"
>
{isFullscreen ? <Minimize size={16} /> : <Maximize size={16} />}
</button>
</div>
</div>
{/* Top-right buttons: minimize + fullscreen */}
<div className="absolute top-4 right-4 flex items-center gap-2 z-10">
<button
onClick={() => setIsMinimized(true)}
className="w-8 h-8 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white/70 hover:text-white transition-colors"
title={t('minimize')}
>
<Minimize2 size={16} />
</button>
<button
onClick={toggleFullscreen}
className="w-8 h-8 rounded-full bg-black/50 backdrop-blur-sm flex items-center justify-center text-white/70 hover:text-white transition-colors"
>
{isFullscreen ? <Minimize size={16} /> : <Maximize size={16} />}
</button>
</div>
</div>
) : (
/* === Voice / calling / incoming layout === */
<div className="px-8 py-12 flex flex-col items-center relative z-10">
{/* Minimize button (top-right) */}
{callState === 'connected' && (
<button
onClick={() => setIsMinimized(true)}
className="absolute top-4 right-4 w-8 h-8 rounded-full bg-white/10 flex items-center justify-center text-white/70 hover:text-white transition-colors"
title={t('minimize')}
>
<Minimize2 size={16} />
</button>
)}
{/* Avatar with pulse — right-click for volume */}
<div
className="relative mb-8 mt-4"
onContextMenu={(e) => {
if (callState === 'connected') {
e.preventDefault();
setShowVolumeSlider(true);
}
}}
title={callState === 'connected' ? t('rightClickVolume') : undefined}
>
{(callState === 'calling' || callState === 'connected') && (
<>
<div className="absolute inset-0 rounded-full bg-vortex-500/30 animate-call-wave" />
<div className="absolute inset-0 rounded-full bg-vortex-500/20 animate-call-wave-delayed" />
</>
) : (
/* === Voice / calling / incoming layout === */
<div className="flex-1 px-8 py-12 flex flex-col items-center justify-center relative z-10">
{/* Minimize button (top-right) */}
{callState === 'connected' && (
<button
onClick={() => setIsMinimized(true)}
className="absolute top-4 right-4 w-8 h-8 rounded-full bg-white/10 flex items-center justify-center text-white/70 hover:text-white transition-colors"
title={t('minimize')}
>
<Minimize2 size={16} />
</button>
)}
{callState === 'incoming' && (
<>
<div className="absolute inset-0 rounded-full bg-emerald-500/30 animate-call-wave" />
<div className="absolute inset-0 rounded-full bg-emerald-500/20 animate-call-wave-delayed" />
</>
)}
<div className="relative z-10 p-1.5 rounded-full bg-gradient-to-br from-white/10 to-transparent backdrop-blur-md border border-white/10 shadow-2xl cursor-pointer">
{displayAvatar ? (
<img src={displayAvatar} alt="" className="w-32 h-32 rounded-full object-cover shadow-inner" />
) : (
<div className="w-32 h-32 rounded-full bg-gradient-to-br from-vortex-500 to-purple-600 flex items-center justify-center text-white font-bold text-4xl shadow-inner">
{initials}
</div>
<div className="relative mb-8 mt-4">
{(callState === 'calling' || callState === 'connected') && (
<>
<div className="absolute inset-0 rounded-full bg-vortex-500/30 animate-call-wave" />
<div className="absolute inset-0 rounded-full bg-vortex-500/20 animate-call-wave-delayed" />
</>
)}
{callState === 'incoming' && (
<>
<div className="absolute inset-0 rounded-full bg-emerald-500/30 animate-call-wave" />
<div className="absolute inset-0 rounded-full bg-emerald-500/20 animate-call-wave-delayed" />
</>
)}
<div className="relative z-10 p-1.5 rounded-full bg-gradient-to-br from-white/10 to-transparent backdrop-blur-md border border-white/10 shadow-2xl cursor-pointer">
{displayAvatar ? (
<img src={displayAvatar} alt="" className="w-32 h-32 rounded-full object-cover shadow-inner" />
) : (
<div className="w-32 h-32 rounded-full bg-gradient-to-br from-vortex-500 to-purple-600 flex items-center justify-center text-white font-bold text-4xl shadow-inner">
{initials}
</div>
)}
</div>
</div>
<h3 className="text-2xl font-bold text-white mb-2 tracking-tight">{displayName}</h3>
<p className="text-sm text-zinc-400 mb-8">
{callState === 'calling' && t('calling')}
{callState === 'incoming' && (callType === 'video' ? t('incomingVideoCall') : t('incomingCall'))}
{callState === 'connected' && formatDuration(duration)}
{callState === 'ended' && t('callEnded')}
</p>
{/* Local video preview (during calling with camera) */}
{hasLocalVideo && callState === 'calling' && (
<div className="mb-6 rounded-2xl overflow-hidden bg-zinc-900 w-48 shadow-2xl border border-white/10">
<video
ref={localVideoRef}
autoPlay
playsInline
muted
className="w-full h-auto"
/>
</div>
)}
</div>
<h3 className="text-2xl font-bold text-white mb-2 tracking-tight">{displayName}</h3>
<p className="text-sm text-zinc-400 mb-8">
{callState === 'calling' && t('calling')}
{callState === 'incoming' && (callType === 'video' ? t('incomingVideoCall') : t('incomingCall'))}
{callState === 'connected' && formatDuration(duration)}
{callState === 'ended' && t('callEnded')}
</p>
{/* Local video preview (during calling with camera) */}
{hasLocalVideo && callState === 'calling' && (
<div className="mb-6 rounded-2xl overflow-hidden bg-zinc-900 w-48">
<video
ref={localVideoRef}
autoPlay
playsInline
muted
className="w-full h-auto"
/>
</div>
)}
{/* Hidden remote video element */}
<video ref={remoteVideoRef} autoPlay playsInline className="hidden" />
</div>
)}
)}
</div>
{/* === Controls === */}
<div className="px-8 pb-10 flex items-center justify-center gap-4 relative z-10 flex-wrap">