from __future__ import annotations from django.db import transaction from django.db.models import F, Q from django.utils import timezone from communications.mentions import mentioned_user_ids from communications.models import ChatReadState, Message from notifications.push_lifecycle import delete_message_notifications def soft_delete_message( message: Message, *, deleted_by, replacement_content: str | None = None, ) -> bool: """Soft-delete a message and remove it from cached unread counters.""" with transaction.atomic(): locked_message = Message.objects.select_for_update().get(pk=message.pk) if locked_message.is_deleted: return False unread_states = ChatReadState.objects.filter( chat_id=locked_message.chat_id, unread_count__gt=0, ).exclude(user_id=locked_message.author_id) unread_states = unread_states.filter( Q(last_read_message_id__isnull=True) | Q(last_read_message_id__lt=locked_message.id) ) unread_states.update(unread_count=F("unread_count") - 1) mention_recipient_ids = mentioned_user_ids(locked_message.mentions) if mention_recipient_ids: unread_states.filter( user_id__in=mention_recipient_ids, unread_mention_count__gt=0, ).update(unread_mention_count=F("unread_mention_count") - 1) locked_message.is_deleted = True locked_message.deleted_at = timezone.now() locked_message.deleted_by = deleted_by update_fields = ["is_deleted", "deleted_at", "deleted_by"] if replacement_content is not None: locked_message.content = replacement_content update_fields.append("content") locked_message.save(update_fields=update_fields) delete_message_notifications( chat_id=locked_message.chat_id, message_id=locked_message.id, ) message.is_deleted = locked_message.is_deleted message.deleted_at = locked_message.deleted_at message.deleted_by = locked_message.deleted_by if replacement_content is not None: message.content = replacement_content return True