'use client'; import { useState, useRef, useEffect, useCallback } from 'react'; import Image from 'next/image'; import { useSession } from 'next-auth/react'; import { ChatHeader } from './ChatHeader'; import { MessageInput } from './MessageInput'; import { useMessaging } from '@/hooks/useMessaging'; import { connectionRequestsService, type ConnectionRequest } from '@/services/connection-requests.service'; import { uploadService } from '@/services/upload.service'; import type { Conversation, Message } from '@/types/messaging'; // Helper to get a valid avatar URL (must start with http://, https://, or /) function getValidAvatarUrl(avatar: string | null | undefined): string { const placeholder = '/assets/icons/user-placeholder-icon.svg'; if (!avatar) return placeholder; // Check if it's a valid URL format if (avatar.startsWith('http://') || avatar.startsWith('https://') || avatar.startsWith('/')) { return avatar; } // Invalid URL format (like raw storage key), use placeholder return placeholder; } // Custom scrollbar styles const customScrollbarStyles = ` .custom-scrollbar::-webkit-scrollbar { width: 8px; } .custom-scrollbar::-webkit-scrollbar-track { background: transparent; } .custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(0, 41, 61, 0.5); border-radius: 10px; } .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(0, 41, 61, 0.7); } `; // Helper to format date for conversation list function formatConversationDate(dateString: string | null): string { if (!dateString) return ''; const date = new Date(dateString); const now = new Date(); const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24)); if (diffDays === 0) { return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); } else if (diffDays === 1) { return 'Yesterday'; } else if (diffDays < 7) { return date.toLocaleDateString('en-US', { weekday: 'short' }); } else { return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } } // Helper to format message timestamp function formatMessageTime(dateString: string): string { const date = new Date(dateString); return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); } // Helper to get last seen text function formatLastSeen(lastSeenAt: string | null | undefined, isOnline: boolean): string { if (isOnline) return 'Online'; if (!lastSeenAt) return 'Offline'; const date = new Date(lastSeenAt); const now = new Date(); const diffHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60)); if (diffHours < 1) return 'Last seen recently'; if (diffHours < 24) return `${diffHours}h ago`; const diffDays = Math.floor(diffHours / 24); if (diffDays === 1) return 'Last seen yesterday'; return `Last seen ${diffDays}d ago`; } // Connected agent with avatar URL interface ConnectedAgent { agentProfileId: string; name: string; avatar: string | null; avatarUrl: string | null; headline: string | null; city: string | null; state: string | null; } export function MessagingPage() { const { data: session } = useSession(); const { conversations, currentConversation, messages, isConnected, isLoading, isLoadingMessages, typingUsers, selectConversation, sendMessage, startTyping, stopTyping, loadMoreMessages, hasMoreMessages, startConversation, } = useMessaging(); const [searchQuery, setSearchQuery] = useState(''); const [showScrollButton, setShowScrollButton] = useState(false); const [connectedAgents, setConnectedAgents] = useState([]); const [isLoadingAgents, setIsLoadingAgents] = useState(false); const [isStartingConversation, setIsStartingConversation] = useState(null); // Store converted avatar URLs by conversation ID const [conversationAvatarUrls, setConversationAvatarUrls] = useState>({}); const messagesContainerRef = useRef(null); const messagesEndRef = useRef(null); const currentUserId = session?.user?.id; const userRole = session?.user?.role; // Fetch connected users/agents (accepted connections) based on role useEffect(() => { const fetchConnectedContacts = async () => { if (!session || !userRole) return; setIsLoadingAgents(true); try { // USER sees agents, AGENT sees users if (userRole === 'USER') { const requests = await connectionRequestsService.getMyRequests('ACCEPTED'); // Map to connected agents with avatar URLs const agents: ConnectedAgent[] = await Promise.all( requests.map(async (req) => { const agent = req.agentProfile; let avatarUrl: string | null = null; if (agent?.avatar) { try { avatarUrl = await uploadService.getPresignedDownloadUrl(agent.avatar); } catch { avatarUrl = null; } } return { agentProfileId: req.agentProfileId, name: agent ? `${agent.firstName || ''} ${agent.lastName || ''}`.trim() || 'Agent' : 'Agent', avatar: agent?.avatar || null, avatarUrl, headline: agent?.headline || null, city: agent?.city || null, state: agent?.state || null, }; }) ); setConnectedAgents(agents); } else { // For AGENT role - they don't start conversations, they receive them // So we don't need to show a list to start conversations setConnectedAgents([]); } } catch (error) { console.error('Failed to fetch connected contacts:', error); setConnectedAgents([]); } finally { setIsLoadingAgents(false); } }; fetchConnectedContacts(); }, [session, userRole]); // Convert conversation avatar storage keys to presigned URLs useEffect(() => { const convertAvatarUrls = async () => { const newAvatarUrls: Record = {}; for (const conversation of conversations) { const avatar = conversation.otherParty?.avatar; if (!avatar) continue; // Skip if already converted or if it's already a valid URL if (conversationAvatarUrls[conversation.id]) { newAvatarUrls[conversation.id] = conversationAvatarUrls[conversation.id]; continue; } // Check if it's already a valid URL format if (avatar.startsWith('http://') || avatar.startsWith('https://') || avatar.startsWith('/')) { newAvatarUrls[conversation.id] = avatar; continue; } // Convert storage key to presigned URL try { const presignedUrl = await uploadService.getPresignedDownloadUrl(avatar); newAvatarUrls[conversation.id] = presignedUrl; } catch { // Keep as null/undefined if conversion fails } } if (Object.keys(newAvatarUrls).length > 0) { setConversationAvatarUrls(prev => ({ ...prev, ...newAvatarUrls })); } }; if (conversations.length > 0) { convertAvatarUrls(); } }, [conversations]); // Convert current conversation avatar when selected useEffect(() => { const convertCurrentAvatar = async () => { if (!currentConversation) return; const avatar = currentConversation.otherParty?.avatar; if (!avatar) return; // Skip if already converted if (conversationAvatarUrls[currentConversation.id]) return; // Check if it's already a valid URL format if (avatar.startsWith('http://') || avatar.startsWith('https://') || avatar.startsWith('/')) { setConversationAvatarUrls(prev => ({ ...prev, [currentConversation.id]: avatar })); return; } // Convert storage key to presigned URL try { const presignedUrl = await uploadService.getPresignedDownloadUrl(avatar); setConversationAvatarUrls(prev => ({ ...prev, [currentConversation.id]: presignedUrl })); } catch { // Keep as null/undefined if conversion fails } }; convertCurrentAvatar(); }, [currentConversation]); // Handle starting a new conversation with an agent const handleStartConversation = useCallback(async (agentProfileId: string) => { setIsStartingConversation(agentProfileId); try { const conversation = await startConversation(agentProfileId); await selectConversation(conversation.id); } catch (error) { console.error('Failed to start conversation:', error); } finally { setIsStartingConversation(null); } }, [startConversation, selectConversation]); // Handle sending a message const handleSendMessage = useCallback(async (content: string) => { if (!content.trim()) return; await sendMessage(content); }, [sendMessage]); // Handle typing events const handleTypingStart = useCallback(() => { startTyping(); }, [startTyping]); const handleTypingStop = useCallback(() => { stopTyping(); }, [stopTyping]); // Handle scroll events const handleScroll = () => { if (messagesContainerRef.current) { const { scrollTop, scrollHeight, clientHeight } = messagesContainerRef.current; // Show button if not at bottom (with 100px threshold) setShowScrollButton(scrollHeight - scrollTop - clientHeight > 100); // Load more messages when scrolled to top if (scrollTop === 0 && hasMoreMessages && !isLoadingMessages) { loadMoreMessages(); } } }; const scrollToBottom = () => { if (messagesContainerRef.current) { messagesContainerRef.current.scrollTo({ top: messagesContainerRef.current.scrollHeight, behavior: 'smooth' }); } }; // Scroll to bottom when new message arrives or conversation changes useEffect(() => { scrollToBottom(); }, [currentConversation?.id]); // Scroll to bottom when new messages are added (not when loading more) useEffect(() => { if (messages.length > 0) { const lastMessage = messages[messages.length - 1]; const isVeryRecent = new Date().getTime() - new Date(lastMessage.createdAt).getTime() < 2000; if (isVeryRecent) { scrollToBottom(); } } }, [messages]); // Filter conversations by search const filteredConversations = conversations.filter((conv) => conv.otherParty?.name?.toLowerCase().includes(searchQuery.toLowerCase()) ); // Build selected user info for ChatHeader const selectedUserInfo = currentConversation ? { name: currentConversation.otherParty?.name || 'Unknown', role: currentConversation.otherParty?.headline || 'Agent', lastSeen: formatLastSeen(currentConversation.otherParty?.lastSeenAt, currentConversation.otherParty?.isOnline), avatar: getValidAvatarUrl(conversationAvatarUrls[currentConversation.id]), isOnline: currentConversation.otherParty?.isOnline || false, } : null; // Get typing user names const typingUserNames = Array.from(typingUsers) .filter(userId => userId !== currentUserId) .map(userId => { // The other party is typing if (currentConversation?.otherParty.userId === userId || currentConversation?.otherParty.id === userId) { return currentConversation.otherParty.name.split(' ')[0]; } return 'Someone'; }); return ( <>
{/* Header */}

