testing the new room discovery changes. you can neow discover public rooms ( ones with no password )

This commit is contained in:
2025-08-31 22:28:14 +02:00
parent b21759b16a
commit 1aefa1a58d
4 changed files with 854 additions and 19 deletions

View File

@@ -41,6 +41,7 @@ room_keys = {}
user_sessions = {}
room_passwords = {} # Separate password storage with hashing
room_creation_times = {}
room_metadata = {} # New: Store room titles and descriptions
ip_room_count = {} # Track rooms created per IP
failed_password_attempts = {} # Track failed password attempts
message_hashes = {} # Store message hashes for duplicate detection
@@ -151,6 +152,7 @@ def cleanup_room(room_id):
room_keys.pop(room_id, None)
room_passwords.pop(room_id, None)
room_creation_times.pop(room_id, None)
room_metadata.pop(room_id, None) # Clean up metadata
message_hashes.pop(room_id, None)
def get_client_ip():
@@ -201,15 +203,58 @@ def get_room_info(room_id):
return jsonify({'error': 'Invalid room ID'}), 400
if room_id in chat_rooms:
return jsonify({
room_info = {
'exists': True,
'message_count': chat_rooms[room_id].get_message_count(),
'max_messages': MAX_MESSAGES,
'user_count': len(room_keys.get(room_id, {})),
'max_users': MAX_USERS_PER_ROOM
})
'max_users': MAX_USERS_PER_ROOM,
'is_password_protected': room_id in room_passwords,
'created_at': room_creation_times.get(room_id)
}
# Add metadata if available
if room_id in room_metadata:
room_info.update(room_metadata[room_id])
return jsonify(room_info)
return jsonify({'exists': False})
@app.route('/api/rooms/public')
def get_public_rooms():
"""Get list of public (non-password-protected) rooms"""
try:
# Optional query parameters for filtering/sorting
sort_by = request.args.get('sort', 'activity') # activity, created, users, messages
limit = min(int(request.args.get('limit', 50)), 100) # Max 100 rooms
min_users = int(request.args.get('min_users', 0))
rooms_data = get_public_rooms_data(sort_by, min_users, limit)
return jsonify({
'rooms': rooms_data,
'total_count': len(rooms_data),
'sort_by': sort_by,
'timestamp': time.time()
})
except ValueError as e:
return jsonify({'error': 'Invalid parameter values'}), 400
except Exception as e:
logger.error(f"Error in get_public_rooms: {str(e)}")
return jsonify({'error': 'Internal server error'}), 500
@app.route('/api/rooms/stats')
def get_room_stats():
"""Get general statistics about rooms"""
try:
stats_data = get_room_stats_data()
return jsonify(stats_data)
except Exception as e:
logger.error(f"Error in get_room_stats: {str(e)}")
return jsonify({'error': 'Internal server error'}), 500
# Socket event handlers
@socketio.on('connect')
def handle_connect():
@@ -255,6 +300,10 @@ def handle_disconnect():
# Clean up empty rooms
if room_id in room_keys and len(room_keys[room_id]) == 0:
cleanup_room(room_id)
else:
# Broadcast public rooms update if this was a public room with remaining users
if room_id not in room_passwords:
broadcast_public_rooms_update()
del user_sessions[request.sid]
logger.info(f"User {user_id} disconnected")
@@ -266,6 +315,8 @@ def handle_join_room(data):
room_id = data.get('room_id', '').strip()
public_key = data.get('public_key', '')
password = data.get('password', '')
room_title = data.get('room_title', '').strip()
room_description = data.get('room_description', '').strip()
# Validation
if not is_valid_room_id(room_id):
@@ -276,6 +327,15 @@ def handle_join_room(data):
emit('room_joined', {'error': 'Invalid public key format'})
return
# Validate metadata length
if room_title and len(room_title) > 100:
emit('room_joined', {'error': 'Room title too long (max 100 characters)'})
return
if room_description and len(room_description) > 500:
emit('room_joined', {'error': 'Room description too long (max 500 characters)'})
return
client_ip = get_client_ip()
user_id = user_sessions[request.sid]['user_id']
@@ -295,6 +355,13 @@ def handle_join_room(data):
room_creation_times[room_id] = time.time()
message_hashes[room_id] = set()
# Store room metadata
room_metadata[room_id] = {
'title': room_title or f'Room {room_id}',
'description': room_description or 'A chat room',
'creator_ip': client_ip # For potential moderation
}
# Store hashed password if provided
if password:
room_passwords[room_id] = hash_password(password)
@@ -303,7 +370,7 @@ def handle_join_room(data):
ip_room_count[client_ip] = rooms_by_ip + 1
is_first_user = True
logger.info(f"Room {room_id} created by {user_id}")
logger.info(f"Room {room_id} created by {user_id} with title: {room_title}")
else:
# Check room capacity
if len(room_keys[room_id]) >= MAX_USERS_PER_ROOM:
@@ -358,7 +425,8 @@ def handle_join_room(data):
'users': list(user_keys.keys()),
'user_keys': user_keys,
'message_count': chat_rooms[room_id].get_message_count(),
'is_first_user': is_first_user
'is_first_user': is_first_user,
'room_metadata': room_metadata.get(room_id, {})
})
# Notify others about new user
@@ -382,6 +450,10 @@ def handle_join_room(data):
logger.info(f"User {new_user_id} joined room {room_id}")
# Broadcast public rooms update if this was a public room
if room_id not in room_passwords:
broadcast_public_rooms_update()
except Exception as e:
logger.error(f"Error in join_room: {str(e)}")
emit('room_joined', {'error': 'Internal server error'})
@@ -446,6 +518,10 @@ def handle_send_message(data):
'message_count': chat_rooms[room_id].get_message_count()
}, room=room_id)
# Broadcast public rooms update if this was a public room
if room_id not in room_passwords:
broadcast_public_rooms_update()
except Exception as e:
logger.error(f"Error in send_message: {str(e)}")
@@ -495,12 +571,104 @@ def handle_share_session_key(data):
except Exception as e:
logger.error(f"Error in share_session_key: {str(e)}")
@socketio.on('request_public_rooms')
def handle_request_public_rooms(data):
"""Handle request for public rooms data via WebSocket"""
try:
sort_by = data.get('sort', 'activity')
min_users = data.get('min_users', 0)
limit = data.get('limit', 50)
# Validate parameters
if sort_by not in ['activity', 'created', 'users', 'messages']:
sort_by = 'activity'
min_users = max(0, min(int(min_users), MAX_USERS_PER_ROOM))
limit = max(1, min(int(limit), 100))
rooms_data = get_public_rooms_data(sort_by, min_users, limit)
stats_data = get_room_stats_data()
emit('public_rooms_data', {
'rooms': rooms_data,
'stats': stats_data,
'sort_by': sort_by,
'timestamp': time.time()
})
except Exception as e:
logger.error(f"Error in request_public_rooms: {str(e)}")
emit('public_rooms_error', {'error': 'Failed to load rooms'})
@socketio.on('subscribe_public_rooms')
def handle_subscribe_public_rooms():
"""Subscribe client to public rooms updates"""
if request.sid not in user_sessions:
return
user_sessions[request.sid]['subscribed_to_public_rooms'] = True
logger.info(f"User {user_sessions[request.sid]['user_id']} subscribed to public rooms updates")
@socketio.on('unsubscribe_public_rooms')
def handle_unsubscribe_public_rooms():
"""Unsubscribe client from public rooms updates"""
if request.sid in user_sessions:
user_sessions[request.sid]['subscribed_to_public_rooms'] = False
logger.info(f"User {user_sessions[request.sid]['user_id']} unsubscribed from public rooms updates")
@socketio.on('request_public_rooms')
def handle_request_public_rooms(data):
"""Handle request for public rooms data via WebSocket"""
try:
sort_by = data.get('sort', 'activity')
min_users = data.get('min_users', 0)
limit = data.get('limit', 50)
# Validate parameters
if sort_by not in ['activity', 'created', 'users', 'messages']:
sort_by = 'activity'
min_users = max(0, min(int(min_users), MAX_USERS_PER_ROOM))
limit = max(1, min(int(limit), 100))
rooms_data = get_public_rooms_data(sort_by, min_users, limit)
stats_data = get_room_stats_data()
emit('public_rooms_data', {
'rooms': rooms_data,
'stats': stats_data,
'sort_by': sort_by,
'timestamp': time.time()
})
except Exception as e:
logger.error(f"Error in request_public_rooms: {str(e)}")
emit('public_rooms_error', {'error': 'Failed to load rooms'})
@socketio.on('subscribe_public_rooms')
def handle_subscribe_public_rooms():
"""Subscribe client to public rooms updates"""
if request.sid not in user_sessions:
return
user_sessions[request.sid]['subscribed_to_public_rooms'] = True
logger.info(f"User {user_sessions[request.sid]['user_id']} subscribed to public rooms updates")
@socketio.on('unsubscribe_public_rooms')
def handle_unsubscribe_public_rooms():
"""Unsubscribe client from public rooms updates"""
if request.sid in user_sessions:
user_sessions[request.sid]['subscribed_to_public_rooms'] = False
logger.info(f"User {user_sessions[request.sid]['user_id']} unsubscribed from public rooms updates")
# Background cleanup task
def start_cleanup_task():
def cleanup_worker():
while True:
try:
cleanup_expired_rooms()
# Broadcast updated room stats after cleanup
broadcast_public_rooms_update()
time.sleep(ROOM_CLEANUP_INTERVAL)
except Exception as e:
logger.error(f"Error in cleanup task: {str(e)}")
@@ -508,6 +676,112 @@ def start_cleanup_task():
cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
cleanup_thread.start()
def get_public_rooms_data(sort_by='activity', min_users=0, limit=50):
"""Get public rooms data - shared function for API and WebSocket"""
try:
public_rooms = []
current_time = time.time()
for room_id, room_buffer in chat_rooms.items():
# Skip password-protected rooms
if room_id in room_passwords:
continue
# Skip expired rooms
if room_buffer.is_expired():
continue
user_count = len(room_keys.get(room_id, {}))
# Apply user count filters
if user_count < min_users:
continue
room_data = {
'room_id': room_id,
'user_count': user_count,
'message_count': room_buffer.get_message_count(),
'created_at': room_creation_times.get(room_id, current_time),
'last_activity': room_buffer.last_activity,
'is_password_protected': False
}
# Add metadata if available
if room_id in room_metadata:
room_data.update(room_metadata[room_id])
else:
room_data.update({
'title': f'Room {room_id}',
'description': 'A public chat room'
})
# Calculate activity metrics
room_data['minutes_since_activity'] = int((current_time - room_buffer.last_activity) / 60)
room_data['hours_since_created'] = int((current_time - room_data['created_at']) / 3600)
public_rooms.append(room_data)
# Sort rooms
if sort_by == 'activity':
public_rooms.sort(key=lambda x: x['last_activity'], reverse=True)
elif sort_by == 'created':
public_rooms.sort(key=lambda x: x['created_at'], reverse=True)
elif sort_by == 'users':
public_rooms.sort(key=lambda x: x['user_count'], reverse=True)
elif sort_by == 'messages':
public_rooms.sort(key=lambda x: x['message_count'], reverse=True)
return public_rooms[:limit]
except Exception as e:
logger.error(f"Error getting public rooms data: {str(e)}")
return []
def get_room_stats_data():
"""Get room statistics - shared function for API and WebSocket"""
try:
current_time = time.time()
total_rooms = len(chat_rooms)
public_rooms = sum(1 for room_id in chat_rooms.keys() if room_id not in room_passwords)
private_rooms = total_rooms - public_rooms
total_users = sum(len(users) for users in room_keys.values())
total_messages = sum(room.get_message_count() for room in chat_rooms.values())
# Active rooms (activity in last hour)
active_rooms = sum(1 for room in chat_rooms.values()
if current_time - room.last_activity < 3600)
return {
'total_rooms': total_rooms,
'public_rooms': public_rooms,
'private_rooms': private_rooms,
'active_rooms': active_rooms,
'total_users': total_users,
'total_messages': total_messages,
'timestamp': current_time
}
except Exception as e:
logger.error(f"Error getting room stats: {str(e)}")
return {}
def broadcast_public_rooms_update():
"""Broadcast updated public rooms data to all connected clients"""
try:
rooms_data = get_public_rooms_data()
stats_data = get_room_stats_data()
socketio.emit('public_rooms_updated', {
'rooms': rooms_data,
'stats': stats_data,
'timestamp': time.time()
}, broadcast=True)
except Exception as e:
logger.error(f"Error broadcasting public rooms update: {str(e)}")
# Error handlers
@app.errorhandler(404)
def not_found(e):

