Починенные групповые звонки
This commit is contained in:
@@ -342,6 +342,7 @@ public sealed class ChatHub : Hub
|
|||||||
{
|
{
|
||||||
from = _userContext.UserId.ToString(),
|
from = _userContext.UserId.ToString(),
|
||||||
callType = request.CallType,
|
callType = request.CallType,
|
||||||
|
isScreenSharing = request.IsScreenSharing
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,7 +626,7 @@ public sealed class ChatHub : Hub
|
|||||||
public record IceCandidateRequest(string TargetUserId, object Candidate);
|
public record IceCandidateRequest(string TargetUserId, object Candidate);
|
||||||
public record RenegotiateRequest(string TargetUserId, object Offer);
|
public record RenegotiateRequest(string TargetUserId, object Offer);
|
||||||
public record RenegotiateAnswerRequest(string TargetUserId, object Answer);
|
public record RenegotiateAnswerRequest(string TargetUserId, object Answer);
|
||||||
public record CallTypeChangedRequest(string TargetUserId, string CallType);
|
public record CallTypeChangedRequest(string TargetUserId, string CallType, bool IsScreenSharing = false);
|
||||||
public record AddReactionRequest(Guid MessageId, Guid ChatId, string Emoji);
|
public record AddReactionRequest(Guid MessageId, Guid ChatId, string Emoji);
|
||||||
public record RemoveReactionRequest(Guid MessageId, Guid ChatId, string Emoji);
|
public record RemoveReactionRequest(Guid MessageId, Guid ChatId, string Emoji);
|
||||||
public record DeleteMessagesHubRequest(Guid ChatId, List<string> MessageIds, bool DeleteForAll);
|
public record DeleteMessagesHubRequest(Guid ChatId, List<string> MessageIds, bool DeleteForAll);
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
|||||||
const [isMuted, setIsMuted] = useState(false);
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
const [isVideoOff, setIsVideoOff] = useState(false);
|
const [isVideoOff, setIsVideoOff] = useState(false);
|
||||||
const [isScreenSharing, setIsScreenSharing] = useState(false);
|
const [isScreenSharing, setIsScreenSharing] = useState(false);
|
||||||
|
const [remoteScreenSharing, setRemoteScreenSharing] = useState(false);
|
||||||
const [hasRemoteVideo, setHasRemoteVideo] = useState(false);
|
const [hasRemoteVideo, setHasRemoteVideo] = useState(false);
|
||||||
const hasRemoteVideoRef = useRef(false);
|
const hasRemoteVideoRef = useRef(false);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
@@ -183,6 +184,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
|||||||
setIsVideoOff(false);
|
setIsVideoOff(false);
|
||||||
setIsScreenSharing(false);
|
setIsScreenSharing(false);
|
||||||
setHasRemoteVideo(false);
|
setHasRemoteVideo(false);
|
||||||
|
setRemoteScreenSharing(false);
|
||||||
setIsFullscreen(false);
|
setIsFullscreen(false);
|
||||||
setIsMinimized(false);
|
setIsMinimized(false);
|
||||||
setShowCameraMenu(false);
|
setShowCameraMenu(false);
|
||||||
@@ -1131,7 +1133,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
|||||||
setCallType('voice');
|
setCallType('voice');
|
||||||
setIsVideoOff(false);
|
setIsVideoOff(false);
|
||||||
const socket = getSocket();
|
const socket = getSocket();
|
||||||
socket?.emit('call_type_changed', { targetUserId: targetUserIdRef.current, callType: 'voice' });
|
socket?.emit('call_type_changed', { targetUserId: targetUserIdRef.current, callType: 'voice', isScreenSharing: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsScreenSharing(false);
|
setIsScreenSharing(false);
|
||||||
@@ -1260,14 +1262,14 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
|||||||
}
|
}
|
||||||
setCallType('voice');
|
setCallType('voice');
|
||||||
const socket = getSocket();
|
const socket = getSocket();
|
||||||
socket?.emit('call_type_changed', { targetUserId: targetUserIdRef.current, callType: 'voice' });
|
socket?.emit('call_type_changed', { targetUserId: targetUserIdRef.current, callType: 'voice', isScreenSharing: false });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setIsScreenSharing(true);
|
setIsScreenSharing(true);
|
||||||
setCallType('video');
|
setCallType('video');
|
||||||
const socket = getSocket();
|
const socket = getSocket();
|
||||||
socket?.emit('call_type_changed', { targetUserId: targetUserIdRef.current, callType: 'video' });
|
socket?.emit('call_type_changed', { targetUserId: targetUserIdRef.current, callType: 'video', isScreenSharing: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error starting screen share:', err);
|
console.error('Error starting screen share:', err);
|
||||||
}
|
}
|
||||||
@@ -1402,10 +1404,17 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCallTypeChanged = (data: { from: string; callType: 'voice' | 'video' }) => {
|
const onCallTypeChanged = (data: { from: string; callType: 'voice' | 'video'; isScreenSharing?: boolean }) => {
|
||||||
if (data.from !== targetUserIdRef.current) return;
|
if (data.from !== targetUserIdRef.current) return;
|
||||||
console.log('[onCallTypeChanged] New call type:', data.callType);
|
console.log('[onCallTypeChanged] New call type:', data.callType, 'isRemoteScreenSharing:', data.isScreenSharing);
|
||||||
setCallType(data.callType);
|
setCallType(data.callType);
|
||||||
|
setRemoteScreenSharing(!!data.isScreenSharing);
|
||||||
|
|
||||||
|
// If remote switched to voice, clear their video flag so we collapse the UI
|
||||||
|
if (data.callType === 'voice') {
|
||||||
|
hasRemoteVideoRef.current = false;
|
||||||
|
setHasRemoteVideo(false);
|
||||||
|
}
|
||||||
|
|
||||||
// If remote switched to video, nudge the video element
|
// If remote switched to video, nudge the video element
|
||||||
if (data.callType === 'video' || data.callType === 'voice') {
|
if (data.callType === 'video' || data.callType === 'voice') {
|
||||||
@@ -1540,7 +1549,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
|||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const showVideoArea = callState === 'connected' && (hasRemoteVideo || (callType === 'video' && (!isVideoOff || isScreenSharing)));
|
const showVideoArea = callState === 'connected' && callType === 'video' && (hasRemoteVideo || !isVideoOff || isScreenSharing);
|
||||||
const hasLocalVideo = !!(
|
const hasLocalVideo = !!(
|
||||||
localStreamRef.current?.getVideoTracks().some(t => t.enabled) || isScreenSharing
|
localStreamRef.current?.getVideoTracks().some(t => t.enabled) || isScreenSharing
|
||||||
);
|
);
|
||||||
@@ -1908,9 +1917,15 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={toggleScreenShare}
|
onClick={toggleScreenShare}
|
||||||
className={`w-11 h-11 rounded-full flex items-center justify-center transition-colors ${isScreenSharing ? 'bg-vortex-500/30 text-vortex-400' : 'bg-white/10 text-white hover:bg-white/20'
|
disabled={remoteScreenSharing}
|
||||||
|
className={`w-11 h-11 rounded-full flex items-center justify-center transition-colors ${
|
||||||
|
remoteScreenSharing
|
||||||
|
? 'bg-zinc-800 text-zinc-600 cursor-not-allowed hidden sm:flex'
|
||||||
|
: isScreenSharing
|
||||||
|
? 'bg-vortex-500/30 text-vortex-400'
|
||||||
|
: 'bg-white/10 text-white hover:bg-white/20'
|
||||||
}`}
|
}`}
|
||||||
title={isScreenSharing ? t('stopScreenShare') : t('screenShare')}
|
title={remoteScreenSharing ? (t('remoteSharingScreen' as any) as string) : isScreenSharing ? t('stopScreenShare') : t('screenShare')}
|
||||||
>
|
>
|
||||||
{isScreenSharing ? <MonitorOff size={18} /> : <Monitor size={18} />}
|
{isScreenSharing ? <MonitorOff size={18} /> : <Monitor size={18} />}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -304,9 +304,8 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
|||||||
socket?.emit('group_call_status', { chatId, isMuted, isVideoOff: newVideoOff });
|
socket?.emit('group_call_status', { chatId, isMuted, isVideoOff: newVideoOff });
|
||||||
}, [isVideoOff, isMuted, chatId]);
|
}, [isVideoOff, isMuted, chatId]);
|
||||||
|
|
||||||
// Toggle screen share
|
// Stop screensharing helper
|
||||||
const toggleScreenShare = useCallback(async () => {
|
const stopScreenShareCore = useCallback(async () => {
|
||||||
if (isScreenSharing) {
|
|
||||||
if (screenStreamRef.current) {
|
if (screenStreamRef.current) {
|
||||||
screenStreamRef.current.getTracks().forEach(t => t.stop());
|
screenStreamRef.current.getTracks().forEach(t => t.stop());
|
||||||
screenStreamRef.current = null;
|
screenStreamRef.current = null;
|
||||||
@@ -330,9 +329,20 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
|||||||
}
|
}
|
||||||
setMaximizedUserId(null);
|
setMaximizedUserId(null);
|
||||||
setIsScreenSharing(false);
|
setIsScreenSharing(false);
|
||||||
setIsVideoOff(!localStreamRef.current?.getVideoTracks().length);
|
|
||||||
|
// Check if we actually have an active camera track
|
||||||
|
const hasLiveCam = !!localStreamRef.current?.getVideoTracks().some(t => t.enabled);
|
||||||
|
setIsVideoOff(!hasLiveCam);
|
||||||
|
|
||||||
const socket = getSocket();
|
const socket = getSocket();
|
||||||
socket?.emit('screen_share_stopped', chatId);
|
socket?.emit('screen_share_stopped', chatId);
|
||||||
|
socket?.emit('group_call_status', { chatId, isMuted, isVideoOff: !hasLiveCam });
|
||||||
|
}, [chatId, isMuted]);
|
||||||
|
|
||||||
|
// Toggle screen share
|
||||||
|
const toggleScreenShare = useCallback(async () => {
|
||||||
|
if (isScreenSharing) {
|
||||||
|
await stopScreenShareCore();
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const screenStream = await navigator.mediaDevices.getDisplayMedia({
|
const screenStream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
@@ -359,7 +369,7 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
|||||||
}
|
}
|
||||||
|
|
||||||
screenTrack.onended = () => {
|
screenTrack.onended = () => {
|
||||||
toggleScreenShare(); // reuse functionality on OS level stop (e.g. Stop Sharing button natively)
|
stopScreenShareCore(); // use the helper to avoid stale state issues!
|
||||||
};
|
};
|
||||||
setMaximizedUserId('self');
|
setMaximizedUserId('self');
|
||||||
setIsScreenSharing(true);
|
setIsScreenSharing(true);
|
||||||
@@ -647,7 +657,8 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onScreenShareStarted = (data: { userId: string }) => {
|
const onScreenShareStarted = (data: { chatId: string; userId: string }) => {
|
||||||
|
if (data.chatId !== chatId) return;
|
||||||
const isMe = data.userId === useAuthStore.getState().user?.id;
|
const isMe = data.userId === useAuthStore.getState().user?.id;
|
||||||
if (!isMe) {
|
if (!isMe) {
|
||||||
setSharingUserId(data.userId);
|
setSharingUserId(data.userId);
|
||||||
@@ -661,7 +672,8 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onScreenShareStopped = (data: { userId: string }) => {
|
const onScreenShareStopped = (data: { chatId: string; userId: string }) => {
|
||||||
|
if (data.chatId !== chatId) return;
|
||||||
const isMe = data.userId === useAuthStore.getState().user?.id;
|
const isMe = data.userId === useAuthStore.getState().user?.id;
|
||||||
if (!isMe && sharingUserId === data.userId) {
|
if (!isMe && sharingUserId === data.userId) {
|
||||||
setSharingUserId(null);
|
setSharingUserId(null);
|
||||||
@@ -918,9 +930,9 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
|||||||
style={!isMaximized && theMaximized ? { width: '200px', height: '112px' } : {}}
|
style={!isMaximized && theMaximized ? { width: '200px', height: '112px' } : {}}
|
||||||
>
|
>
|
||||||
{p.isSelf ? (
|
{p.isSelf ? (
|
||||||
p.hasVideo ? <video ref={localVideoRef} autoPlay playsInline muted className="w-full h-full object-contain" /> : <div className="flex flex-col items-center"><div className="w-16 h-16 rounded-full bg-gradient-to-br from-vortex-500 to-purple-600 flex items-center justify-center text-white font-bold text-xl mb-2">{initials}</div>{p.isMuted && <MicOff size={14} className="text-red-400" />}</div>
|
p.hasVideo ? <video ref={localVideoRef} autoPlay playsInline muted className="w-full h-full object-contain" /> : <div className="flex flex-col items-center"><div className="w-16 h-16 rounded-full bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center text-white font-bold text-xl mb-2">{initials}</div>{p.isMuted && <MicOff size={14} className="text-red-400" />}</div>
|
||||||
) : (
|
) : (
|
||||||
p.hasVideo && !p.isVideoOff ? <video id={`video-${p.id}`} autoPlay playsInline muted ref={el => { if (el && p.peer?.remoteStream && el.srcObject !== p.peer.remoteStream) el.srcObject = p.peer.remoteStream; }} className="w-full h-full object-contain" /> : <div className="flex flex-col items-center">{p.avatar ? <img src={getMediaUrl(p.avatar)} alt="" className="w-16 h-16 rounded-full object-cover mb-2" /> : <div className="w-16 h-16 rounded-full bg-gradient-to-br from-vortex-500 to-purple-600 flex items-center justify-center text-white font-bold text-xl mb-2">{initials}</div>}{p.isMuted && <MicOff size={14} className="text-red-400" />}</div>
|
p.hasVideo && (!p.isVideoOff || p.isSharingScreen) ? <video id={`video-${p.id}`} autoPlay playsInline muted ref={el => { if (el && p.peer?.remoteStream && el.srcObject !== p.peer.remoteStream) el.srcObject = p.peer.remoteStream; }} className="w-full h-full object-contain" /> : <div className="flex flex-col items-center">{p.avatar ? <img src={getMediaUrl(p.avatar)} alt="" className="w-16 h-16 rounded-full object-cover mb-2" /> : <div className="w-16 h-16 rounded-full bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center text-white font-bold text-xl mb-2">{initials}</div>}{p.isMuted && <MicOff size={14} className="text-red-400" />}</div>
|
||||||
)}
|
)}
|
||||||
<div className="absolute bottom-2 left-2 px-2 py-0.5 rounded-full bg-black/60 text-xs text-white truncate max-w-[80%] flex items-center gap-1">
|
<div className="absolute bottom-2 left-2 px-2 py-0.5 rounded-full bg-black/60 text-xs text-white truncate max-w-[80%] flex items-center gap-1">
|
||||||
{p.isSelf ? t('you') : (p.displayName || p.username)} {p.isMuted ? <MicOff size={10} className="text-red-400" /> : ''}
|
{p.isSelf ? t('you') : (p.displayName || p.username)} {p.isMuted ? <MicOff size={10} className="text-red-400" /> : ''}
|
||||||
|
|||||||
Reference in New Issue
Block a user