feat: Implement cooldown mechanism for emoji picker toggling to prevent rapid switching
This commit is contained in:
@@ -461,8 +461,57 @@ fun ChatDetailScreen(
|
|||||||
isDarkTheme
|
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)
|
// 🚀 Весь контент без дополнительной анимации (анимация в 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)
|
// Telegram-style solid header background (без blur)
|
||||||
val headerBackground = if (isDarkTheme) Color(0xFF212121) else Color(0xFFFFFFFF)
|
val headerBackground = if (isDarkTheme) Color(0xFF212121) else Color(0xFFFFFFFF)
|
||||||
|
|
||||||
@@ -2056,11 +2105,31 @@ private fun MessageInputBar(
|
|||||||
var isKeyboardVisible by remember { mutableStateOf(false) }
|
var isKeyboardVisible by remember { mutableStateOf(false) }
|
||||||
var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) }
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
with(density) { imeInsets.getBottom(density).toDp() }
|
with(density) { imeInsets.getBottom(density).toDp() }
|
||||||
}.collect { currentImeHeight ->
|
}.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
|
isKeyboardVisible = currentImeHeight > 50.dp
|
||||||
|
|
||||||
@@ -2097,6 +2166,9 @@ private fun MessageInputBar(
|
|||||||
// Состояние отправки - можно отправить если есть текст ИЛИ есть reply
|
// Состояние отправки - можно отправить если есть текст ИЛИ есть reply
|
||||||
val canSend = remember(value, hasReply) { value.isNotBlank() || hasReply }
|
val canSend = remember(value, hasReply) { value.isNotBlank() || hasReply }
|
||||||
|
|
||||||
|
// 🔥 Флаг отправки - предотвращает исчезновение кнопки Send во время отправки reply
|
||||||
|
var isSending by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// 🔥 УДАЛЕНО: автоматическое закрытие emoji при открытии клавиатуры
|
// 🔥 УДАЛЕНО: автоматическое закрытие emoji при открытии клавиатуры
|
||||||
// Теперь это контролируется только через toggleEmojiPicker()
|
// Теперь это контролируется только через toggleEmojiPicker()
|
||||||
|
|
||||||
@@ -2118,6 +2190,29 @@ private fun MessageInputBar(
|
|||||||
|
|
||||||
// 🔥 Функция переключения emoji picker с Telegram-style transitions
|
// 🔥 Функция переключения emoji picker с Telegram-style transitions
|
||||||
fun toggleEmojiPicker() {
|
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
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
|
||||||
android.util.Log.d("EmojiPicker", "=".repeat(60))
|
android.util.Log.d("EmojiPicker", "=".repeat(60))
|
||||||
@@ -2165,7 +2260,14 @@ private fun MessageInputBar(
|
|||||||
fun handleSend() {
|
fun handleSend() {
|
||||||
// Можно отправить если есть текст ИЛИ есть reply (как в React Native)
|
// Можно отправить если есть текст ИЛИ есть reply (как в React Native)
|
||||||
if (value.isNotBlank() || hasReply) {
|
if (value.isNotBlank() || hasReply) {
|
||||||
|
// 🔥 Устанавливаем флаг отправки чтобы кнопка не исчезла во время отправки
|
||||||
|
isSending = true
|
||||||
onSend()
|
onSend()
|
||||||
|
// Сбрасываем флаг через небольшую задержку (после того как reply очистится)
|
||||||
|
scope.launch {
|
||||||
|
kotlinx.coroutines.delay(150) // Даём время на анимацию
|
||||||
|
isSending = false
|
||||||
|
}
|
||||||
// Очищаем инпут, но клавиатура остаётся открытой
|
// Очищаем инпут, но клавиатура остаётся открытой
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2394,8 +2496,9 @@ private fun MessageInputBar(
|
|||||||
Spacer(modifier = Modifier.width(2.dp))
|
Spacer(modifier = Modifier.width(2.dp))
|
||||||
|
|
||||||
// SEND BUTTON (всегда справа) - с анимацией
|
// SEND BUTTON (всегда справа) - с анимацией
|
||||||
|
// 🔥 Кнопка видна если: есть текст ИЛИ есть reply ИЛИ идёт отправка
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = canSend,
|
visible = canSend || isSending,
|
||||||
enter = scaleIn(tween(150)) + fadeIn(tween(150)),
|
enter = scaleIn(tween(150)) + fadeIn(tween(150)),
|
||||||
exit = scaleOut(tween(100)) + fadeOut(tween(100))
|
exit = scaleOut(tween(100)) + fadeOut(tween(100))
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user