import { useEffect, useRef, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useChatStore } from '../application/chatStore'; import { useAuthStore } from '../../auth/application/authStore'; import { getSocket, disconnectSocket } from '../../../core/infrastructure/socket'; import { ChatApi } from '../infrastructure/chatApi'; import { playNotificationSound, isChatMuted, playCallRingtone, stopCallRingtone } from '../../../core/utils/sounds'; import { useLang } from '../../../core/infrastructure/i18n'; import type { Message, UserBasic, CallInfo, ChatMember } from '../../../core/domain/types'; import { Send, Check, Phone, PhoneOff, Users } from 'lucide-react'; import Sidebar from '../../../core/presentation/layouts/Sidebar'; import GlobalNavBar from '../../../core/presentation/layouts/GlobalNavBar'; import ChatView from './components/ChatView'; import CallModal from '../../calls/presentation/components/CallModal'; 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 { loadChats, addMessage, updateMessage, removeMessage, removeMessages, hideMessages, addReaction, removeReaction, markRead, addTypingUser, removeTypingUser, updateUserOnlineStatus, setPinnedMessage, removePinnedMessage, clearStore, addChat, } = useChatStore(); const { user } = useAuthStore(); const initialized = useRef(false); // Call state const [callOpen, setCallOpen] = useState(false); const [callTarget, setCallTarget] = useState(null); const [callType, setCallType] = useState<'voice' | 'video'>('voice'); const [incomingCall, setIncomingCall] = useState(null); const [callSessionId, setCallSessionId] = useState(0); const [deliveryNotification, setDeliveryNotification] = useState(null); const deliveryTimerRef = useRef | null>(null); // Group call state const [groupCallOpen, setGroupCallOpen] = useState(false); const [groupCallChatId, setGroupCallChatId] = useState(''); const [groupCallChatName, setGroupCallChatName] = useState(''); const [groupCallType, setGroupCallType] = useState<'voice' | 'video'>('voice'); const [groupCallSessionId, setGroupCallSessionId] = useState(0); const [incomingGroupCall, setIncomingGroupCall] = useState<{ chatId: string; from: string; callerInfo: any; callType: string; chatName: string } | null>(null); const groupCallOpenRef = useRef(false); const groupCallChatIdRef = useRef(''); const [activeTab, setActiveTab] = useState('chats'); const { t } = useLang(); useEffect(() => { groupCallOpenRef.current = groupCallOpen; groupCallChatIdRef.current = groupCallChatId; }, [groupCallOpen, groupCallChatId]); useEffect(() => { if (initialized.current) return; initialized.current = true; loadChats(); }, [loadChats]); // Обработка закрытия вкладки — отправить disconnect useEffect(() => { const handleBeforeUnload = () => { const socket = getSocket(); if (socket) { socket.disconnect(); } }; window.addEventListener('beforeunload', handleBeforeUnload); return () => { window.removeEventListener('beforeunload', handleBeforeUnload); }; }, []); useEffect(() => { const socket = getSocket(); if (!socket) return; socket.on('new_message', async (message: Message) => { // If this chat isn't in our store yet (e.g. someone just created it and sent a message), // fetch chats so the new chat appears in the sidebar immediately const { chats } = useChatStore.getState(); if (!chats.some(c => c.id === message.chatId)) { try { const allChats = await ChatApi.getChats(); const newChat = allChats.find(c => c.id === message.chatId); if (newChat) { // Reset unreadCount to 0 because addMessage below will increment it by 1 useChatStore.getState().addChat({ ...newChat, unreadCount: 0 }); } } catch (e) { console.error('Failed to fetch new chat:', e); } } addMessage(message); // Play notification sound for messages from others if (message.senderId !== user?.id && !message.storyId && !isChatMuted(message.chatId)) { playNotificationSound(); } }); socket.on('scheduled_delivered', async (message: Message & { _recipientName?: string; _deliveredAt?: string }) => { // If chat unknown, fetch it first const { chats } = useChatStore.getState(); if (!chats.some(c => c.id === message.chatId)) { try { const allChats = await ChatApi.getChats(); const newChat = allChats.find(c => c.id === message.chatId); if (newChat) useChatStore.getState().addChat(newChat); } catch (_) { /* ignore */ } } // A scheduled message was delivered: update it in store (remove scheduledAt) updateMessage({ ...message, scheduledAt: null }); // Show delivery notification to the sender if (message.senderId === user?.id && message._recipientName) { const time = message._deliveredAt ? new Date(message._deliveredAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''; const notifText = `${useLang.getState().t('scheduledDelivered')} ${message._recipientName} ${useLang.getState().t('scheduledDeliveredAt')} ${time}`; setDeliveryNotification(notifText); if (deliveryTimerRef.current) clearTimeout(deliveryTimerRef.current); deliveryTimerRef.current = setTimeout(() => setDeliveryNotification(null), 5000); } // Notify others with sound if (message.senderId !== user?.id && !isChatMuted(message.chatId)) { playNotificationSound(); } }); socket.on('message_edited', (data: { messageId: string; chatId: string; content: string; isEdited: boolean }) => { updateMessage({ id: data.messageId, chatId: data.chatId, content: data.content, isEdited: data.isEdited } as Message); }); socket.on('new_chat', (chat: any) => { addChat(chat); socket.emit('join_chat', chat.id); }); socket.on('message_deleted', (data: { messageId: string; chatId: string }) => { removeMessage(data.messageId, data.chatId); }); socket.on('messages_deleted', (data: { messageIds: string[]; chatId: string }) => { removeMessages(data.messageIds, data.chatId); }); socket.on('messages_hidden', (data: { messageIds: string[]; chatId: string }) => { hideMessages(data.messageIds, data.chatId); }); socket.on('reaction_added', (data: { messageId: string; chatId: string; userId: string; username: string; emoji: string }) => { console.log('[Socket] reaction_added received:', data); addReaction(data.messageId, data.chatId, data.userId, data.username, data.emoji); }); socket.on('reaction_removed', (data: { messageId: string; chatId: string; userId: string; emoji: string }) => { console.log('[Socket] reaction_removed received:', data); removeReaction(data.messageId, data.chatId, data.userId, data.emoji); }); socket.on('messages_read', (data: any) => { markRead(data.chatId || data.ChatId, data.userId || data.UserId, data.lastReadSequenceId || data.LastReadSequenceId || 0); }); socket.on('user_typing', (data: { chatId: string; userId: string }) => { if (data.userId !== user?.id) { addTypingUser(data.chatId, data.userId); setTimeout(() => removeTypingUser(data.chatId, data.userId), 3000); } }); socket.on('user_stopped_typing', (data: { chatId: string; userId: string }) => { removeTypingUser(data.chatId, data.userId); }); socket.on('user_online', (data: { userId: string }) => { updateUserOnlineStatus(data.userId, true); }); socket.on('user_offline', (data: { userId: string; lastSeen?: string }) => { updateUserOnlineStatus(data.userId, false, data.lastSeen); }); socket.on('message_pinned', (data: { chatId: string; message: Message }) => { setPinnedMessage(data.chatId, data.message); }); socket.on('message_unpinned', (data: { chatId: string; messageId: string }) => { removePinnedMessage(data.chatId, data.messageId); }); socket.on('poll_updated', (message: Message) => { updateMessage(message); }); socket.on('call_incoming', async (data: CallInfo) => { // Use callerInfo from server if available, otherwise look up from chats let callerInfo: UserBasic | null = data.callerInfo || null; if (!callerInfo) { const { chats } = useChatStore.getState(); for (const chat of chats) { const member = chat.members.find((m) => m.user.id === data.from); if (member) { callerInfo = member.user; break; } } } setCallTarget(null); // Clear any previous outgoing target setIncomingCall({ from: data.from, offer: data.offer, callType: data.callType, chatId: data.chatId, callerInfo, }); setCallType(data.callType); setCallSessionId(id => id + 1); setCallOpen(true); }); // Story events - registered globally so they work even when StoryViewer is closed socket.on('story_viewed', (data: { storyId: string; userId: string; username: string; displayName: string; avatar: string | null; viewedAt: string; viewCount: number; ownerId: string }) => { console.log('[Socket] story_viewed received:', data); // Only process if this user is the owner if (data.ownerId === user?.id) { console.log('[Socket] This is my story, updating view count'); } }); socket.on('story_reply', (data: { storyId: string; userId: string; username: string; displayName: string; avatar: string | null; content: string; createdAt: string; ownerId: string }) => { console.log('[Socket] story_reply received:', data); // Only process if this user is the owner if (data.ownerId === user?.id) { console.log('[Socket] This is my story, got reply:', data.content); } }); socket.on('story_reaction', (data: { storyId: string; userId: string; username: string; displayName: string; avatar: string | null; emoji: string; createdAt: string; ownerId: string }) => { console.log('[Socket] story_reaction received:', data); // Only process if this user is the owner if (data.ownerId === user?.id) { console.log('[Socket] This is my story, got reaction:', data.emoji); } }); socket.on('group_call_incoming', (data: { chatId: string; from: string; callerInfo: any; callType: string }) => { if (data.from === user?.id) return; if (groupCallOpenRef.current && groupCallChatIdRef.current === data.chatId) return; const { chats } = useChatStore.getState(); const chat = chats.find(c => c.id === data.chatId); if (!chat) return; playCallRingtone(); setIncomingGroupCall({ chatId: data.chatId, from: data.from, callerInfo: data.callerInfo, callType: data.callType, chatName: chat.name || 'Group', }); // Auto-dismiss after 15 seconds if ignored setTimeout(() => { setIncomingGroupCall(prev => { if (prev?.chatId === data.chatId) { stopCallRingtone(); return null; } return prev; }); }, 15000); }); socket.on('group_call_ended', (data: { chatId: string }) => { setIncomingGroupCall(prev => { if (prev?.chatId === data.chatId) { stopCallRingtone(); return null; } return prev; }); }); return () => { socket.off('new_message'); socket.off('scheduled_delivered'); socket.off('message_edited'); socket.off('new_chat'); socket.off('message_deleted'); socket.off('messages_deleted'); socket.off('messages_hidden'); socket.off('reaction_added'); socket.off('reaction_removed'); socket.off('messages_read'); socket.off('user_typing'); socket.off('user_stopped_typing'); socket.off('user_online'); socket.off('user_offline'); socket.off('message_pinned'); socket.off('message_unpinned'); socket.off('call_incoming'); socket.off('story_viewed'); socket.off('story_reply'); socket.off('story_reaction'); socket.off('group_call_incoming'); socket.off('group_call_ended'); }; }, [user?.id]); const handleStartCall = (targetUser: UserBasic, type: 'voice' | 'video') => { setCallTarget(targetUser); setCallType(type); setIncomingCall(null); setCallSessionId(id => id + 1); setCallOpen(true); }; useEffect(() => { const handleCustomCallEvent = ((e: CustomEvent) => { if (e.detail?.targetUser && e.detail?.type) { handleStartCall(e.detail.targetUser, e.detail.type); } }) as EventListener; window.addEventListener('START_CALL', handleCustomCallEvent); return () => window.removeEventListener('START_CALL', handleCustomCallEvent); }, []); const handleStartGroupCall = (chatId: string, chatName: string, type: 'voice' | 'video') => { setGroupCallChatId(chatId); setGroupCallChatName(chatName); setGroupCallType(type); setGroupCallSessionId(id => id + 1); setGroupCallOpen(true); }; const handleCloseCall = () => { setCallOpen(false); setCallTarget(null); setIncomingCall(null); }; const handleCloseGroupCall = () => { setGroupCallOpen(false); }; const activeChat = useChatStore((state) => state.activeChat); return (
{activeTab === 'chats' ? ( <> {/* Chat List (Sidebar) */}
{/* Selected Chat View (Main Area) */}
) : activeTab === 'contacts' ? (
{/* Contacts Sidebar List */}
setActiveTab('chats')} />
{/* Right side placeholder / Profile detail */}

{t('contacts')}

{t('selectContactToChat')}

) : activeTab === 'settings' ? ( ) : (

{t('comingSoon') || 'Coming soon'}

)}
{/* Scheduled message delivery notification */} {deliveryNotification && (
{deliveryNotification}
)}
{/* Incoming Group Call Overlay */} {incomingGroupCall && (

{incomingGroupCall.chatName}

{incomingGroupCall.callerInfo?.displayName || incomingGroupCall.callerInfo?.username || 'User'} {t('isCalling' as any) || 'звонит...'}

{t('groupCall' as any) || 'Групповой звонок'}

)} ); }