feat: mentions badge in chat list and muted-mention delivery
All checks were successful
CI / test (push) Successful in 21s

This commit is contained in:
2026-03-08 12:23:39 +03:00
parent fc7a9cc3a6
commit 0594b890c3
9 changed files with 92 additions and 75 deletions

View File

@@ -237,6 +237,42 @@ async def get_unread_count_for_chat(db: AsyncSession, *, chat_id: int, user_id:
return int(result.scalar_one() or 0)
async def get_unread_mentions_count_for_chat(
db: AsyncSession,
*,
chat_id: int,
user_id: int,
username: str | None,
) -> int:
normalized_username = (username or "").strip().lower()
if not normalized_username:
return 0
last_read_subquery = (
select(MessageReceipt.last_read_message_id)
.where(MessageReceipt.chat_id == chat_id, MessageReceipt.user_id == user_id)
.limit(1)
.scalar_subquery()
)
mention_like = f"%@{normalized_username}%"
stmt = (
select(func.count(Message.id))
.outerjoin(
MessageHidden,
(MessageHidden.message_id == Message.id) & (MessageHidden.user_id == user_id),
)
.where(
Message.chat_id == chat_id,
Message.sender_id != user_id,
MessageHidden.id.is_(None),
Message.id > func.coalesce(last_read_subquery, 0),
Message.text.is_not(None),
func.lower(Message.text).like(mention_like),
)
)
result = await db.execute(stmt)
return int(result.scalar_one() or 0)
async def get_last_visible_message_for_user(
db: AsyncSession,
*,

View File

@@ -21,6 +21,7 @@ class ChatRead(BaseModel):
archived: bool = False
pinned: bool = False
unread_count: int = 0
unread_mentions_count: int = 0
pinned_message_id: int | None = None
members_count: int | None = None
online_count: int | None = None

View File

@@ -30,7 +30,13 @@ from app.realtime.presence import get_users_online_map
from app.users.repository import get_user_by_id, has_block_relation_between_users
async def serialize_chat_for_user(db: AsyncSession, *, user_id: int, chat: Chat) -> ChatRead:
async def serialize_chat_for_user(
db: AsyncSession,
*,
user_id: int,
chat: Chat,
current_username: str | None = None,
) -> ChatRead:
display_title = chat.title
my_role = None
membership = await repository.get_chat_member(db, chat_id=chat.id, user_id=user_id)
@@ -66,7 +72,16 @@ async def serialize_chat_for_user(db: AsyncSession, *, user_id: int, chat: Chat)
if chat.type == ChatType.CHANNEL:
subscribers_count = members_count
if not current_username:
current_user = await get_user_by_id(db, user_id)
current_username = current_user.username if current_user else None
unread_count = await repository.get_unread_count_for_chat(db, chat_id=chat.id, user_id=user_id)
unread_mentions_count = await repository.get_unread_mentions_count_for_chat(
db,
chat_id=chat.id,
user_id=user_id,
username=current_username,
)
last_message = await repository.get_last_visible_message_for_user(db, chat_id=chat.id, user_id=user_id)
user_setting = await repository.get_chat_user_setting(db, chat_id=chat.id, user_id=user_id)
archived = bool(user_setting and user_setting.archived)
@@ -86,6 +101,7 @@ async def serialize_chat_for_user(db: AsyncSession, *, user_id: int, chat: Chat)
"archived": archived,
"pinned": pinned,
"unread_count": unread_count,
"unread_mentions_count": unread_mentions_count,
"pinned_message_id": chat.pinned_message_id,
"members_count": members_count,
"online_count": online_count,
@@ -105,7 +121,12 @@ async def serialize_chat_for_user(db: AsyncSession, *, user_id: int, chat: Chat)
async def serialize_chats_for_user(db: AsyncSession, *, user_id: int, chats: list[Chat]) -> list[ChatRead]:
return [await serialize_chat_for_user(db, user_id=user_id, chat=chat) for chat in chats]
current_user = await get_user_by_id(db, user_id)
current_username = current_user.username if current_user else None
return [
await serialize_chat_for_user(db, user_id=user_id, chat=chat, current_username=current_username)
for chat in chats
]
async def create_chat_for_user(db: AsyncSession, *, creator_id: int, payload: ChatCreateRequest) -> Chat:

View File

@@ -44,8 +44,6 @@ async def dispatch_message_notifications(db: AsyncSession, message: Message) ->
sender_name = sender_users[0].username if sender_users else "Someone"
for recipient in users:
if await is_chat_muted_for_user(db, chat_id=message.chat_id, user_id=recipient.id):
continue
base_payload = {
"chat_id": message.chat_id,
"message_id": message.id,
@@ -71,6 +69,9 @@ async def dispatch_message_notifications(db: AsyncSession, message: Message) ->
)
continue
if await is_chat_muted_for_user(db, chat_id=message.chat_id, user_id=recipient.id):
continue
if not await is_user_online(recipient.id):
payload = {
**base_payload,