T.ME/BIBIL_0DAY
CasperSecurity


Server : Apache/2
System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64
User : gositeme ( 1004)
PHP Version : 8.2.29
Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
Directory :  /home/gositeme/domains/lavocat.ca/private_html/src/components/Chat/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/gositeme/domains/lavocat.ca/private_html/src/components/Chat/PrivateChat.tsx
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useSession } from 'next-auth/react';
import { motion, AnimatePresence } from 'framer-motion';
import { format, formatDistanceToNow } from 'date-fns';
import { useWebSocket } from '../../context/StableWebSocketContext';
import DocumentViewer from '../DocumentViewer';
import FileUpload from '../ui/FileUpload';
import { toast } from 'react-hot-toast';
// import VideoCall from './VideoCall'; // ✅ DISABLED - Using global SimpleVideoCall
import Peer from 'simple-peer';
import UserAvatar from '../UserAvatar';
import ProfilePopover from '../ProfilePopover';

interface PrivateMessage {
  id: string;
  content: string;
  type: string;
  fileUrl?: string;
  fileName?: string;
  fileSize?: number;
  mimeType?: string;
  createdAt: string;
  sender: {
    id: string;
    name: string;
    email: string;
    role: string;
    profilePicture?: string;
    title?: string;
    specialization?: string;
    availability?: string;
    lastActive?: string;
    bio?: string;
  };
}

interface PrivateChat {
  id: string;
  registrationId: string;
  userId: string;
  adminId?: string;
  messages: PrivateMessage[];
  registration: {
    id: string;
    firstName: string;
    lastName: string;
    email: string;
    userId: string;
    documents?: {
      id: string;
      name: string;
      url: string;
      type: string;
    }[];
  };
}

interface PrivateChatProps {
  registrationId: string;
  onClose: () => void;
}

