import React, { useEffect, useRef, useState, useCallback } from 'react'; import { io, Socket } from 'socket.io-client'; import { Video, VideoOff, Mic, MicOff, Send, SkipForward, StopCircle, MessageSquare, User, Loader2, Camera, Shield, Globe, Zap, Target, ChevronDown, ChevronUp, MessageCircle, Sparkles, Users, Lock } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; type Page = 'landing' | 'setup' | 'chat'; type ChatType = 'text' | 'video'; type Message = { sender: 'me' | 'stranger' | 'system'; text: string }; // ✅ FIXED: ExpressTURN free TURN server (replaces broken openrelay) const STUN_SERVERS = { iceServers: [ { urls: "stun:stun.l.google.com:19302" }, { urls: "stun:stun1.l.google.com:19302" }, { urls: "stun:stun2.l.google.com:19302" }, { urls: "turn:free.expressturn.com:3478", username: "000000002092388734", credential: "hLJdGxtDrgYmtIB4JWcBgXm6Xw0=" }, { urls: "turn:free.expressturn.com:3478?transport=tcp", username: "000000002092388734", credential: "hLJdGxtDrgYmtIB4JWcBgXm6Xw0=" } ] }; export default function App() { const [currentPage, setCurrentPage] = useState('landing'); const [chatType, setChatType] = useState('text'); const [interests, setInterests] = useState(''); const [onlineCount, setOnlineCount] = useState(0); const [messages, setMessages] = useState([]); const [inputText, setInputText] = useState(''); const [isMuted, setIsMuted] = useState(false); const [isVideoOff, setIsVideoOff] = useState(false); const [showPermissionModal, setShowPermissionModal] = useState(false); const [isSearching, setIsSearching] = useState(false); const [isConnected, setIsConnected] = useState(false); // ✅ NEW: track if matched const [error, setError] = useState(null); const socketRef = useRef(null); const localStreamRef = useRef(null); const pcRef = useRef(null); const localVideoRef = useRef(null); const remoteVideoRef = useRef(null); const chatEndRef = useRef(null); const hasJoinedQueueRef = useRef(false); const isMatchedRef = useRef(false); // ✅ NEW: prevent re-joining after match // Initialize socket useEffect(() => { const socket = io({ transports: ["websocket"] }); socketRef.current = socket; socket.on('online_count', (count: number) => { setOnlineCount(count); }); socket.on('waiting', () => { setIsSearching(true); setIsConnected(false); setMessages([{ sender: 'system', text: 'Looking for a stranger...' }]); }); socket.on('matched', async ({ role, type }) => { // ✅ FIXED: Mark as matched immediately so no more queue joins happen isMatchedRef.current = true; hasJoinedQueueRef.current = true; setIsSearching(false); setIsConnected(true); setMessages([{ sender: 'system', text: `You are now chatting with a random stranger. Say hi!` }]); if (type === 'video') { await setupWebRTC(role); } }); socket.on('signal', async (data) => { if (!pcRef.current) return; try { if (data.type === 'offer') { await pcRef.current.setRemoteDescription(new RTCSessionDescription(data)); const answer = await pcRef.current.createAnswer(); await pcRef.current.setLocalDescription(answer); socketRef.current?.emit('signal', answer); } else if (data.type === 'answer') { await pcRef.current.setRemoteDescription(new RTCSessionDescription(data)); } else if (data.type === 'ice') { await pcRef.current.addIceCandidate(new RTCIceCandidate(data.candidate)); } } catch (err) { console.error('Signaling error:', err); } }); socket.on('receive-message', (text: string) => { setMessages(prev => [...prev, { sender: 'stranger', text }]); }); socket.on('partner-disconnected', () => { cleanupPC(); isMatchedRef.current = false; setIsConnected(false); setMessages(prev => [...prev, { sender: 'system', text: 'Stranger has disconnected.' }]); }); return () => { socket.disconnect(); }; }, []); useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); const cleanupPC = () => { if (pcRef.current) { pcRef.current.close(); pcRef.current = null; } if (remoteVideoRef.current) { remoteVideoRef.current.srcObject = null; } if (localStreamRef.current) { localStreamRef.current.getTracks().forEach(track => track.stop()); localStreamRef.current = null; } if (localVideoRef.current) { localVideoRef.current.srcObject = null; } }; const setupWebRTC = async (role: 'initiator' | 'receiver') => { cleanupPC(); // ✅ Get fresh media stream before creating peer connection const stream = await getMedia(); if (!stream) { console.error('No media stream available for WebRTC'); return; } const pc = new RTCPeerConnection(STUN_SERVERS); pcRef.current = pc; // ✅ Log ICE connection state for debugging pc.oniceconnectionstatechange = () => { console.log('ICE state:', pc.iceConnectionState); }; pc.onicecandidate = (event) => { if (event.candidate) { socketRef.current?.emit('signal', { type: 'ice', candidate: event.candidate }); } }; pc.ontrack = (event) => { console.log('Got remote track!', event.streams[0]); if (remoteVideoRef.current && event.streams[0]) { remoteVideoRef.current.srcObject = event.streams[0]; // ✅ Force play on mobile remoteVideoRef.current.play().catch(e => console.log('Play error:', e)); } }; // ✅ Add tracks from fresh stream stream.getTracks().forEach(track => { pc.addTrack(track, stream); }); if (role === 'initiator') { const offer = await pc.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true }); await pc.setLocalDescription(offer); socketRef.current?.emit('signal', offer); } }; const getMedia = async () => { if (localStreamRef.current) return localStreamRef.current; try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } }, audio: true }); localStreamRef.current = stream; if (localVideoRef.current) { localVideoRef.current.srcObject = stream; localVideoRef.current.play().catch(e => console.log('Local play error:', e)); } return stream; } catch (err) { console.error('Media access error:', err); setError('Please allow camera & microphone access to use video chat.'); return null; } }; const startChatting = async (type: ChatType) => { setChatType(type); if (type === 'video') { const hasPermission = localStorage.getItem('camera_permission_granted'); if (!hasPermission) { setShowPermissionModal(true); return; } const stream = await getMedia(); if (!stream) return; } setCurrentPage('chat'); setMessages([]); isMatchedRef.current = false; // ✅ Reset match state if (!hasJoinedQueueRef.current) { hasJoinedQueueRef.current = true; socketRef.current?.emit('join-queue', { type, interests }); } }; const handlePermissionConfirm = async () => { setShowPermissionModal(false); const stream = await getMedia(); if (stream) { localStorage.setItem('camera_permission_granted', 'true'); setCurrentPage('chat'); setMessages([]); isMatchedRef.current = false; // ✅ Reset match state if (!hasJoinedQueueRef.current) { hasJoinedQueueRef.current = true; socketRef.current?.emit('join-queue', { type: 'video', interests }); } } }; const handleNext = () => { cleanupPC(); setMessages([]); isMatchedRef.current = false; // ✅ Reset for next match hasJoinedQueueRef.current = true; setIsConnected(false); socketRef.current?.emit('leave-chat'); socketRef.current?.emit('join-queue', { type: chatType, interests }); }; const handleStop = () => { cleanupPC(); hasJoinedQueueRef.current = false; isMatchedRef.current = false; socketRef.current?.emit('leave-chat'); setCurrentPage('setup'); setMessages([]); setIsSearching(false); setIsConnected(false); }; const sendMessage = (e: React.FormEvent) => { e.preventDefault(); if (inputText.trim()) { socketRef.current?.emit('send-message', inputText); setMessages(prev => [...prev, { sender: 'me', text: inputText }]); setInputText(''); } }; const toggleMute = () => { if (localStreamRef.current) { localStreamRef.current.getAudioTracks().forEach(track => { track.enabled = !track.enabled; }); setIsMuted(prev => !prev); } }; const toggleVideo = () => { if (localStreamRef.current) { localStreamRef.current.getVideoTracks().forEach(track => { track.enabled = !track.enabled; }); setIsVideoOff(prev => !prev); } }; const FeatureCard = ({ icon: Icon, title, desc }: { icon: any, title: string, desc: string }) => (

