From e5ff42ce1d39af8c3766d7085d1d72d8f5ac0a9d Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Fri, 10 Apr 2026 01:55:26 +0500 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=BD=D0=B8=D0=BC=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20(Teleg?= =?UTF-8?q?ram-style):=20shrink=20+=20fade=20out=20250ms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Двухэтапное удаление: pendingDeleteIds → AnimatedVisibility(shrinkVertically + fadeOut) → remove. Остальные сообщения плавно сдвигаются на место удалённого. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../messenger/ui/chats/ChatDetailScreen.kt | 11 +++++ .../messenger/ui/chats/ChatViewModel.kt | 40 ++++++++++++------- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 1031d06..5c084a2 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -803,6 +803,7 @@ fun ChatDetailScreen( // �🔥 Reply/Forward state val replyMessages by viewModel.replyMessages.collectAsState() val isForwardMode by viewModel.isForwardMode.collectAsState() + val pendingDeleteIds by viewModel.pendingDeleteIds.collectAsState() // Avatar-сообщения не должны попадать в selection ни при каких условиях. val avatarMessageIds = @@ -3120,6 +3121,15 @@ fun ChatDetailScreen( isTailPhase && isGroupStart)) + val isDeleting = message.id in pendingDeleteIds + androidx.compose.animation.AnimatedVisibility( + visible = !isDeleting, + exit = androidx.compose.animation.shrinkVertically( + animationSpec = androidx.compose.animation.core.tween(250, easing = androidx.compose.animation.core.FastOutSlowInEasing) + ) + androidx.compose.animation.fadeOut( + animationSpec = androidx.compose.animation.core.tween(200) + ) + ) { Column { if (showDate ) { @@ -3527,6 +3537,7 @@ fun ChatDetailScreen( } // contextMenuContent ) } + } // AnimatedVisibility } } androidx.compose.animation.AnimatedVisibility( diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt index 8231702..9fd0e16 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt @@ -203,6 +203,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { val replyMessages: StateFlow> = _replyMessages.asStateFlow() private val _isForwardMode = MutableStateFlow(false) + + // Animated deletion: IDs of messages currently animating out + private val _pendingDeleteIds = MutableStateFlow>(emptySet()) + val pendingDeleteIds: StateFlow> = _pendingDeleteIds.asStateFlow() val isForwardMode: StateFlow = _isForwardMode.asStateFlow() // 📌 Pinned messages state @@ -2660,22 +2664,28 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { val opponent = opponentKey ?: return val dialogKey = getDialogKey(account, opponent) - // Удаляем из UI сразу на main - val updatedMessages = _messages.value.filter { it.id != messageId } - _messages.value = updatedMessages - // Синхронизируем глобальный кэш диалога, иначе удалённые сообщения могут вернуться - // при повторном открытии чата из stale cache. - updateCacheWithLimit(account, dialogKey, updatedMessages) - messageRepository.clearDialogCache(opponent) + // 1. Mark as pending delete (triggers shrink+fade animation) + _pendingDeleteIds.value = _pendingDeleteIds.value + messageId - // Удаляем из БД в IO + удаляем pin если был - viewModelScope.launch(Dispatchers.IO) { - pinnedMessageDao.removePin(account, dialogKey, messageId) - messageDao.deleteMessage(account, messageId) - if (account == opponent) { - dialogDao.updateSavedMessagesDialogFromMessages(account) - } else { - dialogDao.updateDialogFromMessages(account, opponent) + // 2. After animation completes, remove from list and DB + viewModelScope.launch { + kotlinx.coroutines.delay(300) // wait for animation + + val updatedMessages = _messages.value.filter { it.id != messageId } + _messages.value = updatedMessages + _pendingDeleteIds.value = _pendingDeleteIds.value - messageId + + updateCacheWithLimit(account, dialogKey, updatedMessages) + messageRepository.clearDialogCache(opponent) + + withContext(Dispatchers.IO) { + pinnedMessageDao.removePin(account, dialogKey, messageId) + messageDao.deleteMessage(account, messageId) + if (account == opponent) { + dialogDao.updateSavedMessagesDialogFromMessages(account) + } else { + dialogDao.updateDialogFromMessages(account, opponent) + } } } }