feat: Implement cooldown mechanism for emoji picker toggling to prevent rapid switching

This commit is contained in:
k1ngsterr1
2026-01-15 16:20:53 +05:00
parent 4cf51a8a6e
commit 3203cbf9f9

View File

@@ -461,8 +461,57 @@ fun ChatDetailScreen(
isDarkTheme
)
// <20> 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 (БЕЗ рекомпозиции!)
// <EFBFBD> Защита от слишком частого переключения клавиатуры (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
// <20>🔥 Обновляем 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))
) {