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/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/gositeme/domains/lavocat.ca/private_html/src/components/LiveCaseChat.tsx
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useSession } from 'next-auth/react';
import { motion, AnimatePresence } from 'framer-motion';
import { format } from 'date-fns';
import { 
  MessageSquare, 
  X, 
  Send, 
  Paperclip, 
  Smile, 
  Users, 
  User, 
  Clock, 
  MoreHorizontal,
  Volume2,
  VolumeX,
  Settings,
  Shield,
  Star,
  Heart,
  ThumbsUp,
  AlertCircle,
  CheckCircle,
  Zap,
  Sparkles
} from 'lucide-react';
import { useWebSocket } from '../context/EnhancedWebSocketContext';

interface ChatMessage {
  id: string;
  content: string;
  senderId: string;
  senderName: string;
  senderAvatar?: string;
  senderRole: string;
  timestamp: number;
  type: 'message' | 'system' | 'action';
  isPublic: boolean;
  reactions?: {
    [key: string]: string[]; // reaction -> array of user IDs
  };
}

interface LiveCaseChatProps {
  caseId: string;
  caseTitle: string;
  caseOwner: {
    id: string;
    name: string;
    avatar?: string;
    role: string;
  };
  className?: string;
}

const LiveCaseChat: React.FC<LiveCaseChatProps> = ({ 
  caseId, 
  caseTitle, 
  caseOwner, 
  className = '' 
}) => {
  const { data: session } = useSession();
  const { 
    ws, 
    connected, 
    joinCaseChat, 
    leaveCaseChat, 
    sendCaseTyping,
    userPresence,
    typingUsers
  } = useWebSocket();

  // State
  const [isOpen, setIsOpen] = useState(false);
  const [isMinimized, setIsMinimized] = useState(false);
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [newMessage, setNewMessage] = useState('');
  const [isTyping, setIsTyping] = useState(false);
  const [chatMode, setChatMode] = useState<'public' | 'private'>('public');
  const [isMuted, setIsMuted] = useState(false);
  const [showSettings, setShowSettings] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Refs
  const messagesEndRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLTextAreaElement>(null);
  const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);

  // Auto-join case chat when component mounts
  useEffect(() => {
    if (connected && caseId) {
      joinCaseChat(caseId);
      
      // Add system message
      setMessages(prev => [{
        id: `system-${Date.now()}`,
        content: `Welcome to the live chat for "${caseTitle}"! 👋`,
        senderId: 'system',
        senderName: 'System',
        senderRole: 'system',
        timestamp: Date.now(),
        type: 'system',
        isPublic: true
      }]);
    }

    return () => {
      if (connected && caseId) {
        leaveCaseChat(caseId);
      }
    };
  }, [connected, caseId, caseTitle, joinCaseChat, leaveCaseChat]);

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

    const handleMessage = (event: MessageEvent) => {
      try {
        const message = JSON.parse(event.data);
        
        switch (message.type) {
          case 'CASE_MESSAGE':
            if (message.data.caseId === caseId) {
              setMessages(prev => [...prev, {
                id: message.data.id,
                content: message.data.content,
                senderId: message.data.senderId,
                senderName: message.data.senderName,
                senderAvatar: message.data.senderAvatar,
                senderRole: message.data.senderRole,
                timestamp: message.data.timestamp,
                type: 'message',
                isPublic: message.data.isPublic,
                reactions: message.data.reactions || {}
              }]);
              
              // Play notification sound if not muted
              if (!isMuted && message.data.senderId !== session?.user?.id) {
                playNotificationSound();
              }
            }
            break;

          case 'CASE_TYPING':
            if (message.data.caseId === caseId) {
              // Handle typing indicators
              // This will be managed by the WebSocket context
            }
            break;

          case 'CASE_USER_JOINED':
            if (message.data.caseId === caseId) {
              setMessages(prev => [...prev, {
                id: `join-${Date.now()}`,
                content: `${message.data.userName} joined the chat`,
                senderId: 'system',
                senderName: 'System',
                senderRole: 'system',
                timestamp: Date.now(),
                type: 'system',
                isPublic: true
              }]);
            }
            break;

          case 'CASE_USER_LEFT':
            if (message.data.caseId === caseId) {
              setMessages(prev => [...prev, {
                id: `leave-${Date.now()}`,
                content: `${message.data.userName} left the chat`,
                senderId: 'system',
                senderName: 'System',
                senderRole: 'system',
                timestamp: Date.now(),
                type: 'system',
                isPublic: true
              }]);
            }
            break;
        }
      } catch (error) {
        console.error('Error parsing WebSocket message:', error);
      }
    };

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

  // Auto-scroll to bottom when new messages arrive
  useEffect(() => {
    if (messagesEndRef.current && isOpen) {
      messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
    }
  }, [messages, isOpen]);

  // Typing indicator
  const handleTyping = useCallback((isTyping: boolean) => {
    if (isTyping !== isTyping) {
      setIsTyping(isTyping);
      sendCaseTyping(caseId, isTyping);
    }
  }, [caseId, sendCaseTyping, isTyping]);

  // Send message
  const sendMessage = async () => {
    if (!newMessage.trim() || !session?.user?.id) return;

    const messageData = {
      caseId,
      content: newMessage.trim(),
      senderId: session.user.id,
      senderName: session.user.name || 'Anonymous',
      senderAvatar: session.user.image,
      senderRole: session.user.role || 'USER',
      timestamp: Date.now(),
      isPublic: chatMode === 'public'
    };

    try {
      // Send via WebSocket
      ws?.send(JSON.stringify({
        type: 'CASE_MESSAGE',
        data: messageData
      }));

      // Optimistically add to local state
      setMessages(prev => [...prev, {
        id: `temp-${Date.now()}`,
        content: newMessage.trim(),
        senderId: session.user.id,
        senderName: session.user.name || 'Anonymous',
        senderAvatar: session.user.image || undefined,
        senderRole: session.user.role || 'USER',
        timestamp: Date.now(),
        type: 'message',
        isPublic: chatMode === 'public'
      }]);

      setNewMessage('');
      handleTyping(false);
    } catch (error) {
      console.error('Error sending message:', error);
      setError('Failed to send message');
    }
  };

  // Handle Enter key
  const handleKeyPress = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      sendMessage();
    }
  };

  // Quick actions
  const sendQuickAction = (action: string) => {
    const actionMessages = {
      'interested': 'I\'m interested in this case! 🤔',
      'question': 'I have a question about this case.',
      'support': 'I support this case! ❤️',
      'apply': 'I\'d like to apply for this case! 📝'
    };

    setNewMessage(actionMessages[action as keyof typeof actionMessages] || action);
  };

  // Notification sound
  const playNotificationSound = () => {
    // Create a simple notification sound
    const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
    const oscillator = audioContext.createOscillator();
    const gainNode = audioContext.createGain();
    
    oscillator.connect(gainNode);
    gainNode.connect(audioContext.destination);
    
    oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
    oscillator.frequency.setValueAtTime(600, audioContext.currentTime + 0.1);
    
    gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
    gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
    
    oscillator.start(audioContext.currentTime);
    oscillator.stop(audioContext.currentTime + 0.2);
  };

  // Get online users count
  const getOnlineUsersCount = () => {
    return Array.from(userPresence.values()).filter(user => user.status === 'online').length;
  };

  // Get typing users
  const getTypingUsers = () => {
    const caseTyping = typingUsers.get(`case_${caseId}`) || [];
    return caseTyping.filter(t => t.userId !== session?.user?.id);
  };

  if (!session) {
    return null; // Don't show chat for non-authenticated users
  }

  return (
    <>
      {/* Floating Chat Button */}
      <AnimatePresence>
        {!isOpen && (
          <motion.button
            initial={{ scale: 0, opacity: 0 }}
            animate={{ scale: 1, opacity: 1 }}
            exit={{ scale: 0, opacity: 0 }}
            onClick={() => setIsOpen(true)}
            className={`fixed bottom-6 right-6 z-50 bg-gradient-to-r from-blue-600 to-purple-600 text-white p-4 rounded-full shadow-2xl hover:shadow-3xl transition-all duration-300 hover:scale-110 ${className}`}
          >
            <div className="relative">
              <MessageSquare className="w-6 h-6" />
              {messages.length > 1 && (
                <motion.div
                  initial={{ scale: 0 }}
                  animate={{ scale: 1 }}
                  className="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center"
                >
                  {messages.length - 1}
                </motion.div>
              )}
            </div>
          </motion.button>
        )}
      </AnimatePresence>

      {/* Chat Window */}
      <AnimatePresence>
        {isOpen && (
          <motion.div
            initial={{ opacity: 0, scale: 0.8, y: 20 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.8, y: 20 }}
            className={`fixed bottom-6 right-6 z-50 w-96 h-[500px] bg-white rounded-2xl shadow-2xl border border-gray-200 overflow-hidden ${className}`}
          >
            {/* Header */}
            <div className="bg-gradient-to-r from-blue-600 to-purple-600 text-white p-4">
              <div className="flex items-center justify-between">
                <div className="flex items-center gap-3">
                  <div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center">
                    <MessageSquare className="w-4 h-4" />
                  </div>
                  <div>
                    <h3 className="font-semibold text-sm">Live Chat</h3>
                    <p className="text-xs text-blue-100">{caseTitle}</p>
                  </div>
                </div>
                <div className="flex items-center gap-2">
                  <button
                    onClick={() => setIsMuted(!isMuted)}
                    className="p-1 hover:bg-white/20 rounded transition-colors"
                  >
                    {isMuted ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
                  </button>
                  <button
                    onClick={() => setShowSettings(!showSettings)}
                    className="p-1 hover:bg-white/20 rounded transition-colors"
                  >
                    <Settings className="w-4 h-4" />
                  </button>
                  <button
                    onClick={() => setIsMinimized(!isMinimized)}
                    className="p-1 hover:bg-white/20 rounded transition-colors"
                  >
                    <X className="w-4 h-4" />
                  </button>
                </div>
              </div>
              
              {/* Online users */}
              <div className="flex items-center gap-2 mt-2 text-xs text-blue-100">
                <Users className="w-3 h-3" />
                <span>{getOnlineUsersCount()} online</span>
                {connected && (
                  <div className="flex items-center gap-1">
                    <div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
                    <span>Live</span>
                  </div>
                )}
              </div>
            </div>

            {/* Chat Content */}
            <AnimatePresence>
              {!isMinimized && (
                <motion.div
                  initial={{ height: 0 }}
                  animate={{ height: 'auto' }}
                  exit={{ height: 0 }}
                  className="flex flex-col h-full"
                >
                  {/* Messages */}
                  <div className="flex-1 overflow-y-auto p-4 space-y-3">
                    {messages.map((message) => (
                      <motion.div
                        key={message.id}
                        initial={{ opacity: 0, y: 10 }}
                        animate={{ opacity: 1, y: 0 }}
                        className={`flex ${message.senderId === session.user?.id ? 'justify-end' : 'justify-start'}`}
                      >
                        <div className={`max-w-[80%] ${message.senderId === session.user?.id ? 'order-2' : 'order-1'}`}>
                          {message.type === 'system' ? (
                            <div className="text-center">
                              <span className="inline-block bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full">
                                {message.content}
                              </span>
                            </div>
                          ) : (
                            <div className={`rounded-2xl px-4 py-2 ${
                              message.senderId === session.user?.id
                                ? 'bg-blue-600 text-white'
                                : 'bg-gray-100 text-gray-800'
                            }`}>
                              {message.senderId !== session.user?.id && (
                                <div className="flex items-center gap-2 mb-1">
                                  <div className="w-6 h-6 bg-gray-300 rounded-full flex items-center justify-center">
                                    {message.senderAvatar ? (
                                      <img 
                                        src={message.senderAvatar} 
                                        alt={message.senderName}
                                        className="w-6 h-6 rounded-full object-cover"
                                      />
                                    ) : (
                                      <User className="w-3 h-3 text-gray-600" />
                                    )}
                                  </div>
                                  <span className="text-xs font-medium">{message.senderName}</span>
                                  {message.senderRole === 'LAWYER' && (
                                    <Shield className="w-3 h-3 text-blue-500" />
                                  )}
                                </div>
                              )}
                              <p className="text-sm">{message.content}</p>
                              <div className="flex items-center justify-between mt-1">
                                <span className="text-xs opacity-70">
                                  {format(message.timestamp, 'HH:mm')}
                                </span>
                                {message.isPublic && (
                                  <span className="text-xs opacity-70">Public</span>
                                )}
                              </div>
                            </div>
                          )}
                        </div>
                      </motion.div>
                    ))}
                    
                    {/* Typing indicators */}
                    {getTypingUsers().length > 0 && (
                      <motion.div
                        initial={{ opacity: 0 }}
                        animate={{ opacity: 1 }}
                        className="flex items-center gap-2 text-gray-500 text-sm"
                      >
                        <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>{getTypingUsers().map(t => t.userName).join(', ')} typing...</span>
                      </motion.div>
                    )}
                    
                    <div ref={messagesEndRef} />
                  </div>

                  {/* Quick Actions */}
                  <div className="px-4 py-2 border-t border-gray-100">
                    <div className="flex gap-2 mb-2">
                      {['interested', 'question', 'support', 'apply'].map((action) => (
                        <button
                          key={action}
                          onClick={() => sendQuickAction(action)}
                          className="px-3 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded-full transition-colors capitalize"
                        >
                          {action}
                        </button>
                      ))}
                    </div>
                  </div>

                  {/* Input */}
                  <div className="p-4 border-t border-gray-100">
                    <div className="flex items-end gap-2">
                      <div className="flex-1">
                        <textarea
                          ref={inputRef}
                          value={newMessage}
                          onChange={(e) => {
                            setNewMessage(e.target.value);
                            handleTyping(e.target.value.length > 0);
                          }}
                          onKeyPress={handleKeyPress}
                          placeholder="Type your message..."
                          className="w-full p-3 border border-gray-200 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                          rows={1}
                          maxLength={500}
                        />
                      </div>
                      <button
                        onClick={sendMessage}
                        disabled={!newMessage.trim()}
                        className="p-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
                      >
                        <Send className="w-4 h-4" />
                      </button>
                    </div>
                    
                    {/* Chat mode toggle */}
                    <div className="flex items-center justify-between mt-2">
                      <div className="flex items-center gap-2">
                        <button
                          onClick={() => setChatMode('public')}
                          className={`px-3 py-1 text-xs rounded-full transition-colors ${
                            chatMode === 'public'
                              ? 'bg-blue-100 text-blue-700'
                              : 'bg-gray-100 text-gray-600'
                          }`}
                        >
                          Public
                        </button>
                        <button
                          onClick={() => setChatMode('private')}
                          className={`px-3 py-1 text-xs rounded-full transition-colors ${
                            chatMode === 'private'
                              ? 'bg-purple-100 text-purple-700'
                              : 'bg-gray-100 text-gray-600'
                          }`}
                        >
                          Private
                        </button>
                      </div>
                      <span className="text-xs text-gray-500">
                        {newMessage.length}/500
                      </span>
                    </div>
                  </div>
                </motion.div>
              )}
            </AnimatePresence>
          </motion.div>
        )}
      </AnimatePresence>
    </>
  );
};

export default LiveCaseChat; 

CasperSecurity Mini