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