diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs b/apps/server-net/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs index 8774d48..351b64c 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs @@ -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 MessageIds, bool DeleteForAll); diff --git a/apps/server-net/uploads/20d7f83f-f6dd-4e7a-a731-f7b820e74569.jpg b/apps/server-net/uploads/20d7f83f-f6dd-4e7a-a731-f7b820e74569.jpg new file mode 100644 index 0000000..94ea23a Binary files /dev/null and b/apps/server-net/uploads/20d7f83f-f6dd-4e7a-a731-f7b820e74569.jpg differ diff --git a/apps/server-net/uploads/731d076c-2b2d-46d8-b6d4-fe19890c4fc7.jpg b/apps/server-net/uploads/731d076c-2b2d-46d8-b6d4-fe19890c4fc7.jpg new file mode 100644 index 0000000..9b3a162 Binary files /dev/null and b/apps/server-net/uploads/731d076c-2b2d-46d8-b6d4-fe19890c4fc7.jpg differ diff --git a/apps/web/src/components/CallModal.tsx b/apps/web/src/components/CallModal.tsx index 8e4534b..47a0190 100644 --- a/apps/web/src/components/CallModal.tsx +++ b/apps/web/src/components/CallModal.tsx @@ -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 )} + {/* Remote muted indicator for video call over remote stream */} + {hasRemoteVideo && remoteIsMuted && ( +
+ + Микрофон выключен +
+ )} + {/* Autoplay block overlay */} {needsInteraction && hasRemoteVideo && (
@@ -1833,7 +1858,14 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
-

{displayName}

+
+

{displayName}

+ {remoteIsMuted && callState === 'connected' && ( +
+ +
+ )} +

{callState === 'calling' && t('calling')} {callState === 'incoming' && (callType === 'video' ? t('incomingVideoCall') : t('incomingCall'))} diff --git a/apps/web/src/components/SideMenu.tsx b/apps/web/src/components/SideMenu.tsx index f807e6e..a203189 100644 --- a/apps/web/src/components/SideMenu.tsx +++ b/apps/web/src/components/SideMenu.tsx @@ -725,13 +725,16 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro

{t('aboutApp')}

- Vortex -

Vortex Messenger

+
+
+ +
+

SelfHost Messenger

{t('version')} 1.0.0

{t('modernMessenger')}

{t('onPrivacy')}

-

© 2026 Vortex Team

+

© 2026 SelfHost Team

diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index bef121b..d954edf 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -109,8 +109,9 @@ export default function Sidebar() {
-
- +
+
+

SelfHost Messenger

diff --git a/apps/web/src/pages/AuthPage.tsx b/apps/web/src/pages/AuthPage.tsx index ddba1a2..50a8d64 100644 --- a/apps/web/src/pages/AuthPage.tsx +++ b/apps/web/src/pages/AuthPage.tsx @@ -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" > - Logo +
+

SelfHost Messenger