From bccbf12a11cfd90618fc44ece80787ce19fc61c6 Mon Sep 17 00:00:00 2001 From: max Date: Fri, 17 Apr 2026 22:24:09 +0300 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20<=D0=B1=D1=8B=D0=BB?= =?UTF-8?q?=20=D0=BD=D0=B5=D0=B4=D0=B0=D0=B2=D0=BD=D0=BE>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/components/ChatListItem.tsx | 8 ++--- .../presentation/components/ChatView.tsx | 31 +++++++++++++++++-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/client-web/src/modules/chats/presentation/components/ChatListItem.tsx b/client-web/src/modules/chats/presentation/components/ChatListItem.tsx index 32367cc..b815f86 100644 --- a/client-web/src/modules/chats/presentation/components/ChatListItem.tsx +++ b/client-web/src/modules/chats/presentation/components/ChatListItem.tsx @@ -13,7 +13,7 @@ import Avatar from '../../../../core/presentation/components/ui/Avatar'; import type { Chat } from '../../../../core/domain/types'; import { UserApi } from '../../../users/infrastructure/userApi'; -const userStatusCache = new Map(); +const userStatusCache = new Map(); interface ChatListItemProps { chat: Chat; @@ -27,7 +27,7 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) { const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number } | null>(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - const [otherUserStatus, setOtherUserStatus] = useState<{ emoji?: string | null; text?: string | null } | null>(null); + const [otherUserStatus, setOtherUserStatus] = useState<{ emoji?: string | null; text?: string | null; isInvisible?: boolean } | null>(null); const ctxRef = useRef(null); const myMember = chat.members.find((m) => m.user.id === user?.id); @@ -49,7 +49,7 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) { ? otherMember?.user.avatarUrl || otherMember?.user.avatar || null : chat.avatarUrl || chat.avatar || null; - const isOnline = chat.type === 'personal' && !user?.isInvisible && !!otherMember?.user.isOnline; + const isOnline = chat.type === 'personal' && !user?.isInvisible && !otherUserStatus?.isInvisible && !!otherMember?.user.isOnline; // Check if someone is typing in this chat const typingInChat = typingUsers.filter((t) => t.chatId === chat.id && t.userId !== user?.id); @@ -108,7 +108,7 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) { UserApi.getUser(otherId) .then((profile) => { if (cancelled) return; - const status = { emoji: profile.statusEmoji, text: profile.statusText }; + const status = { emoji: profile.statusEmoji, text: profile.statusText, isInvisible: !!profile.isInvisible }; userStatusCache.set(otherId, status); setOtherUserStatus(status); }) diff --git a/client-web/src/modules/chats/presentation/components/ChatView.tsx b/client-web/src/modules/chats/presentation/components/ChatView.tsx index ac8c8d2..2f16cf2 100644 --- a/client-web/src/modules/chats/presentation/components/ChatView.tsx +++ b/client-web/src/modules/chats/presentation/components/ChatView.tsx @@ -24,6 +24,7 @@ import { useChatStore } from '../../application/chatStore'; import { httpClient } from '../../../../core/infrastructure/httpClient'; import { useAuthStore } from '../../../auth/application/authStore'; import { ChatApi } from '../../infrastructure/chatApi'; +import { UserApi } from '../../../users/infrastructure/userApi'; import { getSocket } from '../../../../core/infrastructure/socket'; import { isChatMuted, toggleMuteChat } from '../../../../core/utils/sounds'; import { useLang } from '../../../../core/infrastructure/i18n'; @@ -75,6 +76,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal const [confirmAction, setConfirmAction] = useState<{ message: string; action: () => void } | null>(null); const [scrollReady, setScrollReady] = useState(false); const [activeGroupCallParticipants, setActiveGroupCallParticipants] = useState([]); + const [otherUserInvisible, setOtherUserInvisible] = useState(false); const messagesEndRef = useRef(null); const scrollContainerRef = useRef(null); @@ -182,6 +184,31 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal : chat?.avatarUrl || chat?.avatar || null; const isOnline = chat?.type === 'personal' && otherMember?.user.isOnline; const privacyExchangeMode = !!user?.isInvisible; + const shouldMaskOtherUserPresence = privacyExchangeMode || otherUserInvisible; + useEffect(() => { + if (chat?.type !== 'personal' || !otherMember?.user.id) { + setOtherUserInvisible(false); + return; + } + + let cancelled = false; + UserApi.getUser(otherMember.user.id) + .then((profile) => { + if (!cancelled) { + setOtherUserInvisible(!!profile.isInvisible); + } + }) + .catch(() => { + if (!cancelled) { + setOtherUserInvisible(false); + } + }); + + return () => { + cancelled = true; + }; + }, [chat?.type, otherMember?.user.id]); + const typingInChat = typingUsers.filter((t) => t.chatId === activeChat && t.userId !== user?.id); @@ -846,7 +873,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal src={chatAvatar} name={chatName} size="md" - online={isOnline ? true : undefined} + online={!shouldMaskOtherUserPresence && isOnline ? true : undefined} className="avatar-knot-container group-hover:border-primary/30" /> )} @@ -860,7 +887,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal ? {t('typing')} : isOnline ? {t('online')} - : chat.type === 'personal' && privacyExchangeMode + : chat.type === 'personal' && shouldMaskOtherUserPresence ? t('wasRecently') : chat.type === 'personal' && otherMember?.user.lastSeen ? `${formatLastSeen(otherMember.user.lastSeen, lang)}`