Анимация удаления сообщений (Telegram-style): shrink + fade out 250ms

Двухэтапное удаление: pendingDeleteIds → AnimatedVisibility(shrinkVertically + fadeOut) → remove.
Остальные сообщения плавно сдвигаются на место удалённого.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 01:55:26 +05:00
parent 06f43b9d4e
commit e5ff42ce1d
2 changed files with 36 additions and 15 deletions

View File

@@ -803,6 +803,7 @@ fun ChatDetailScreen(
// <20>🔥 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(

View File

@@ -203,6 +203,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
val replyMessages: StateFlow<List<ReplyMessage>> = _replyMessages.asStateFlow()
private val _isForwardMode = MutableStateFlow(false)
// Animated deletion: IDs of messages currently animating out
private val _pendingDeleteIds = MutableStateFlow<Set<String>>(emptySet())
val pendingDeleteIds: StateFlow<Set<String>> = _pendingDeleteIds.asStateFlow()
val isForwardMode: StateFlow<Boolean> = _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)
}
}
}
}