let publicRoomsData = []; let roomsRefreshInterval = null; let currentSortBy = 'activity'; let currentMinUsers = 0; let isSubscribedToRooms = false; let isRoomsBrowserOpen = false; let retryAttempts = 0; const MAX_RETRY_ATTEMPTS = 3; // Configuration const ROOMS_CONFIG = { refreshInterval: 30000, // 30 seconds maxRetries: 3, retryDelay: 2000, // 2 seconds socketTimeout: 25000, // 25 seconds to wait for socket fallbackToHttp: true, autoRefresh: true }; // Show the public rooms browser function showPublicRoomsBrowser() { console.log('Opening public rooms browser'); const browserElement = document.getElementById('publicRoomsBrowser'); if (!browserElement) { console.error('Public rooms browser element not found'); return; } browserElement.style.display = 'flex'; isRoomsBrowserOpen = true; retryAttempts = 0; // Reset scroll position const browserContent = document.getElementById('browserContent'); if (browserContent) { browserContent.scrollTop = 0; } // Load rooms and subscribe to updates loadPublicRoomsWebSocket(); subscribeToPublicRooms(); // Set up auto-refresh if enabled if (ROOMS_CONFIG.autoRefresh && !roomsRefreshInterval) { roomsRefreshInterval = setInterval(() => { if (isRoomsBrowserOpen) { console.log('Auto-refreshing public rooms'); loadPublicRoomsWebSocket(); } }, ROOMS_CONFIG.refreshInterval); } } // Close the public rooms browser function closePublicRoomsBrowser() { console.log('Closing public rooms browser'); const browserElement = document.getElementById('publicRoomsBrowser'); if (browserElement) { browserElement.style.display = 'none'; } isRoomsBrowserOpen = false; unsubscribeFromPublicRooms(); // Clear auto-refresh if (roomsRefreshInterval) { clearInterval(roomsRefreshInterval); roomsRefreshInterval = null; } } // Subscribe to live public rooms updates function subscribeToPublicRooms() { if (isSocketAvailable() && !isSubscribedToRooms) { try { socket.emit('subscribe_public_rooms'); isSubscribedToRooms = true; console.log('Subscribed to public rooms updates'); } catch (error) { console.error('Error subscribing to public rooms:', error); } } } // Unsubscribe from public rooms updates function unsubscribeFromPublicRooms() { if (isSocketAvailable() && isSubscribedToRooms) { try { socket.emit('unsubscribe_public_rooms'); isSubscribedToRooms = false; console.log('Unsubscribed from public rooms updates'); } catch (error) { console.error('Error unsubscribing from public rooms:', error); } } } // Check if socket is available and connected function isSocketAvailable() { return typeof socket !== 'undefined' && socket !== null && socket.connected; } // Load public rooms via WebSocket with fallback function loadPublicRoomsWebSocket() { if (!isRoomsBrowserOpen) { console.log('Rooms browser not open, skipping load'); return; } try { showLoadingState(); updateFiltersFromUI(); if (isSocketAvailable()) { console.log('Loading rooms via WebSocket'); socket.emit('request_public_rooms', { sort: currentSortBy, min_users: currentMinUsers, limit: 50 }); // Set a timeout for WebSocket response setTimeout(() => { if (document.getElementById('roomsLoading')?.style.display !== 'none') { console.warn('WebSocket timeout, falling back to HTTP'); loadPublicRoomsHTTP(); } }, 5000); } else { console.log('WebSocket not available, using HTTP fallback'); loadPublicRoomsHTTP(); } } catch (error) { console.error('Error requesting public rooms via WebSocket:', error); loadPublicRoomsHTTP(); } } // Update filters from UI elements function updateFiltersFromUI() { const sortSelect = document.getElementById('roomsSortSelect'); const minUsersFilter = document.getElementById('minUsersFilter'); currentSortBy = sortSelect?.value || 'activity'; currentMinUsers = parseInt(minUsersFilter?.value || '0') || 0; console.log(`Updated filters: sort=${currentSortBy}, minUsers=${currentMinUsers}`); } // Enhanced HTTP fallback with retry logic async function loadPublicRoomsHTTP() { if (!isRoomsBrowserOpen) { console.log('Rooms browser not open, skipping HTTP load'); return; } try { showLoadingState(); updateFiltersFromUI(); console.log(`Loading rooms via HTTP (attempt ${retryAttempts + 1}/${MAX_RETRY_ATTEMPTS})`); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout const response = await fetch( `/api/rooms/public?sort=${encodeURIComponent(currentSortBy)}&min_users=${currentMinUsers}&limit=50`, { signal: controller.signal } ); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (!data || !Array.isArray(data.rooms)) { throw new Error('Invalid response format'); } publicRoomsData = data.rooms; console.log(`Loaded ${publicRoomsData.length} rooms via HTTP`); // Load stats separately await loadRoomStats(); displayRooms(); retryAttempts = 0; // Reset on success } catch (error) { console.error('Error loading public rooms via HTTP:', error); if (error.name === 'AbortError') { console.error('Request timed out'); } retryAttempts++; if (retryAttempts < MAX_RETRY_ATTEMPTS) { console.log(`Retrying in ${ROOMS_CONFIG.retryDelay}ms...`); setTimeout(() => { if (isRoomsBrowserOpen) { loadPublicRoomsHTTP(); } }, ROOMS_CONFIG.retryDelay); } else { showErrorState(); retryAttempts = 0; } } } // Load room statistics async function loadRoomStats() { try { const statsResponse = await fetch('/api/rooms/stats'); if (statsResponse.ok) { const stats = await statsResponse.json(); updateStatsDisplay(stats); } } catch (error) { console.warn('Failed to load room stats:', error); } } // Refresh public rooms with user feedback function refreshPublicRooms() { console.log('Manual refresh requested'); retryAttempts = 0; // Reset retry counter on manual refresh // Show brief loading indicator const refreshBtn = document.getElementById('refreshPublicRoomsBtn'); const originalText = refreshBtn?.textContent; if (refreshBtn) { refreshBtn.textContent = 'Refreshing...'; refreshBtn.disabled = true; } loadPublicRoomsWebSocket(); // Reset button after delay setTimeout(() => { if (refreshBtn) { refreshBtn.textContent = originalText || 'Refresh'; refreshBtn.disabled = false; } }, 2000); } // Update stats display with enhanced formatting function updateStatsDisplay(stats) { const statsElement = document.getElementById('roomsStats'); if (!statsElement || !stats) return; const publicRooms = stats.public_rooms || 0; const totalUsers = stats.total_users || 0; const lastUpdated = new Date().toLocaleTimeString(); statsElement.innerHTML = ` ${publicRooms} public room${publicRooms !== 1 ? 's' : ''} ${totalUsers} user${totalUsers !== 1 ? 's' : ''} online • Updated ${lastUpdated} `; } // Enhanced loading state function showLoadingState() { const loadingElement = document.getElementById('roomsLoading'); const listElement = document.getElementById('roomsList'); const emptyElement = document.getElementById('roomsEmpty'); if (loadingElement) { loadingElement.style.display = 'flex'; loadingElement.innerHTML = `
Loading public rooms...
`; } if (listElement) listElement.style.display = 'none'; if (emptyElement) emptyElement.style.display = 'none'; } // Enhanced error state with retry options function showErrorState() { const loadingElement = document.getElementById('roomsLoading'); const listElement = document.getElementById('roomsList'); const emptyElement = document.getElementById('roomsEmpty'); if (loadingElement) loadingElement.style.display = 'none'; if (listElement) listElement.style.display = 'none'; if (emptyElement) { emptyElement.style.display = 'flex'; emptyElement.innerHTML = `
⚠️

