feat: Update ChatDetailScreen to use inputBackgroundColor for dropdown menus and menu items

This commit is contained in:
k1ngsterr1
2026-01-15 13:21:07 +05:00
parent 1000f017f5
commit 911f9ebb5a
3 changed files with 184 additions and 246 deletions

View File

@@ -1,31 +1,33 @@
package app.rosette.android.ui.keyboard package app.rosette.android.ui.keyboard
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import android.util.Log import android.util.Log
import kotlinx.coroutines.launch
/** /**
* Анимированный контейнер для emoji панели с Telegram-style переходами. * 🚀 МЕТОД: Animated Height + Content Offset (Telegram-style)
* *
* Паттерны Telegram: * Ключевые принципы:
* - Keyboard → Emoji: панель выезжает снизу за 250ms (FastOutSlowIn) * 1. ВЫСОТА контейнера анимируется от 0dp до emojiHeight
* - Emoji → Keyboard: панель уезжает вниз за 200ms (FastOutLinearIn) * 2. Контент внутри clipToBounds - обрезается по границам
* - TranslationY + Alpha для плавности * 3. НЕТ предварительного резервирования места когда скрыт
* - Немедленное резервирование места * 4. graphicsLayer для GPU-ускорения
*
* Паттерн:
* - Показать: height анимируется от 0dp до emojiHeight (контент появляется снизу)
* - Скрыть: height анимируется от emojiHeight до 0dp (контент уходит вниз)
* - Длительность: 250ms с FastOutSlowInEasing
*/ */
@Composable @Composable
fun AnimatedKeyboardTransition( fun AnimatedKeyboardTransition(
@@ -35,97 +37,65 @@ fun AnimatedKeyboardTransition(
) { ) {
val tag = "AnimatedTransition" val tag = "AnimatedTransition"
// Animatable для плавной анимации offset // 🎯 Целевая высота: emojiHeight = видно, 0dp = скрыто
val scope = rememberCoroutineScope() val targetHeight = if (showEmojiPicker) {
val offsetY = remember { Animatable(0f) } coordinator.emojiHeight
val alpha = remember { Animatable(1f) } } else {
0.dp
}
// Отслеживаем изменения showEmojiPicker // 🎬 Декларативная анимация высоты
LaunchedEffect(showEmojiPicker) { val animatedHeight by animateDpAsState(
Log.d(tag, "════════════════════════════════════════════════════════") targetValue = targetHeight,
Log.d(tag, "🎬 Animation triggered: showEmojiPicker=$showEmojiPicker")
Log.d(tag, "📊 Current state: offsetY=${offsetY.value}dp, alpha=${alpha.value}, emojiHeight=${coordinator.emojiHeight.value}dp")
if (showEmojiPicker) {
// ============ ПОКАЗАТЬ EMOJI ============
val height = coordinator.emojiHeight.value
Log.d(tag, "📤 Animating emoji IN from ${height}dp to 0dp")
Log.d(tag, "⏱️ Duration: ${KeyboardTransitionCoordinator.TRANSITION_DURATION}ms, Easing: FastOutSlowIn")
// Начальная позиция: внизу за экраном
Log.d(tag, "🎯 Setting initial offset to ${height}dp (off-screen)")
offsetY.snapTo(height)
scope.launch {
offsetY.animateTo(
targetValue = 0f,
animationSpec = tween( animationSpec = tween(
durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(), durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(),
easing = FastOutSlowInEasing easing = FastOutSlowInEasing
),
label = "emojiPanelHeight"
) )
)
Log.d(tag, "✅ Emoji slide animation completed")
}
// Fade in (немного быстрее) Log.d(tag, "🎨 Emoji panel: show=$showEmojiPicker, targetH=$targetHeight, currentH=$animatedHeight, emojiH=${coordinator.emojiHeight}")
Log.d(tag, "✨ Starting fade in animation (200ms)")
scope.launch {
alpha.animateTo(
targetValue = 1f,
animationSpec = tween(durationMillis = 200)
)
Log.d(tag, "✅ Fade in completed, alpha=${alpha.value}")
}
} else if (offsetY.value < coordinator.emojiHeight.value * 0.9f) { // 🔥 Рендерим только если есть высота (показан или анимируется)
// ============ СКРЫТЬ EMOJI ============ if (animatedHeight > 0.dp) {
Log.d(tag, "📥 Hiding emoji panel (offsetY=${offsetY.value}dp < threshold=${coordinator.emojiHeight.value * 0.9f}dp)") Box(
modifier = Modifier
val height = coordinator.emojiHeight.value .fillMaxWidth()
Log.d(tag, "🎯 Animating emoji OUT from ${offsetY.value}dp to ${height}dp") .height(animatedHeight)
Log.d(tag, "⏱️ Duration: 200ms, Easing: FastOutLinearIn") .clipToBounds() // 🔥 Обрезаем контент по границам контейнера
scope.launch { .graphicsLayer {
offsetY.animateTo( // GPU-ускорение для плавности
targetValue = height, clip = true
animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
)
Log.d(tag, "✅ Emoji hide animation completed")
} }
) {
// Fade out (быстрее) // Внутренний контейнер с полной высотой emoji
Log.d(tag, "🌑 Starting fade out animation (150ms)") // Он обрезается clipToBounds родителя
scope.launch {
alpha.animateTo(
targetValue = 0f,
animationSpec = tween(durationMillis = 150)
)
Log.d(tag, "✅ Fade out completed, alpha=${alpha.value}")
}
}
}
// 🔥 Рендерим контент ТОЛЬКО если showEmojiPicker = true
// Не проверяем offsetY, чтобы избежать показа emoji при открытии клавиатуры
if (showEmojiPicker) {
Log.d(tag, "✅ Rendering emoji content (showEmojiPicker=true)")
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(coordinator.emojiHeight) .height(coordinator.emojiHeight)
.offset(y = offsetY.value.dp) // 🔥 Позиционируем снизу: когда animatedHeight < emojiHeight,
.alpha(alpha.value) // верхняя часть обрезается, создавая эффект "выезда снизу"
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints.copy(
minHeight = 0,
maxHeight = coordinator.emojiHeight.roundToPx()
))
layout(placeable.width, placeable.height) {
// Смещаем вниз так, чтобы нижняя часть была видна
val offsetY = placeable.height - animatedHeight.roundToPx()
placeable.place(0, -offsetY)
}
}
) { ) {
content() content()
} }
} }
}
} }
/** /**
* Упрощенная версия без fade анимации. * Алиас для обратной совместимости
* Только slide (как в оригинальном Telegram).
*/ */
@Composable @Composable
fun SimpleAnimatedKeyboardTransition( fun SimpleAnimatedKeyboardTransition(
@@ -133,39 +103,9 @@ fun SimpleAnimatedKeyboardTransition(
showEmojiPicker: Boolean, showEmojiPicker: Boolean,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val offsetY = remember { Animatable(0f) } AnimatedKeyboardTransition(
coordinator = coordinator,
LaunchedEffect(showEmojiPicker) { showEmojiPicker = showEmojiPicker,
if (showEmojiPicker) { content = content
// Показать: снизу вверх
offsetY.snapTo(coordinator.emojiHeight.value)
offsetY.animateTo(
targetValue = 0f,
animationSpec = tween(
durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(),
easing = FastOutSlowInEasing
) )
)
} else {
// Скрыть: сверху вниз
offsetY.animateTo(
targetValue = coordinator.emojiHeight.value,
animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
)
}
}
if (showEmojiPicker || offsetY.value < coordinator.emojiHeight.value * 0.95f) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(coordinator.emojiHeight)
.offset(y = offsetY.value.dp)
) {
content()
}
}
} }