319
src/static/rooms.js Normal file
View File

@@ -0,0 +1,319 @@
let publicRoomsData = [];
let roomsRefreshInterval = null;
let currentSortBy = 'activity';
let currentMinUsers = 0;
let isSubscribedToRooms = false;
// Show the public rooms browser
function showPublicRoomsBrowser() {
document.getElementById('publicRoomsBrowser').style.display = 'block';
loadPublicRoomsWebSocket();
subscribeToPublicRooms();
}
// Close the public rooms browser
function closePublicRoomsBrowser() {
document.getElementById('publicRoomsBrowser').style.display = 'none';
unsubscribeFromPublicRooms();
// Clear auto-refresh
if (roomsRefreshInterval) {
clearInterval(roomsRefreshInterval);
roomsRefreshInterval = null;
}
}
// Subscribe to live public rooms updates
function subscribeToPublicRooms() {
if (typeof socket !== 'undefined' && socket.connected && !isSubscribedToRooms) {
socket.emit('subscribe_public_rooms');
isSubscribedToRooms = true;
console.log('Subscribed to public rooms updates');
}
}
// Unsubscribe from public rooms updates
function unsubscribeFromPublicRooms() {
if (typeof socket !== 'undefined' && socket.connected && isSubscribedToRooms) {
socket.emit('unsubscribe_public_rooms');
isSubscribedToRooms = false;
console.log('Unsubscribed from public rooms updates');
}
}
// Load public rooms via WebSocket
function loadPublicRoomsWebSocket() {
try {
showLoadingState();
currentSortBy = document.getElementById('roomsSortSelect').value;
currentMinUsers = document.getElementById('minUsersFilter').value || 0;
if (typeof socket !== 'undefined' && socket.connected) {
socket.emit('request_public_rooms', {
sort: currentSortBy,
min_users: currentMinUsers,
limit: 50
});
} else {
// Fallback to HTTP API if WebSocket not available
loadPublicRoomsHTTP();
}
} catch (error) {
console.error('Error requesting public rooms via WebSocket:', error);
loadPublicRoomsHTTP(); // Fallback to HTTP
}
}
// Fallback HTTP method
async function loadPublicRoomsHTTP() {
try {
showLoadingState();
const sortBy = document.getElementById('roomsSortSelect').value;
const minUsers = document.getElementById('minUsersFilter').value || 0;
const response = await fetch(`/api/rooms/public?sort=${sortBy}&min_users=${minUsers}&limit=50`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
publicRoomsData = data.rooms || [];
// Also load stats
const statsResponse = await fetch('/api/rooms/stats');
if (statsResponse.ok) {
const stats = await statsResponse.json();
updateStatsDisplay(stats);
}
displayRooms();
} catch (error) {
console.error('Error loading public rooms via HTTP:', error);
showErrorState();
}
}
// Refresh public rooms
function refreshPublicRooms() {
loadPublicRoomsWebSocket();
}
// Update stats display
function updateStatsDisplay(stats) {
const statsElement = document.getElementById('roomsStats');
statsElement.textContent = `${stats.public_rooms} public rooms • ${stats.total_users} users online`;
}
// Show loading state
function showLoadingState() {
document.getElementById('roomsLoading').style.display = 'flex';
document.getElementById('roomsList').style.display = 'none';
document.getElementById('roomsEmpty').style.display = 'none';
}
// Show error state
function showErrorState() {
document.getElementById('roomsLoading').style.display = 'none';
document.getElementById('roomsList').style.display = 'none';
document.getElementById('roomsEmpty').style.display = 'flex';
const emptyElement = document.getElementById('roomsEmpty');
emptyElement.innerHTML = `
<div style="font-size: 3rem; margin-bottom: 1rem;">⚠️</div>
<h3 style="margin: 0 0 0.5rem 0; color: #ffffff;">Failed to Load Rooms</h3>
<p style="margin: 0 0 1rem 0;">Unable to connect to the server. Please try again.</p>
<button class="btn" onclick="refreshPublicRooms()">Retry</button>
`;
}
// Display rooms
function displayRooms() {
const roomsList = document.getElementById('roomsList');
const roomsLoading = document.getElementById('roomsLoading');
const roomsEmpty = document.getElementById('roomsEmpty');
roomsLoading.style.display = 'none';
if (publicRoomsData.length === 0) {
roomsEmpty.style.display = 'flex';
roomsList.style.display = 'none';
return;
}
roomsEmpty.style.display = 'none';
roomsList.style.display = 'block';
roomsList.innerHTML = publicRoomsData.map(room => createRoomCard(room)).join('');
}
// Create room card HTML
function createRoomCard(room) {
const activityClass = getActivityClass(room.minutes_since_activity);
const timeAgo = formatTimeAgo(room.minutes_since_activity);
return `
<div class="room-card" onclick="joinPublicRoom('${room.room_id}')">
<div class="room-title">
${escapeHtml(room.title || room.room_id)}
<span class="activity-indicator ${activityClass}"></span>
</div>
<div class="room-description">
${escapeHtml(room.description || 'No description')}
</div>
<div class="room-stats">
<div class="room-stat">
<span>👥</span>
<span>${room.user_count} user${room.user_count !== 1 ? 's' : ''}</span>
</div>
<div class="room-stat">
<span>💬</span>
<span>${room.message_count} message${room.message_count !== 1 ? 's' : ''}</span>
</div>
<div class="room-stat">
<span>🕐</span>
<span>Active ${timeAgo}</span>
</div>
<div class="room-stat">
<span>🆔</span>
<span>${room.room_id}</span>
</div>
</div>
</div>
`;
}
// Get activity indicator class
function getActivityClass(minutesAgo) {
if (minutesAgo <= 5) return 'activity-active';
if (minutesAgo <= 30) return 'activity-recent';
return 'activity-old';
}
// Format time ago
function formatTimeAgo(minutes) {
if (minutes < 1) return 'just now';
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Join a public room - integrated with your existing system
function joinPublicRoom(roomId) {
console.log(`Joining public room: ${roomId}`);
// Close the browser
closePublicRoomsBrowser();
// Fill in the room input with the selected room
document.getElementById('roomInput').value = roomId;
// Clear password field since these are public rooms
document.getElementById('roomPasswordInput').value = '';
// Trigger your existing join room functionality
const joinButton = document.getElementById('joinRoomBtn');
if (joinButton) {
joinButton.click();
}
}
// Event listeners for controls
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('roomsSortSelect').addEventListener('change', refreshPublicRooms);
document.getElementById('minUsersFilter').addEventListener('change', refreshPublicRooms);
});
// Close on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && document.getElementById('publicRoomsBrowser').style.display === 'block') {
closePublicRoomsBrowser();
}
});
// Close on backdrop click
document.getElementById('publicRoomsBrowser').addEventListener('click', function(e) {
if (e.target === this) {
closePublicRoomsBrowser();
}
});
// WebSocket event handlers for real-time updates
function setupPublicRoomsWebSocketHandlers() {
if (typeof socket === 'undefined') {
console.log('Socket not available, using HTTP fallback');
return;
}
// Handle public rooms data response
socket.on('public_rooms_data', function(data) {
console.log('Received public rooms data via WebSocket:', data);
publicRoomsData = data.rooms || [];
if (data.stats) {
updateStatsDisplay(data.stats);
}
displayRooms();
});
// Handle live updates to public rooms
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
if (document.getElementById('publicRoomsBrowser').style.display === 'block' && isSubscribedToRooms) {
publicRoomsData = data.rooms || [];
if (data.stats) {
updateStatsDisplay(data.stats);
}
displayRooms();
}
});
// Handle WebSocket errors for public rooms
socket.on('public_rooms_error', function(data) {
console.error('Public rooms WebSocket error:', data);
showErrorState();
});
console.log('Public rooms WebSocket handlers attached');
}
// Auto-setup WebSocket handlers when page loads
function waitForSocketAndSetupHandlers() {
if (typeof socket !== 'undefined' && socket.connected) {
setupPublicRoomsWebSocketHandlers();
} else {
setTimeout(waitForSocketAndSetupHandlers, 100);
}
}
// Start the setup process
document.addEventListener('DOMContentLoaded', function() {
waitForSocketAndSetupHandlers();
// Auto-refresh every 30 seconds when browser is open
setInterval(function() {
if (document.getElementById('publicRoomsBrowser').style.display === 'block') {
refreshPublicRooms();
}
}, 30000);
});

