chats: add chat avatars and profile view-only modal
All checks were successful
CI / test (push) Successful in 23s
All checks were successful
CI / test (push) Successful in 23s
This commit is contained in:
@@ -32,6 +32,7 @@ class Chat(Base):
|
||||
public_id: Mapped[str] = mapped_column(String(24), unique=True, index=True, nullable=False, default=generate_public_id)
|
||||
type: Mapped[ChatType] = mapped_column(SAEnum(ChatType), nullable=False, index=True)
|
||||
title: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
avatar_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
handle: Mapped[str | None] = mapped_column(String(64), nullable=True, unique=True, index=True)
|
||||
description: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
is_public: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="false")
|
||||
|
||||
@@ -15,6 +15,7 @@ from app.chats.schemas import (
|
||||
ChatNotificationSettingsRead,
|
||||
ChatNotificationSettingsUpdate,
|
||||
ChatPinMessageRequest,
|
||||
ChatProfileUpdateRequest,
|
||||
ChatRead,
|
||||
ChatTitleUpdateRequest,
|
||||
)
|
||||
@@ -40,6 +41,7 @@ from app.chats.service import (
|
||||
set_chat_pinned_for_user,
|
||||
update_chat_member_role_for_user,
|
||||
update_chat_notification_settings_for_user,
|
||||
update_chat_profile_for_user,
|
||||
update_chat_title_for_user,
|
||||
)
|
||||
from app.database.session import get_db
|
||||
@@ -139,6 +141,18 @@ async def update_chat_title(
|
||||
return await serialize_chat_for_user(db, user_id=current_user.id, chat=chat)
|
||||
|
||||
|
||||
@router.patch("/{chat_id}/profile", response_model=ChatRead)
|
||||
async def update_chat_profile(
|
||||
chat_id: int,
|
||||
payload: ChatProfileUpdateRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> ChatRead:
|
||||
chat = await update_chat_profile_for_user(db, chat_id=chat_id, user_id=current_user.id, payload=payload)
|
||||
await realtime_gateway.publish_chat_updated(chat_id=chat.id)
|
||||
return await serialize_chat_for_user(db, user_id=current_user.id, chat=chat)
|
||||
|
||||
|
||||
@router.get("/{chat_id}/members", response_model=list[ChatMemberRead])
|
||||
async def list_chat_members(
|
||||
chat_id: int,
|
||||
|
||||
@@ -13,6 +13,7 @@ class ChatRead(BaseModel):
|
||||
public_id: str
|
||||
type: ChatType
|
||||
title: str | None = None
|
||||
avatar_url: str | None = None
|
||||
display_title: str | None = None
|
||||
handle: str | None = None
|
||||
description: str | None = None
|
||||
@@ -29,6 +30,7 @@ class ChatRead(BaseModel):
|
||||
counterpart_user_id: int | None = None
|
||||
counterpart_name: str | None = None
|
||||
counterpart_username: str | None = None
|
||||
counterpart_avatar_url: str | None = None
|
||||
counterpart_is_online: bool | None = None
|
||||
counterpart_last_seen_at: datetime | None = None
|
||||
last_message_text: str | None = None
|
||||
@@ -72,6 +74,12 @@ class ChatTitleUpdateRequest(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=255)
|
||||
|
||||
|
||||
class ChatProfileUpdateRequest(BaseModel):
|
||||
title: str | None = Field(default=None, min_length=1, max_length=255)
|
||||
description: str | None = Field(default=None, max_length=512)
|
||||
avatar_url: str | None = Field(default=None, max_length=512)
|
||||
|
||||
|
||||
class ChatPinMessageRequest(BaseModel):
|
||||
message_id: int | None = None
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from app.chats.schemas import (
|
||||
ChatNotificationSettingsUpdate,
|
||||
ChatInviteLinkRead,
|
||||
ChatPinMessageRequest,
|
||||
ChatProfileUpdateRequest,
|
||||
ChatRead,
|
||||
ChatTitleUpdateRequest,
|
||||
)
|
||||
@@ -40,6 +41,16 @@ async def _can_view_last_seen(*, db: AsyncSession, target_user, viewer_user_id:
|
||||
return await is_user_in_contacts(db, owner_user_id=target_user.id, candidate_user_id=viewer_user_id)
|
||||
|
||||
|
||||
async def _can_view_avatar(*, db: AsyncSession, target_user, viewer_user_id: int) -> bool:
|
||||
if target_user.id == viewer_user_id:
|
||||
return True
|
||||
if target_user.privacy_avatar == "everyone":
|
||||
return True
|
||||
if target_user.privacy_avatar == "nobody":
|
||||
return False
|
||||
return await is_user_in_contacts(db, owner_user_id=target_user.id, candidate_user_id=viewer_user_id)
|
||||
|
||||
|
||||
async def _can_invite_to_group(*, db: AsyncSession, target_user, actor_user_id: int) -> bool:
|
||||
if target_user.id == actor_user_id:
|
||||
return False
|
||||
@@ -66,6 +77,7 @@ async def serialize_chat_for_user(
|
||||
counterpart_user_id: int | None = None
|
||||
counterpart_name: str | None = None
|
||||
counterpart_username: str | None = None
|
||||
counterpart_avatar_url: str | None = None
|
||||
counterpart_is_online: bool | None = None
|
||||
counterpart_last_seen_at = None
|
||||
if chat.is_saved:
|
||||
@@ -81,6 +93,8 @@ async def serialize_chat_for_user(
|
||||
counterpart_username = counterpart.username
|
||||
presence_allowed = await _can_view_last_seen(db=db, target_user=counterpart, viewer_user_id=user_id)
|
||||
counterpart_last_seen_at = counterpart.last_seen_at if presence_allowed else None
|
||||
avatar_allowed = await _can_view_avatar(db=db, target_user=counterpart, viewer_user_id=user_id)
|
||||
counterpart_avatar_url = counterpart.avatar_url if avatar_allowed else None
|
||||
presence = await get_users_online_map([counterpart_id])
|
||||
if counterpart:
|
||||
presence_allowed = await _can_view_last_seen(db=db, target_user=counterpart, viewer_user_id=user_id)
|
||||
@@ -114,6 +128,7 @@ async def serialize_chat_for_user(
|
||||
"public_id": chat.public_id,
|
||||
"type": chat.type,
|
||||
"title": chat.title,
|
||||
"avatar_url": chat.avatar_url,
|
||||
"display_title": display_title,
|
||||
"handle": chat.handle,
|
||||
"description": chat.description,
|
||||
@@ -130,6 +145,7 @@ async def serialize_chat_for_user(
|
||||
"counterpart_user_id": counterpart_user_id,
|
||||
"counterpart_name": counterpart_name,
|
||||
"counterpart_username": counterpart_username,
|
||||
"counterpart_avatar_url": counterpart_avatar_url,
|
||||
"counterpart_is_online": counterpart_is_online,
|
||||
"counterpart_last_seen_at": counterpart_last_seen_at,
|
||||
"last_message_text": last_message.text if last_message else None,
|
||||
@@ -300,6 +316,29 @@ async def update_chat_title_for_user(
|
||||
return chat
|
||||
|
||||
|
||||
async def update_chat_profile_for_user(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
chat_id: int,
|
||||
user_id: int,
|
||||
payload: ChatProfileUpdateRequest,
|
||||
) -> Chat:
|
||||
chat, membership = await _get_chat_and_membership(db, chat_id=chat_id, user_id=user_id)
|
||||
_ensure_group_or_channel(chat.type)
|
||||
_ensure_manage_permission(membership.role)
|
||||
|
||||
if payload.title is not None:
|
||||
chat.title = payload.title.strip() or chat.title
|
||||
if payload.description is not None:
|
||||
chat.description = payload.description.strip() or None
|
||||
if payload.avatar_url is not None:
|
||||
chat.avatar_url = payload.avatar_url.strip() or None
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(chat)
|
||||
return chat
|
||||
|
||||
|
||||
async def add_chat_member_for_user(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
@@ -454,6 +493,7 @@ async def discover_public_chats_for_user(db: AsyncSession, *, user_id: int, quer
|
||||
"public_id": chat.public_id,
|
||||
"type": chat.type,
|
||||
"title": chat.title,
|
||||
"avatar_url": chat.avatar_url,
|
||||
"handle": chat.handle,
|
||||
"description": chat.description,
|
||||
"is_public": chat.is_public,
|
||||
|
||||
Reference in New Issue
Block a user