testing the new room discovery changes. you can neow discover public rooms ( ones with no password )
This commit is contained in:
284
src/app.py
284
src/app.py
@@ -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
319
src/static/rooms.js
Normal 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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user