feat: Add mobile responsive layout, open room access, and admin room management

Mobile Responsive Layout:
- Add useMediaQuery, useIsMobile, useIsTablet, useIsDesktop hooks for device detection
- Create MobileHeader component with hamburger menu and action drawer
- Create BottomToolbar for mobile navigation (Files, Members)
- Create SlidePanel component for full-screen mobile sidebars
- Update RoomDetail.tsx with mobile/desktop conditional rendering
- Update RoomList.tsx with single-column grid and touch-friendly buttons
- Add CSS custom properties for safe areas and touch targets (min 44px)
- Add mobile viewport meta tags for notched devices

Open Room Access:
- All authenticated users can view all rooms (not just their own)
- Users can join active rooms they're not members of
- Add is_member field to room responses
- Update room list API to return all rooms by default

Admin Room Management:
- Add permanent delete functionality for system admins
- Add delete confirmation dialog with room title verification
- Broadcast room deletion via WebSocket to connected users
- Add users search API for adding members

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-05 09:12:10 +08:00
parent 1e44a63a8e
commit 1d5d4d447d
48 changed files with 3505 additions and 401 deletions

View File

@@ -8,8 +8,9 @@ import json
from app.core.database import get_db
from app.modules.auth.dependencies import get_current_user
from app.modules.auth.services.session_service import session_service
from app.modules.chat_room.models import RoomMember, MemberRole
from app.modules.realtime.websocket_manager import manager
from app.modules.realtime.websocket_manager import manager, json_serializer
from app.modules.realtime.services.message_service import MessageService
from app.modules.realtime.schemas import (
WebSocketMessageIn,
@@ -34,6 +35,11 @@ router = APIRouter(prefix="/api", tags=["realtime"])
SYSTEM_ADMIN_EMAIL = "ymirliu@panjit.com.tw"
async def ws_send_json(websocket: WebSocket, data: dict):
"""Send JSON with custom datetime serializer"""
await websocket.send_text(json.dumps(data, default=json_serializer))
def get_user_room_membership(db: Session, room_id: str, user_id: str) -> Optional[RoomMember]:
"""Check if user is a member of the room"""
return db.query(RoomMember).filter(
@@ -79,9 +85,17 @@ async def websocket_endpoint(
db: Session = next(get_db())
try:
# For now, we'll extract user from cookie or token
# TODO: Implement proper WebSocket token authentication
user_id = token if token else "anonymous@example.com" # Placeholder
# Authenticate token via session lookup
if not token:
await websocket.close(code=4001, reason="Authentication required")
return
user_session = session_service.get_session_by_token(db, token)
if not user_session:
await websocket.close(code=4001, reason="Invalid or expired token")
return
user_id = user_session.username
# Check room membership
membership = get_user_room_membership(db, room_id, user_id)
@@ -114,7 +128,7 @@ async def websocket_endpoint(
try:
ws_message = WebSocketMessageIn(**message_data)
except Exception as e:
await websocket.send_json(
await ws_send_json(websocket,
ErrorMessage(error=str(e), code="INVALID_MESSAGE").dict()
)
continue
@@ -123,7 +137,7 @@ async def websocket_endpoint(
if ws_message.type == WebSocketMessageType.MESSAGE:
# Check write permission
if not can_write_message(membership, user_id):
await websocket.send_json(
await ws_send_json(websocket,
ErrorMessage(
error="Insufficient permissions",
code="PERMISSION_DENIED"
@@ -142,7 +156,7 @@ async def websocket_endpoint(
)
# Send acknowledgment to sender
await websocket.send_json(
await ws_send_json(websocket,
MessageAck(
message_id=message.message_id,
sequence_number=message.sequence_number,
@@ -167,7 +181,7 @@ async def websocket_endpoint(
elif ws_message.type == WebSocketMessageType.EDIT_MESSAGE:
if not ws_message.message_id or not ws_message.content:
await websocket.send_json(
await ws_send_json(websocket,
ErrorMessage(error="Missing message_id or content", code="INVALID_REQUEST").dict()
)
continue
@@ -181,7 +195,7 @@ async def websocket_endpoint(
)
if not edited_message:
await websocket.send_json(
await ws_send_json(websocket,
ErrorMessage(error="Cannot edit message", code="EDIT_FAILED").dict()
)
continue
@@ -205,7 +219,7 @@ async def websocket_endpoint(
elif ws_message.type == WebSocketMessageType.DELETE_MESSAGE:
if not ws_message.message_id:
await websocket.send_json(
await ws_send_json(websocket,
ErrorMessage(error="Missing message_id", code="INVALID_REQUEST").dict()
)
continue
@@ -220,7 +234,7 @@ async def websocket_endpoint(
)
if not deleted_message:
await websocket.send_json(
await ws_send_json(websocket,
ErrorMessage(error="Cannot delete message", code="DELETE_FAILED").dict()
)
continue
@@ -233,7 +247,7 @@ async def websocket_endpoint(
elif ws_message.type == WebSocketMessageType.ADD_REACTION:
if not ws_message.message_id or not ws_message.emoji:
await websocket.send_json(
await ws_send_json(websocket,
ErrorMessage(error="Missing message_id or emoji", code="INVALID_REQUEST").dict()
)
continue
@@ -260,7 +274,7 @@ async def websocket_endpoint(
elif ws_message.type == WebSocketMessageType.REMOVE_REACTION:
if not ws_message.message_id or not ws_message.emoji:
await websocket.send_json(
await ws_send_json(websocket,
ErrorMessage(error="Missing message_id or emoji", code="INVALID_REQUEST").dict()
)
continue

View File

@@ -1,12 +1,19 @@
"""WebSocket connection pool management"""
from fastapi import WebSocket
from typing import Dict, List, Set
from typing import Dict, List, Set, Any
from datetime import datetime
import asyncio
import json
from collections import defaultdict
def json_serializer(obj: Any) -> str:
"""Custom JSON serializer for objects not serializable by default json code"""
if isinstance(obj, datetime):
return obj.isoformat()
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
class ConnectionInfo:
"""Information about a WebSocket connection"""
def __init__(self, websocket: WebSocket, user_id: str, room_id: str):
@@ -93,7 +100,7 @@ class WebSocketManager:
if room_id not in self._room_connections:
return
message_json = json.dumps(message)
message_json = json.dumps(message, default=json_serializer)
# Collect disconnected connections
disconnected = []
@@ -124,7 +131,7 @@ class WebSocketManager:
return
conn_info = self._user_connections[user_id]
message_json = json.dumps(message)
message_json = json.dumps(message, default=json_serializer)
try:
await conn_info.websocket.send_text(message_json)