diff --git a/src/components/message/MessagingPage.tsx b/src/components/message/MessagingPage.tsx index 27951ae..b941822 100644 --- a/src/components/message/MessagingPage.tsx +++ b/src/components/message/MessagingPage.tsx @@ -6,6 +6,7 @@ import { useSession } from 'next-auth/react'; import { ChatHeader } from './ChatHeader'; import { MessageInput } from './MessageInput'; import { ChatDropdownMenu, type DropdownMenuItem } from './ChatDropdownMenu'; +import { NewConversationModal, type ConnectedContact } from './NewConversationModal'; import { useMessaging } from '@/hooks/useMessaging'; import { connectionRequestsService, type ConnectionRequest } from '@/services/connection-requests.service'; import { uploadService } from '@/services/upload.service'; @@ -112,13 +113,15 @@ function AvatarWithPlaceholder({ src, alt, size }: { src: string; alt: string; s ); } -// Connected agent with avatar URL +// Connected contact with avatar URL (used for both user-side agents and agent-side users) interface ConnectedAgent { agentProfileId: string; + userId?: string; // The user's ID (for agent-side contacts) name: string; avatar: string | null; avatarUrl: string | null; headline: string | null; + email: string | null; city: string | null; state: string | null; } @@ -154,6 +157,7 @@ export function MessagingPage() { const [connectedAgents, setConnectedAgents] = useState([]); const [isLoadingAgents, setIsLoadingAgents] = useState(false); const [isStartingConversation, setIsStartingConversation] = useState(null); + const [isComposeOpen, setIsComposeOpen] = useState(false); // Store converted avatar URLs by conversation ID const [conversationAvatarUrls, setConversationAvatarUrls] = useState>({}); // Store converted file URLs by message ID (for S3-stored images/files) @@ -196,6 +200,7 @@ export function MessagingPage() { avatar: agent?.avatar || null, avatarUrl, headline: agent?.headline || null, + email: null, city: agent?.city || null, state: agent?.state || null, }; @@ -204,9 +209,43 @@ export function MessagingPage() { 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([]); + // AGENT role — fetch accepted connections (users who connected with this agent) + const requests = await connectionRequestsService.getReceivedRequests('ACCEPTED'); + + const users: ConnectedAgent[] = await Promise.all( + requests.map(async (req) => { + const user = req.user; + const profile = user?.userProfile; + let avatarUrl: string | null = null; + + const avatarKey = profile?.avatar || user?.avatar; + if (avatarKey) { + try { + avatarUrl = await uploadService.getPresignedDownloadUrl(avatarKey); + } catch { + avatarUrl = null; + } + } + + const fullName = profile + ? `${profile.firstName || ''} ${profile.lastName || ''}`.trim() + : ''; + + return { + agentProfileId: req.agentProfileId, + userId: req.userId, + name: fullName || user?.email || 'User', + avatar: avatarKey || null, + avatarUrl, + headline: null, + email: user?.email || null, + city: null, + state: null, + }; + }) + ); + + setConnectedAgents(users); } } catch (error) { console.error('Failed to fetch connected contacts:', error); @@ -321,18 +360,21 @@ export function MessagingPage() { setMessageFileUrls({}); }, [currentConversation?.id]); - // Handle starting a new conversation with an agent - const handleStartConversation = useCallback(async (agentProfileId: string) => { - setIsStartingConversation(agentProfileId); + // Handle starting a new conversation — role-aware + const handleStartConversation = useCallback(async (contactId: string) => { + setIsStartingConversation(contactId); try { - const conversation = await startConversation(agentProfileId); + const body = userRole === 'AGENT' + ? { userId: contactId } + : { agentProfileId: contactId }; + const conversation = await startConversation(body); await selectConversation(conversation.id); } catch (error) { console.error('Failed to start conversation:', error); } finally { setIsStartingConversation(null); } - }, [startConversation, selectConversation]); + }, [startConversation, selectConversation, userRole]); // Handle sending a message const handleSendMessage = useCallback(async (content: string) => { @@ -518,6 +560,26 @@ export function MessagingPage() { )} + {/* New conversation modal */} + setIsComposeOpen(false)} + contacts={connectedAgents.map((agent) => ({ + id: userRole === 'AGENT' ? agent.userId! : agent.agentProfileId, + name: agent.name, + avatar: agent.avatar, + avatarUrl: agent.avatarUrl, + headline: agent.headline, + email: agent.email, + city: agent.city, + state: agent.state, + }))} + isLoading={isLoadingAgents} + onStartConversation={async (contactId) => { + await handleStartConversation(contactId); + }} + /> +
{/* Header */}
@@ -572,7 +634,10 @@ export function MessagingPage() { onClose={() => setIsPageMenuOpen(false)} />
-
@@ -605,7 +670,7 @@ export function MessagingPage() { {connectedAgents.length > 0 ? (

- Your Connected Agents + {userRole === 'AGENT' ? 'Your Connected Users' : 'Your Connected Agents'}

{connectedAgents .filter((agent) => @@ -614,7 +679,7 @@ export function MessagingPage() { .map((agent) => (
) : (
-

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

+

{userRole === 'AGENT' ? 'No connected users yet.' : 'No connected agents yet. Connect with agents to start messaging!'}

)} diff --git a/src/components/message/NewConversationModal.tsx b/src/components/message/NewConversationModal.tsx new file mode 100644 index 0000000..36252b1 --- /dev/null +++ b/src/components/message/NewConversationModal.tsx @@ -0,0 +1,197 @@ +'use client'; + +import { useState } from 'react'; +import Image from 'next/image'; + +// Helper to get a valid avatar URL +function getValidAvatarUrl(avatar: string | null | undefined): string { + const placeholder = '/assets/icons/user-placeholder-icon.svg'; + if (!avatar) return placeholder; + if (avatar.startsWith('http://') || avatar.startsWith('https://') || avatar.startsWith('/')) { + return avatar; + } + return placeholder; +} + +export interface ConnectedContact { + id: string; // agentProfileId (for users) or userId (for agents) + name: string; + avatar: string | null; + avatarUrl: string | null; + headline: string | null; + email: string | null; + city: string | null; + state: string | null; +} + +interface NewConversationModalProps { + isOpen: boolean; + onClose: () => void; + contacts: ConnectedContact[]; + isLoading: boolean; + onStartConversation: (contactId: string) => Promise; +} + +export function NewConversationModal({ + isOpen, + onClose, + contacts, + isLoading, + onStartConversation, +}: NewConversationModalProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [startingId, setStartingId] = useState(null); + + if (!isOpen) return null; + + const filteredContacts = contacts.filter((contact) => + contact.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const handleStart = async (contactId: string) => { + setStartingId(contactId); + try { + await onStartConversation(contactId); + onClose(); + } catch (error) { + console.error('Failed to start conversation:', error); + } finally { + setStartingId(null); + } + }; + + return ( +
{ + if (e.target === e.currentTarget) onClose(); + }} + > +
+ {/* Header */} +
+

+ New Conversation +

+ +
+ + {/* Search */} +
+
+ Search + setSearchQuery(e.target.value)} + placeholder="Search connections..." + className="flex-1 font-serif font-normal text-[14px] text-[#00293D] placeholder-[#00293D]/50 focus:outline-none" + autoFocus + /> +
+
+ + {/* Contact list */} +
+ {isLoading ? ( +
+
+
+ ) : filteredContacts.length === 0 ? ( +
+

+ {searchQuery + ? 'No connections match your search' + : 'No connections yet'} +

+
+ ) : ( +
+ {filteredContacts.map((contact) => ( + + ))} +
+ )} +
+
+
+ ); +} diff --git a/src/components/message/index.ts b/src/components/message/index.ts index b1530fa..eec86ec 100644 --- a/src/components/message/index.ts +++ b/src/components/message/index.ts @@ -2,3 +2,4 @@ export { ChatHeader } from './ChatHeader'; export { MessageInput } from './MessageInput'; export { MessagingPage } from './MessagingPage'; +export { NewConversationModal } from './NewConversationModal'; diff --git a/src/hooks/useMessaging.ts b/src/hooks/useMessaging.ts index 55da78c..ddbcf14 100644 --- a/src/hooks/useMessaging.ts +++ b/src/hooks/useMessaging.ts @@ -34,7 +34,7 @@ interface UseMessagingReturn { disconnect: () => void; loadConversations: () => Promise; selectConversation: (conversationId: string) => Promise; - startConversation: (agentProfileId: string) => Promise; + startConversation: (body: { agentProfileId?: string; userId?: string }) => Promise; sendMessage: (content: string, messageType?: MessageType, fileFields?: { fileUrl?: string; mimeType?: string; fileName?: string; fileSize?: number }) => Promise; loadMoreMessages: () => Promise; startTyping: () => void; @@ -163,10 +163,10 @@ export function useMessaging(options: UseMessagingOptions = {}): UseMessagingRet [isConnected] ); - // Start a new conversation with an agent + // Start a new conversation with an agent (for users) or a user (for agents) const startConversation = useCallback( - async (agentProfileId: string): Promise => { - const conversation = await messagesService.startConversation(agentProfileId); + async (body: { agentProfileId?: string; userId?: string }): Promise => { + const conversation = await messagesService.startConversation(body); // Add to conversations list if not exists setConversations((prev) => { diff --git a/src/services/connection-requests.service.ts b/src/services/connection-requests.service.ts index e899f25..de96158 100644 --- a/src/services/connection-requests.service.ts +++ b/src/services/connection-requests.service.ts @@ -16,6 +16,11 @@ export interface ConnectionRequest { id: string; email: string; avatar: string | null; + userProfile?: { + firstName: string | null; + lastName: string | null; + avatar: string | null; + } | null; }; agentProfile?: { id: string; diff --git a/src/services/messages.service.ts b/src/services/messages.service.ts index 296b149..5d1b594 100644 --- a/src/services/messages.service.ts +++ b/src/services/messages.service.ts @@ -22,12 +22,10 @@ class MessagesService { } /** - * Start or get existing conversation with an agent + * Start or get existing conversation with an agent (for users) or a user (for agents) */ - async startConversation(agentProfileId: string): Promise { - const response = await api.post>('/messages/conversations', { - agentProfileId, - }); + async startConversation(body: { agentProfileId?: string; userId?: string }): Promise { + const response = await api.post>('/messages/conversations', body); return response.data.data; }