'use client' import { useEffect, useState, useRef } from 'react' import { io, Socket } from 'socket.io-client' import { useSession, signIn } from 'next-auth/react' import useSWR from 'swr' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { ScrollArea } from '@/components/ui/scroll-area' import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { MessageCircle, Send, X, Minimize2, Maximize2, Trash2, CloudRain } from 'lucide-react' import { cn } from '@/lib/utils' import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card" import { ExternalLink } from "lucide-react" import Link from "next/link" import { toast } from 'sonner'; import { VerifiedBadge } from '@/components/ui/verified-badge' import { UserBadges } from '@/components/ui/user-badges' import { NET_EVENTS, verifyMessage } from "@/lib/vendor-metrics"; interface Notification { _id: string; coinId: string; userId: string; userName: string; userImage: string; text: string; createdAt: string; userVerified?: boolean; userIsAdmin?: boolean; userIsBetaTester?: boolean; userIsBugHunter?: boolean; } interface RainEvent { _id: string; amount: number; hostId: string; hostName: string; endsAt: string; participants: { id: string, name: string, image: string }[]; active: boolean; } interface SessionConfig { market: string; nonce: string; } export function GlobalChat() { const { data: session } = useSession() const [isOpen, setIsOpen] = useState(false) const [socket, setSocket] = useState(null) const [messages, setMessages] = useState([]) const [newMessage, setNewMessage] = useState('') const [isConnected, setIsConnected] = useState(false) const [activeRain, setActiveRain] = useState(null) const [sessionConfig, setSessionConfig] = useState(null) const scrollRef = useRef(null) const renderTextWithLinks = (text: string) => { const parts = text.split(/(@\w+|\$\w+)/); return parts.map((part, i) => { if (part.startsWith('@')) { const username = part.slice(1); return {part}; } else if (part.startsWith('$')) { const ticker = part.slice(1); return {part}; } else { return {part}; } }); }; useEffect(() => { fetch('/api/rain').then(res => res.json()).then(data => { if(data.rain && data.rain.active) setActiveRain(data.rain); }).catch(() => {}); }, []); useEffect(() => { const saved = localStorage.getItem('pummmp_global_chat') if (saved) { try { setMessages(JSON.parse(saved)) } catch (e) { console.error('Failed to parse chat history', e) } } }, []) useEffect(() => { if (messages.length > 0) { const toSave = messages.slice(-100); localStorage.setItem('pummmp_global_chat', JSON.stringify(toSave)) } }, [messages]) useEffect(() => { const SOCKET_URL = window.location.hostname === 'localhost' ? "http://localhost:6767" : "https://pummmp.fun"; const url = process.env.NEXT_PUBLIC_SOCKET_URL || SOCKET_URL; const newSocket = io(url, { path: "/socket.io", transports: ["websocket"], withCredentials: true, reconnection: true, reconnectionAttempts: 10, reconnectionDelay: 1000, reconnectionDelayMax: 5000, timeout: 20000, forceNew: false, }); newSocket.on('connect', () => { console.log('Chat connected'); setIsConnected(true); }); newSocket.on(NET_EVENTS.CONNECT, (data: any) => { setSessionConfig({ market: data._h_mid, nonce: data._h_nonce }); }); newSocket.on('nonce_update', (data: any) => { setSessionConfig(prev => prev ? { ...prev, nonce: data.nonce } : null); }); newSocket.on('rain_update', (rain: any) => { if(rain) setActiveRain(rain); }); newSocket.on('rain_ended', (data: any) => { setActiveRain(null); if (data && data.amount) { toast.success(`Rain Ended! ${data.totalParticipants} users got ${(data.payoutPerUser || 0).toFixed(4)} SOL!`); } }); newSocket.on(NET_EVENTS.INCOMING, (signedData: string) => { if (!signedData || typeof signedData !== 'string') return; }); newSocket.on('disconnect', (reason) => { console.log('Chat disconnected:', reason); setIsConnected(false); setSessionConfig(null); }); newSocket.on('connect_error', (error) => { console.error('Chat connection error:', error); setIsConnected(false); }); newSocket.on('reconnect', (attemptNumber) => { console.log('Chat reconnected after', attemptNumber, 'attempts'); setIsConnected(true); }); newSocket.on('reconnect_error', (error) => { console.error('Chat reconnection failed:', error); }); newSocket.on('reconnect_failed', () => { console.error('Chat reconnection failed completely'); setIsConnected(false); }); newSocket.on('error_message', (msg) => toast.error(msg)); setSocket(newSocket) return () => { newSocket.disconnect() } }, []) useEffect(() => { if (!socket || !sessionConfig) return; const handler = async (signedData: string) => { const raw = await verifyMessage(signedData, sessionConfig.market); if (!raw) { console.log('[Chat] Failed to verify message'); return; } try { const clean = raw.replace(/\0/g, '').trim(); if (!clean.startsWith('{') && !clean.startsWith('[')) { console.log('[Chat] Message not JSON:', clean.substring(0, 50)); return; } const msg: Notification = JSON.parse(clean); console.log('[Chat] Received message:', msg.userName, msg.text.substring(0, 50)); setMessages((prev) => { if (prev.find(m => m._id === msg._id)) return prev; return [...prev, msg].slice(-100); }); } catch(e) { console.error('[Chat] Error parsing message:', e); } }; socket.on(NET_EVENTS.INCOMING, handler); return () => { socket.off(NET_EVENTS.INCOMING, handler); } }, [socket, sessionConfig]); useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollIntoView({ behavior: 'smooth' }) } }, [messages, isOpen]) useEffect(() => { if (activeRain && !isOpen) { setIsOpen(true); toast.success(`🌧️ Rain started by ${activeRain.hostName}! You could win ${activeRain.amount} SOL!`, { duration: 5000, }); } }, [activeRain, isOpen]) const handleJoinRain = async () => { if (!activeRain || !session) return; try { const res = await fetch('/api/rain', { method: 'PUT' }); const data = await res.json(); if (res.ok) { toast.success('You entered the rain!'); setActiveRain(prev => { if(!prev) return null; const uid = (session.user as any).id || (session as any).id; if (!uid) return prev; return { ...prev, participants: prev.participants.some(p => p.id === uid) ? prev.participants : [...prev.participants, { id: uid, name: session.user.name || 'Unknown', image: session.user.image || '/logo.ico' }] } }) } else { toast.error(data.error); } } catch (e) { toast.error('Failed to join rain'); } } const handleSendMessage = async (e: React.FormEvent) => { e.preventDefault() if (!newMessage.trim() || !session || !socket || !sessionConfig || !sessionConfig.nonce) return; if (newMessage.startsWith('.help')) { const isAdmin = session?.user?.isAdmin; const helpMsg = { _id: Math.random().toString(), coinId: 'system', userId: 'system', userName: 'System', userImage: '/logo.ico', text: isAdmin ? `Commands: .tip - Tip someone SOL ~ .gamble - Roll the dice (1-95%) ~ .ban - Ban user (30m/2h/1d/perm) ~ .unban - Unban user` : `Commands: .tip - Tip someone SOL ~ .gamble - Roll the dice (1-95%)`, createdAt: new Date().toISOString(), userVerified: true } setMessages(prev => [...prev, helpMsg].slice(-100)) setNewMessage('') return } if (newMessage.startsWith('.gamble ')) { const parts = newMessage.split(' '); if (parts.length >= 3) { const amount = parts[1]; const chance = parts[2]; try { const res = await fetch('/api/gamble', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount, chance }) }); const data = await res.json(); if (res.ok) { setNewMessage(''); const payloadObj = { type: 'gamble', amount: parseFloat(amount), won: data.won, payout: data.payout, chance: parseFloat(chance), _nonce: sessionConfig.nonce }; socket.emit(NET_EVENTS.OUTGOING, payloadObj); const resultMsg = { _id: Math.random().toString(), coinId: 'system', userId: 'system', userName: 'pummmp.casino', userImage: '/logo.ico', text: data.won ? `You WON ${data.payout.toFixed(4)} SOL! (Rolled ${data.roll.toFixed(2)})` : `You lost ${amount} SOL (Rolled ${data.roll.toFixed(2)})`, createdAt: new Date().toISOString(), userVerified: true } setMessages(prev => [...prev, resultMsg].slice(-100)) } else { toast.error(data.error); } } catch (err) { console.error(err); toast.error('Gamble failed'); } return; } } if (newMessage.startsWith('.rain ')) { const parts = newMessage.split(' '); if (parts.length >= 2) { const amount = parts[1]; try { const res = await fetch('/api/rain', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount }) }); const data = await res.json(); if (res.ok) { setNewMessage(''); if (data.rain) setActiveRain(data.rain); // Immediate update toast.success('Rain started! 🌧️'); } else { toast.error(data.error); } } catch (err) { toast.error('Failed to start rain'); } return; } } if (newMessage.startsWith('.tip ')) { const parts = newMessage.split(' '); if (parts.length >= 3) { const recipient = parts[1]; const amount = parts[2]; try { const res = await fetch('/api/tip', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ recipientUsername: recipient, amount: amount }) }); const data = await res.json(); if (res.ok) { setNewMessage(''); const payloadObj = { type: 'tip', recipientName: recipient, amount: amount, _nonce: sessionConfig.nonce }; socket.emit(NET_EVENTS.OUTGOING, payloadObj); const confirmMsg = { _id: Math.random().toString(), coinId: 'system', userId: 'system', userName: 'pummmp.bot', userImage: '/logo.ico', text: `✅ Tipped ${recipient} ${amount} SOL!`, createdAt: new Date().toISOString(), userVerified: true } setMessages(prev => [...prev, confirmMsg].slice(-100)) } else { toast.error(data.error); } } catch (err) { console.error(err); toast.error('Failed to send tip'); } return; } } socket.emit(NET_EVENTS.OUTGOING, { text: newMessage, _nonce: sessionConfig.nonce }); setNewMessage('') } const handleClearChat = () => { localStorage.removeItem('pummmp_global_chat') setMessages([]) toast.success('Chat history cleared', {position: 'bottom-center'}) } if (!isOpen) { return (
setIsOpen(true)} className="fixed right-0 top-20 z-40 flex h-12 w-10 cursor-pointer items-center justify-center rounded-l-xl border-y border-l border-border bg-background shadow-md transition-all hover:w-12 hover:bg-accent/50 group" >
{isConnected && ( )}
) } return (

pummmp.chat

{isConnected ? 'Connected' : 'Connecting...'}
{activeRain && (
RAIN EVENT ACTIVE! Hosted by {activeRain.hostName}
{activeRain.amount} SOL
Total distributed
{activeRain.participants.slice(0, 3).map((participant, i) => ( {participant.name?.[0]} ))} {(activeRain.participants?.length || 0) > 3 && (
+{(activeRain.participants?.length || 0) - 3}
)} {(activeRain.participants?.length || 0) === 0 && 0 participants | Be the first!}
)}
{messages.length === 0 && (

No messages yet.

Say hello! 👋

)} {messages.map((msg, i) => (
{msg.userName?.[0]}
{msg.userName} {new Date(msg.createdAt).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}

{renderTextWithLinks(msg.text)}

))}
{!session ? ( ) : (session?.user?.chatBannedUntil && new Date(session.user.chatBannedUntil) > new Date()) ? ( ) : ( <> setNewMessage(e.target.value)} placeholder="Type a message... | .help for commands" className="h-8 text-xs" disabled={!isConnected || !sessionConfig || !sessionConfig.nonce} /> )}
) }