View File

@@ -72,7 +72,9 @@ class KeyboardTransitionCoordinator {
/** /**
* Переход от системной клавиатуры к emoji панели. * Переход от системной клавиатуры к emoji панели.
* Telegram паттерн: сначала скрыть клавиатуру, затем показать emoji. *
* 🔥 КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: Показываем emoji СРАЗУ!
* Не ждем закрытия клавиатуры - emoji начинает выезжать синхронно.
*/ */
fun requestShowEmoji( fun requestShowEmoji(
hideKeyboard: () -> Unit, hideKeyboard: () -> Unit,
@@ -84,40 +86,25 @@ class KeyboardTransitionCoordinator {
Log.d(TAG, " 📊 Current state:") Log.d(TAG, " 📊 Current state:")
Log.d(TAG, " - currentState=$currentState") Log.d(TAG, " - currentState=$currentState")
Log.d(TAG, " - keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight") Log.d(TAG, " - keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight")
Log.d(TAG, " - isTransitioning=$isTransitioning") Log.d(TAG, " - maxKeyboardHeight=$maxKeyboardHeight")
Log.d(TAG, " - isKeyboardVisible=$isKeyboardVisible, isEmojiVisible=$isEmojiVisible")
Log.d(TAG, " - pendingShowEmojiCallback=${if (pendingShowEmojiCallback != null) "EXISTS" else "null"}")
Log.d(TAG, " 📝 Setting currentState = KEYBOARD_TO_EMOJI")
currentState = TransitionState.KEYBOARD_TO_EMOJI currentState = TransitionState.KEYBOARD_TO_EMOJI
isTransitioning = true isTransitioning = true
// 🔥 Сохраняем коллбэк для вызова после закрытия клавиатуры // 🔥 Гарантируем что emojiHeight = maxKeyboardHeight (не меняется при закрытии клавиатуры)
Log.d(TAG, " 💾 Saving pending emoji callback...") if (maxKeyboardHeight > 0.dp) {
pendingShowEmojiCallback = { emojiHeight = maxKeyboardHeight
try { Log.d(TAG, " 📌 Locked emojiHeight to maxKeyboardHeight: $emojiHeight")
Log.d(TAG, "🎬 Executing pending emoji show callback")
Log.d(TAG, " Current keyboard height at execution: $keyboardHeight")
Log.d(TAG, " 📞 Calling showEmoji()...")
showEmoji()
Log.d(TAG, " 📞 Setting isEmojiVisible = true")
isEmojiVisible = true
isKeyboardVisible = false
currentState = TransitionState.IDLE
isTransitioning = false
pendingShowEmojiCallback = null
Log.d(TAG, "✅ Emoji shown via pending callback")
} catch (e: Exception) {
Log.e(TAG, "❌ Error in pendingShowEmojiCallback", e)
currentState = TransitionState.IDLE
isTransitioning = false
pendingShowEmojiCallback = null
} }
}
Log.d(TAG, " ✅ Pending callback saved")
// Шаг 1: Скрыть системную клавиатуру // 🔥 ПОКАЗЫВАЕМ EMOJI СРАЗУ! Не ждем закрытия клавиатуры
Log.d(TAG, " 📞 Calling hideKeyboard()...") Log.d(TAG, " 🚀 IMMEDIATELY showing emoji (no waiting for keyboard close)")
showEmoji()
isEmojiVisible = true
Log.d(TAG, " ✅ showEmoji() called, isEmojiVisible=true")
// Теперь скрываем клавиатуру (она будет закрываться синхронно с появлением emoji)
Log.d(TAG, " ⌨️ Hiding keyboard...")
try { try {
hideKeyboard() hideKeyboard()
Log.d(TAG, " ✅ hideKeyboard() completed") Log.d(TAG, " ✅ hideKeyboard() completed")
@@ -125,12 +112,14 @@ class KeyboardTransitionCoordinator {
Log.e(TAG, "❌ Error hiding keyboard", e) Log.e(TAG, "❌ Error hiding keyboard", e)
} }
// Если клавиатура уже закрыта (height = 0), показываем emoji сразу isKeyboardVisible = false
if (keyboardHeight <= 0.dp) { currentState = TransitionState.IDLE
Log.d(TAG, "⚡ Keyboard already closed, showing emoji immediately") isTransitioning = false
pendingShowEmojiCallback?.invoke()
} // Очищаем pending callback - больше не нужен
// Иначе ждем когда updateKeyboardHeight получит 0 pendingShowEmojiCallback = null
Log.d(TAG, "✅ requestShowEmoji() completed")
} }
// ============ Главный метод: Emoji → Keyboard ============ // ============ Главный метод: Emoji → Keyboard ============
@@ -293,27 +282,13 @@ class KeyboardTransitionCoordinator {
Log.d(TAG, " After: emojiHeight restored to $maxKeyboardHeight") Log.d(TAG, " After: emojiHeight restored to $maxKeyboardHeight")
} }
// Обнуляем keyboardHeight только после восстановления emoji // Обнуляем keyboardHeight
keyboardHeight = 0.dp keyboardHeight = 0.dp
} else { } else {
Log.d(TAG, "⏭️ No update needed (height too small or unchanged)") Log.d(TAG, "⏭️ No update needed (height too small or unchanged)")
} }
// 🔥 Если клавиатура закрылась И есть pending коллбэк → показываем emoji // 🔥 УБРАН pending callback - теперь emoji показывается сразу в requestShowEmoji()
if (height == 0.dp && pendingShowEmojiCallback != null) {
Log.d(TAG, "🎯 Keyboard closed (0dp), triggering pending emoji callback")
val callback = pendingShowEmojiCallback
pendingShowEmojiCallback = null // 🔥 Сразу обнуляем чтобы не вызвать дважды
// Вызываем через небольшую задержку чтобы UI стабилизировался
Handler(Looper.getMainLooper()).postDelayed({
try {
callback?.invoke()
} catch (e: Exception) {
Log.e(TAG, "❌ Error invoking pending emoji callback", e)
}
}, 50)
}
} }
/** /**
@@ -328,15 +303,29 @@ class KeyboardTransitionCoordinator {
/** /**
* Синхронизировать высоты (emoji = keyboard). * Синхронизировать высоты (emoji = keyboard).
*
* 🔥 ВАЖНО: Синхронизируем ТОЛЬКО когда клавиатура ОТКРЫВАЕТСЯ!
* При закрытии клавиатуры emojiHeight должна оставаться фиксированной!
*/ */
fun syncHeights() { fun syncHeights() {
Log.d(TAG, "🔄 syncHeights called: keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight") // 🔥 Синхронизируем ТОЛЬКО если клавиатура ОТКРЫТА и высота больше текущей emoji
if (keyboardHeight > 100.dp) { if (keyboardHeight > 100.dp && keyboardHeight > emojiHeight) {
val oldEmojiHeight = emojiHeight Log.d(TAG, "🔄 syncHeights: updating emoji $emojiHeight$keyboardHeight")
emojiHeight = keyboardHeight emojiHeight = keyboardHeight
Log.d(TAG, "✅ Heights synced: $oldEmojiHeight$keyboardHeight")
} else { } else {
Log.d(TAG, "⏭️ Keyboard height too small ($keyboardHeight), skipping sync") Log.d(TAG, "⏭️ syncHeights: skipped (keyboard=$keyboardHeight, emoji=$emojiHeight)")
}
}
/**
* Инициализация высоты emoji панели (для pre-rendered подхода).
* Должна быть вызвана при старте для избежания 0dp высоты.
*/
fun initializeEmojiHeight(height: Dp) {
if (emojiHeight == 0.dp && height > 0.dp) {
Log.d(TAG, "🚀 Initializing emoji height: $height")
emojiHeight = height
maxKeyboardHeight = height
} }
} }

