import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Send, Image as ImageIcon, Loader2, AlertCircle, CheckCheck, Clock, Bot, } from 'lucide-react'; import { useAuth } from '@/contexts/AuthContext'; import { toast } from 'sonner'; import { supabase } from '@/lib/supabase'; interface Message { id: string; trade_id: string; sender_id: string; message: string; message_type: 'text' | 'image' | 'system'; attachment_url?: string; is_read: boolean; created_at: string; } interface TradeChatProps { tradeId: string; counterpartyId: string; counterpartyWallet: string; isTradeActive: boolean; } export function TradeChat({ tradeId, counterpartyId, counterpartyWallet, isTradeActive, }: TradeChatProps) { const { user } = useAuth(); const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(''); const [loading, setLoading] = useState(true); const [sending, setSending] = useState(false); const [uploading, setUploading] = useState(false); const scrollRef = useRef(null); const fileInputRef = useRef(null); // Scroll to bottom const scrollToBottom = useCallback(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, []); // Fetch messages const fetchMessages = useCallback(async () => { try { const { data, error } = await supabase .from('p2p_messages') .select('*') .eq('trade_id', tradeId) .order('created_at', { ascending: true }); if (error) throw error; setMessages(data || []); // Mark messages as read if (user && data && data.length > 0) { const unreadIds = data .filter(m => m.sender_id !== user.id && !m.is_read) .map(m => m.id); if (unreadIds.length > 0) { await supabase .from('p2p_messages') .update({ is_read: true }) .in('id', unreadIds); } } } catch (error) { console.error('Fetch messages error:', error); } finally { setLoading(false); } }, [tradeId, user]); // Initial fetch useEffect(() => { fetchMessages(); }, [fetchMessages]); // Real-time subscription useEffect(() => { const channel = supabase .channel(`chat-${tradeId}`) .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'p2p_messages', filter: `trade_id=eq.${tradeId}`, }, (payload) => { const newMsg = payload.new as Message; setMessages(prev => { // Avoid duplicates if (prev.some(m => m.id === newMsg.id)) return prev; return [...prev, newMsg]; }); // Mark as read if from counterparty if (user && newMsg.sender_id !== user.id) { supabase .from('p2p_messages') .update({ is_read: true }) .eq('id', newMsg.id); } } ) .subscribe(); return () => { supabase.removeChannel(channel); }; }, [tradeId, user]); // Scroll to bottom on new messages useEffect(() => { scrollToBottom(); }, [messages, scrollToBottom]); // Send message const handleSendMessage = async () => { if (!newMessage.trim() || !user || sending) return; const messageText = newMessage.trim(); setNewMessage(''); setSending(true); try { const { error } = await supabase.from('p2p_messages').insert({ trade_id: tradeId, sender_id: user.id, message: messageText, message_type: 'text', is_read: false, }); if (error) throw error; // Create notification for counterparty await supabase.from('p2p_notifications').insert({ user_id: counterpartyId, type: 'new_message', title: 'New Message', message: messageText.slice(0, 100), reference_type: 'trade', reference_id: tradeId, is_read: false, }); } catch (error) { console.error('Send message error:', error); toast.error('Failed to send message'); setNewMessage(messageText); // Restore message } finally { setSending(false); } }; // Handle Enter key const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; // Upload image const handleImageUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !user) return; // Validate file if (!file.type.startsWith('image/')) { toast.error('Please select an image file'); return; } if (file.size > 5 * 1024 * 1024) { toast.error('Image must be less than 5MB'); return; } setUploading(true); try { // Upload to Supabase Storage const fileName = `${tradeId}/${Date.now()}-${file.name}`; const { data: uploadData, error: uploadError } = await supabase.storage .from('p2p-chat-images') .upload(fileName, file); if (uploadError) throw uploadError; // Get public URL const { data: urlData } = supabase.storage .from('p2p-chat-images') .getPublicUrl(uploadData.path); // Insert message with image const { error: msgError } = await supabase.from('p2p_messages').insert({ trade_id: tradeId, sender_id: user.id, message: 'Sent an image', message_type: 'image', attachment_url: urlData.publicUrl, is_read: false, }); if (msgError) throw msgError; // Create notification await supabase.from('p2p_notifications').insert({ user_id: counterpartyId, type: 'new_message', title: 'New Image', message: 'Sent an image', reference_type: 'trade', reference_id: tradeId, is_read: false, }); toast.success('Image sent'); } catch (error) { console.error('Upload image error:', error); toast.error('Failed to upload image'); } finally { setUploading(false); if (fileInputRef.current) { fileInputRef.current.value = ''; } } }; // Format timestamp const formatTime = (dateString: string) => { const date = new Date(dateString); return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; // Render message const renderMessage = (message: Message) => { const isOwn = message.sender_id === user?.id; const isSystem = message.message_type === 'system'; if (isSystem) { return (
{message.message}
); } return (
{!isOwn && ( {counterpartyWallet.slice(0, 2).toUpperCase()} )}
{message.message_type === 'image' && message.attachment_url ? ( Shared image ) : (

{message.message}

)}
{formatTime(message.created_at)} {isOwn && ( message.is_read ? ( ) : ( ) )}
); }; return ( Chat {messages.filter(m => m.sender_id !== user?.id && !m.is_read).length > 0 && ( {messages.filter(m => m.sender_id !== user?.id && !m.is_read).length} )} {/* Messages */} {loading ? (
) : messages.length === 0 ? (

No messages yet

Start the conversation

) : ( messages.map(renderMessage) )}
{/* Input */} {isTradeActive ? (
setNewMessage(e.target.value)} onKeyPress={handleKeyPress} placeholder="Type a message..." disabled={sending} className="flex-1 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500" />
) : (

Chat is disabled for completed/cancelled trades

)}
); }