Messaging

{/* Connection status indicator */}
{/* Search bar */}
Search setSearchQuery(e.target.value)} placeholder="Search Message" className="flex-1 font-serif font-normal text-[18px] leading-[24px] text-[#00293D] placeholder-[#00293D]/50 focus:outline-none" />
{/* Header actions */}
{/* Main content */}
{/* Left sidebar - Conversation list */}
{isLoading || isLoadingAgents ? (
) : filteredConversations.length === 0 ? ( /* Show connected agents when no conversations */
{/* Header for connected agents */}

{searchQuery ? 'No conversations found' : 'No conversations yet. Start a conversation with an agent!'}

{/* List of connected agents */} {connectedAgents.length > 0 ? (

Your Connected Agents

{connectedAgents .filter((agent) => agent.name.toLowerCase().includes(searchQuery.toLowerCase()) ) .map((agent) => ( ))}
) : (

No connected agents yet. Connect with agents to start messaging!

)}
) : (
{filteredConversations.map((conversation) => (
selectConversation(conversation.id)} className="relative flex items-start gap-3 p-4 rounded-[15px] cursor-pointer transition-colors hover:bg-[#00293d]/5" style={{ border: '1px solid rgba(0, 41, 61, 0.1)' }} > {/* Active indicator line */} {currentConversation?.id === conversation.id && (
)} {/* Avatar with online indicator */}
{conversation.otherParty?.name
{conversation.otherParty?.isOnline && (
)}
{/* Content */}

{conversation.otherParty?.name || 'Unknown'}

{conversation.unreadCount > 0 && ( {conversation.unreadCount} )}

{conversation.lastMessageText || 'No messages yet'}

{/* Date */}

{formatConversationDate(conversation.lastMessageAt)}

))}
)}
{/* Right panel - Chat area */}
{currentConversation && selectedUserInfo ? ( <> {/* Chat header */} 0} typingText={typingUserNames.length > 0 ? `${typingUserNames.join(', ')} is typing...` : undefined} /> {/* Messages area wrapper */}
{isLoadingMessages && messages.length === 0 ? (
) : (
{/* Load more indicator */} {isLoadingMessages && messages.length > 0 && (
)}
{messages.map((message) => { const isOwn = message.senderId === currentUserId; return (

{message.content}

{formatMessageTime(message.createdAt)}

{isOwn && ( {message.status === 'READ' ? '✓✓' : message.status === 'DELIVERED' ? '✓✓' : '✓'} )}
); })} {/* Typing indicator in messages */} {typingUserNames.length > 0 && (
)}
)} {/* Scroll to bottom button */} {showScrollButton && ( )}
{/* Message input */} ) : (
Messages

Select a conversation to start messaging

)}
); }