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
) : (

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 ? (
) : 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 */}
);
}