import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import { useQuery } from '@apollo/client/react'; import { useState, useRef, useEffect } from 'react'; import { GET_CHAT_SUMMARY_QUERY } from '../api/operations/queries'; import { useChannel, useStreamChat } from '../context/StreamChatContext'; import type { StreamMessage } from '../context/StreamChatContext'; import { LoadingPage } from '../components/ui/Loading'; // Proxy cloudinary images through our server to avoid CORS function proxyImageUrl(url: string | undefined): string | undefined { if (!url) return undefined; if (url.includes('cloudinary.com')) { return url.replace('https://res.cloudinary.com', '/api/images'); } if (url.includes('fldcdn.com')) { return url.replace('https://prod.fldcdn.com', '/api/fldcdn'); } return url; } function formatTime(dateString: string | undefined): string { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); } function formatDate(dateString: string | undefined): string { if (!dateString) return ''; const date = new Date(dateString); 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: 'short', month: 'short', day: 'numeric' }); } } interface MessageBubbleProps { message: StreamMessage; isOwn: boolean; showDate?: boolean; showAvatar?: boolean; isFirstInGroup?: boolean; isLastInGroup?: boolean; } function MessageBubble({ message, isOwn, showDate, showAvatar, isFirstInGroup, isLastInGroup }: MessageBubbleProps) { const [imageLoaded, setImageLoaded] = useState(false); const [imageError, setImageError] = useState(false); // Get border radius based on position in group const getBorderRadius = () => { if (isOwn) { if (isFirstInGroup && isLastInGroup) return 'rounded-[20px] rounded-br-md'; if (isFirstInGroup) return 'rounded-[20px] rounded-br-md rounded-br-md'; if (isLastInGroup) return 'rounded-[20px] rounded-tr-md rounded-br-md'; return 'rounded-[20px] rounded-r-md'; } else { if (isFirstInGroup && isLastInGroup) return 'rounded-[20px] rounded-bl-md'; if (isFirstInGroup) return 'rounded-[20px] rounded-bl-md'; if (isLastInGroup) return 'rounded-[20px] rounded-tl-md rounded-bl-md'; return 'rounded-[20px] rounded-l-md'; } }; return ( <> {showDate && (
{formatDate(message.created_at)}
)}
{/* Avatar placeholder for alignment */} {!isOwn && (
{showAvatar && message.user?.image && ( )}
)}
{/* Attachments */} {message.attachments?.map((att, i) => { const imageUrl = proxyImageUrl(att.image_url || att.thumb_url); if (!imageUrl) return null; return (
{!imageLoaded && !imageError && (
)} {imageError ? (
Image unavailable
) : ( attachment setImageLoaded(true)} onError={() => setImageError(true)} /> )}
); })} {/* Message text */} {message.text && (

{message.text}

)}
{/* Timestamp - only show for last message in group */} {isLastInGroup && (

{formatTime(message.created_at)}

)}
); } export function ChatPage() { const { channelId } = useParams<{ channelId: string }>(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); const chatName = searchParams.get('name') || 'Chat'; const chatAvatar = searchParams.get('avatar') || undefined; const [inputText, setInputText] = useState(''); const [isSending, setIsSending] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); // Stream Chat connection const { isConnected, isConnecting: streamConnecting, error: streamError, userId } = useStreamChat(); // Channel and messages const { messages, loading: channelLoading, error: channelError, sendMessage, } = useChannel(channelId || ''); // Get chat summary for additional info const { data: summaryData, loading: summaryLoading } = useQuery(GET_CHAT_SUMMARY_QUERY, { variables: { streamChatId: channelId }, skip: !channelId, }); const summary = summaryData?.summary; const displayName = summary?.name || chatName; const displayAvatar = proxyImageUrl(summary?.avatarSet?.[0] || chatAvatar); // Scroll to bottom on new messages useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); const handleSend = async () => { if (!inputText.trim() || isSending) return; try { setIsSending(true); await sendMessage(inputText.trim()); setInputText(''); inputRef.current?.focus(); } catch (err) { console.error('Failed to send message:', err); } finally { setIsSending(false); } }; const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; // Loading states if (summaryLoading || streamConnecting) { return ; } // Stream connection error if (streamError || channelError) { return (

Connection Lost

{streamError || channelError}

); } // Process messages for grouping let lastDate = ''; let lastUserId = ''; const processedMessages = messages.map((msg, index) => { const msgDate = msg.created_at ? formatDate(msg.created_at) : ''; const showDate = msgDate !== lastDate; lastDate = msgDate; const currentUserId = msg.user?.id || ''; const nextMsg = messages[index + 1]; const nextUserId = nextMsg?.user?.id || ''; const isFirstInGroup = showDate || currentUserId !== lastUserId; const isLastInGroup = !nextMsg || nextUserId !== currentUserId || (nextMsg.created_at && msg.created_at && new Date(nextMsg.created_at).getTime() - new Date(msg.created_at).getTime() > 60000); lastUserId = currentUserId; return { ...msg, showDate, showAvatar: isLastInGroup && msg.user?.id !== userId, isFirstInGroup, isLastInGroup, }; }); return (
{/* Header */}
{/* Avatar with glow effect */}
{displayAvatar ? ( ) : (
{displayName.charAt(0)}
)} {/* Online indicator */}

{displayName}

{summary?.targetProfile && (

{summary.targetProfile.age} ยท {summary.targetProfile.gender?.toLowerCase()}

)}
{/* More options */}
{/* Messages area */}
{channelLoading ? (

Loading messages...

) : messages.length === 0 ? (

Start the conversation

Send a message to begin chatting with {displayName}

) : ( <> {processedMessages.map((msg) => ( ))} )}
{/* Input area */}
{/* Attachment button */} {/* Input field */}
setInputText(e.target.value)} onKeyDown={handleKeyPress} placeholder="Type a message..." disabled={!isConnected} className=" w-full px-5 py-3 bg-white/[0.05] border border-white/[0.08] rounded-full text-[var(--color-text-primary)] text-[15px] placeholder-[var(--color-text-muted)]/50 focus:outline-none focus:border-[var(--color-primary)]/40 focus:bg-white/[0.07] transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed " />
{/* Send button */}
); }