![]() 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/ |
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;