Анимация удаления сообщений (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:
@@ -803,6 +803,7 @@ fun ChatDetailScreen(
|
|||||||
// <20>🔥 Reply/Forward state
|
// <20>🔥 Reply/Forward state
|
||||||
val replyMessages by viewModel.replyMessages.collectAsState()
|
val replyMessages by viewModel.replyMessages.collectAsState()
|
||||||
val isForwardMode by viewModel.isForwardMode.collectAsState()
|
val isForwardMode by viewModel.isForwardMode.collectAsState()
|
||||||
|
val pendingDeleteIds by viewModel.pendingDeleteIds.collectAsState()
|
||||||
|
|
||||||
// Avatar-сообщения не должны попадать в selection ни при каких условиях.
|
// Avatar-сообщения не должны попадать в selection ни при каких условиях.
|
||||||
val avatarMessageIds =
|
val avatarMessageIds =
|
||||||
@@ -3120,6 +3121,15 @@ fun ChatDetailScreen(
|
|||||||
isTailPhase &&
|
isTailPhase &&
|
||||||
isGroupStart))
|
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 {
|
Column {
|
||||||
if (showDate
|
if (showDate
|
||||||
) {
|
) {
|
||||||
@@ -3527,6 +3537,7 @@ fun ChatDetailScreen(
|
|||||||
} // contextMenuContent
|
} // contextMenuContent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} // AnimatedVisibility
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
androidx.compose.animation.AnimatedVisibility(
|
||||||
|
|||||||
@@ -203,6 +203,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val replyMessages: StateFlow<List<ReplyMessage>> = _replyMessages.asStateFlow()
|
val replyMessages: StateFlow<List<ReplyMessage>> = _replyMessages.asStateFlow()
|
||||||
|
|
||||||
private val _isForwardMode = MutableStateFlow(false)
|
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()
|
val isForwardMode: StateFlow<Boolean> = _isForwardMode.asStateFlow()
|
||||||
|
|
||||||
// 📌 Pinned messages state
|
// 📌 Pinned messages state
|
||||||
@@ -2660,22 +2664,28 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
val opponent = opponentKey ?: return
|
val opponent = opponentKey ?: return
|
||||||
val dialogKey = getDialogKey(account, opponent)
|
val dialogKey = getDialogKey(account, opponent)
|
||||||
|
|
||||||
// Удаляем из UI сразу на main
|
// 1. Mark as pending delete (triggers shrink+fade animation)
|
||||||
val updatedMessages = _messages.value.filter { it.id != messageId }
|
_pendingDeleteIds.value = _pendingDeleteIds.value + messageId
|
||||||
_messages.value = updatedMessages
|
|
||||||
// Синхронизируем глобальный кэш диалога, иначе удалённые сообщения могут вернуться
|
|
||||||
// при повторном открытии чата из stale cache.
|
|
||||||
updateCacheWithLimit(account, dialogKey, updatedMessages)
|
|
||||||
messageRepository.clearDialogCache(opponent)
|
|
||||||
|
|
||||||
// Удаляем из БД в IO + удаляем pin если был
|
// 2. After animation completes, remove from list and DB
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch {
|
||||||
pinnedMessageDao.removePin(account, dialogKey, messageId)
|
kotlinx.coroutines.delay(300) // wait for animation
|
||||||
messageDao.deleteMessage(account, messageId)
|
|
||||||
if (account == opponent) {
|
val updatedMessages = _messages.value.filter { it.id != messageId }
|
||||||
dialogDao.updateSavedMessagesDialogFromMessages(account)
|
_messages.value = updatedMessages
|
||||||
} else {
|
_pendingDeleteIds.value = _pendingDeleteIds.value - messageId
|
||||||
dialogDao.updateDialogFromMessages(account, opponent)
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user