Failed to Load Rooms

Unable to connect to the server. This could be due to network issues or server maintenance.

Attempted ${retryAttempts}/${MAX_RETRY_ATTEMPTS} retries

`; } } // Enhanced room display with better error handling function displayRooms() { const roomsList = document.getElementById('roomsList'); const roomsLoading = document.getElementById('roomsLoading'); const roomsEmpty = document.getElementById('roomsEmpty'); if (roomsLoading) roomsLoading.style.display = 'none'; if (!Array.isArray(publicRoomsData) || publicRoomsData.length === 0) { if (roomsEmpty) { roomsEmpty.style.display = 'flex'; roomsEmpty.innerHTML = `
🏠

No Public Rooms Found

${currentMinUsers > 0 ? `Try reducing the minimum users filter (currently ${currentMinUsers})` : 'Check back later or create your own room'}

`; } if (roomsList) roomsList.style.display = 'none'; return; } if (roomsEmpty) roomsEmpty.style.display = 'none'; if (roomsList) { roomsList.style.display = 'block'; try { const roomsHtml = publicRoomsData.map(room => createRoomCard(room)).join(''); roomsList.innerHTML = roomsHtml; console.log(`Displayed ${publicRoomsData.length} rooms`); } catch (error) { console.error('Error rendering rooms:', error); showErrorState(); } } } // Enhanced room card with better data handling function createRoomCard(room) { if (!room || typeof room !== 'object') { console.warn('Invalid room data:', room); return ''; } const roomId = sanitizeText(room.room_id || 'unknown'); const title = sanitizeText(room.title || room.room_id || 'Unnamed Room'); const description = sanitizeText(room.description || 'No description available'); const userCount = Math.max(0, parseInt(room.user_count) || 0); const messageCount = Math.max(0, parseInt(room.message_count) || 0); const minutesAgo = Math.max(0, parseInt(room.minutes_since_activity) || 0); const activityClass = getActivityClass(minutesAgo); const timeAgo = formatTimeAgo(minutesAgo); return `
${title}
${description}
👥 ${userCount} user${userCount !== 1 ? 's' : ''}
💬 ${messageCount} message${messageCount !== 1 ? 's' : ''}
🕐 Active ${timeAgo}
🆔 ${roomId}
`; } // Enhanced activity classification function getActivityClass(minutesAgo) { if (minutesAgo <= 5) return 'activity-active'; if (minutesAgo <= 30) return 'activity-recent'; if (minutesAgo <= 120) return 'activity-moderate'; return 'activity-old'; } // Enhanced time formatting function formatTimeAgo(minutes) { if (minutes < 1) return 'just now'; if (minutes < 60) return `${Math.floor(minutes)}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 7) return `${days}d ago`; const weeks = Math.floor(days / 7); return `${weeks}w ago`; } // Enhanced text sanitization function sanitizeText(text) { if (typeof text !== 'string') return ''; return text.trim().substring(0, 200); // Limit length and trim } // Enhanced HTML escaping function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = String(text); return div.innerHTML; } // Enhanced room joining with validation function joinPublicRoom(roomId) { if (!roomId || typeof roomId !== 'string') { console.error('Invalid room ID provided:', roomId); return; } // Validate room ID format const roomIdPattern = /^[a-zA-Z0-9\-_]{1,32}$/; if (!roomIdPattern.test(roomId)) { console.error('Invalid room ID format:', roomId); return; } console.log(`Joining public room: ${roomId}`); // Close the browser closePublicRoomsBrowser(); // Fill in the room input const roomInput = document.getElementById('roomInput'); if (roomInput) { roomInput.value = roomId; roomInput.dispatchEvent(new Event('input', { bubbles: true })); } // Clear password field for public rooms const roomPasswordInput = document.getElementById('roomPasswordInput'); if (roomPasswordInput) { roomPasswordInput.value = ''; } // Trigger join room functionality with delay to ensure UI updates setTimeout(() => { const joinButton = document.getElementById('joinRoomBtn'); if (joinButton) { joinButton.click(); } else { console.error('Join button not found'); } }, 100); } // Enhanced WebSocket handlers function setupPublicRoomsWebSocketHandlers() { if (!isSocketAvailable()) { console.log('Socket not available, WebSocket handlers not attached'); return; } // Handle public rooms data response socket.on('public_rooms_data', function(data) { console.log('Received public rooms data via WebSocket:', data); if (data && Array.isArray(data.rooms)) { publicRoomsData = data.rooms; } else { console.warn('Invalid public rooms data format'); publicRoomsData = []; } if (data && data.stats) { updateStatsDisplay(data.stats); } if (isRoomsBrowserOpen) { displayRooms(); } }); // Handle live updates socket.on('public_rooms_updated', function(data) { console.log('Received live public rooms update:', data); if (isRoomsBrowserOpen && isSubscribedToRooms) { if (data && Array.isArray(data.rooms)) { publicRoomsData = data.rooms; } if (data && data.stats) { updateStatsDisplay(data.stats); } displayRooms(); } }); // Handle WebSocket errors socket.on('public_rooms_error', function(data) { console.error('Public rooms WebSocket error:', data); if (isRoomsBrowserOpen) { showErrorState(); } }); // Handle connection events socket.on('connect', function() { console.log('Socket connected, resubscribing if browser is open'); if (isRoomsBrowserOpen && !isSubscribedToRooms) { subscribeToPublicRooms(); } }); socket.on('disconnect', function() { console.log('Socket disconnected'); isSubscribedToRooms = false; }); console.log('Enhanced public rooms WebSocket handlers attached'); } // Enhanced socket initialization function waitForSocketAndSetupHandlers(retryCount = 0) { if (isSocketAvailable()) { setupPublicRoomsWebSocketHandlers(); console.log('WebSocket handlers setup successfully'); return; } if (retryCount > 100) { // 100 * 250ms = 25 seconds max wait console.warn('Socket failed to initialize after 25 seconds, using HTTP-only mode'); return; } if (retryCount % 20 === 0) { // Log every 5 seconds console.log(`Waiting for socket... (attempt ${retryCount + 1})`); } setTimeout(() => waitForSocketAndSetupHandlers(retryCount + 1), 250); } // Enhanced event listeners with better error handling document.addEventListener('DOMContentLoaded', function() { console.log('Setting up public rooms browser event listeners'); // Browse button (this was missing!) const browseButton = document.getElementById('browsePublicRoomsBtn'); if (browseButton) { browseButton.addEventListener('click', showPublicRoomsBrowser); console.log('Browse public rooms button listener attached'); } else { console.warn('Browse public rooms button not found'); } // Close button const closeButton = document.getElementById('closePublicRoomsBrowserBtn'); if (closeButton) { closeButton.addEventListener('click', closePublicRoomsBrowser); } // Sort selector const sortSelect = document.getElementById('roomsSortSelect'); if (sortSelect) { sortSelect.addEventListener('change', refreshPublicRooms); } // Min users filter const minUsersFilter = document.getElementById('minUsersFilter'); if (minUsersFilter) { minUsersFilter.addEventListener('change', refreshPublicRooms); minUsersFilter.addEventListener('input', debounce(refreshPublicRooms, 1000)); } // Refresh button const refreshButton = document.getElementById('refreshPublicRoomsBtn'); if (refreshButton) { refreshButton.addEventListener('click', refreshPublicRooms); } // Backdrop click handler const browserElement = document.getElementById('publicRoomsBrowser'); if (browserElement) { browserElement.addEventListener('click', function(e) { if (e.target === this) { closePublicRoomsBrowser(); } }); } // Keyboard shortcuts document.addEventListener('keydown', function(e) { if (!isRoomsBrowserOpen) return; if (e.key === 'Escape') { e.preventDefault(); closePublicRoomsBrowser(); } else if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) { e.preventDefault(); refreshPublicRooms(); } }); // Start WebSocket setup waitForSocketAndSetupHandlers(); console.log('Public rooms browser setup complete'); }); // Utility: Debounce function function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func.apply(this, args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Cleanup on page unload window.addEventListener('beforeunload', function() { if (isSubscribedToRooms) { unsubscribeFromPublicRooms(); } if (roomsRefreshInterval) { clearInterval(roomsRefreshInterval); } }); // Add CSS for activity indicators if not already present if (!document.getElementById('roomsActivityStyles')) { const style = document.createElement('style'); style.id = 'roomsActivityStyles'; style.textContent = ` .activity-active { background-color: #00ff88 !important; box-shadow: 0 0 6px rgba(0, 255, 136, 0.6); } .activity-recent { background-color: #ffaa00 !important; } .activity-moderate { background-color: #888 !important; } .activity-old { background-color: #444 !important; } .room-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } `; document.head.appendChild(style); }