const PrivateChat = ({ registrationId, onClose }: PrivateChatProps) => {
  console.log('[PrivateChat] 🚀 Component mounted with registrationId:', registrationId);
  
  const { data: session } = useSession();
  const { ws, connected } = useWebSocket();
  const [chat, setChat] = useState<PrivateChat | null>(null);
  const [newMessage, setNewMessage] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState('');
  const [isTyping, setIsTyping] = useState(false);
  const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());
  const [showFileUpload, setShowFileUpload] = useState(false);
  const [showDocumentPicker, setShowDocumentPicker] = useState(false);
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [selectedDocument, setSelectedDocument] = useState<{
    url: string;
    type: string;
    name: string;
  } | null>(null);
  const [isUploading, setIsUploading] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);
  const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  
  // Video Call State
  const [localStream, setLocalStream] = useState<MediaStream | null>(null);
  const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null);
  const [callInProgress, setCallInProgress] = useState(false);
  const [incomingCall, setIncomingCall] = useState<{ senderId: string; signal: any; } | null>(null);
  const peerRef = useRef<Peer.Instance | null>(null);
  
  // Profile popover state
  const [profilePopover, setProfilePopover] = useState<{ userId: string; isOpen: boolean } | null>(null);

  const getRecipientId = () => {
    if (!chat || !session) return null;
    return session.user.role === 'ADMIN' ? chat.registration.userId : chat.adminId;
  };

  // Request notification permission on component mount
  useEffect(() => {
    if ('Notification' in window && Notification.permission === 'default') {
      Notification.requestPermission().then(permission => {
        console.log('[PrivateChat] Notification permission:', permission);
      });
    }
  }, []);

  // WebSocket message handling
  useEffect(() => {
    if (!ws) return;

    const handleMessage = (event: MessageEvent) => {
      try {
        const message = JSON.parse(event.data);
        console.log('Private chat WebSocket message received:', message);
        
        switch (message.type) {
          case 'PRIVATE_MESSAGE':
            if (message.data.registrationId === registrationId) {
              // Prevent duplicate messages from WebSocket for messages sent by current user
              const isOwnMessage = message.data.message.sender.id === session?.user?.id;
              const messageExists = chat?.messages.some(msg => msg.id === message.data.message.id);
              
              if (!isOwnMessage && !messageExists) {
                setChat(prev => prev ? { ...prev, messages: [...prev.messages, message.data.message] } : null);
              }
              
              // Show notification for incoming messages from other users
              if (!isOwnMessage) {
                const isTabVisible = !document.hidden;
                
                // Show notification if tab is not visible or focused
                if (!isTabVisible) {
                  // Show toast notification
                  toast((t) => (
                    <div className="flex items-center space-x-3">
                      <div className="flex-shrink-0">
                        <div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
                          <span className="text-white text-sm font-bold">
                            {message.data.message.sender.name?.charAt(0) || 'U'}
                          </span>
                        </div>
                      </div>
                      <div className="flex-1">
                        <p className="font-medium text-gray-900">
                          💬 {message.data.message.sender.name || 'User'}
                        </p>
                        <p className="text-sm text-gray-600 truncate max-w-48">
                          {message.data.message.content}
                        </p>
                      </div>
                    </div>
                  ), {
                    duration: 4000,
                    position: 'top-right',
                    style: {
                      maxWidth: '350px',
                    },
                  });

                  // Show browser notification
                  if (Notification.permission === 'granted') {
                    new Notification(`New message from ${message.data.message.sender.name || 'User'}`, {
                      body: message.data.message.content,
                      icon: '/icons/apple-touch-icon-180x180.png'
                    });
                  }

                  console.log(`[PrivateChat] 🔔 Notification shown for message from ${message.data.message.sender.name}`);
                }
              }
            }
            break;
          case 'TYPING':
            if (message.data.registrationId === registrationId) {
              setTypingUsers(prev => {
                const newSet = new Set(prev);
                if (message.data.isTyping) newSet.add(message.data.userName);
                else newSet.delete(message.data.userName);
                return newSet;
              });
            }
            break;
          
          // WebRTC Signaling
          case 'webrtc-offer':
            setIncomingCall({ senderId: message.senderId, signal: message.data.signal });
            break;
          case 'webrtc-answer':
            if (peerRef.current) {
              peerRef.current.signal(message.data.signal);
            }
            break;
          case 'webrtc-ice-candidate':
            if (peerRef.current) {
              peerRef.current.signal(message.data.candidate);
            }
            break;
          case 'webrtc-end-call':
            endCallCleanup();
            break;
        }
      } catch (error) {
        console.error('Error parsing private chat WebSocket message:', error);
      }
    };

    ws.addEventListener('message', handleMessage);
    return () => ws.removeEventListener('message', handleMessage);
  }, [ws, registrationId, session?.user?.id]);

  const fetchChat = async () => {
    try {
      setIsLoading(true);
      const response = await fetch(`/api/chat/private/${registrationId}/messages`, {
        method: 'GET',
        credentials: 'same-origin',
      });
      if (!response.ok) throw new Error('Failed to fetch chat');
      const data = await response.json();
      setChat(data);
      scrollToBottom();
    } catch (err) {
      setError('Failed to load chat');
    } finally {
      setIsLoading(false);
    }
  };

  useEffect(() => {
    console.log('[PrivateChat] 🔄 registrationId effect triggered:', registrationId);
    if (registrationId) {
      console.log('[PrivateChat] 📞 Fetching chat for registration:', registrationId);
      fetchChat();
    }
  }, [registrationId]);

  // Handle typing
  const handleTyping = useCallback((isTyping: boolean) => {
    if (!ws || !connected || !session?.user?.name) return;

    setIsTyping(isTyping);
    
    try {
      ws.send(JSON.stringify({
        type: 'TYPING',
        data: {
          registrationId,
          userName: session.user.name,
          isTyping
        }
      }));
    } catch (error) {
      console.error('Failed to send typing status:', error);
    }

    // Clear typing indicator after delay
    if (typingTimeoutRef.current) {
      clearTimeout(typingTimeoutRef.current);
    }
    
    if (isTyping) {
      typingTimeoutRef.current = setTimeout(() => {
        handleTyping(false);
      }, 3000);
    }
  }, [ws, connected, registrationId]);

  const sendMessage = async () => {
    if (!newMessage.trim() || !chat || !session?.user) return;

    const messageContent = newMessage.trim();
    setNewMessage('');
    handleTyping(false);

    // Create optimistic message
    const optimisticMessage: PrivateMessage = {
      id: `temp-${Date.now()}`,
      content: messageContent,
      type: 'TEXT',
      createdAt: new Date().toISOString(),
      sender: {
        id: session.user.id,
        name: session.user.name || 'Unknown',
        email: session.user.email || '',
        role: session.user.role || 'USER'
      }
    };

    // Add to messages immediately (optimistic update)
    setChat(prev => prev ? {
      ...prev,
      messages: [...prev.messages, optimisticMessage]
    } : null);

    try {
      const response = await fetch(`/api/chat/private/${registrationId}/messages`, {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ content: messageContent })
      });

      if (!response.ok) {
        let errorMsg = 'Failed to send message';
        try {
          const errorData = await response.json();
          if (errorData && (errorData.error || errorData.message)) {
            errorMsg = errorData.error || errorData.message;
          }
        } catch (e) {
          // fallback to text if not JSON
          try {
            const errorText = await response.text();
            if (errorText) errorMsg = errorText;
          } catch {}
        }
        console.error('Send message error:', errorMsg);
        throw new Error(errorMsg);
      }

      const savedMessage = await response.json();
      
      // Replace optimistic message with real one
      setChat(prev => prev ? {
        ...prev,
        messages: prev.messages.map(msg => 
          msg.id === optimisticMessage.id ? savedMessage : msg
        )
      } : null);

      // Note: Server handles WebSocket broadcast after receiving this HTTP POST
      // No need to manually broadcast here to avoid duplication
    } catch (error) {
      console.error('Error sending private message:', error);
      // Remove optimistic message on error
      setChat(prev => prev ? {
        ...prev,
        messages: prev.messages.filter(msg => msg.id !== optimisticMessage.id)
      } : null);
      setError(
        error instanceof Error &&
        typeof error.message === 'string' &&
        error.message.toLowerCase().includes('message limit')
          ? "You’ve reached the message limit. Please wait for an admin to reply before sending more messages."
          : error instanceof Error
            ? error.message
            : 'Failed to send message. Please try again.'
      );
    }
  };

  const handleFileUpload = async () => {
    if (!selectedFile || !chat || !session) return;
    setIsUploading(true);
    
    const formData = new FormData();
    formData.append('file', selectedFile);
    formData.append('userId', session.user.id);
    formData.append('registrationId', registrationId);

    try {
        const response = await fetch('/api/user/upload', {
            method: 'POST',
            credentials: 'same-origin',
            body: formData,
        });

        if (!response.ok) {
            let errorData;
            try {
                errorData = await response.json();
                console.error('Upload error response:', errorData);
            } catch (parseError) {
                console.error('Failed to parse error response:', parseError);
                errorData = {};
            }
            
            console.error('Response status:', response.status);
            console.error('Response status text:', response.statusText);
            console.error('Response headers:', Object.fromEntries(response.headers.entries()));
            
            throw new Error(errorData.message || errorData.error || `File upload failed (${response.status}: ${response.statusText})`);
        }

        const newDocument = await response.json();

        // Optimistically update UI
         const optimisticMessage: PrivateMessage = {
            id: `temp-doc-${Date.now()}`,
            content: `Shared a document: ${newDocument.name}`,
            type: 'DOCUMENT',
            fileUrl: newDocument.url,
            fileName: newDocument.name,
            mimeType: newDocument.type,
            createdAt: new Date().toISOString(),
            sender: {
                id: session.user.id,
                name: session.user.name || 'Unknown',
                email: session.user.email || '',
                role: session.user.role || 'USER',
            },
        };

        setChat(prev => prev ? { ...prev, messages: [...prev.messages, optimisticMessage] } : null);
        setShowFileUpload(false);
        setSelectedFile(null);
        
    } catch (error) {
        console.error('File upload error:', error);
        setError((error as Error).message);
    } finally {
        setIsUploading(false);
    }
  };

  const handleDocumentShare = async (document: { id: string; name: string; url: string; type: string }) => {
    if (!chat || !session) return;
    
    const message = {
        id: `temp-doc-share-${Date.now()}`,
        content: `Shared a document: ${document.name}`,
        type: 'DOCUMENT',
        fileUrl: document.url,
        fileName: document.name,
        mimeType: document.type,
        createdAt: new Date().toISOString(),
        sender: {
          id: session.user.id,
          name: session.user.name || "Unknown",
          email: session.user.email || "",
          role: session.user.role || "USER",
        },
    };

    setChat(prev => prev ? { ...prev, messages: [...prev.messages, message] } : null);
    setShowDocumentPicker(false);
    
    if (ws && ws.readyState === WebSocket.OPEN) {
        try {
            ws.send(JSON.stringify({
                type: 'PRIVATE_MESSAGE',
                data: {
                    registrationId,
                    message,
                }
            }));
        } catch (error) {
            console.error('Error broadcasting shared document:', error);
        }
    }
  };

  const getFileType = (mimeType: string) => {
    if (mimeType.startsWith('image/')) return 'IMAGE';
    if (mimeType.startsWith('video/')) return 'VIDEO';
    if (mimeType.startsWith('audio/')) return 'AUDIO';
    return 'FILE';
  };

  const scrollToBottom = () => {
    if (messagesEndRef.current) {
      messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
    }
  };

  // Scroll to bottom when messages change
  useEffect(() => {
    scrollToBottom();
  }, [chat?.messages]);

  const renderMessage = (message: PrivateMessage, index: number) => {
    const isSender = message.sender.id === session?.user?.id;
    const prevMessage = chat?.messages[index - 1];
    const showAvatar = !isSender && (!prevMessage || prevMessage.sender.id !== message.sender.id);
    const isGroupStart = !prevMessage || prevMessage.sender.id !== message.sender.id;
    const userName = message.sender?.name || 'Unknown User';
    const messageTime = format(new Date(message.createdAt), 'HH:mm');

    return (
      <div key={message.id} className={`flex items-start gap-3 ${isGroupStart ? 'mt-4' : 'mt-1'}`}>
        <div className="w-10 flex-shrink-0">
          {showAvatar && (
            <div className="relative">
              <UserAvatar 
                user={message.sender}
                size="sm"
                showStatus={true}
                clickable={true}
                onClick={() => setProfilePopover({ 
                  userId: message.sender.id, 
                  isOpen: profilePopover?.userId !== message.sender.id || !profilePopover?.isOpen 
                })}
              />
              {profilePopover?.userId === message.sender.id && profilePopover?.isOpen && (
                <ProfilePopover
                  userId={message.sender.id}
                  isOpen={true}
                  onClose={() => setProfilePopover(null)}
                  position="right"
                />
              )}
            </div>
          )}
        </div>
        <div className={`max-w-lg ${isSender ? 'ml-auto' : ''}`}>
          {isGroupStart && (
            <div className={`flex items-center gap-2 mb-1 ${isSender ? 'justify-end' : 'justify-start'}`}>
              <p className={`text-xs font-medium ${isSender ? 'text-blue-600 dark:text-blue-400' : 'text-gray-600 dark:text-gray-400'}`}>
                {isSender ? 'You' : (
                  <span className="flex items-center gap-1">
                    {message.sender.role === 'ADMIN' ? '⚖️ ' : ''}{userName}
                    {message.sender.title && (
                      <span className="text-gray-500 dark:text-gray-400">• {message.sender.title}</span>
                    )}
                  </span>
                )}
              </p>
              <span className="text-xs text-gray-400">{messageTime}</span>
            </div>
          )}
          <div className={`px-4 py-2 rounded-2xl inline-block shadow-sm ${
            isSender 
              ? 'bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-br-none' 
              : 'bg-white text-gray-800 border border-gray-200 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200 rounded-bl-none'
          }`}>
            {message.type === 'FILE' || message.type === 'IMAGE' || message.type === 'VIDEO' || message.type === 'AUDIO' ? (
              <div className="space-y-2">
                <p className="text-sm">{message.content}</p>
                {/* Enhanced image preview for IMAGE type */}
                {message.type === 'IMAGE' && message.fileUrl ? (
                  <div 
                    className="relative rounded-lg overflow-hidden cursor-pointer hover:opacity-90 transition-opacity"
                    onClick={() => setSelectedDocument({
                      url: message.fileUrl!,
                      type: message.mimeType!,
                      name: message.fileName!
                    })}
                  >
                    <img
                      src={message.fileUrl}
                      alt={message.fileName}
                      className="max-w-full max-h-64 object-contain rounded-lg shadow-md"
                      onError={(e) => {
                        // Fallback to file display if image fails to load
                        e.currentTarget.style.display = 'none';
                        (e.currentTarget.nextElementSibling as HTMLElement).style.display = 'block';
                      }}
                    />
                    {/* Fallback file display (hidden by default) */}
                    <div className="bg-gray-100 dark:bg-gray-600 rounded-lg p-3" style={{ display: 'none' }}>
                      <div className="flex items-center space-x-2">
                        <svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
                        </svg>
                        <div className="flex-1 min-w-0">
                          <p className="text-sm font-medium truncate">{message.fileName}</p>
                          <p className="text-xs text-gray-500">
                            {message.fileSize ? `${(message.fileSize / 1024).toFixed(1)} KB` : ''} • Image
                          </p>
                        </div>
                        <span className="text-blue-500 hover:text-blue-700 text-sm font-medium">
                          View
                        </span>
                      </div>
                    </div>
                    {/* Click overlay indicator */}
                    <div className="absolute inset-0 bg-black bg-opacity-0 hover:bg-opacity-10 transition-all duration-200 flex items-center justify-center">
                      <div className="bg-black bg-opacity-50 text-white px-3 py-1 rounded-full text-sm opacity-0 group-hover:opacity-100 transition-opacity">
                        Click to enlarge
                      </div>
                    </div>
                  </div>
                ) : (
                  /* Regular file display for non-images */
                  <div className="bg-gray-100 dark:bg-gray-600 rounded-lg p-3">
                    <div className="flex items-center space-x-2">
                      <svg className="w-5 h-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
                      </svg>
                      <div className="flex-1 min-w-0">
                        <p className="text-sm font-medium truncate">{message.fileName}</p>
                        <p className="text-xs text-gray-500">
                          {message.fileSize ? `${(message.fileSize / 1024).toFixed(1)} KB` : ''} • {message.type}
                        </p>
                      </div>
                      <button
                        onClick={() => setSelectedDocument({
                          url: message.fileUrl!,
                          type: message.mimeType!,
                          name: message.fileName!
                        })}
                        className="text-blue-500 hover:text-blue-700 text-sm font-medium"
                      >
                        View
                      </button>
                    </div>
                  </div>
                )}
              </div>
            ) : (
              <p className="text-sm">{message.content}</p>
            )}
          </div>
          {message.id.startsWith('temp-') && (
            <p className="text-xs text-right text-gray-400 mt-1 flex items-center justify-end gap-1">
              <div className="w-3 h-3 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
              Sending...
            </p>
          )}
        </div>
      </div>
    );
  };

  const createPeer = (recipientId: string, stream: MediaStream, initiator: boolean) => {
    const peer = new Peer({
      initiator,
      trickle: true,
      stream,
    });

    peer.on('signal', (signal) => {
      const type = initiator ? 'webrtc-offer' : 'webrtc-answer';
      if (ws?.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type, data: { recipientId, signal } }));
      }
    });

    peer.on('stream', (remoteStream) => {
      setRemoteStream(remoteStream);
    });
    
    peer.on('icecandidate', (candidate) => {
        if (ws?.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify({ type: 'webrtc-ice-candidate', data: { recipientId, candidate } }));
        }
    });

    peer.on('close', () => endCallCleanup());
    peer.on('error', (err) => {
      console.error('Peer error:', err);
      endCallCleanup();
    });

    return peer;
  };
  
  const startCall = async () => {
    const recipientId = getRecipientId();
    if (!recipientId || !ws) return;

    setCallInProgress(true);
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
      setLocalStream(stream);
      peerRef.current = createPeer(recipientId, stream, true);
    } catch (err) {
      console.error('Failed to get user media', err);
      setCallInProgress(false);
    }
  };

  const answerCall = async () => {
    if (!incomingCall || !ws) return;

    setCallInProgress(true);
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
      setLocalStream(stream);
      peerRef.current = createPeer(incomingCall.senderId, stream, false);
      peerRef.current.signal(incomingCall.signal);
      setIncomingCall(null);
    } catch (err) {
      console.error('Failed to get user media for answering call', err);
      setCallInProgress(false);
    }
  };

  const endCallCleanup = () => {
    if (peerRef.current) {
      peerRef.current.destroy();
      peerRef.current = null;
    }
    if (localStream) {
      localStream.getTracks().forEach(track => track.stop());
      setLocalStream(null);
    }
    setRemoteStream(null);
    setCallInProgress(false);
    setIncomingCall(null);
  };

  const endCall = () => {
    const recipientId = getRecipientId();
    if (ws && recipientId) {
      ws.send(JSON.stringify({ type: 'webrtc-end-call', data: { recipientId } }));
    }
    endCallCleanup();
  };

  if (isLoading) {
    return <div className="text-center p-8">Loading chat...</div>;
  }

  if (error) {
    return <div className="text-center p-8 text-red-500">{error}</div>;
  }

  if (!chat) {
    return <div className="text-center p-8">Chat not found.</div>;
  }

  // ✅ OLD VIDEO CALL DISABLED - Using global SimpleVideoCall now
  if (false) {
    return <div>Old video call disabled</div>;
  }

  if (incomingCall) {
    return (
        <div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
            <div className="bg-white dark:bg-gray-800 p-8 rounded-lg shadow-xl text-center">
                <h2 className="text-2xl font-bold mb-4 text-gray-900 dark:text-white">Incoming Call</h2>
                <p className="mb-6 text-gray-700 dark:text-gray-300">You have an incoming video call.</p>
                <div className="flex justify-center space-x-4">
                    <button onClick={answerCall} className="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-6 rounded-full transition-colors">
                        Accept
                    </button>
                    <button onClick={() => setIncomingCall(null)} className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-6 rounded-full transition-colors">
                        Decline
                    </button>
                </div>
            </div>
        </div>
    )
  }

  return (
    <div className="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center z-50 p-4 font-sans">
      <motion.div
        initial={{ opacity: 0, scale: 0.95 }}
        animate={{ opacity: 1, scale: 1 }}
        exit={{ opacity: 0, scale: 0.95 }}
        className="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-2xl h-[70vh] flex flex-col"
      >
        {/* Header */}
        <div className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 p-4 flex items-center justify-between rounded-t-lg flex-shrink-0">
          <div className="flex items-center gap-3">
            <div className="w-10 h-10 rounded-full bg-gradient-to-br from-green-500 to-teal-600 text-white flex items-center justify-center text-lg font-bold flex-shrink-0">
              {chat.registration.firstName.charAt(0).toUpperCase()}
            </div>
            <div className="min-w-0">
              <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-200 truncate">
                {chat.registration.firstName} {chat.registration.lastName}
              </h3>
              <p className="text-sm text-gray-500 dark:text-gray-400 truncate">{chat.registration.email}</p>
            </div>
          </div>
          <div className="flex items-center space-x-3">
            <div className={`w-2.5 h-2.5 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`} title={connected ? 'Connected' : 'Disconnected'}></div>
            {/* Video call removed for now */}
            <button
              onClick={onClose}
              className="p-1 rounded-full text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
            >
              <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
              </svg>
            </button>
          </div>
        </div>

        {/* Messages */}
        <div className="flex-1 overflow-y-auto p-6 space-y-4">
          {chat.messages.length === 0 ? (
            <div className="flex flex-col items-center justify-center h-full text-center text-gray-500 dark:text-gray-400">
              <div className="w-20 h-20 bg-gray-200 dark:bg-gray-700 rounded-full flex items-center justify-center mb-4">
                <svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
                </svg>
              </div>
              <h3 className="text-xl font-semibold mb-2">No messages yet</h3>
              <p>Start the conversation by sending a message!</p>
            </div>
          ) : (
            chat.messages.map(renderMessage)
          )}
          
          {/* Typing indicator */}
          {typingUsers.size > 0 && (
            <div className="flex justify-start">
              <div className="bg-white dark:bg-gray-700 rounded-2xl px-4 py-2 rounded-bl-none shadow">
                <div className="flex items-center space-x-2">
                  <div className="flex space-x-1">
                    <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
                    <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
                    <div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
                  </div>
                  <span className="text-xs text-gray-500 dark:text-gray-400">
                    {Array.from(typingUsers).join(', ')} is typing...
                  </span>
                </div>
              </div>
            </div>
          )}
          
          <div ref={messagesEndRef} />
        </div>

        {/* Input */}
        <div className="p-4 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
          {error && typeof error === 'string' && error.toLowerCase().includes('message limit') ? (
            <div className="w-full flex items-center justify-center py-6">
              <span className="text-gray-500 text-base font-medium">Waiting for lawyer response…</span>
            </div>
          ) : (
            <div className="flex items-center bg-gray-100 dark:bg-gray-800 rounded-lg px-2 py-1">
              <div className="flex items-center space-x-1">
                <button 
                  onClick={() => setShowFileUpload(true)}
                  className="p-2 rounded-full text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
                  title="Attach File"
                >
                  <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
                  </svg>
                </button>
                {session?.user.role === 'ADMIN' && chat.registration.documents && chat.registration.documents.length > 0 && (
                  <button 
                    onClick={() => setShowDocumentPicker(true)}
                    className="p-2 rounded-full text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
                    title="Share Document"
                  >
                    <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
                    </svg>
                  </button>
                )}
              </div>
              <input
                type="text"
                value={newMessage}
                onChange={(e) => {
                  setNewMessage(e.target.value);
                  if (!isTyping) handleTyping(true);
                }}
                onKeyPress={(e) => {
                  if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    sendMessage();
                  }
                }}
                placeholder="Type a message..."
                className="flex-1 bg-transparent border-none focus:ring-0 text-sm px-3 text-gray-900 dark:text-gray-200 placeholder-gray-500 dark:placeholder-gray-400"
              />
              <button
                onClick={sendMessage}
                disabled={!newMessage.trim()}
                className="p-2 rounded-full text-white bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed transition-colors"
              >
                <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" viewBox="0 0 20 20" fill="currentColor">
                  <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" />
                </svg>
              </button>
            </div>
          )}
        </div>
      </motion.div>

      {/* Document/Image Viewer Modal */}
      <AnimatePresence>
        {selectedDocument && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="fixed inset-0 bg-black bg-opacity-75 z-[60] flex items-center justify-center p-4"
            onClick={() => setSelectedDocument(null)}
          >
            <motion.div
              initial={{ scale: 0.9, opacity: 0 }}
              animate={{ scale: 1, opacity: 1 }}
              exit={{ scale: 0.9, opacity: 0 }}
              className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-7xl max-h-[90vh] w-full overflow-hidden"
              onClick={(e) => e.stopPropagation()}
            >
              <div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
                <h3 className="text-lg font-semibold text-gray-900 dark:text-white truncate">
                  {selectedDocument.name}
                </h3>
                <div className="flex items-center space-x-2">
                  <a
                    href={selectedDocument.url}
                    download={selectedDocument.name}
                    className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
                    title="Download"
                  >
                    <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
                    </svg>
                  </a>
                  <button
                    onClick={() => setSelectedDocument(null)}
                    className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
                    title="Close"
                  >
                    <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
                    </svg>
                  </button>
                </div>
              </div>
              <div className="p-4 max-h-[80vh] overflow-y-auto">
                <DocumentViewer
                  url={selectedDocument.url}
                  type={selectedDocument.type}
                  name={selectedDocument.name}
                />
              </div>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>

      {/* File Upload Modal */}
      <AnimatePresence>
        {showFileUpload && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            className="fixed inset-0 bg-black bg-opacity-50 z-[60] flex items-center justify-center p-4"
            onClick={() => setShowFileUpload(false)}
          >
            <motion.div
              initial={{ scale: 0.95, opacity: 0 }}
              animate={{ scale: 1, opacity: 1 }}
              exit={{ scale: 0.95, opacity: 0 }}
              className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md"
              onClick={(e) => e.stopPropagation()}
            >
              <div className="flex items-center justify-between mb-4">
                <h3 className="text-lg font-semibold text-gray-900 dark:text-white">Upload File</h3>
                <button
                  onClick={() => setShowFileUpload(false)}
                  className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
                >
                  <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
                  </svg>
                </button>
              </div>
              
              <FileUpload
                onFileSelect={setSelectedFile}
                onRemoveFile={() => setSelectedFile(null)}
                selectedFile={selectedFile}
                uploading={isUploading}
                className="mb-4"
              />
              
              {selectedFile && (
                <div className="flex justify-end space-x-3">
                  <button
                    onClick={() => {
                      setShowFileUpload(false);
                      setSelectedFile(null);
                    }}
                    className="px-4 py-2 text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
                  >
                    Cancel
                  </button>
                  <button
                    onClick={handleFileUpload}
                    disabled={isUploading}
                    className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
                  >
                    {isUploading ? 'Uploading...' : 'Send File'}
                  </button>
                </div>
              )}
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
};

export default PrivateChat; 

CasperSecurity Mini