View File

@@ -28,6 +28,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -71,6 +72,7 @@ import com.rosetta.messenger.ui.components.AppleEmojiTextField
import com.rosetta.messenger.ui.components.VerifiedBadge import com.rosetta.messenger.ui.components.VerifiedBadge
import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
import app.rosette.android.ui.keyboard.KeyboardTransitionCoordinator
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.content.Context import android.content.Context
@@ -219,6 +221,8 @@ fun ChatDetailScreen(
onUserProfileClick: () -> Unit = {}, onUserProfileClick: () -> Unit = {},
viewModel: ChatViewModel = viewModel() viewModel: ChatViewModel = viewModel()
) { ) {
// 🔥 ОПТИМИЗАЦИЯ: Убрано логирование из композиции чтобы не засорять logcat
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
@@ -247,26 +251,36 @@ fun ChatDetailScreen(
// 🔥 Emoji picker state (поднят из MessageInputBar для KeyboardAvoidingView) // 🔥 Emoji picker state (поднят из MessageInputBar для KeyboardAvoidingView)
var showEmojiPicker by remember { mutableStateOf(false) } var showEmojiPicker by remember { mutableStateOf(false) }
// Высота эмодзи панели - берём высоту клавиатуры если она открыта, иначе 280dp
// 🎯 Координатор плавных переходов клавиатуры (Telegram-style)
val coordinator = rememberKeyboardTransitionCoordinator()
// 🔥 ОПТИМИЗАЦИЯ: НЕ читаем imeHeight напрямую в композиции!
// Используем snapshotFlow чтобы избежать рекомпозиции на каждый пиксель анимации
val imeInsets = WindowInsets.ime val imeInsets = WindowInsets.ime
val imeHeight = with(density) { imeInsets.getBottom(density).toDp() }
val isKeyboardVisible = imeHeight > 50.dp
// 🔥 Запоминаем высоту клавиатуры когда она открыта // 🔥 Синхронизируем coordinator с IME высотой через snapshotFlow (БЕЗ рекомпозиции!)
var savedKeyboardHeight by remember { mutableStateOf(280.dp) } LaunchedEffect(Unit) {
LaunchedEffect(imeHeight) { snapshotFlow {
if (imeHeight > 50.dp) { with(density) { imeInsets.getBottom(density).toDp() }
savedKeyboardHeight = imeHeight }.collect { currentImeHeight ->
coordinator.updateKeyboardHeight(currentImeHeight)
if (currentImeHeight > 100.dp) {
coordinator.syncHeights()
}
} }
} }
// 🔥 Высота панели эмодзи = сохранённая высота клавиатуры (минимум 280.dp)
val emojiPanelHeight = maxOf(savedKeyboardHeight, 280.dp)
// 🔥 Флаг видимости панели эмодзи (тот же что в MessageInputBar) - единый источник правды // 🔥 Инициализируем высоту emoji панели из сохранённой высоты клавиатуры
val isEmojiPanelVisible = showEmojiPicker && !isKeyboardVisible LaunchedEffect(Unit) {
val savedHeightPx = com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context)
// <20> Простой отступ без анимации - AnimatedVisibility сама анимирует if (savedHeightPx > 0) {
val emojiPanelPadding = if (isEmojiPanelVisible) emojiPanelHeight else 0.dp val savedHeightDp = with(density) { savedHeightPx.toDp() }
coordinator.initializeEmojiHeight(savedHeightDp)
} else {
coordinator.initializeEmojiHeight(280.dp) // fallback
}
}
// 🔥 Reply/Forward state // 🔥 Reply/Forward state
val replyMessages by viewModel.replyMessages.collectAsState() val replyMessages by viewModel.replyMessages.collectAsState()
@@ -736,7 +750,7 @@ fun ChatDetailScreen(
// Выпадающее меню - чистый дизайн без артефактов // Выпадающее меню - чистый дизайн без артефактов
MaterialTheme( MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy( colorScheme = MaterialTheme.colorScheme.copy(
surface = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White surface = inputBackgroundColor
) )
) { ) {
DropdownMenu( DropdownMenu(
@@ -745,7 +759,7 @@ fun ChatDetailScreen(
modifier = Modifier modifier = Modifier
.width(220.dp) .width(220.dp)
.background( .background(
color = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White, color = inputBackgroundColor,
shape = RoundedCornerShape(16.dp) shape = RoundedCornerShape(16.dp)
) )
) { ) {
@@ -775,7 +789,8 @@ fun ChatDetailScreen(
showMenu = false showMenu = false
showDeleteConfirm = true showDeleteConfirm = true
}, },
modifier = Modifier.padding(horizontal = 8.dp), modifier = Modifier.padding(horizontal = 8.dp)
.background(inputBackgroundColor),
colors = MenuDefaults.itemColors( colors = MenuDefaults.itemColors(
textColor = Color(0xFFE53935) textColor = Color(0xFFE53935)
) )
@@ -822,7 +837,8 @@ fun ChatDetailScreen(
showBlockConfirm = true showBlockConfirm = true
} }
}, },
modifier = Modifier.padding(horizontal = 8.dp), modifier = Modifier.padding(horizontal = 8.dp)
.background(inputBackgroundColor),
colors = MenuDefaults.itemColors( colors = MenuDefaults.itemColors(
textColor = PrimaryBlue textColor = PrimaryBlue
) )
@@ -863,7 +879,8 @@ fun ChatDetailScreen(
showMenu = false showMenu = false
showLogs = true showLogs = true
}, },
modifier = Modifier.padding(horizontal = 8.dp), modifier = Modifier.padding(horizontal = 8.dp)
.background(inputBackgroundColor),
colors = MenuDefaults.itemColors( colors = MenuDefaults.itemColors(
textColor = textColor textColor = textColor
) )
@@ -937,7 +954,9 @@ fun ChatDetailScreen(
showEmojiPicker = showEmojiPicker, showEmojiPicker = showEmojiPicker,
onToggleEmojiPicker = { showEmojiPicker = it }, onToggleEmojiPicker = { showEmojiPicker = it },
// Focus requester для автофокуса при reply // Focus requester для автофокуса при reply
focusRequester = inputFocusRequester focusRequester = inputFocusRequester,
// Coordinator для плавных переходов
coordinator = coordinator
) )
} }
} }
@@ -1978,8 +1997,12 @@ private fun MessageInputBar(
showEmojiPicker: Boolean = false, showEmojiPicker: Boolean = false,
onToggleEmojiPicker: (Boolean) -> Unit = {}, onToggleEmojiPicker: (Boolean) -> Unit = {},
// Focus requester для автофокуса при reply // Focus requester для автофокуса при reply
focusRequester: FocusRequester? = null focusRequester: FocusRequester? = null,
// Coordinator для плавных переходов клавиатуры
coordinator: KeyboardTransitionCoordinator
) { ) {
// 🔥 ОПТИМИЗАЦИЯ: Убрано логирование чтобы не засорять logcat
val hasReply = replyMessages.isNotEmpty() val hasReply = replyMessages.isNotEmpty()
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
@@ -1991,9 +2014,6 @@ private fun MessageInputBar(
val view = LocalView.current val view = LocalView.current
val density = LocalDensity.current val density = LocalDensity.current
// 🎯 Координатор плавных переходов клавиатуры (Telegram-style)
val coordinator = rememberKeyboardTransitionCoordinator()
// 🔥 Ссылка на EditText для программного фокуса // 🔥 Ссылка на EditText для программного фокуса
var editTextView by remember { mutableStateOf<com.rosetta.messenger.ui.components.AppleEmojiEditTextView?>(null) } var editTextView by remember { mutableStateOf<com.rosetta.messenger.ui.components.AppleEmojiEditTextView?>(null) }
@@ -2021,56 +2041,47 @@ private fun MessageInputBar(
} }
} }
// 🔥 Отслеживаем высоту клавиатуры (Telegram-style) // 🔥 ОПТИМИЗАЦИЯ: НЕ читаем imeHeight напрямую - это вызывает рекомпозицию!
val imeInsets = WindowInsets.ime val imeInsets = WindowInsets.ime
val imeHeight = with(density) { imeInsets.getBottom(density).toDp() }
val isKeyboardVisible = imeHeight > 50.dp // 🔥 Согласованный порог с ChatDetailScreen
// 🔥 Флаг "клавиатура в процессе анимации" // 🔥 Флаг "клавиатура видна" - обновляется через snapshotFlow, НЕ вызывает рекомпозицию
var isKeyboardAnimating by remember { mutableStateOf(false) } var isKeyboardVisible by remember { mutableStateOf(false) }
var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) }
// 🔥 Логирование изменений высоты клавиатуры + обновление coordinator // 🔥 Обновляем coordinator через snapshotFlow (БЕЗ рекомпозиции!)
LaunchedEffect(imeHeight) { LaunchedEffect(Unit) {
android.util.Log.d("KeyboardHeight", "═══════════════════════════════════════════════════════") snapshotFlow {
android.util.Log.d("KeyboardHeight", "📊 IME height changed: $imeHeight") with(density) { imeInsets.getBottom(density).toDp() }
android.util.Log.d("KeyboardHeight", " isKeyboardVisible=$isKeyboardVisible") }.collect { currentImeHeight ->
android.util.Log.d("KeyboardHeight", " showEmojiPicker=$showEmojiPicker") // Обновляем флаг видимости (это НЕ вызывает рекомпозицию напрямую)
android.util.Log.d("KeyboardHeight", " isKeyboardAnimating=$isKeyboardAnimating") isKeyboardVisible = currentImeHeight > 50.dp
// Обновляем coordinator с актуальной высотой клавиатуры // Обновляем coordinator
android.util.Log.d("KeyboardHeight", "🔄 Updating coordinator...") coordinator.updateKeyboardHeight(currentImeHeight)
coordinator.updateKeyboardHeight(imeHeight) if (currentImeHeight > 100.dp) {
// Синхронизируем высоту emoji с клавиатурой
if (imeHeight > 100.dp) {
android.util.Log.d("KeyboardHeight", "🔄 Syncing heights...")
coordinator.syncHeights() coordinator.syncHeights()
lastStableKeyboardHeight = currentImeHeight
}
} }
android.util.Log.d("KeyboardHeight", "✅ Coordinator updated")
} }
// 🔥 Запоминаем высоту клавиатуры когда она открыта (Telegram-style with SharedPreferences) // 🔥 Запоминаем высоту клавиатуры когда она открыта (Telegram-style with SharedPreferences)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Загружаем сохранённую высоту при старте // Загружаем сохранённую высоту при старте
val savedHeightPx = com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context) com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context)
android.util.Log.d("KeyboardHeight", "📱 MessageInputBar initialized, loaded height: ${savedHeightPx}px")
} }
// 🔥 Сохраняем высоту ТОЛЬКО когда клавиатура стабильна // 🔥 Сохраняем высоту ТОЛЬКО когда клавиатура стабильна
// Используем отдельный LaunchedEffect который НЕ реагирует на каждое изменение
LaunchedEffect(isKeyboardVisible, showEmojiPicker) { LaunchedEffect(isKeyboardVisible, showEmojiPicker) {
// Если клавиатура стала видимой и emoji закрыт // Если клавиатура стала видимой и emoji закрыт
if (isKeyboardVisible && !showEmojiPicker && !isKeyboardAnimating) { if (isKeyboardVisible && !showEmojiPicker) {
// Ждем стабилизации // Ждем стабилизации
isKeyboardAnimating = true kotlinx.coroutines.delay(350) // Анимация клавиатуры ~300ms
kotlinx.coroutines.delay(300) // Анимация клавиатуры ~250ms
isKeyboardAnimating = false
// Сохраняем только если всё еще видна и emoji закрыт // Сохраняем только если всё еще видна и emoji закрыт
if (isKeyboardVisible && !showEmojiPicker && imeHeight > 300.dp) { if (isKeyboardVisible && !showEmojiPicker && lastStableKeyboardHeight > 300.dp) {
val heightPx = with(density) { imeHeight.toPx().toInt() } val heightPx = with(density) { lastStableKeyboardHeight.toPx().toInt() }
com.rosetta.messenger.ui.components.KeyboardHeightProvider.saveKeyboardHeight(context, heightPx) com.rosetta.messenger.ui.components.KeyboardHeightProvider.saveKeyboardHeight(context, heightPx)
android.util.Log.d("KeyboardHeight", "✅ Stable keyboard height saved: ${imeHeight} (${heightPx}px)")
} }
} }
} }
@@ -2343,8 +2354,6 @@ private fun MessageInputBar(
android.util.Log.d("EmojiPicker", "🔄 TextField focused while emoji open → closing emoji") android.util.Log.d("EmojiPicker", "🔄 TextField focused while emoji open → closing emoji")
android.util.Log.d("EmojiPicker", " 📞 Calling onToggleEmojiPicker(false)...") android.util.Log.d("EmojiPicker", " 📞 Calling onToggleEmojiPicker(false)...")
onToggleEmojiPicker(false) onToggleEmojiPicker(false)
android.util.Log.d("EmojiPicker", " 📞 Setting coordinator.isEmojiVisible = false...")
coordinator.isEmojiVisible = false
android.util.Log.d("EmojiPicker", " ✅ Emoji close requested") android.util.Log.d("EmojiPicker", " ✅ Emoji close requested")
} else if (hasFocus && !showEmojiPicker) { } else if (hasFocus && !showEmojiPicker) {
android.util.Log.d("EmojiPicker", "⌨️ TextField focused with emoji closed → normal keyboard behavior") android.util.Log.d("EmojiPicker", "⌨️ TextField focused with emoji closed → normal keyboard behavior")