Suppress chat notifications for active websocket session

This commit is contained in:
lizerk 2026-04-02 10:05:20 +03:00
parent a0cda94107
commit f03887aa00
2 changed files with 112 additions and 2 deletions

View File

@ -186,6 +186,30 @@ class UserConsumer(ChatConsumerMixin, AsyncJsonWebsocketConsumer):
elif action == "vote_poll":
await self._handle_vote_poll(content)
@staticmethod
def _extract_notification_chat_id(notification):
"""Достаёт chat_id из payload уведомления, если он есть."""
if not isinstance(notification, dict):
return None
data = notification.get("data")
if not isinstance(data, dict):
return None
chat_id = data.get("chat_id")
try:
return int(chat_id)
except (TypeError, ValueError):
return None
def _should_suppress_active_chat_notification(self, notification):
"""Не показываем уведомление о сообщении в уже открытом чате."""
if self.active_chat_id is None:
return False
notification_chat_id = self._extract_notification_chat_id(notification)
return notification_chat_id == self.active_chat_id
# ==================== Обработчики уведомлений ====================
async def notification_message(self, event):
@ -193,16 +217,36 @@ class UserConsumer(ChatConsumerMixin, AsyncJsonWebsocketConsumer):
Новое уведомление от WebSocketNotificationSender.
Вызывается из notifications/senders/websocket.py
"""
notification = event.get("message", {})
if self._should_suppress_active_chat_notification(notification):
logger.info(
"[UserWS] Suppressed notification for active chat %s in session %s",
self.active_chat_id,
self.channel_name,
)
return
await self.send_json(
{"type": "notification", "notification": event.get("message", {})}
{"type": "notification", "notification": notification}
)
async def notification_new(self, event):
"""Новое уведомление (вызывается из notifications/services.py)"""
notification = event.get("notification", {})
if self._should_suppress_active_chat_notification(notification):
logger.info(
"[UserWS] Suppressed notification_new for active chat %s in session %s",
self.active_chat_id,
self.channel_name,
)
return
await self.send_json(
{
"type": "notification",
"notification": event.get("notification", {}),
"notification": notification,
}
)

View File

@ -558,6 +558,72 @@ class TestChatMarkedReadEvent:
class TestNotificationStateEvents:
"""Тесты синхронизации состояния уведомлений через user channel."""
@pytest.mark.asyncio
async def test_active_chat_session_does_not_receive_own_chat_notification(
self,
ws_communicator,
user,
test_chat,
):
"""Уведомление о сообщении не должно приходить в сессию с открытым этим чатом."""
import asyncio
comm_active = await ws_communicator(user=user)
comm_background = await ws_communicator(user=user)
connected_active, _ = await comm_active.connect()
connected_background, _ = await comm_background.connect()
assert connected_active and connected_background
await comm_active.send_json_to(
{
"action": "open_chat",
"chat_id": test_chat.id,
"load_history": False,
}
)
await comm_active.receive_json_from(timeout=5)
channel_layer = get_channel_layer()
payload = {
"type": "notification",
"id": 777,
"verb": "chat_new_message",
"description": "New message in active chat",
"action_url": f"/messages/{test_chat.id}",
"timestamp": "2026-04-02T10:00:00Z",
"unread": True,
"data": {
"chat_id": test_chat.id,
"message_id": 123,
},
}
await channel_layer.group_send(
f"user_{user.id}",
{
"type": "notification_message",
"message": payload,
}
)
with pytest.raises(asyncio.TimeoutError):
await comm_active.receive_json_from(timeout=2)
response = await comm_background.receive_json_from(timeout=5)
assert response["type"] == "notification"
assert response["notification"]["id"] == 777
assert response["notification"]["data"]["chat_id"] == test_chat.id
try:
await comm_background.disconnect()
except asyncio.CancelledError:
pass
try:
await comm_active.disconnect()
except asyncio.CancelledError:
pass
@pytest.mark.asyncio
async def test_notification_read_sync_between_sessions(self, ws_communicator, user):
comm1 = await ws_communicator(user=user)