Оформление
This commit is contained in:
@@ -346,6 +346,17 @@ public sealed class ChatHub : Hub
|
||||
});
|
||||
}
|
||||
|
||||
[HubMethodName("call_status")]
|
||||
public async Task CallStatus(CallStatusRequest request)
|
||||
{
|
||||
await SendToUserAsync(request.TargetUserId, "call_status_updated", new
|
||||
{
|
||||
from = _userContext.UserId.ToString(),
|
||||
isMuted = request.IsMuted,
|
||||
isVideoOff = request.IsVideoOff
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Group Call signals
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
@@ -627,6 +638,7 @@ public sealed class ChatHub : Hub
|
||||
public record RenegotiateRequest(string TargetUserId, object Offer);
|
||||
public record RenegotiateAnswerRequest(string TargetUserId, object Answer);
|
||||
public record CallTypeChangedRequest(string TargetUserId, string CallType, bool IsScreenSharing = false);
|
||||
public record CallStatusRequest(string TargetUserId, bool IsMuted, bool IsVideoOff);
|
||||
public record AddReactionRequest(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);
|
||||
|
||||
BIN
apps/server-net/uploads/20d7f83f-f6dd-4e7a-a731-f7b820e74569.jpg
Normal file
BIN
apps/server-net/uploads/20d7f83f-f6dd-4e7a-a731-f7b820e74569.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
BIN
apps/server-net/uploads/731d076c-2b2d-46d8-b6d4-fe19890c4fc7.jpg
Normal file
BIN
apps/server-net/uploads/731d076c-2b2d-46d8-b6d4-fe19890c4fc7.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 724 KiB |
@@ -114,6 +114,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
||||
const [isVideoOff, setIsVideoOff] = useState(false);
|
||||
const [isScreenSharing, setIsScreenSharing] = useState(false);
|
||||
const [remoteScreenSharing, setRemoteScreenSharing] = useState(false);
|
||||
const [remoteIsMuted, setRemoteIsMuted] = useState(false);
|
||||
const [hasRemoteVideo, setHasRemoteVideo] = useState(false);
|
||||
const hasRemoteVideoRef = useRef(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
@@ -185,6 +186,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
||||
setIsScreenSharing(false);
|
||||
setHasRemoteVideo(false);
|
||||
setRemoteScreenSharing(false);
|
||||
setRemoteIsMuted(false);
|
||||
setIsFullscreen(false);
|
||||
setIsMinimized(false);
|
||||
setShowCameraMenu(false);
|
||||
@@ -620,6 +622,14 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
||||
}
|
||||
};
|
||||
|
||||
// Sync status to remote when changed
|
||||
useEffect(() => {
|
||||
if (callState === 'connected') {
|
||||
const socket = getSocket();
|
||||
socket?.emit('call_status', { targetUserId: targetUserIdRef.current, isMuted, isVideoOff });
|
||||
}
|
||||
}, [callState, isMuted, isVideoOff]);
|
||||
|
||||
// Volume control for remote audio
|
||||
const handleVolumeChange = useCallback((vol: number) => {
|
||||
const v = Math.max(0, Math.min(1, vol));
|
||||
@@ -1431,6 +1441,11 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
||||
}
|
||||
};
|
||||
|
||||
const onCallStatusUpdated = (data: { from: string; isMuted: boolean; isVideoOff: boolean }) => {
|
||||
if (data.from !== targetUserIdRef.current) return;
|
||||
setRemoteIsMuted(data.isMuted);
|
||||
};
|
||||
|
||||
socket.on('call_answered', onCallAnswered);
|
||||
socket.on('ice_candidate', onIceCandidate);
|
||||
socket.on('call_ended', onCallEnded);
|
||||
@@ -1439,6 +1454,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
||||
socket.on('renegotiate', onRenegotiate);
|
||||
socket.on('renegotiate_answer', onRenegotiateAnswer);
|
||||
socket.on('call_type_changed', onCallTypeChanged);
|
||||
socket.on('call_status_updated', onCallStatusUpdated);
|
||||
|
||||
return () => {
|
||||
socket.off('call_answered', onCallAnswered);
|
||||
@@ -1449,6 +1465,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
||||
socket.off('renegotiate', onRenegotiate);
|
||||
socket.off('renegotiate_answer', onRenegotiateAnswer);
|
||||
socket.off('call_type_changed', onCallTypeChanged);
|
||||
socket.off('call_status_updated', onCallStatusUpdated);
|
||||
};
|
||||
}, [cleanup, scheduleClose]);
|
||||
|
||||
@@ -1549,7 +1566,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const showVideoArea = callState === 'connected' && callType === 'video' && (hasRemoteVideo || !isVideoOff || isScreenSharing);
|
||||
const showVideoArea = callState === 'connected' && (callType === 'video' || remoteScreenSharing || isScreenSharing) && (hasRemoteVideo || !isVideoOff || isScreenSharing || remoteScreenSharing);
|
||||
const hasLocalVideo = !!(
|
||||
localStreamRef.current?.getVideoTracks().some(t => t.enabled) || isScreenSharing
|
||||
);
|
||||
@@ -1734,6 +1751,14 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Remote muted indicator for video call over remote stream */}
|
||||
{hasRemoteVideo && remoteIsMuted && (
|
||||
<div className="absolute top-4 left-4 z-20 flex items-center gap-2 bg-black/60 backdrop-blur-sm px-3 py-1.5 rounded-full text-red-400">
|
||||
<MicOff size={16} />
|
||||
<span className="text-xs font-medium">Микрофон выключен</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Autoplay block overlay */}
|
||||
{needsInteraction && hasRemoteVideo && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/60 backdrop-blur-md z-30">
|
||||
@@ -1833,7 +1858,14 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-bold text-white mb-2 tracking-tight">{displayName}</h3>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-2xl font-bold text-white tracking-tight">{displayName}</h3>
|
||||
{remoteIsMuted && callState === 'connected' && (
|
||||
<div className="flex items-center justify-center p-1.5 rounded-full bg-red-500/20 text-red-500" title="Микрофон выключен">
|
||||
<MicOff size={18} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-zinc-400 mb-8">
|
||||
{callState === 'calling' && t('calling')}
|
||||
{callState === 'incoming' && (callType === 'video' ? t('incomingVideoCall') : t('incomingCall'))}
|
||||
|
||||
@@ -725,13 +725,16 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro
|
||||
<h3 className="text-sm font-semibold text-white flex-1">{t('aboutApp')}</h3>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-8 text-center">
|
||||
<img src="/logo.png" alt="Vortex" className="w-20 h-20 rounded-2xl object-cover mb-4 ring-2 ring-white/10" />
|
||||
<h2 className="text-xl font-bold gradient-text mb-1">Vortex Messenger</h2>
|
||||
<div className="w-20 h-20 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-accent/20 to-purple-600/20 flex items-center justify-center shadow-[0_0_30px_-5px_var(--color-accent)] ring-1 ring-white/10 relative overflow-hidden">
|
||||
<div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-white/[0.05] to-transparent pointer-events-none" />
|
||||
<MessageSquare size={36} className="text-accent drop-shadow-md relative z-10" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold gradient-text mb-1">SelfHost Messenger</h2>
|
||||
<p className="text-sm text-zinc-400 mb-6">{t('version')} 1.0.0</p>
|
||||
<div className="text-xs text-zinc-500 space-y-1">
|
||||
<p>{t('modernMessenger')}</p>
|
||||
<p>{t('onPrivacy')}</p>
|
||||
<p className="mt-4 text-zinc-600">© 2026 Vortex Team</p>
|
||||
<p className="mt-4 text-zinc-600">© 2026 SelfHost Team</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -109,8 +109,9 @@ export default function Sidebar() {
|
||||
<Menu size={20} />
|
||||
</button>
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<div className="flex items-center justify-center w-9 h-9 rounded-xl overflow-hidden shadow-lg border border-white/5">
|
||||
<img src="/logo.png" className="w-full h-full object-cover" alt="" />
|
||||
<div className="flex items-center justify-center w-9 h-9 rounded-xl bg-gradient-to-br from-accent/20 to-purple-600/20 shadow-[0_0_15px_-3px_var(--color-accent)] ring-1 ring-white/10 relative overflow-hidden">
|
||||
<div className="absolute inset-0 rounded-xl bg-gradient-to-br from-white/[0.05] to-transparent pointer-events-none" />
|
||||
<MessageSquare size={18} className="text-accent drop-shadow-md relative z-10" />
|
||||
</div>
|
||||
<h1 className="text-lg font-bold gradient-text truncate">SelfHost Messenger</h1>
|
||||
</div>
|
||||
|
||||
@@ -65,9 +65,10 @@ export default function AuthPage() {
|
||||
initial={{ rotate: -180, scale: 0 }}
|
||||
animate={{ rotate: 0, scale: 1 }}
|
||||
transition={{ duration: 0.6, type: 'spring', bounce: 0.4 }}
|
||||
className="w-24 h-24 rounded-[2rem] overflow-hidden shadow-2xl shadow-accent/20 border-2 border-white/10"
|
||||
className="w-24 h-24 rounded-[2rem] bg-gradient-to-br from-accent/20 to-purple-600/20 flex items-center justify-center shadow-2xl shadow-accent/20 border-2 border-white/10 relative overflow-hidden"
|
||||
>
|
||||
<img src="/logo.png" className="w-full h-full object-cover" alt="Logo" />
|
||||
<div className="absolute inset-0 rounded-[2rem] bg-gradient-to-br from-white/[0.05] to-transparent pointer-events-none" />
|
||||
<MessageSquare className="w-10 h-10 text-accent drop-shadow-md relative z-10" />
|
||||
</motion.div>
|
||||
<h1 className="text-3xl font-bold gradient-text mt-6">SelfHost Messenger</h1>
|
||||
<p className="text-zinc-500 text-sm mt-2 tracking-wide uppercase">
|
||||
|
||||
Reference in New Issue
Block a user