From 3203cbf9f900598db26d9c5e404c39184b644a7f Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Thu, 15 Jan 2026 16:20:53 +0500 Subject: [PATCH] feat: Implement cooldown mechanism for emoji picker toggling to prevent rapid switching --- .../messenger/ui/chats/ChatDetailScreen.kt | 109 +++++++++++++++++- 1 file changed, 106 insertions(+), 3 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 ed7bafe..bd19460 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 @@ -461,8 +461,57 @@ fun ChatDetailScreen( isDarkTheme ) + // � Edge swipe to go back (iOS/Telegram style) + var edgeSwipeOffset by remember { mutableStateOf(0f) } + val edgeSwipeThreshold = 100f // px threshold для активации + val edgeZoneWidth = 30f // px зона от левого края для начала свайпа + var isEdgeSwiping by remember { mutableStateOf(false) } + + // Анимация возврата + val animatedEdgeOffset by animateFloatAsState( + targetValue = edgeSwipeOffset, + animationSpec = spring(dampingRatio = 0.8f, stiffness = 300f), + label = "edgeSwipe" + ) + // 🚀 Весь контент без дополнительной анимации (анимация в MainActivity) - Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxSize() + .pointerInput(Unit) { + detectHorizontalDragGestures( + onDragStart = { offset -> + // Начинаем свайп только если палец у левого края + isEdgeSwiping = offset.x < edgeZoneWidth + }, + onDragEnd = { + if (isEdgeSwiping && edgeSwipeOffset > edgeSwipeThreshold) { + // Свайп достаточный - переходим назад + hideKeyboardAndBack() + } + edgeSwipeOffset = 0f + isEdgeSwiping = false + }, + onDragCancel = { + edgeSwipeOffset = 0f + isEdgeSwiping = false + }, + onHorizontalDrag = { _, dragAmount -> + if (isEdgeSwiping) { + // Только вправо (положительный dragAmount) + val newOffset = edgeSwipeOffset + dragAmount + edgeSwipeOffset = newOffset.coerceIn(0f, 300f) + } + } + ) + } + .graphicsLayer { + // Сдвигаем контент при свайпе + translationX = animatedEdgeOffset + // Легкое затемнение при свайпе + alpha = 1f - (animatedEdgeOffset / 600f).coerceIn(0f, 0.3f) + } + ) { // Telegram-style solid header background (без blur) val headerBackground = if (isDarkTheme) Color(0xFF212121) else Color(0xFFFFFFFF) @@ -2056,11 +2105,31 @@ private fun MessageInputBar( var isKeyboardVisible by remember { mutableStateOf(false) } var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) } - // 🔥 Обновляем coordinator через snapshotFlow (БЕЗ рекомпозиции!) + // � Защита от слишком частого переключения клавиатуры (300ms cooldown) + var lastToggleTime by remember { mutableLongStateOf(0L) } + val toggleCooldownMs = 300L + + // 🚫 Флаг "клавиатура анимируется" - блокирует переключение пока высота не стабилизируется + var isKeyboardAnimating by remember { mutableStateOf(false) } + var lastKeyboardHeightChange by remember { mutableLongStateOf(0L) } + val keyboardAnimationStabilizeMs = 250L // Ждем стабилизации 250ms + // �🔥 Обновляем coordinator через snapshotFlow (БЕЗ рекомпозиции!) LaunchedEffect(Unit) { snapshotFlow { with(density) { imeInsets.getBottom(density).toDp() } }.collect { currentImeHeight -> + // 🚫 Отслеживаем изменения высоты - если меняется, клава анимируется + val now = System.currentTimeMillis() + val heightChanged = kotlin.math.abs((currentImeHeight - lastStableKeyboardHeight).value) > 5f + if (heightChanged && currentImeHeight.value > 0) { + lastKeyboardHeightChange = now + isKeyboardAnimating = true + } + // Если высота не менялась > 250ms - анимация завершена + if (now - lastKeyboardHeightChange > keyboardAnimationStabilizeMs && isKeyboardAnimating) { + isKeyboardAnimating = false + } + // Обновляем флаг видимости (это НЕ вызывает рекомпозицию напрямую) isKeyboardVisible = currentImeHeight > 50.dp @@ -2097,6 +2166,9 @@ private fun MessageInputBar( // Состояние отправки - можно отправить если есть текст ИЛИ есть reply val canSend = remember(value, hasReply) { value.isNotBlank() || hasReply } + // 🔥 Флаг отправки - предотвращает исчезновение кнопки Send во время отправки reply + var isSending by remember { mutableStateOf(false) } + // 🔥 УДАЛЕНО: автоматическое закрытие emoji при открытии клавиатуры // Теперь это контролируется только через toggleEmojiPicker() @@ -2118,6 +2190,29 @@ private fun MessageInputBar( // 🔥 Функция переключения emoji picker с Telegram-style transitions fun toggleEmojiPicker() { + // 🚫 Защита от слишком частого переключения + val currentTime = System.currentTimeMillis() + val timeSinceLastToggle = currentTime - lastToggleTime + + if (timeSinceLastToggle < toggleCooldownMs) { + android.util.Log.d("EmojiPicker", "⏸️ Toggle blocked: ${timeSinceLastToggle}ms < ${toggleCooldownMs}ms") + return + } + + // 🚫 БЛОКИРОВКА: Если идет анимация - запрещаем переключение + if (coordinator.currentState != KeyboardTransitionCoordinator.TransitionState.IDLE) { + android.util.Log.d("EmojiPicker", "⏸️ Toggle blocked: animation in progress (state=${coordinator.currentState})") + return + } + + // 🚫 БЛОКИРОВКА: Если клавиатура еще анимируется (не стабилизировалась) + if (isKeyboardAnimating) { + android.util.Log.d("EmojiPicker", "⏸️ Toggle blocked: keyboard still animating") + return + } + + lastToggleTime = currentTime + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager android.util.Log.d("EmojiPicker", "=".repeat(60)) @@ -2165,7 +2260,14 @@ private fun MessageInputBar( fun handleSend() { // Можно отправить если есть текст ИЛИ есть reply (как в React Native) if (value.isNotBlank() || hasReply) { + // 🔥 Устанавливаем флаг отправки чтобы кнопка не исчезла во время отправки + isSending = true onSend() + // Сбрасываем флаг через небольшую задержку (после того как reply очистится) + scope.launch { + kotlinx.coroutines.delay(150) // Даём время на анимацию + isSending = false + } // Очищаем инпут, но клавиатура остаётся открытой } } @@ -2394,8 +2496,9 @@ private fun MessageInputBar( Spacer(modifier = Modifier.width(2.dp)) // SEND BUTTON (всегда справа) - с анимацией + // 🔥 Кнопка видна если: есть текст ИЛИ есть reply ИЛИ идёт отправка AnimatedVisibility( - visible = canSend, + visible = canSend || isSending, enter = scaleIn(tween(150)) + fadeIn(tween(150)), exit = scaleOut(tween(100)) + fadeOut(tween(100)) ) {