from dataclasses import dataclass from django.db import transaction from django.db.models import Q from django.utils import timezone from communications.notifications.config import NotificationVerbs from notifications.cache import invalidate_unread_summary from notifications.models import Notification from notifications.push_lifecycle import dismiss_chat_push from notifications.realtime import send_notifications_read_all_event @dataclass(frozen=True) class ChatNotificationReadResult: notification_ids: list[int] count: int class ChatNotificationReadService: """Use case for reading notifications related to a specific chat.""" chat_related_verbs = ( NotificationVerbs.NEW_MESSAGE, NotificationVerbs.ANNOUNCEMENT, NotificationVerbs.MENTION, NotificationVerbs.REPLY, NotificationVerbs.ADDED_TO_CHAT, NotificationVerbs.COMMENTED, ) def mark_chat_notifications_as_read( self, *, user, chat_id: int, up_to_message_id: int | None = None, ) -> ChatNotificationReadResult: queryset = self._get_unread_queryset(user_id=user.id, chat_id=chat_id) candidates = list(queryset.only("id", "data")) notification_ids = [ notification.id for notification in candidates if self._is_at_or_before_message( notification.data, up_to_message_id=up_to_message_id, ) ] if not notification_ids: return ChatNotificationReadResult(notification_ids=[], count=0) read_at = timezone.now() count = queryset.filter(id__in=notification_ids).update( unread=False, timestamp_read=read_at, ) invalidate_unread_summary(user.id) def notify_after_commit(): invalidate_unread_summary(user.id) send_notifications_read_all_event(user.id, notification_ids) dismiss_chat_push( user_id=user.id, chat_id=chat_id, notification_ids=notification_ids, ) transaction.on_commit(notify_after_commit) return ChatNotificationReadResult( notification_ids=notification_ids, count=count, ) @staticmethod def _is_at_or_before_message(data, *, up_to_message_id: int | None) -> bool: if up_to_message_id is None: return True if not isinstance(data, dict) or data.get("message_id") is None: return True try: return int(data["message_id"]) <= up_to_message_id except (TypeError, ValueError): return False def _get_unread_queryset(self, *, user_id: int, chat_id: int): return Notification.objects.filter( Q(data__chat_id=chat_id) | Q(data__chat_id=str(chat_id)), recipient_id=user_id, unread=True, deleted=False, verb__in=self.chat_related_verbs, )