diff --git a/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs b/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs index bdc59b9..94480f0 100644 --- a/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs +++ b/backend/src/Modules/Conversations/Infrastructure/SignalR/ChatHub.cs @@ -11,6 +11,8 @@ using Knot.Modules.Conversations.Application.Messages.React; using Knot.Modules.Conversations.Domain; using Knot.Shared.Kernel; using Microsoft.Extensions.Caching.Memory; +using Knot.Contracts.Auth.Domain; +using Knot.Contracts.Auth.Application.Abstractions; namespace Knot.Modules.Conversations.Infrastructure.SignalR; @@ -31,14 +33,16 @@ public sealed class ChatHub : Hub private readonly ISender _sender; private readonly IUserContext _userContext; private readonly IChatRepository _chatRepository; + private readonly IUserRepository _userRepository; private readonly ILogger _logger; private readonly IMemoryCache _cache; - public ChatHub(ISender sender, IUserContext userContext, IChatRepository chatRepository, ILogger logger, IMemoryCache cache) + public ChatHub(ISender sender, IUserContext userContext, IChatRepository chatRepository, IUserRepository userRepository, ILogger logger, IMemoryCache cache) { _sender = sender; _userContext = userContext; _chatRepository = chatRepository; + _userRepository = userRepository; _logger = logger; _cache = cache; } @@ -284,10 +288,12 @@ public sealed class ChatHub : Hub [HubMethodName("call_offer")] public async Task CallOffer(CallOfferRequest request) { - // Try to get caller info from current user's claims - var displayName = Context.User?.FindFirstValue("name") ?? Context.User?.FindFirstValue(ClaimTypes.Name) ?? "User"; - var avatar = Context.User?.FindFirstValue("avatar"); - var username = Context.User?.FindFirstValue("unique_name") ?? Context.User?.Identity?.Name; + // Fetch fresh user info from repository instead of relying on potentially stale JWT claims + var user = await _userRepository.GetByIdAsync(_userContext.UserId); + + var displayName = user?.DisplayName ?? Context.User?.FindFirstValue("name") ?? Context.User?.FindFirstValue(ClaimTypes.Name) ?? "User"; + var avatar = user?.Avatar ?? Context.User?.FindFirstValue("avatar"); + var username = user?.Username ?? Context.User?.FindFirstValue("unique_name") ?? Context.User?.Identity?.Name; await SendToUserAsync(request.TargetUserId, "call_incoming", new { @@ -297,7 +303,6 @@ public sealed class ChatHub : Hub chatId = request.ChatId, callerInfo = new { - id = _userContext.UserId.ToString(), displayName = displayName, avatar = avatar, @@ -396,10 +401,12 @@ public sealed class ChatHub : Hub var chatId = request.ChatId; var userId = _userContext.UserId.ToString(); + // Fetch fresh user info from repository + var user = await _userRepository.GetByIdAsync(_userContext.UserId); - var displayName = Context.User?.FindFirstValue("name") ?? Context.User?.FindFirstValue(ClaimTypes.Name) ?? "User"; - var avatar = Context.User?.FindFirstValue("avatar"); - var username = Context.User?.FindFirstValue("unique_name") ?? Context.User?.Identity?.Name ?? "user"; + var displayName = user?.DisplayName ?? Context.User?.FindFirstValue("name") ?? Context.User?.FindFirstValue(ClaimTypes.Name) ?? "User"; + var avatar = user?.Avatar ?? Context.User?.FindFirstValue("avatar"); + var username = user?.Username ?? Context.User?.FindFirstValue("unique_name") ?? Context.User?.Identity?.Name ?? "user"; var userInfo = new ParticipantInfo(userId, username, displayName, avatar); diff --git a/client-web/src/index.css b/client-web/src/index.css index 4484e64..d27c9bd 100644 --- a/client-web/src/index.css +++ b/client-web/src/index.css @@ -1,246 +1,270 @@ -@import "tailwindcss"; - -:root { - color-scheme: dark; - --primary: #9acbff; - --primary-container: #3096e5; - --on-primary: #003355; - --on-primary-container: #e0f2ff; - - --secondary: #c7c6ca; - --secondary-container: #48494c; - --on-secondary: #2f3033; - --on-secondary-container: #e3e2e6; - - --tertiary: #53e16f; - --tertiary-container: #006e25; - --on-tertiary: #00390a; - --on-tertiary-container: #affca0; - - --error: #ffb4ab; - --error-container: #93000a; - --on-error: #690005; - --on-error-container: #ffdad6; - - --surface: #131313; - --surface-container-lowest: #0b0b0b; - --surface-container-low: #1b1b1b; - --surface-container: #201f1f; - --surface-container-high: #2a2a2a; - --surface-container-highest: #353535; - - --on-surface: #f5f5fa; - --on-surface-variant: #b4b4c3; - --outline: #3c3c4b; - --outline-variant: #2d2d37; - - --ease-ice: cubic-bezier(0.4, 0, 0.2, 1); - --shadow-ambient: 0 40px 80px -20px rgba(0, 0, 0, 0.5); -} - -@theme { - --font-sans: 'Inter', system-ui, -apple-system, sans-serif; - --font-headline: 'Inter', sans-serif; - --font-body: 'Inter', sans-serif; - - --color-primary: var(--primary); - --color-primary-container: var(--primary-container); - --color-on-primary: var(--on-primary); - --color-on-primary-container: var(--on-primary-container); - - --color-secondary: var(--secondary); - --color-secondary-container: var(--secondary-container); - --color-on-secondary: var(--on-secondary); - --color-on-secondary-container: var(--on-secondary-container); - - --color-tertiary: var(--tertiary); - --color-tertiary-container: var(--tertiary-container); - --color-on-tertiary: var(--on-tertiary); - --color-on-tertiary-container: var(--on-tertiary-container); - - --color-error: var(--error); - --color-error-container: var(--error-container); - --color-on-error: var(--on-error); - --color-on-error-container: var(--on-error-container); - - --color-surface: var(--surface); - --color-surface-container-lowest: var(--surface-container-lowest); - --color-surface-container-low: var(--surface-container-low); - --color-surface-container: var(--surface-container); - --color-surface-container-high: var(--surface-container-high); - --color-surface-container-highest: var(--surface-container-highest); - - --color-on-surface: var(--on-surface); - --color-on-surface-variant: var(--on-surface-variant); - --color-outline: var(--outline); - --color-outline-variant: var(--outline-variant); - - --animate-kinetic-in: slideInUp 0.6s var(--ease-ice) forwards; -} - -/* Global resets and base styles */ -html, -body, -#root { - height: 100%; - margin: 0; - padding: 0; - overflow: hidden; - background-color: #131313 !important; - /* Force to Obsidian surface */ - color: #f5f5fa !important; - /* Force to high-contrast white */ - font-family: var(--font-sans); - -webkit-font-smoothing: antialiased; -} - -/* Base style for all elements if they inherit color incorrectly */ -* { - box-sizing: border-box; -} - -/* Kinetic Core Components */ -.knot-card { - background-color: var(--surface-container-low) !important; - border-radius: 2rem; - /* 32px */ - transition: all 0.4s var(--ease-ice); - overflow: hidden; -} - -.knot-card-active { - background-color: var(--surface-container) !important; -} - -.knot-input-group { - background-color: rgba(53, 53, 53, 0.4) !important; - border-radius: 1.75rem; - padding: 1.25rem 1.6rem; - border: 1px solid rgba(154, 203, 255, 0.1); - transition: all 0.4s var(--ease-ice); - display: flex; - flex-direction: column; - backdrop-filter: blur(10px); -} - -.knot-input-group:focus-within { - background-color: rgba(53, 53, 53, 0.6) !important; - border-color: #9acbff !important; - box-shadow: 0 0 0 1px #9acbff, 0 0 40px rgba(154, 203, 255, 0.1); -} - -.knot-input-group input { - background: transparent !important; - border: none !important; - outline: none !important; - width: 100%; - color: #f5f5fa !important; -} - -.knot-button-primary { - background: linear-gradient(135deg, #9acbff, #3096e5) !important; - color: #e0f2ff !important; - border-radius: 1.5rem; - font-weight: 900; - letter-spacing: 0.05em; - transition: all 0.4s var(--ease-ice); -} - -.knot-button-primary:hover { - transform: translateY(-2px); - filter: brightness(1.1); - box-shadow: 0 24px 32px -12px rgba(48, 150, 229, 0.4); -} - -/* Utilities */ -.glass-effect { - backdrop-filter: blur(20px); - background: rgba(19, 19, 19, 0.6); -} - -.slide-on-ice { - transition: all 0.4s var(--ease-ice); -} - -.animate-kinetic { - animation: var(--animate-kinetic-in); -} - -.custom-scrollbar::-webkit-scrollbar { - width: 4px; -} - -.custom-scrollbar::-webkit-scrollbar-thumb { - background: var(--outline-variant); - border-radius: 10px; -} - -/* Overrides for legacy code */ -.bg-surface { - background-color: var(--surface) !important; -} - -.bg-surface-container-low { - background-color: var(--surface-container-low) !important; -} - -.bg-surface-container-lowest { - background-color: var(--surface-container-lowest) !important; -} - -.bg-surface-container-high { - background-color: var(--surface-container-high) !important; -} - -.text-on-surface { - color: var(--on-surface) !important; -} - -.text-on-surface-variant { - color: var(--on-surface-variant) !important; -} - -.text-primary { - color: var(--primary) !important; -} - -.text-accent { - color: #9acbff !important; -} - -/* Bubble overrides */ -.bubble-sent { - background: linear-gradient(135deg, #9acbff, #3096e5) !important; - color: #e0f2ff !important; - border-radius: 1.5rem; - border-bottom-right-radius: 0.25rem; -} - -.bubble-received { - background: var(--surface-container-highest) !important; - color: var(--on-surface) !important; - border-radius: 1.5rem; - border-bottom-left-radius: 0.25rem; -} - -/* Autofill fix - prevent browser blue background on dark theme */ -input:-webkit-autofill, -input:-webkit-autofill:hover, -input:-webkit-autofill:focus, -input:-webkit-autofill:active { - -webkit-box-shadow: 0 0 0 1000px #0b0b0b inset !important; - -webkit-text-fill-color: #f5f5fa !important; - transition: background-color 5000s ease-in-out 0s; -} - -@keyframes highlightFlash { - 0% { background-color: rgba(0, 163, 255, 0.3); } - 100% { background-color: transparent; } -} - -.highlight-message { - animation: highlightFlash 2s cubic-bezier(0.4, 0, 0.2, 1) forwards !important; - border-radius: 12px; - position: relative; - z-index: 10; -} \ No newline at end of file +@import "tailwindcss"; + +:root { + color-scheme: dark; + --primary: #9acbff; + --primary-container: #3096e5; + --on-primary: #003355; + --on-primary-container: #e0f2ff; + + --secondary: #c7c6ca; + --secondary-container: #48494c; + --on-secondary: #2f3033; + --on-secondary-container: #e3e2e6; + + --tertiary: #53e16f; + --tertiary-container: #006e25; + --on-tertiary: #00390a; + --on-tertiary-container: #affca0; + + --error: #ffb4ab; + --error-container: #93000a; + --on-error: #690005; + --on-error-container: #ffdad6; + + --surface: #131313; + --surface-container-lowest: #0b0b0b; + --surface-container-low: #1b1b1b; + --surface-container: #201f1f; + --surface-container-high: #2a2a2a; + --surface-container-highest: #353535; + + --on-surface: #f5f5fa; + --on-surface-variant: #b4b4c3; + --outline: #3c3c4b; + --outline-variant: #2d2d37; + + --ease-ice: cubic-bezier(0.4, 0, 0.2, 1); + --shadow-ambient: 0 40px 80px -20px rgba(0, 0, 0, 0.5); +} + +@theme { + --font-sans: 'Inter', system-ui, -apple-system, sans-serif; + --font-headline: 'Inter', sans-serif; + --font-body: 'Inter', sans-serif; + + --color-primary: var(--primary); + --color-primary-container: var(--primary-container); + --color-on-primary: var(--on-primary); + --color-on-primary-container: var(--on-primary-container); + + --color-secondary: var(--secondary); + --color-secondary-container: var(--secondary-container); + --color-on-secondary: var(--on-secondary); + --color-on-secondary-container: var(--on-secondary-container); + + --color-tertiary: var(--tertiary); + --color-tertiary-container: var(--tertiary-container); + --color-on-tertiary: var(--on-tertiary); + --color-on-tertiary-container: var(--on-tertiary-container); + + --color-error: var(--error); + --color-error-container: var(--error-container); + --color-on-error: var(--on-error); + --color-on-error-container: var(--on-error-container); + + --color-surface: var(--surface); + --color-surface-container-lowest: var(--surface-container-lowest); + --color-surface-container-low: var(--surface-container-low); + --color-surface-container: var(--surface-container); + --color-surface-container-high: var(--surface-container-high); + --color-surface-container-highest: var(--surface-container-highest); + + --color-on-surface: var(--on-surface); + --color-on-surface-variant: var(--on-surface-variant); + --color-outline: var(--outline); + --color-outline-variant: var(--outline-variant); + + --animate-kinetic-in: slideInUp 0.6s var(--ease-ice) forwards; +} + +/* Global resets and base styles */ +html, +body, +#root { + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; + background-color: #131313 !important; + /* Force to Obsidian surface */ + color: #f5f5fa !important; + /* Force to high-contrast white */ + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; +} + +/* Base style for all elements if they inherit color incorrectly */ +* { + box-sizing: border-box; +} + +/* Kinetic Core Components */ +.knot-card { + background-color: var(--surface-container-low) !important; + border-radius: 2rem; + /* 32px */ + transition: all 0.4s var(--ease-ice); + overflow: hidden; +} + +.knot-card-active { + background-color: var(--surface-container) !important; +} + +.knot-input-group { + background-color: rgba(53, 53, 53, 0.4) !important; + border-radius: 1.75rem; + padding: 1.25rem 1.6rem; + border: 1px solid rgba(154, 203, 255, 0.1); + transition: all 0.4s var(--ease-ice); + display: flex; + flex-direction: column; + backdrop-filter: blur(10px); +} + +.knot-input-group:focus-within { + background-color: rgba(53, 53, 53, 0.6) !important; + border-color: #9acbff !important; + box-shadow: 0 0 0 1px #9acbff, 0 0 40px rgba(154, 203, 255, 0.1); +} + +.knot-input-group input { + background: transparent !important; + border: none !important; + outline: none !important; + width: 100%; + color: #f5f5fa !important; +} + +.knot-button-primary { + background: linear-gradient(135deg, #9acbff, #3096e5) !important; + color: #e0f2ff !important; + border-radius: 1.5rem; + font-weight: 900; + letter-spacing: 0.05em; + transition: all 0.4s var(--ease-ice); +} + +.knot-button-primary:hover { + transform: translateY(-2px); + filter: brightness(1.1); + box-shadow: 0 24px 32px -12px rgba(48, 150, 229, 0.4); +} + +/* Utilities */ +.glass-effect { + backdrop-filter: blur(20px); + background: rgba(19, 19, 19, 0.6); +} + +.slide-on-ice { + transition: all 0.4s var(--ease-ice); +} + +.animate-kinetic { + animation: var(--animate-kinetic-in); +} + +.custom-scrollbar::-webkit-scrollbar { + width: 4px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: var(--outline-variant); + border-radius: 10px; +} + +/* Overrides for legacy code */ +.bg-surface { + background-color: var(--surface) !important; +} + +.bg-surface-container-low { + background-color: var(--surface-container-low) !important; +} + +.bg-surface-container-lowest { + background-color: var(--surface-container-lowest) !important; +} + +.bg-surface-container-high { + background-color: var(--surface-container-high) !important; +} + +.text-on-surface { + color: var(--on-surface) !important; +} + +.text-on-surface-variant { + color: var(--on-surface-variant) !important; +} + +.text-primary { + color: var(--primary) !important; +} + +.text-accent { + color: #9acbff !important; +} + +/* Bubble overrides */ +.bubble-sent { + background: linear-gradient(135deg, #9acbff, #3096e5) !important; + color: #e0f2ff !important; + border-radius: 1.5rem; + border-bottom-right-radius: 0.25rem; +} + +.bubble-received { + background: var(--surface-container-highest) !important; + color: var(--on-surface) !important; + border-radius: 1.5rem; + border-bottom-left-radius: 0.25rem; +} + +/* Autofill fix - prevent browser blue background on dark theme */ +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 1000px #0b0b0b inset !important; + -webkit-text-fill-color: #f5f5fa !important; + transition: background-color 5000s ease-in-out 0s; +} + +@keyframes highlightFlash { + 0% { background-color: rgba(0, 163, 255, 0.3); } + 100% { background-color: transparent; } +} + +@keyframes callWave { + 0% { transform: scale(1); opacity: 0.5; } + 100% { transform: scale(1.5); opacity: 0; } +} + +@keyframes wiggle { + 0%, 100% { transform: rotate(0); } + 25% { transform: rotate(-10deg); } + 75% { transform: rotate(10deg); } +} + +.animate-call-wave { + animation: callWave 2s cubic-bezier(0, 0, 0.2, 1) infinite; +} + +.animate-call-wave-delayed { + animation: callWave 2s cubic-bezier(0, 0, 0.2, 1) infinite; + animation-delay: 1s; +} + +.animate-wiggle { + animation: wiggle 0.5s ease-in-out infinite; +} + +.highlight-message { + animation: highlightFlash 2s cubic-bezier(0.4, 0, 0.2, 1) forwards !important; + border-radius: 12px; + position: relative; + z-index: 10; +} \ No newline at end of file diff --git a/client-web/src/modules/calls/presentation/components/CallModal.tsx b/client-web/src/modules/calls/presentation/components/CallModal.tsx index f4d9f51..b0a3dee 100644 --- a/client-web/src/modules/calls/presentation/components/CallModal.tsx +++ b/client-web/src/modules/calls/presentation/components/CallModal.tsx @@ -8,20 +8,16 @@ import { useAuthStore } from '../../../auth/application/authStore'; import { useLang } from '../../../../core/infrastructure/i18n'; import { playCallRingtone, stopCallRingtone, playUnavailableSound } from '../../../../core/utils/sounds'; import { getMediaUrl } from '../../../../core/utils/utils'; +import { UserBasic, CallInfo } from '../../../../core/domain/types'; type CallState = 'idle' | 'calling' | 'incoming' | 'connected' | 'ended'; interface CallModalProps { isOpen: boolean; onClose: () => void; - targetUser: { id: string; displayName?: string; username?: string; avatar?: string | null } | null; + targetUser: UserBasic | null; callType: 'voice' | 'video'; - incoming?: { - from: string; - offer: RTCSessionDescriptionInit; - callType: 'voice' | 'video'; - callerInfo?: { displayName?: string; username?: string; avatar?: string | null } | null; - } | null; + incoming?: CallInfo | null; } // ICE servers cache (fetched from server) @@ -1614,12 +1610,12 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi }; const displayName = incoming - ? incoming.callerInfo?.displayName || incoming.callerInfo?.username || '...' - : targetUser?.displayName || targetUser?.username || '...'; + ? incoming.callerInfo?.displayName || incoming.callerInfo?.userName || incoming.callerInfo?.username || '...' + : targetUser?.displayName || targetUser?.userName || targetUser?.username || '...'; const displayAvatar = incoming - ? incoming.callerInfo?.avatar - : targetUser?.avatar; + ? incoming.callerInfo?.avatarUrl || incoming.callerInfo?.avatar + : targetUser?.avatarUrl || targetUser?.avatar; const initials = displayName .split(' ') @@ -1655,16 +1651,15 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi className="fixed bottom-6 right-6 z-[100] flex items-center gap-3 px-4 py-3 rounded-2xl glass-strong shadow-2xl shadow-black/50 border border-white/10 cursor-pointer select-none" onClick={() => setIsMinimized(false)} > - {/* Avatar */}
-
- {displayAvatar ? ( - - ) : ( -
- {initials} -
+ {callState === 'connected' && ( +
)} +

{displayName}

@@ -1801,7 +1796,7 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi {(!hasRemoteVideo || remoteIsVideoOffSignal) && !remoteScreenSharing && (
-
+
{(callState === 'calling' || callState === 'connected') && ( <> -
-
+
+
)} {callState === 'incoming' && ( <> -
-
+
+
)} -
+
setIsMinimized(false)} >
-
-
+
+
{participantList.length + 1}
@@ -949,7 +951,7 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
diff --git a/client-web/src/modules/chats/presentation/ChatPage.tsx b/client-web/src/modules/chats/presentation/ChatPage.tsx index 102cb60..9f19522 100644 --- a/client-web/src/modules/chats/presentation/ChatPage.tsx +++ b/client-web/src/modules/chats/presentation/ChatPage.tsx @@ -16,6 +16,8 @@ import GroupCallModal from '../../calls/presentation/components/GroupCallModal'; import ContactsSidebar from '../../friends/presentation/components/ContactsSidebar'; import SettingsPage from '../../users/presentation/components/SettingsPage'; import UserProfile from '../../users/presentation/components/UserProfile'; +import Avatar from '../../../core/presentation/components/ui/Avatar'; +import { getMediaUrl } from '../../../core/utils/utils'; export default function ChatPage() { const { @@ -476,14 +478,13 @@ export default function ChatPage() { className="bg-zinc-900 border border-white/10 p-8 rounded-3xl w-full max-w-sm flex flex-col items-center shadow-2xl" >
-
-
- {incomingGroupCall.callerInfo?.avatar ? ( - - ) : ( - <>{incomingGroupCall.chatName.charAt(0)} - )} -
+
+

{incomingGroupCall.chatName}