diff --git a/src/static/rooms.js b/src/static/rooms.js index 6465c02..504659b 100644 --- a/src/static/rooms.js +++ b/src/static/rooms.js @@ -3,23 +3,63 @@ 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) { - browserElement.style.display = 'block'; - loadPublicRoomsWebSocket(); - subscribeToPublicRooms(); + 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 @@ -31,114 +71,232 @@ function closePublicRoomsBrowser() { // Subscribe to live public rooms updates function subscribeToPublicRooms() { - if (typeof socket !== 'undefined' && socket !== null && socket.connected && !isSubscribedToRooms) { - socket.emit('subscribe_public_rooms'); - isSubscribedToRooms = true; - console.log('Subscribed to public rooms updates'); + 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 (typeof socket !== 'undefined' && socket !== null && socket.connected && isSubscribedToRooms) { - socket.emit('unsubscribe_public_rooms'); - isSubscribedToRooms = false; - console.log('Unsubscribed from public rooms updates'); + 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); + } } } -// Load public rooms via WebSocket +// 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(); - const sortSelect = document.getElementById('roomsSortSelect'); - const minUsersFilter = document.getElementById('minUsersFilter'); - - currentSortBy = sortSelect ? sortSelect.value : 'activity'; - currentMinUsers = minUsersFilter ? (minUsersFilter.value || 0) : 0; - - if (typeof socket !== 'undefined' && socket !== null && socket.connected) { + 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 { - // Fallback to HTTP API if WebSocket not available + console.log('WebSocket not available, using HTTP fallback'); loadPublicRoomsHTTP(); } } catch (error) { console.error('Error requesting public rooms via WebSocket:', error); - loadPublicRoomsHTTP(); // Fallback to HTTP + loadPublicRoomsHTTP(); } } -// Fallback HTTP method +// 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(); - const sortSelect = document.getElementById('roomsSortSelect'); - const minUsersFilter = document.getElementById('minUsersFilter'); + console.log(`Loading rooms via HTTP (attempt ${retryAttempts + 1}/${MAX_RETRY_ATTEMPTS})`); - const sortBy = sortSelect ? sortSelect.value : 'activity'; - const minUsers = minUsersFilter ? (minUsersFilter.value || 0) : 0; + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout - const response = await fetch(`/api/rooms/public?sort=${sortBy}&min_users=${minUsers}&limit=50`); + 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}`); + throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); - publicRoomsData = data.rooms || []; - // Also load stats - try { - const statsResponse = await fetch('/api/rooms/stats'); - if (statsResponse.ok) { - const stats = await statsResponse.json(); - updateStatsDisplay(stats); - } - } catch (statsError) { - console.warn('Failed to load room stats:', statsError); + 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); - showErrorState(); + + 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; + } } } -// Refresh public rooms -function refreshPublicRooms() { - loadPublicRoomsWebSocket(); +// 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); + } } -// Update stats display +// 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) { - statsElement.textContent = `${stats.public_rooms || 0} public rooms • ${stats.total_users || 0} users online`; - } + 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} + `; } -// Show loading state +// 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'; + if (loadingElement) { + loadingElement.style.display = 'flex'; + loadingElement.innerHTML = ` +
+
+ Loading public rooms... +
+ `; + } if (listElement) listElement.style.display = 'none'; if (emptyElement) emptyElement.style.display = 'none'; } -// Show error state +// Enhanced error state with retry options function showErrorState() { const loadingElement = document.getElementById('roomsLoading'); const listElement = document.getElementById('roomsList'); @@ -146,18 +304,29 @@ function showErrorState() { 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. Please try again.

- +
+
⚠️
+

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 +

+
`; } } -// Display rooms +// Enhanced room display with better error handling function displayRooms() { const roomsList = document.getElementById('roomsList'); const roomsLoading = document.getElementById('roomsLoading'); @@ -165,8 +334,20 @@ function displayRooms() { if (roomsLoading) roomsLoading.style.display = 'none'; - if (!publicRoomsData || publicRoomsData.length === 0) { - if (roomsEmpty) roomsEmpty.style.display = 'flex'; + 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; } @@ -174,56 +355,112 @@ function displayRooms() { if (roomsEmpty) roomsEmpty.style.display = 'none'; if (roomsList) { roomsList.style.display = 'block'; - roomsList.innerHTML = publicRoomsData.map(room => createRoomCard(room)).join(''); + + 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(); + } } } -// Create room card HTML +// Enhanced room card with better data handling function createRoomCard(room) { - if (!room) return ''; + if (!room || typeof room !== 'object') { + console.warn('Invalid room data:', room); + return ''; + } - const activityClass = getActivityClass(room.minutes_since_activity || 0); - const timeAgo = formatTimeAgo(room.minutes_since_activity || 0); + 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 ` -
-
- ${escapeHtml(room.title || room.room_id || 'Unnamed Room')} - +
+ +
+ ${title} +
-
- ${escapeHtml(room.description || 'No description')} -
-
-
+ +
${description}
+ +
+
👥 - ${room.user_count || 0} user${(room.user_count || 0) !== 1 ? 's' : ''} + ${userCount} user${userCount !== 1 ? 's' : ''}
-
+
💬 - ${room.message_count || 0} message${(room.message_count || 0) !== 1 ? 's' : ''} + ${messageCount} message${messageCount !== 1 ? 's' : ''}
-
+
🕐 Active ${timeAgo}
-
+
🆔 - ${escapeHtml(room.room_id || 'Unknown')} + ${roomId}
`; } -// Get activity indicator class +// 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'; } -// Format time ago +// Enhanced time formatting function formatTimeAgo(minutes) { if (minutes < 1) return 'just now'; if (minutes < 60) return `${Math.floor(minutes)}m ago`; @@ -232,21 +469,37 @@ function formatTimeAgo(minutes) { if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); - return `${days}d ago`; + if (days < 7) return `${days}d ago`; + + const weeks = Math.floor(days / 7); + return `${weeks}w ago`; } -// Escape HTML to prevent XSS +// 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 = text; + div.textContent = String(text); return div.innerHTML; } -// Join a public room - integrated with your existing system +// Enhanced room joining with validation function joinPublicRoom(roomId) { - if (!roomId) { - console.error('No room ID provided'); + 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; } @@ -255,52 +508,65 @@ function joinPublicRoom(roomId) { // Close the browser closePublicRoomsBrowser(); - // Fill in the room input with the selected room + // 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 since these are public rooms + // Clear password field for public rooms const roomPasswordInput = document.getElementById('roomPasswordInput'); if (roomPasswordInput) { roomPasswordInput.value = ''; } - // Trigger your existing join room functionality - const joinButton = document.getElementById('joinRoomBtn'); - if (joinButton) { - joinButton.click(); - } + // 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); } -// WebSocket event handlers for real-time updates +// Enhanced WebSocket handlers function setupPublicRoomsWebSocketHandlers() { - if (typeof socket === 'undefined' || socket === null) { - console.log('Socket not available, using HTTP fallback'); + 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); - publicRoomsData = data && data.rooms ? data.rooms : []; + + 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); } - displayRooms(); + if (isRoomsBrowserOpen) { + displayRooms(); + } }); - // Handle live updates to public rooms + // Handle live updates socket.on('public_rooms_updated', function(data) { console.log('Received live public rooms update:', data); - // Only update if the browser is currently open and we're subscribed - const browserElement = document.getElementById('publicRoomsBrowser'); - if (browserElement && browserElement.style.display === 'block' && isSubscribedToRooms) { - publicRoomsData = data && data.rooms ? data.rooms : []; + if (isRoomsBrowserOpen && isSubscribedToRooms) { + if (data && Array.isArray(data.rooms)) { + publicRoomsData = data.rooms; + } if (data && data.stats) { updateStatsDisplay(data.stats); @@ -310,57 +576,89 @@ function setupPublicRoomsWebSocketHandlers() { } }); - // Handle WebSocket errors for public rooms + // Handle WebSocket errors socket.on('public_rooms_error', function(data) { console.error('Public rooms WebSocket error:', data); - showErrorState(); + if (isRoomsBrowserOpen) { + showErrorState(); + } }); - console.log('Public rooms WebSocket handlers attached'); + // 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'); } -// Auto-setup WebSocket handlers when page loads +// Enhanced socket initialization function waitForSocketAndSetupHandlers(retryCount = 0) { - // Check if socket is properly initialized and connected - if (typeof socket !== 'undefined' && socket !== null && typeof socket.connected !== 'undefined' && socket.connected) { + if (isSocketAvailable()) { setupPublicRoomsWebSocketHandlers(); console.log('WebSocket handlers setup successfully'); return; } - // If we've retried too many times, give up and use HTTP only if (retryCount > 100) { // 100 * 250ms = 25 seconds max wait - console.warn('Socket failed to initialize after 25 seconds, falling back to HTTP-only mode'); + console.warn('Socket failed to initialize after 25 seconds, using HTTP-only mode'); return; } - // Log current socket state for debugging if (retryCount % 20 === 0) { // Log every 5 seconds console.log(`Waiting for socket... (attempt ${retryCount + 1})`); - console.log('Socket type:', typeof socket); - console.log('Socket value:', socket); - console.log('Socket.io library loaded:', typeof io !== 'undefined'); } - // Continue waiting setTimeout(() => waitForSocketAndSetupHandlers(retryCount + 1), 250); } -// Event listeners for controls +// Enhanced event listeners with better error handling document.addEventListener('DOMContentLoaded', function() { - // Add null checks for DOM elements - const sortSelect = document.getElementById('roomsSortSelect'); - const minUsersFilter = document.getElementById('minUsersFilter'); + 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)); } - // Setup backdrop click handler + // 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) { @@ -370,22 +668,70 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // Start the WebSocket setup process - waitForSocketAndSetupHandlers(); - - // Auto-refresh every 30 seconds when browser is open - setInterval(function() { - const browserElement = document.getElementById('publicRoomsBrowser'); - if (browserElement && browserElement.style.display === 'block') { + // 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(); } - }, 30000); + }); + + // Start WebSocket setup + waitForSocketAndSetupHandlers(); + + console.log('Public rooms browser setup complete'); }); -// Close on escape key -document.addEventListener('keydown', function(e) { - const browserElement = document.getElementById('publicRoomsBrowser'); - if (e.key === 'Escape' && browserElement && browserElement.style.display === 'block') { - closePublicRoomsBrowser(); +// 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(); } -}); \ No newline at end of file + 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); +} \ No newline at end of file