chats: add chat avatars and profile view-only modal
All checks were successful
CI / test (push) Successful in 23s

This commit is contained in:
2026-03-08 13:53:29 +03:00
parent f7413bc626
commit bc9d943d11
10 changed files with 236 additions and 114 deletions

View File

@@ -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")

View File

@@ -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,

View File

@@ -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

View File

@@ -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,