fix
This commit is contained in:
@@ -1535,141 +1535,137 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* === Video layout (connected, has video) === */}
|
{/* === Main Content Area === */}
|
||||||
{showVideoArea ? (
|
<div className="flex-1 flex flex-col relative overflow-hidden">
|
||||||
<div
|
{/*
|
||||||
ref={videoContainerRef}
|
CRITICAL: Single stable remote video element.
|
||||||
className="relative bg-black w-full"
|
We keep it always mounted so WebRTC srcObject is never lost
|
||||||
style={{ aspectRatio: '16 / 9' }}
|
during transitions between voice and video modes.
|
||||||
>
|
*/}
|
||||||
<div className="absolute inset-0 bg-black flex items-center justify-center">
|
<video
|
||||||
{/* Unified Remote video — always rendered if area is shown to keep stream alive & prevent ref-swapping issues */}
|
ref={remoteVideoRef}
|
||||||
<video
|
autoPlay
|
||||||
ref={remoteVideoRef}
|
playsInline
|
||||||
autoPlay
|
className={`
|
||||||
playsInline
|
bg-black transition-all duration-500
|
||||||
className={`absolute inset-0 w-full h-full object-contain bg-black transition-opacity duration-300 ${hasRemoteVideo ? 'opacity-100' : 'opacity-0'}`}
|
${showVideoArea
|
||||||
onContextMenu={(e) => { e.preventDefault(); setShowVolumeSlider(true); }}
|
? 'w-full h-full object-contain'
|
||||||
/>
|
: 'w-0 h-0 opacity-0 absolute pointer-events-none'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
onContextMenu={(e) => { e.preventDefault(); setShowVolumeSlider(true); }}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Placeholder shown when video track is muted or not yet arrived */}
|
{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 && (
|
{!hasRemoteVideo && (
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-zinc-900/50 backdrop-blur-sm z-10">
|
<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" />
|
<VideoOff size={48} className="text-zinc-500 mb-2" />
|
||||||
<span className="text-sm text-zinc-500">{displayName}</span>
|
<span className="text-sm text-zinc-500">{displayName}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Local video PIP (bottom-right) */}
|
{/* Local video PIP (bottom-right) */}
|
||||||
{hasLocalVideo && (
|
{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"
|
<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' }}
|
style={{ aspectRatio: '16 / 9' }}
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
ref={localVideoRef}
|
ref={localVideoRef}
|
||||||
autoPlay
|
autoPlay
|
||||||
playsInline
|
playsInline
|
||||||
muted
|
muted
|
||||||
className="w-full h-full object-contain bg-black"
|
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>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Duration badge */}
|
{/* Top-right buttons */}
|
||||||
<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">
|
<div className="absolute top-4 right-4 flex items-center gap-2 z-20">
|
||||||
<span className="text-sm text-white font-mono">{formatDuration(duration)}</span>
|
<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>
|
</div>
|
||||||
|
) : (
|
||||||
|
/* === 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>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Top-right buttons: minimize + fullscreen */}
|
<div className="relative mb-8 mt-4">
|
||||||
<div className="absolute top-4 right-4 flex items-center gap-2 z-10">
|
{(callState === 'calling' || callState === 'connected') && (
|
||||||
<button
|
<>
|
||||||
onClick={() => setIsMinimized(true)}
|
<div className="absolute inset-0 rounded-full bg-vortex-500/30 animate-call-wave" />
|
||||||
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"
|
<div className="absolute inset-0 rounded-full bg-vortex-500/20 animate-call-wave-delayed" />
|
||||||
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" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{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>
|
|
||||||
)}
|
)}
|
||||||
|
{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>
|
</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>
|
</div>
|
||||||
|
)}
|
||||||
<h3 className="text-2xl font-bold text-white mb-2 tracking-tight">{displayName}</h3>
|
</div>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* === Controls === */}
|
{/* === Controls === */}
|
||||||
<div className="px-8 pb-10 flex items-center justify-center gap-4 relative z-10 flex-wrap">
|
<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