{title}

{desc}

); const FAQItem = ({ question }: { question: string }) => { const [isOpen, setIsOpen] = useState(false); return (
{isOpen && (
MingleHub uses advanced matching algorithms to connect you with people who share your interests while maintaining a safe and moderated environment for everyone.
)}
); }; return (
{/* PAGE 1: LANDING */} {currentPage === 'landing' && (
MingleHub
Safety Community Support

Connect with the
World Instantly.

MingleHub is a premium anonymous video and text chat platform. Meet interesting people, make new friends, and explore the world from your screen.

{onlineCount.toLocaleString()} PEOPLE CURRENTLY ONLINE

Common Questions

)} {/* PAGE 2: SETUP */} {currentPage === 'setup' && (
MingleHub

Configure Chat

Choose how you want to connect today.

Optional
setInterests(e.target.value)} placeholder="e.g. Music, Travel, Coding..." className="w-full px-6 py-4 bg-zinc-900 border border-zinc-800 rounded-2xl text-sm focus:outline-none focus:border-indigo-600 transition-all text-white placeholder:text-zinc-700" />

Safety First

MingleHub is a moderated community. Please be respectful to others. Harassment or inappropriate behavior will result in a permanent ban.

)} {/* PAGE 3: CHAT */} {currentPage === 'chat' && (
MingleHub
Live
{chatType === 'video' && (
)}
{chatType === 'video' ? (
) : (

Secure Text Chat

You are now connected with a stranger. Be kind and enjoy the conversation.

)} {isSearching && (

Searching...

Matching with a global stranger

)}
{messages.map((msg, i) => ( {msg.sender === 'system' ? (
{msg.text}
) : ( <> {msg.sender === 'me' ? 'You' : 'Stranger'}
{msg.text}
)}
))}
setInputText(e.target.value)} placeholder="Type something..." className="w-full bg-zinc-900 border border-zinc-800 rounded-2xl py-5 pl-6 pr-16 text-sm focus:outline-none focus:border-indigo-600 transition-all text-white placeholder:text-zinc-700" />
)}
{/* Permission Modal */} {showPermissionModal && (

Enable
Camera

To connect via video, MingleHub requires access to your camera and microphone.

)}
); }