feat: Implement cooldown mechanism for emoji picker toggling to prevent rapid switching
This commit is contained in:
@@ -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))
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user