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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user