import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useDesignTokens } from '@/theme/useDesignTokens'; import { useNavigate, useLocation } from 'react-router-dom'; import { ConversationList } from './components/ConversationList'; import { ConversationView } from './components/ConversationView'; import { useMessageStore, getUserInfo } from '@/store/messageStore'; import { NewConversationModal } from './components/NewConversationModal'; import { MessageSearch } from './components/MessageSearch'; import { useSidebar } from '@/contexts/SidebarContext'; import { useNavigationStore } from '@/store/navigationStore'; import { useTheme } from '@/theme/ThemeProvider'; import { useToast } from '@/hooks/useToast'; import { useAuthStore } from '@/store/authStore'; import { usePullToRefresh } from '@/hooks/usePullToRefresh'; import { PullToRefreshIndicator } from '@/components/ui/PullToRefreshIndicator'; import { MagnifyingGlassIcon, ChatBubbleLeftRightIcon, PencilIcon, } from '@heroicons/react/24/outline'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { Button } from '@/components/ui/design-system/Button'; import { PageTemplate } from '@/components/layout/PageTemplate'; import { MessagesPageSkeleton } from './components/MessagesSkeleton'; import { MessagesErrorBoundary } from './components/MessagesErrorBoundary'; import { Input } from '@/components/ui/design-system/Input'; import { Avatar } from '@/components/ui/design-system/Avatar'; import { useTranslation } from '@/hooks/useTranslation'; import { useNoIndex } from '@/hooks/useNoIndex'; export const MessagesPage: React.FC<{ insideModal?: boolean }> = ({ insideModal = false }) => { const { semantic } = useDesignTokens(); const { messages, common } = useTranslation(); // SEO: do not index messages page useNoIndex(true); const { user } = useAuthStore(); const { actualTheme } = useTheme(); const { setForceCollapsed } = useSidebar(); const { activeConversationId, setActiveConversation, conversations, sendMessage, isConnected, currentUserId, setCurrentUser, connectWebSocket, disconnectWebSocket, loadUserConversations, typingUsers, } = useMessageStore(); const [showMobileConversation, setShowMobileConversation] = useState(false); const [showNewConversationModal, setShowNewConversationModal] = useState(false); const [showSearchModal, setShowSearchModal] = useState(false); const [windowWidth, setWindowWidth] = useState(window.innerWidth); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [suggestedMessage, setSuggestedMessage] = useState(null); const [suggestedPins, setSuggestedPins] = useState< Array<{ id: string; title: string; imageUrl?: string }> >([]); const navigate = useNavigate(); const location = useLocation(); const { getLastVisitedState } = useNavigationStore(); const { success, error: showError } = useToast(); // Responsive breakpoints following design system const isMobile = windowWidth < 768; const isTablet = windowWidth >= 768 && windowWidth < 1024; const isDesktop = windowWidth >= 1024; // Pull to refresh handler const handlePullRefresh = useCallback(async () => { await loadUserConversations(); }, [loadUserConversations]); // Pull to refresh hook const { containerRef: pullToRefreshContainerRef, isPulling, isRefreshing, pullDistance, progress, } = usePullToRefresh({ onRefresh: handlePullRefresh, threshold: 80, resistance: 0.5, disabled: isLoading, }); // Force sidebar to collapse when entering messages page (Instagram-style layout) useEffect(() => { setForceCollapsed(true); // Cleanup: restore sidebar state when leaving messages page return () => { setForceCollapsed(false); }; }, [setForceCollapsed]); // Auto-match: preencher mensagem/pins e abrir conversa alvo useEffect(() => { const searchParams = new URLSearchParams(location.search); const userId = searchParams.get('user'); const autoCtx = location.state?.autoMatchContext; // Preencher mensagem e pins sugeridos if (autoCtx?.suggestedMessage) { setSuggestedMessage(autoCtx.suggestedMessage); } if (autoCtx) { const pins: Array<{ id: string; title: string; imageUrl?: string }> = []; const seen = new Set(); const addPins = (list?: Array<{ id: string; title: string; imageUrl?: string }>) => { list?.forEach((p) => { if (p.id && !seen.has(p.id)) { seen.add(p.id); pins.push(p); } }); }; addPins(autoCtx.mutualPins); addPins(autoCtx.pinsYouWant); addPins(autoCtx.pinsTheyWant); setSuggestedPins(pins.slice(0, 3)); } const handleUserConversation = async () => { // Only execute if there's a userId in the URL (don't open modal on regular navigation) if (!userId || isLoading) return; const conv = conversations.find((c) => c.participants.some((p) => { const participantId = typeof p === 'string' ? p : p?.id; return participantId === userId; }) ); if (conv) { setActiveConversation(conv.id); if (isMobile) setShowMobileConversation(true); if (autoCtx) { navigate(location.pathname + location.search, { replace: true, state: {} }); } } else if (autoCtx && currentUserId) { try { const { findOrCreateConversation } = useMessageStore.getState(); const newId = await findOrCreateConversation([currentUserId, userId]); if (newId) { setActiveConversation(newId); if (isMobile) setShowMobileConversation(true); navigate(location.pathname + location.search, { replace: true, state: {} }); } } catch (error) { console.error('Error creating conversation from auto-match:', error); setShowNewConversationModal(true); } } else if (!autoCtx && userId) { // Only open modal if there's a specific userId we're trying to message setShowNewConversationModal(true); } }; handleUserConversation(); if (location.state?.openNewMessageModal) { setShowNewConversationModal(true); navigate(location.pathname, { replace: true }); } }, [ location.search, location.state, conversations, setActiveConversation, isMobile, navigate, isLoading, currentUserId, ]); // Initialize user and WebSocket connection useEffect(() => { // Safety timeout: force loading to false after 10 seconds const safetyTimeout = setTimeout(() => { console.warn('[MessagesPage] Safety timeout triggered - forcing isLoading to false'); setIsLoading(false); }, 10000); const initializeMessaging = async () => { if (!user?.id) { clearTimeout(safetyTimeout); navigate('/auth/login'); return; } try { // Set current user in store setCurrentUser(user.id); // Load user conversations first await loadUserConversations(); // Try to connect WebSocket (non-blocking) try { await connectWebSocket(); } catch (wsError) { console.warn( '[MessagesPage] WebSocket connection failed; continuing without real-time features', wsError ); // Don't block the app if WebSocket fails } clearTimeout(safetyTimeout); setIsLoading(false); } catch (error) { console.error('[MessagesPage] Failed to initialize messaging:', error); clearTimeout(safetyTimeout); showError(messages('toasts.loadFailed', { defaultValue: 'Failed to load conversations' })); setIsLoading(false); } }; initializeMessaging(); // Cleanup on unmount return () => { clearTimeout(safetyTimeout); disconnectWebSocket(); }; }, [ user?.id, setCurrentUser, connectWebSocket, disconnectWebSocket, loadUserConversations, navigate, showError, ]); // Handle new message notifications - simplified without toast.custom useEffect(() => { const handleNewMessage = async (message: any) => { if (message.senderId === currentUserId) { return; } try { const senderInfo = await getUserInfo(message.senderId); const senderName = senderInfo?.name || 'Unknown User'; // Simple notification instead of complex toast.custom success( messages('toasts.newMessageFrom', { name: senderName, preview: `${message.content.substring(0, 50)}${message.content.length > 50 ? '...' : ''}`, defaultValue: `New message from ${senderName}: ${message.content.substring(0, 50)}${message.content.length > 50 ? '...' : ''}`, }) ); } catch (error) { console.error('Failed to load sender info:', error); success(messages('toasts.newMessageReceived', { defaultValue: 'New message received' })); } }; }, [currentUserId, activeConversationId, setActiveConversation, isMobile, success]); // Update window width state when window is resized useEffect(() => { const handleResize = () => { setWindowWidth(window.innerWidth); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // Handle auto-match context and URL parameters useEffect(() => { const searchParams = new URLSearchParams(location.search); const userId = searchParams.get('user'); const hasAutoMatchContext = location.state?.autoMatchContext?.suggestedMessage; console.log('🔍 [MessagesPage] Auto-match check:', { userId, hasAutoMatchContext, locationState: location.state, }); // Check for auto-match context with suggested message and pins if (hasAutoMatchContext) { console.log('📧 [MessagesPage] Auto-match context found:', location.state.autoMatchContext); setSuggestedMessage(location.state.autoMatchContext.suggestedMessage); // Collect all unique pins from the match context const allPins: Array<{ id: string; title: string; imageUrl?: string }> = []; const seenIds = new Set(); const addPins = (pins?: Array<{ id: string; title: string; imageUrl?: string }>) => { pins?.forEach((pin) => { if (pin.id && !seenIds.has(pin.id)) { seenIds.add(pin.id); allPins.push(pin); } }); }; addPins(location.state.autoMatchContext.mutualPins); addPins(location.state.autoMatchContext.pinsYouWant); addPins(location.state.autoMatchContext.pinsTheyWant); console.log('📌 [MessagesPage] Collected pins from auto-match:', allPins); setSuggestedPins(allPins.slice(0, 3)); // Limit to 3 pins max } const handleUserConversation = async () => { if (!userId || isLoading) return; // Find conversation - participants can be objects with id or strings const conv = conversations.find((c) => c.participants.some((p) => { const participantId = typeof p === 'string' ? p : p?.id; return participantId === userId; }) ); if (conv) { // Existing conversation - open it directly console.log('✅ [MessagesPage] Found existing conversation, opening:', conv.id); setActiveConversation(conv.id); if (isMobile) { setShowMobileConversation(true); } // Clear state after using if (hasAutoMatchContext) { navigate(location.pathname + location.search, { replace: true, state: {} }); } } else if (hasAutoMatchContext && currentUserId) { // Coming from auto-match without existing conversation - create one automatically console.log('🆕 [MessagesPage] Creating new conversation for auto-match'); try { const { findOrCreateConversation } = useMessageStore.getState(); const newConversationId = await findOrCreateConversation([currentUserId, userId]); if (newConversationId) { console.log('✅ [MessagesPage] Created conversation:', newConversationId); setActiveConversation(newConversationId); if (isMobile) { setShowMobileConversation(true); } // Clear state after using navigate(location.pathname + location.search, { replace: true, state: {} }); } } catch (error) { console.error('❌ [MessagesPage] Error creating conversation from auto-match:', error); // Fallback to modal setShowNewConversationModal(true); } } else { // No auto-match context - show selection modal console.log('📝 [MessagesPage] No existing conversation, showing modal'); setShowNewConversationModal(true); } }; handleUserConversation(); if (location.state?.openNewMessageModal) { setShowNewConversationModal(true); navigate(location.pathname, { replace: true }); } }, [ location.search, location.state, conversations, setActiveConversation, isMobile, navigate, isLoading, currentUserId, ]); // Set the first conversation as active if none is selected useEffect(() => { if (!activeConversationId && conversations.length > 0 && !isMobile && !isLoading) { const unreadConv = conversations.find((conv) => conv.unreadCount > 0 && !conv.isArchived); if (unreadConv) { setActiveConversation(unreadConv.id); } else { const nonArchivedConv = conversations.find((conv) => !conv.isArchived); if (nonArchivedConv) { setActiveConversation(nonArchivedConv.id); } } } }, [activeConversationId, conversations, setActiveConversation, isMobile, isLoading]); const handleSelectConversation = (conversationId: string) => { setActiveConversation(conversationId); if (isMobile) { // Ao selecionar uma conversa no mobile, alterna para a view da conversa setShowMobileConversation(true); } }; const handleBackToList = () => { setShowMobileConversation(false); }; const handleCreateConversation = () => { setShowNewConversationModal(true); }; const handleConversationCreated = (conversationId: string) => { // On mobile, switch to the conversation view when a conversation is created/selected if (isMobile) { setShowMobileConversation(true); } }; const handleBack = () => { if (location.state && location.state.from) { const fromPath = location.state.from.pathname; const savedState = getLastVisitedState(fromPath); if (savedState) { navigate(fromPath, { state: { ...location.state, restored: true, ...savedState, }, }); } else { navigate(fromPath, { state: location.state }); } } else { const { user } = useAuthStore.getState(); const destination = user?.username || user?.id ? `/${user.username || user.id}` : '/'; navigate(destination); } }; // Loading state if (isLoading) { return ; } // Show connection status const ConnectionStatus = () => { if (!isConnected) { return (

{messages('status.offline', { defaultValue: 'Messages are working in offline mode. Real-time updates are not available.', })}

); } return null; }; // Mobile view - show either list or conversation if (isMobile) { if (!showMobileConversation) { return ( <>
{/* Pull to refresh indicator */} {/* Header Section */}
setSearchQuery(e.target.value)} leftIcon={} variant="default" className="w-full" />
{/* Modals - need higher z-index when inside another modal */} setShowNewConversationModal(false)} onConversationCreated={handleConversationCreated} zIndex={insideModal ? 'z-[100000000]' : 'z-[2000]'} /> setShowSearchModal(false)} /> ); } else { return ( <> {/* Container customizado sem PageTemplate para evitar scroll desnecessário */}
{/* Scrollable content area starting right after main app header */}
{activeConversationId && ( { setSuggestedMessage(null); setSuggestedPins([]); }} hasExternalHeader={true} headerHeight={0} /> )}
{/* Modals - need higher z-index when inside another modal */} setShowNewConversationModal(false)} onConversationCreated={handleConversationCreated} zIndex={insideModal ? 'z-[100000000]' : 'z-[2000]'} /> setShowSearchModal(false)} /> ); } } // Desktop view - Layout inspirado no Instagram com 3 colunas return ( <>
{/* Lista de Conversas - Responsiva */}
{/* Header da lista de conversas - STICKY */}

{messages('page.title', { defaultValue: 'Messages' })}

{/* Busca */}
setSearchQuery(e.target.value)} leftIcon={} variant="default" className="w-full" />
{/* Lista de conversas - SCROLLABLE */}
{/* Área da Conversa - Coluna da direita */}
{activeConversationId ? ( { setSuggestedMessage(null); setSuggestedPins([]); }} /> ) : (

{messages('page.selectConversation', { defaultValue: 'Select a conversation' })}

{messages('page.selectConversationDesc', { defaultValue: 'Choose a conversation from the list to start messaging, or create a new one.', })}

)}
{/* Modals */} {showNewConversationModal && ( setShowNewConversationModal(false)} onConversationCreated={handleConversationCreated} zIndex={insideModal ? 'z-[100000000]' : 'z-[2000]'} /> )} setShowSearchModal(false)} /> ); };