diff --git a/src/components/message/MessagingPage.tsx b/src/components/message/MessagingPage.tsx
index be20701..4616f24 100644
--- a/src/components/message/MessagingPage.tsx
+++ b/src/components/message/MessagingPage.tsx
@@ -1,9 +1,26 @@
'use client';
-import { useState, useRef, useEffect } from 'react';
+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 = `
@@ -22,229 +39,251 @@ const customScrollbarStyles = `
}
`;
-// Types
-interface Conversation {
- id: string;
+// 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;
- lastMessage: string;
- date: string;
- isOnline: boolean;
+ avatar: string | null;
+ avatarUrl: string | null;
+ headline: string | null;
+ city: string | null;
+ state: string | null;
}
-interface Message {
- id: string;
- senderId: string;
- text: string;
- timestamp: string;
- isOwn: boolean;
-}
+export function MessagingPage() {
+ const { data: session } = useSession();
+ const {
+ conversations,
+ currentConversation,
+ messages,
+ isConnected,
+ isLoading,
+ isLoadingMessages,
+ typingUsers,
+ selectConversation,
+ sendMessage,
+ startTyping,
+ stopTyping,
+ loadMoreMessages,
+ hasMoreMessages,
+ startConversation,
+ } = useMessaging();
-interface SelectedUser {
- name: string;
- role: string;
- lastSeen: string;
- avatar: string;
- isOnline: boolean;
- pronouns?: string;
- expertise?: string[];
-}
-
-interface MessagingPageProps {
- conversations?: Conversation[];
- messages?: Message[];
- selectedUser?: SelectedUser;
-}
-
-// Mock data for conversations
-const defaultConversations: Conversation[] = [
- {
- id: '1',
- name: 'Pradeep Ram',
- avatar: '/assets/icons/user-placeholder-icon.svg',
- lastMessage: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
- date: 'Dec 4',
- isOnline: true,
- },
- {
- id: '2',
- name: 'Pradeep',
- avatar: '/assets/icons/user-placeholder-icon.svg',
- lastMessage: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
- date: 'Dec 4',
- isOnline: true,
- },
- {
- id: '3',
- name: 'Gokulraj',
- avatar: '/assets/icons/user-placeholder-icon.svg',
- lastMessage: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
- date: 'Dec 4',
- isOnline: true,
- },
- {
- id: '4',
- name: 'Suriya s',
- avatar: '/assets/icons/user-placeholder-icon.svg',
- lastMessage: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
- date: 'Dec 4',
- isOnline: true,
- },
- {
- id: '5',
- name: 'Sanjay',
- avatar: '/assets/icons/user-placeholder-icon.svg',
- lastMessage: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
- date: 'Dec 4',
- isOnline: true,
- },
- {
- id: '6',
- name: 'Pradeep Ram',
- avatar: '/assets/icons/user-placeholder-icon.svg',
- lastMessage: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
- date: 'Dec 4',
- isOnline: false,
- },
- {
- id: '7',
- name: 'Rahul Kumar',
- avatar: '/assets/icons/user-placeholder-icon.svg',
- lastMessage: 'Thanks for your help with the property listing!',
- date: 'Dec 3',
- isOnline: true,
- },
- {
- id: '8',
- name: 'Anita Singh',
- avatar: '/assets/icons/user-placeholder-icon.svg',
- lastMessage: 'Can we schedule a viewing for tomorrow?',
- date: 'Dec 3',
- isOnline: false,
- },
- {
- id: '9',
- name: 'Vikram Patel',
- avatar: '/assets/icons/user-placeholder-icon.svg',
- lastMessage: 'The documents have been submitted.',
- date: 'Dec 2',
- isOnline: true,
- },
- {
- id: '10',
- name: 'Meera Sharma',
- avatar: '/assets/icons/user-placeholder-icon.svg',
- lastMessage: 'Looking forward to our meeting next week.',
- date: 'Dec 2',
- isOnline: false,
- },
-];
-
-// Mock data for selected user
-const defaultSelectedUser: SelectedUser = {
- name: 'Pradeep Ram',
- role: 'Advisor',
- lastSeen: '21h Ago',
- avatar: '/assets/icons/user-placeholder-icon.svg',
- isOnline: true,
- pronouns: 'He/Him',
- expertise: ['Sales', 'Analytics', 'Inspection', 'Residential', 'Commercial'],
-};
-
-// Mock messages data
-const defaultMessages: Message[] = [
- {
- id: '1',
- senderId: '1',
- text: 'Hi! I saw your profile and I think you might be a great fit for what I\'m looking for.',
- timestamp: '10:30 AM',
- isOwn: false,
- },
- {
- id: '2',
- senderId: 'me',
- text: 'Hello! Thank you for reaching out. I\'d be happy to help. What kind of property are you looking for?',
- timestamp: '10:32 AM',
- isOwn: true,
- },
- {
- id: '3',
- senderId: '1',
- text: 'I\'m looking for a residential property in the downtown area. Preferably a 3-bedroom apartment with modern amenities.',
- timestamp: '10:35 AM',
- isOwn: false,
- },
- {
- id: '4',
- senderId: 'me',
- text: 'That sounds great! I have several listings that might interest you. Do you have a specific budget range in mind?',
- timestamp: '10:38 AM',
- isOwn: true,
- },
- {
- id: '5',
- senderId: '1',
- text: 'My budget is around $500,000 to $700,000. I\'m also interested in properties with good investment potential.',
- timestamp: '10:40 AM',
- isOwn: false,
- },
- {
- id: '6',
- senderId: 'me',
- text: 'Perfect! I have a few properties in that range. Would you like to schedule a viewing this weekend?',
- timestamp: '10:42 AM',
- isOwn: true,
- },
- {
- id: '7',
- senderId: '1',
- text: 'Yes, that would be great! Saturday afternoon works best for me.',
- timestamp: '10:45 AM',
- isOwn: false,
- },
- {
- id: '8',
- senderId: 'me',
- text: 'Saturday at 2 PM works for me. I\'ll send you the addresses and details of the properties we\'ll be visiting.',
- timestamp: '10:48 AM',
- isOwn: true,
- },
- {
- id: '9',
- senderId: '1',
- text: 'Sounds perfect! Looking forward to it. Thank you for your quick response.',
- timestamp: '10:50 AM',
- isOwn: false,
- },
- {
- id: '10',
- senderId: 'me',
- text: 'You\'re welcome! See you on Saturday. Feel free to reach out if you have any questions before then.',
- timestamp: '10:52 AM',
- isOwn: true,
- },
-];
-
-export function MessagingPage({
- conversations = defaultConversations,
- messages = defaultMessages,
- selectedUser = defaultSelectedUser,
-}: MessagingPageProps) {
- const [selectedConversation, setSelectedConversation] = useState
('1');
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 handleSendMessage = (message: string) => {
- console.log('Sending message:', message);
- // In production, this would send the message to the API
- };
+ 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();
+ }
}
};
@@ -257,15 +296,47 @@ export function MessagingPage({
}
};
+ // Scroll to bottom when new message arrives or conversation changes
useEffect(() => {
- // Scroll to bottom on initial load
scrollToBottom();
- }, [selectedConversation]);
+ }, [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.name.toLowerCase().includes(searchQuery.toLowerCase())
+ 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 (
<>
@@ -276,6 +347,14 @@ export function MessagingPage({
Messaging
+ {/* Connection status indicator */}
+
+
+
+
{/* Search bar */}
@@ -320,97 +399,238 @@ export function MessagingPage({
{/* Left sidebar - Conversation list */}
-
- {filteredConversations.map((conversation) => (
-
setSelectedConversation(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 */}
- {selectedConversation === conversation.id && (
-
- )}
- {/* Avatar with online indicator */}
-
-
-
-
- {conversation.isOnline && (
-
- )}
-
-
- {/* Content */}
-
-
- {conversation.name}
-
-
- {conversation.lastMessage}
-
-
-
- {/* Date */}
-
- {conversation.date}
+ {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?.isOnline && (
+
+ )}
+
+
+ {/* Content */}
+
+
+
+ {conversation.otherParty?.name || 'Unknown'}
+
+ {conversation.unreadCount > 0 && (
+
+ {conversation.unreadCount}
+
+ )}
+
+
+ {conversation.lastMessageText || 'No messages yet'}
+
+
+
+ {/* Date */}
+
+ {formatConversationDate(conversation.lastMessageAt)}
+
+
+ ))}
+
+ )}
{/* Right panel - Chat area */}
- {selectedConversation ? (
+ {currentConversation && selectedUserInfo ? (
<>
{/* Chat header */}
-
+
0}
+ typingText={typingUserNames.length > 0 ? `${typingUserNames.join(', ')} is typing...` : undefined}
+ />
{/* Messages area wrapper */}
-
-
- {messages.map((message) => (
-
-
-
- {message.text}
-
-
- {message.timestamp}
-
-
-
- ))}
+ {isLoadingMessages && messages.length === 0 ? (
+
-
+ ) : (
+
+ {/* Load more indicator */}
+ {isLoadingMessages && messages.length > 0 && (
+
+ )}
- {/* Scroll to bottom button - only shows when scrolled up */}
+
+ {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 && (