View File

@@ -617,3 +617,77 @@ ul.release-list a:hover {
max-width: 100%;
object-fit: contain;
}
/* discovery */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.room-card {
background: #252525;
border: 1px solid #333;
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.75rem;
transition: all 0.2s ease;
cursor: pointer;
}
.room-card:hover {
background: #2a2a2a;
border-color: #00ff88;
transform: translateY(-1px);
}
.room-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
color: #ffffff;
display: flex;
align-items: center;
gap: 0.5rem;
}
.room-description {
color: #b0b0b0;
font-size: 0.9rem;
margin: 0 0 0.75rem 0;
line-height: 1.4;
}
.room-stats {
display: flex;
gap: 1rem;
flex-wrap: wrap;
font-size: 0.8rem;
color: #888;
}
.room-stat {
display: flex;
align-items: center;
gap: 0.25rem;
}
.activity-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.activity-active { background: #00ff88; }
.activity-recent { background: #ffa500; }
.activity-old { background: #666; }
@media (max-width: 768px) {
.room-stats {
gap: 0.5rem;
}
.room-card {
padding: 0.75rem;
}
}

View File

@@ -7,13 +7,12 @@
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<meta http-equiv="X-Frame-Options" content="DENY">
<link rel="stylesheet" href="{{ url_for('static', filename='stylesheet.css') }}">
<link rel="stylesheet" href="../static/stylesheet.css">
<link rel="icon" type="image/png" href="{{ url_for('static', filename='favicon.png') }}">
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.2/dist/socket.io.min.js"></script>
<!-- Everything Locally Hosted ( javascript files for frontend / client encryption )-->
<script src="{{ url_for('static', filename='script.js') }}"></script>
<script src="{{ url_for('static', filename='frontend.js') }}"></script>
<script src="{{ url_for("static", filename="rss.js") }}" async></script>
<script src="{{ url_for("static", filename="rss.js")}}" async></script>
</head>
<body style="overflow-y: auto;">
<div class="chat-container">
@@ -61,6 +60,12 @@
<button class="btn" id="joinRoomBtn" aria-label="Join existing room" style="flex: 1; min-width: 120px; font-size: clamp(0.85rem, 3.5vw, 1rem); padding: 0.6rem 1rem;">Join Room</button>
<button class="btn btn-secondary" id="createRoomBtn" aria-label="Create new room" style="flex: 1; min-width: 120px; font-size: clamp(0.85rem, 3.5vw, 1rem); padding: 0.6rem 1rem;">Create New Room</button>
</div>
<!-- Browse Public Rooms Button -->
<div style="display: flex; justify-content: center; width: 100%;">
<button class="btn btn-secondary" onclick="showPublicRoomsBrowser()" style="font-size: clamp(0.85rem, 3.5vw, 1rem); padding: 0.6rem 1.5rem;">
Browse Public Rooms
</button>
</div>
</div>
<div class="status-text" id="statusText" style="font-size: clamp(0.8rem, 3vw, 0.9rem);">
<div class="loading-message">
@@ -77,13 +82,6 @@
alt="ByteChat Desktop Application"
style="width: clamp(200px, 50vw, 400px); height: auto; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); transition: transform 0.3s ease, box-shadow 0.3s ease; margin-bottom: 1.5rem; display: block;">
<!-- local testing image. uncomment if developing locally
<img class="laptopimg"
src="../static/laptop.png"
alt="ByteChat Desktop Application"
style="width: clamp(200px, 50vw, 400px); height: auto; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); transition: transform 0.3s ease, box-shadow 0.3s ease; margin-bottom: 1.5rem; display: block;">
-->
<!-- Desktop App Section -->
<div style="text-align: center; max-width: 600px; padding: 0 1rem;">
<h2 style="font-size: clamp(1.25rem, 5vw, 1.75rem); margin-bottom: 1rem; color: #00ff88;">Desktop Application</h2>
@@ -120,6 +118,180 @@
</div>
</div>
<div id="publicRoomsBrowser" style="
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(10px);
z-index: 2000;
padding: 1rem;
/* Center content */
display: flex;
justify-content: center;
align-items: center;
/* Optional: allow content to scroll if it overflows on small screens */
overflow: auto;
">
<div id="browserContent" style="
max-width: 95%;
max-height: 95%;
overflow-y: auto;
background: #fff; /* or whatever your modal background is */
border-radius: 8px;
padding: 1rem;
">
<div style="
max-width: 800px;
margin: 0 auto;
background: #1a1a1a;
border-radius: 12px;
border: 1px solid #333;
min-height: 80vh;
display: flex;
flex-direction: column;
">
<!-- Header -->
<div style="
padding: 1.5rem;
border-bottom: 1px solid #333;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
">
<div>
<h2 style="
margin: 0 0 0.5rem 0;
font-size: clamp(1.25rem, 5vw, 1.75rem);
color: #00ff88;
">Public Rooms</h2>
<p style="
margin: 0;
color: #888;
font-size: clamp(0.85rem, 3.5vw, 1rem);
" id="roomsStats">Browse and join active public rooms</p>
</div>
<button
class="btn btn-secondary"
onclick="closePublicRoomsBrowser()"
style="
padding: 0.5rem 1rem;
font-size: 0.9rem;
border-radius: 6px;
"
>Close</button>
</div>
<!-- Controls -->
<div style="
padding: 1rem 1.5rem;
border-bottom: 1px solid #333;
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
">
<select id="roomsSortSelect" style="
background: #333;
color: #ffffff;
border: 1px solid #555;
border-radius: 6px;
padding: 0.5rem;
font-size: 0.9rem;
min-width: 120px;
">
<option value="activity">Most Active</option>
<option value="created">Newest</option>
<option value="users">Most Users</option>
<option value="messages">Most Messages</option>
</select>
<input
type="number"
id="minUsersFilter"
placeholder="Min users"
min="0"
max="1000"
style="
background: #333;
color: #ffffff;
border: 1px solid #555;
border-radius: 6px;
padding: 0.5rem;
font-size: 0.9rem;
width: 150px;
"
>
<button
class="btn"
onclick="refreshPublicRooms()"
style="
padding: 0.5rem 1rem;
font-size: 0.9rem;
"
>Refresh</button>
</div>
<!-- Loading State -->
<div id="roomsLoading" style="
display: flex;
justify-content: center;
align-items: center;
padding: 3rem;
flex: 1;
">
<div style="
display: flex;
align-items: center;
gap: 1rem;
color: #888;
">
<div style="
width: 20px;
height: 20px;
border: 2px solid #333;
border-top: 2px solid #00ff88;
border-radius: 50%;
animation: spin 1s linear infinite;
"></div>
<span>Loading public rooms...</span>
</div>
</div>
<!-- Rooms List -->
<div id="roomsList" style="
flex: 1;
padding: 1rem 1.5rem;
display: none;
">
<!-- Rooms will be populated here -->
</div>
<!-- Empty State -->
<div id="roomsEmpty" style="
display: none; /* This should be none initially, not flex ; fuck CSS*/
justify-content: center;
align-items: center;
padding: 3rem;
flex: 1;
text-align: center;
">
<div>
<h3 style="margin: 0 0 0.5rem 0; color: #ffffff;">… No Public Rooms Found</h3>
<p style="margin: 0; color: #888;">Check back later or create your own room</p>
</div>
</div>
</div>
</div>
<!-- Chat Screen -->
<div id="chatScreen" style="display: none;" class="chat-screen">
<div class="room-info" id="roomInfo" style="display: none; padding: 0.6rem 0.75rem; flex-wrap: wrap; gap: 0.5rem;">
@@ -165,10 +337,6 @@
</div>
</div>
</div>
<!-- <div class="security-indicator" id="securityIndicator" style="display: none; bottom: calc(0.75rem + 80px); right: 15px; padding: 0.4rem 0.8rem; font-size: clamp(0.7rem, 3vw, 0.8rem); z-index: 999;">
🔒 End-to-End Encrypted
</div>
-->
<script src="{{ url_for('static', filename="rooms.js") }}"></script>
</body>
</html>