import React, { useState, useEffect, useCallback, useRef } 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 } from '@/store/messageStore'; import { MessageSearch } from './components/MessageSearch'; import { useSidebar } from '@/contexts/SidebarContext'; 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 { 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 { 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(); useNoIndex(true); const { user } = useAuthStore(); const { actualTheme } = useTheme(); const { setForceCollapsed } = useSidebar(); const activeConversationId = useMessageStore((s) => s.activeConversationId); const setActiveConversation = useMessageStore((s) => s.setActiveConversation); const conversations = useMessageStore((s) => s.conversations); const isConnected = useMessageStore((s) => s.isConnected); const currentUserId = useMessageStore((s) => s.currentUserId); const setCurrentUser = useMessageStore((s) => s.setCurrentUser); const connectWebSocket = useMessageStore((s) => s.connectWebSocket); const disconnectWebSocket = useMessageStore((s) => s.disconnectWebSocket); const loadUserConversations = useMessageStore((s) => s.loadUserConversations); const [showMobileConversation, setShowMobileConversation] = useState(false); const [showSearchModal, setShowSearchModal] = useState(false); const [windowWidth, setWindowWidth] = useState(window.innerWidth); const [isLoading, setIsLoading] = useState(true); const [initError, setInitError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [suggestedMessage, setSuggestedMessage] = useState(null); const [suggestedPins, setSuggestedPins] = useState< Array<{ id: string; title: string; imageUrl?: string }> >([]); // Ref to prevent multiple initialization attempts const hasInitializedRef = useRef(false); const initializingRef = useRef(false); const navigate = useNavigate(); const location = useLocation(); const { error: showError } = useToast(); // Responsive breakpoints following design system const isMobile = windowWidth < 768; // Pull to refresh handler const handlePullRefresh = useCallback(async () => { await loadUserConversations(); }, [loadUserConversations]); // Pull to refresh hook - TEMPORARILY DISABLED to debug freeze // const { // containerRef: pullToRefreshContainerRef, // isPulling, // isRefreshing, // pullDistance, // progress, // } = usePullToRefresh({ // onRefresh: handlePullRefresh, // threshold: 80, // resistance: 0.5, // disabled: isLoading, // }); const pullToRefreshContainerRef = useRef(null); const isPulling = false; const isRefreshing = false; const pullDistance = 0; const progress = 0; // Force sidebar to collapse when entering messages page (Instagram-style layout) useEffect(() => { // console.log('[EFFECT-A] sidebar collapse'); setForceCollapsed(true); return () => { setForceCollapsed(false); }; }, [setForceCollapsed]); // Auto-match: preencher mensagem/pins e abrir conversa alvo useEffect(() => { // console.log('[EFFECT-B] auto-match'); 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); navigate('/messages/new'); } } else if (!autoCtx && userId) { // Route-based compose to avoid modal-state freeze navigate(`/messages/new?user=${encodeURIComponent(userId)}`); } }; handleUserConversation(); if (location.state?.openNewMessageModal) { navigate('/messages/new', { replace: true }); } }, [ location.pathname, location.search, location.state, conversations, setActiveConversation, isMobile, navigate, isLoading, currentUserId, ]); // Initialize user and WebSocket connection useEffect(() => { // console.log('[EFFECT-C] init websocket'); // Prevent multiple initialization attempts if (hasInitializedRef.current || initializingRef.current) { return; } // Safety timeout: force loading to false after 10 seconds const safetyTimeout = setTimeout(() => { console.warn('[MessagesPage] Safety timeout triggered - forcing isLoading to false'); setIsLoading(false); initializingRef.current = false; }, 10000); const initializeMessaging = async () => { if (!user?.id) { clearTimeout(safetyTimeout); navigate('/auth/login'); return; } // Mark as initializing to prevent concurrent calls initializingRef.current = true; 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); setInitError(null); hasInitializedRef.current = true; } catch (error) { console.error('[MessagesPage] Failed to initialize messaging:', error); clearTimeout(safetyTimeout); const errorMessage = error instanceof Error ? error.message : 'Failed to load conversations'; setInitError(errorMessage); showError(messages('toasts.loadFailed', { defaultValue: 'Failed to load conversations' })); setIsLoading(false); // Mark as initialized even on error to prevent infinite retry hasInitializedRef.current = true; } finally { initializingRef.current = false; } }; initializeMessaging(); // Cleanup on unmount return () => { clearTimeout(safetyTimeout); disconnectWebSocket(); }; // Only depend on user.id to prevent re-initialization on other changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.id]); // Update window width state when window is resized useEffect(() => { // console.log('[EFFECT-D] resize listener'); const handleResize = () => { setWindowWidth(window.innerWidth); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // NOTE: Duplicate auto-match effect was removed - it was causing infinite loops. // The original auto-match effect above (lines ~99-178) handles all the same logic. // Set the first conversation as active if none is selected useEffect(() => { // console.log('[EFFECT-E] set first conversation'); 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 = () => { // Route-based compose to avoid modal-state freeze navigate('/messages/new', { state: { backgroundLocation: location } }); }; const handleConversationCreated = () => { // On mobile, switch to the conversation view when a conversation is created/selected if (isMobile) { setShowMobileConversation(true); } }; // Memoize the callback to prevent infinite re-renders in ConversationView const handleInitialMessageUsed = useCallback(() => { setSuggestedMessage(null); setSuggestedPins([]); }, []); // Loading state if (isLoading) { return ; } // Error state with retry option if (initError && conversations.length === 0) { const handleRetry = async () => { setIsLoading(true); setInitError(null); hasInitializedRef.current = false; initializingRef.current = false; try { await loadUserConversations(); setIsLoading(false); hasInitializedRef.current = true; } catch (error) { console.error('[MessagesPage] Retry failed:', error); setInitError(error instanceof Error ? error.message : 'Failed to load conversations'); setIsLoading(false); hasInitializedRef.current = true; } }; return (

{common('error', { defaultValue: 'Error' })}

{messages('toasts.loadFailed', { defaultValue: 'Failed to load conversations. Please try again.', })}

); } // 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" />
setShowSearchModal(false)} /> ); } else { return ( <> {/* Container customizado sem PageTemplate para evitar scroll desnecessário */}
{/* Scrollable content area starting right after main app header */}
{/* Conversation view */} {activeConversationId && ( )}
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 */}
{/* Conversation view */} {activeConversationId ? ( ) : (

{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.', })}

)}
setShowSearchModal(false)} /> ); };