import { useEffect, useRef, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useChatStore } from '../stores/chatStore'; import { useAuthStore } from '../stores/authStore'; import { getSocket, disconnectSocket } from '../lib/socket'; import { api } from '../lib/api'; import { playNotificationSound, isChatMuted } from '../lib/sounds'; import { useLang } from '../lib/i18n'; import type { Message, UserBasic, CallInfo } from '../lib/types'; import { Send, Check } from 'lucide-react'; import Sidebar from '../components/Sidebar'; import ChatView from '../components/ChatView'; import CallModal from '../components/CallModal'; import GroupCallModal from '../components/GroupCallModal'; 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 { t } = useLang(); 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 api.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 api.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', (message: Message) => { updateMessage(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: { chatId: string; userId: string; messageIds: string[] }) => { markRead(data.chatId, data.userId, data.messageIds); }); 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; newPinnedMessage: Message | null }) => { removePinnedMessage(data.chatId, data.messageId, data.newPinnedMessage); }); 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); } }); 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'); }; }, [user?.id]); const handleStartCall = (targetUser: UserBasic, type: 'voice' | 'video') => { setCallTarget(targetUser); setCallType(type); setIncomingCall(null); setCallSessionId(id => id + 1); setCallOpen(true); }; 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 (
{/* Scheduled message delivery notification */} {deliveryNotification && (
{deliveryNotification}
)}
); }