import React, { useState, useRef, useEffect, useCallback } from 'react'; // Types export interface ChatUser { id: string; name: string; avatar?: string; status?: 'online' | 'offline' | 'away' | 'busy'; lastSeen?: Date; } export interface ChatMessage { id: string; senderId: string; content: string; timestamp: Date; type?: 'text' | 'image' | 'file' | 'system'; fileUrl?: string; fileName?: string; isRead?: boolean; } export interface ChatConversation { id: string; participants: ChatUser[]; lastMessage?: ChatMessage; unreadCount?: number; isGroup?: boolean; groupName?: string; groupAvatar?: string; } // Chat Layout export interface ChatLayoutProps { /** Sidebar content (conversations list) */ sidebar?: React.ReactNode; /** Main chat area */ children: React.ReactNode; /** Show sidebar on mobile */ showSidebar?: boolean; /** Toggle sidebar handler */ onToggleSidebar?: () => void; /** Additional CSS classes */ className?: string; } export const ChatLayout: React.FC = ({ sidebar, children, showSidebar = true, onToggleSidebar, className = '', }) => { return (
{sidebar}
{onToggleSidebar && ( )} {children}
); }; // Chat Sidebar / Conversations List export interface ChatConversationsListProps { /** Conversations to display */ conversations: ChatConversation[]; /** Current user */ currentUser?: ChatUser; /** Active conversation ID */ activeId?: string; /** Conversation click handler */ onConversationClick?: (conversation: ChatConversation) => void; /** Search value */ searchValue?: string; /** Search change handler */ onSearchChange?: (value: string) => void; /** New chat handler */ onNewChat?: () => void; /** Additional CSS classes */ className?: string; } export const ChatConversationsList: React.FC = ({ conversations, currentUser, activeId, onConversationClick, searchValue = '', onSearchChange, onNewChat, className = '', }) => { const getConversationName = (conversation: ChatConversation) => { if (conversation.isGroup && conversation.groupName) { return conversation.groupName; } const otherParticipant = conversation.participants.find( (p) => p.id !== currentUser?.id ); return otherParticipant?.name || 'Unknown'; }; const getConversationAvatar = (conversation: ChatConversation) => { if (conversation.isGroup && conversation.groupAvatar) { return conversation.groupAvatar; } const otherParticipant = conversation.participants.find( (p) => p.id !== currentUser?.id ); return otherParticipant?.avatar; }; const getConversationStatus = (conversation: ChatConversation) => { if (conversation.isGroup) return undefined; const otherParticipant = conversation.participants.find( (p) => p.id !== currentUser?.id ); return otherParticipant?.status; }; const formatTime = (date?: Date) => { if (!date) return ''; const now = new Date(); const diff = now.getTime() - date.getTime(); const days = Math.floor(diff / (1000 * 60 * 60 * 24)); if (days === 0) { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } else if (days === 1) { return 'Yesterday'; } else if (days < 7) { return date.toLocaleDateString([], { weekday: 'short' }); } else { return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); } }; const filteredConversations = conversations.filter((conv) => { if (!searchValue) return true; const name = getConversationName(conv).toLowerCase(); return name.includes(searchValue.toLowerCase()); }); return (
{currentUser && (
{currentUser.avatar ? ( {currentUser.name} ) : ( {currentUser.name.charAt(0).toUpperCase()} )}
{currentUser.name} {currentUser.status || 'Offline'}
{onNewChat && ( )}
)} {onSearchChange && (
onSearchChange(e.target.value)} />
)}
{filteredConversations.map((conversation) => { const name = getConversationName(conversation); const avatar = getConversationAvatar(conversation); const status = getConversationStatus(conversation); return (
onConversationClick?.(conversation)} >
{avatar ? ( {name} ) : ( {name.charAt(0).toUpperCase()} )} {status && ( )}
{name} {conversation.lastMessage && ( {formatTime(conversation.lastMessage.timestamp)} )}
{conversation.lastMessage && (
{conversation.lastMessage.content}
)}
{conversation.unreadCount && conversation.unreadCount > 0 && ( {conversation.unreadCount > 99 ? '99+' : conversation.unreadCount} )}
); })} {filteredConversations.length === 0 && (
No conversations found
)}
); }; // Chat Window export interface ChatWindowProps { /** Messages to display */ messages: ChatMessage[]; /** Current user ID */ currentUserId: string; /** Participants map (id -> user) */ participants: Record; /** Conversation info */ conversation?: ChatConversation; /** Loading state */ loading?: boolean; /** Typing indicator users */ typingUsers?: ChatUser[]; /** Message input value */ inputValue?: string; /** Input change handler */ onInputChange?: (value: string) => void; /** Send message handler */ onSendMessage?: (content: string) => void; /** Back button handler */ onBack?: () => void; /** Info button handler */ onInfo?: () => void; /** Load more handler (infinite scroll) */ onLoadMore?: () => void; /** Additional CSS classes */ className?: string; } export const ChatWindow: React.FC = ({ messages, currentUserId, participants, conversation, loading = false, typingUsers = [], inputValue = '', onInputChange, onSendMessage, onBack, onInfo, onLoadMore, className = '', }) => { const messagesEndRef = useRef(null); const messagesContainerRef = useRef(null); const [localInput, setLocalInput] = useState(inputValue); const scrollToBottom = useCallback(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, []); useEffect(() => { scrollToBottom(); }, [messages, scrollToBottom]); useEffect(() => { setLocalInput(inputValue); }, [inputValue]); const handleInputChange = (value: string) => { setLocalInput(value); onInputChange?.(value); }; const handleSend = () => { if (localInput.trim()) { onSendMessage?.(localInput.trim()); setLocalInput(''); onInputChange?.(''); } }; const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const formatTime = (date: Date) => { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; const formatDate = (date: Date) => { const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); if (date.toDateString() === today.toDateString()) { return 'Today'; } else if (date.toDateString() === yesterday.toDateString()) { return 'Yesterday'; } else { return date.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric', }); } }; const getOtherParticipant = () => { if (!conversation) return null; return conversation.participants.find((p) => p.id !== currentUserId); }; const otherUser = getOtherParticipant(); // Group messages by date const groupedMessages: { date: string; messages: ChatMessage[] }[] = []; let currentDate = ''; messages.forEach((msg) => { const dateStr = msg.timestamp.toDateString(); if (dateStr !== currentDate) { currentDate = dateStr; groupedMessages.push({ date: formatDate(msg.timestamp), messages: [] }); } groupedMessages[groupedMessages.length - 1].messages.push(msg); }); return (
{onBack && ( )} {conversation && (
{conversation.isGroup ? ( conversation.groupAvatar ? ( {conversation.groupName} ) : ( {conversation.groupName?.charAt(0).toUpperCase()} ) ) : otherUser?.avatar ? ( {otherUser.name} ) : ( {otherUser?.name.charAt(0).toUpperCase()} )} {!conversation.isGroup && otherUser?.status && ( )}
{conversation.isGroup ? conversation.groupName : otherUser?.name} {conversation.isGroup ? `${conversation.participants.length} participants` : otherUser?.status === 'online' ? 'Online' : otherUser?.lastSeen ? `Last seen ${formatTime(otherUser.lastSeen)}` : 'Offline'}
)}
{onInfo && ( )}
{loading && (
)} {onLoadMore && messages.length > 0 && ( )} {groupedMessages.map((group, groupIndex) => (
{group.date}
{group.messages.map((message, msgIndex) => { const isOwn = message.senderId === currentUserId; const sender = participants[message.senderId]; const showAvatar = !isOwn && (msgIndex === 0 || group.messages[msgIndex - 1]?.senderId !== message.senderId); return (
{!isOwn && showAvatar && (
{sender?.avatar ? ( {sender.name} ) : ( {sender?.name?.charAt(0).toUpperCase() || '?'} )}
)} {!isOwn && !showAvatar &&
}
{!isOwn && showAvatar && conversation?.isGroup && ( {sender?.name} )} {message.type === 'image' && message.fileUrl && ( Shared image )} {message.type === 'file' && message.fileUrl && ( {message.fileName || 'File'} )} {(message.type === 'text' || !message.type) && (
{message.content}
)} {message.type === 'system' && (
{message.content}
)} {formatTime(message.timestamp)} {isOwn && message.isRead && ( )}
); })}
))} {typingUsers.length > 0 && (
{typingUsers.map((u) => u.name).join(', ')} {typingUsers.length === 1 ? 'is' : 'are'} typing...
)}