fix
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user