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

@@ -7,7 +7,7 @@ from sqlalchemy import and_
from typing import List, Optional
from datetime import datetime
from app.modules.chat_room.models import RoomMember, IncidentRoom, MemberRole
from app.modules.chat_room.models import RoomMember, IncidentRoom, MemberRole, RoomStatus
class MembershipService:
@@ -16,6 +16,73 @@ class MembershipService:
# System admin email (hardcoded as per requirement)
SYSTEM_ADMIN_EMAIL = "ymirliu@panjit.com.tw"
def self_join_room(
self,
db: Session,
room_id: str,
user_id: str
) -> tuple[Optional[RoomMember], str, Optional[RoomMember]]:
"""Self-join a room as a viewer
Allows any authenticated user to join a room without invitation.
User joins as VIEWER role.
Args:
db: Database session
room_id: Room ID
user_id: User joining
Returns:
Tuple of (member, error_code, existing_member)
- On success: (member, "", None)
- If already member: (None, "already_member", existing_member)
- If room archived: (None, "room_archived", None)
- If room not found: (None, "room_not_found", None)
"""
# Check if room exists
room = db.query(IncidentRoom).filter(
IncidentRoom.room_id == room_id
).first()
if not room:
return None, "room_not_found", None
# Check if room is archived
if room.status == RoomStatus.ARCHIVED:
return None, "room_archived", None
# Check if already a member
existing = db.query(RoomMember).filter(
and_(
RoomMember.room_id == room_id,
RoomMember.user_id == user_id,
RoomMember.removed_at.is_(None)
)
).first()
if existing:
return None, "already_member", existing
# Create membership as viewer
member = RoomMember(
room_id=room_id,
user_id=user_id,
role=MemberRole.VIEWER,
added_by=user_id, # Self-added
added_at=datetime.utcnow()
)
db.add(member)
# Update member count
self._update_member_count(db, room_id)
# Update room activity
room.last_activity_at = datetime.utcnow()
db.commit()
db.refresh(member)
return member, "", None
def add_member(
self,
db: Session,
@@ -101,6 +168,120 @@ class MembershipService:
db.commit()
return True
def can_change_member_role(
self,
db: Session,
room_id: str,
changer_id: str,
target_id: str,
current_role: MemberRole,
new_role: MemberRole
) -> tuple[bool, str]:
"""Check if a user can change another member's role
Permission rules:
- OWNER can change any role
- EDITOR can upgrade VIEWER → EDITOR only
- EDITOR cannot downgrade, remove, or set OWNER role
- VIEWER cannot change roles
Args:
db: Database session
room_id: Room ID
changer_id: User attempting the change
target_id: Target user
current_role: Target's current role
new_role: Requested new role
Returns:
Tuple of (allowed, error_message)
"""
# System admin can do anything
if self.is_system_admin(changer_id):
return True, ""
changer_role = self.get_user_role_in_room(db, room_id, changer_id)
if not changer_role:
return False, "Not a member of this room"
# Owner can change any role
if changer_role == MemberRole.OWNER:
return True, ""
# Editor permissions
if changer_role == MemberRole.EDITOR:
# Cannot set owner role
if new_role == MemberRole.OWNER:
return False, "Only owner can transfer ownership"
# Can only upgrade viewer to editor
if current_role == MemberRole.VIEWER and new_role == MemberRole.EDITOR:
return True, ""
# Cannot downgrade
if current_role == MemberRole.EDITOR and new_role == MemberRole.VIEWER:
return False, "Editors can only upgrade members"
return False, "Editors can only upgrade viewers to editor"
# Viewer cannot change roles
return False, "Insufficient permissions"
def can_remove_member(
self,
db: Session,
room_id: str,
remover_id: str,
target_id: str
) -> tuple[bool, str]:
"""Check if a user can remove another member
Permission rules:
- OWNER can remove any member
- EDITOR cannot remove members
- VIEWER cannot remove members
- Users can remove themselves (leave room)
Args:
db: Database session
room_id: Room ID
remover_id: User attempting the removal
target_id: Target user
Returns:
Tuple of (allowed, error_message)
"""
# System admin can do anything
if self.is_system_admin(remover_id):
return True, ""
remover_role = self.get_user_role_in_room(db, room_id, remover_id)
if not remover_role:
return False, "Not a member of this room"
# User can leave the room (remove themselves)
if remover_id == target_id:
# But owner cannot leave if they're the only owner
if remover_role == MemberRole.OWNER:
members = self.get_room_members(db, room_id)
owner_count = sum(1 for m in members if m.role == MemberRole.OWNER)
if owner_count == 1:
return False, "Cannot leave: you are the only owner"
return True, ""
# Owner can remove any member
if remover_role == MemberRole.OWNER:
return True, ""
# Editor cannot remove members
if remover_role == MemberRole.EDITOR:
return False, "Only owner can remove members"
# Viewer cannot remove members
return False, "Insufficient permissions"
def update_member_role(
self,
db: Session,