Оформление

This commit is contained in:
Халимов Рустам
2026-03-15 01:57:19 +03:00
parent fd467210f2
commit c14999d319
7 changed files with 58 additions and 9 deletions

View File

@@ -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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 KiB

View File

@@ -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'))}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">