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

View File

@@ -72,7 +72,9 @@ class KeyboardTransitionCoordinator {
/**
* Переход от системной клавиатуры к emoji панели.
* Telegram паттерн: сначала скрыть клавиатуру, затем показать emoji.
*
* 🔥 КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: Показываем emoji СРАЗУ!
* Не ждем закрытия клавиатуры - emoji начинает выезжать синхронно.
*/
fun requestShowEmoji(
hideKeyboard: () -> Unit,
@@ -84,40 +86,25 @@ class KeyboardTransitionCoordinator {
Log.d(TAG, " 📊 Current state:")
Log.d(TAG, " - currentState=$currentState")
Log.d(TAG, " - keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight")
Log.d(TAG, " - isTransitioning=$isTransitioning")
Log.d(TAG, " - isKeyboardVisible=$isKeyboardVisible, isEmojiVisible=$isEmojiVisible")
Log.d(TAG, " - pendingShowEmojiCallback=${if (pendingShowEmojiCallback != null) "EXISTS" else "null"}")
Log.d(TAG, " - maxKeyboardHeight=$maxKeyboardHeight")
Log.d(TAG, " 📝 Setting currentState = KEYBOARD_TO_EMOJI")
currentState = TransitionState.KEYBOARD_TO_EMOJI
isTransitioning = true
// 🔥 Сохраняем коллбэк для вызова после закрытия клавиатуры
Log.d(TAG, " 💾 Saving pending emoji callback...")
pendingShowEmojiCallback = {
try {
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
}
// 🔥 Гарантируем что emojiHeight = maxKeyboardHeight (не меняется при закрытии клавиатуры)
if (maxKeyboardHeight > 0.dp) {
emojiHeight = maxKeyboardHeight
Log.d(TAG, " 📌 Locked emojiHeight to maxKeyboardHeight: $emojiHeight")
}
Log.d(TAG, " ✅ Pending callback saved")
// Шаг 1: Скрыть системную клавиатуру
Log.d(TAG, " 📞 Calling hideKeyboard()...")
// 🔥 ПОКАЗЫВАЕМ EMOJI СРАЗУ! Не ждем закрытия клавиатуры
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 {
hideKeyboard()
Log.d(TAG, " ✅ hideKeyboard() completed")
@@ -125,12 +112,14 @@ class KeyboardTransitionCoordinator {
Log.e(TAG, "❌ Error hiding keyboard", e)
}
// Если клавиатура уже закрыта (height = 0), показываем emoji сразу
if (keyboardHeight <= 0.dp) {
Log.d(TAG, "⚡ Keyboard already closed, showing emoji immediately")
pendingShowEmojiCallback?.invoke()
}
// Иначе ждем когда updateKeyboardHeight получит 0
isKeyboardVisible = false
currentState = TransitionState.IDLE
isTransitioning = false
// Очищаем pending callback - больше не нужен
pendingShowEmojiCallback = null
Log.d(TAG, "✅ requestShowEmoji() completed")
}
// ============ Главный метод: Emoji → Keyboard ============
@@ -293,27 +282,13 @@ class KeyboardTransitionCoordinator {
Log.d(TAG, " After: emojiHeight restored to $maxKeyboardHeight")
}
// Обнуляем keyboardHeight только после восстановления emoji
// Обнуляем keyboardHeight
keyboardHeight = 0.dp
} else {
Log.d(TAG, "⏭️ No update needed (height too small or unchanged)")
}
// 🔥 Если клавиатура закрылась И есть pending коллбэк → показываем emoji
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)
}
// 🔥 УБРАН pending callback - теперь emoji показывается сразу в requestShowEmoji()
}
/**
@@ -328,15 +303,29 @@ class KeyboardTransitionCoordinator {
/**
* Синхронизировать высоты (emoji = keyboard).
*
* 🔥 ВАЖНО: Синхронизируем ТОЛЬКО когда клавиатура ОТКРЫВАЕТСЯ!
* При закрытии клавиатуры emojiHeight должна оставаться фиксированной!
*/
fun syncHeights() {
Log.d(TAG, "🔄 syncHeights called: keyboardHeight=$keyboardHeight, emojiHeight=$emojiHeight")
if (keyboardHeight > 100.dp) {
val oldEmojiHeight = emojiHeight
// 🔥 Синхронизируем ТОЛЬКО если клавиатура ОТКРЫТА и высота больше текущей emoji
if (keyboardHeight > 100.dp && keyboardHeight > emojiHeight) {
Log.d(TAG, "🔄 syncHeights: updating emoji $emojiHeight$keyboardHeight")
emojiHeight = keyboardHeight
Log.d(TAG, "✅ Heights synced: $oldEmojiHeight$keyboardHeight")
} 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.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
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.onboarding.PrimaryBlue
import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator
import app.rosette.android.ui.keyboard.KeyboardTransitionCoordinator
import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition
import android.view.inputmethod.InputMethodManager
import android.content.Context
@@ -219,6 +221,8 @@ fun ChatDetailScreen(
onUserProfileClick: () -> Unit = {},
viewModel: ChatViewModel = viewModel()
) {
// 🔥 ОПТИМИЗАЦИЯ: Убрано логирование из композиции чтобы не засорять logcat
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
@@ -247,26 +251,36 @@ fun ChatDetailScreen(
// 🔥 Emoji picker state (поднят из MessageInputBar для KeyboardAvoidingView)
var showEmojiPicker by remember { mutableStateOf(false) }
// Высота эмодзи панели - берём высоту клавиатуры если она открыта, иначе 280dp
val imeInsets = WindowInsets.ime
val imeHeight = with(density) { imeInsets.getBottom(density).toDp() }
val isKeyboardVisible = imeHeight > 50.dp
// 🔥 Запоминаем высоту клавиатуры когда она открыта
var savedKeyboardHeight by remember { mutableStateOf(280.dp) }
LaunchedEffect(imeHeight) {
if (imeHeight > 50.dp) {
savedKeyboardHeight = imeHeight
// 🎯 Координатор плавных переходов клавиатуры (Telegram-style)
val coordinator = rememberKeyboardTransitionCoordinator()
// 🔥 ОПТИМИЗАЦИЯ: НЕ читаем imeHeight напрямую в композиции!
// Используем snapshotFlow чтобы избежать рекомпозиции на каждый пиксель анимации
val imeInsets = WindowInsets.ime
// 🔥 Синхронизируем coordinator с IME высотой через snapshotFlow (БЕЗ рекомпозиции!)
LaunchedEffect(Unit) {
snapshotFlow {
with(density) { imeInsets.getBottom(density).toDp() }
}.collect { currentImeHeight ->
coordinator.updateKeyboardHeight(currentImeHeight)
if (currentImeHeight > 100.dp) {
coordinator.syncHeights()
}
}
}
// 🔥 Высота панели эмодзи = сохранённая высота клавиатуры (минимум 280.dp)
val emojiPanelHeight = maxOf(savedKeyboardHeight, 280.dp)
// 🔥 Флаг видимости панели эмодзи (тот же что в MessageInputBar) - единый источник правды
val isEmojiPanelVisible = showEmojiPicker && !isKeyboardVisible
// <20> Простой отступ без анимации - AnimatedVisibility сама анимирует
val emojiPanelPadding = if (isEmojiPanelVisible) emojiPanelHeight else 0.dp
// 🔥 Инициализируем высоту emoji панели из сохранённой высоты клавиатуры
LaunchedEffect(Unit) {
val savedHeightPx = com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context)
if (savedHeightPx > 0) {
val savedHeightDp = with(density) { savedHeightPx.toDp() }
coordinator.initializeEmojiHeight(savedHeightDp)
} else {
coordinator.initializeEmojiHeight(280.dp) // fallback
}
}
// 🔥 Reply/Forward state
val replyMessages by viewModel.replyMessages.collectAsState()
@@ -736,7 +750,7 @@ fun ChatDetailScreen(
// Выпадающее меню - чистый дизайн без артефактов
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(
surface = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White
surface = inputBackgroundColor
)
) {
DropdownMenu(
@@ -745,7 +759,7 @@ fun ChatDetailScreen(
modifier = Modifier
.width(220.dp)
.background(
color = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White,
color = inputBackgroundColor,
shape = RoundedCornerShape(16.dp)
)
) {
@@ -775,7 +789,8 @@ fun ChatDetailScreen(
showMenu = false
showDeleteConfirm = true
},
modifier = Modifier.padding(horizontal = 8.dp),
modifier = Modifier.padding(horizontal = 8.dp)
.background(inputBackgroundColor),
colors = MenuDefaults.itemColors(
textColor = Color(0xFFE53935)
)
@@ -822,7 +837,8 @@ fun ChatDetailScreen(
showBlockConfirm = true
}
},
modifier = Modifier.padding(horizontal = 8.dp),
modifier = Modifier.padding(horizontal = 8.dp)
.background(inputBackgroundColor),
colors = MenuDefaults.itemColors(
textColor = PrimaryBlue
)
@@ -863,7 +879,8 @@ fun ChatDetailScreen(
showMenu = false
showLogs = true
},
modifier = Modifier.padding(horizontal = 8.dp),
modifier = Modifier.padding(horizontal = 8.dp)
.background(inputBackgroundColor),
colors = MenuDefaults.itemColors(
textColor = textColor
)
@@ -937,7 +954,9 @@ fun ChatDetailScreen(
showEmojiPicker = showEmojiPicker,
onToggleEmojiPicker = { showEmojiPicker = it },
// Focus requester для автофокуса при reply
focusRequester = inputFocusRequester
focusRequester = inputFocusRequester,
// Coordinator для плавных переходов
coordinator = coordinator
)
}
}
@@ -1978,8 +1997,12 @@ private fun MessageInputBar(
showEmojiPicker: Boolean = false,
onToggleEmojiPicker: (Boolean) -> Unit = {},
// Focus requester для автофокуса при reply
focusRequester: FocusRequester? = null
focusRequester: FocusRequester? = null,
// Coordinator для плавных переходов клавиатуры
coordinator: KeyboardTransitionCoordinator
) {
// 🔥 ОПТИМИЗАЦИЯ: Убрано логирование чтобы не засорять logcat
val hasReply = replyMessages.isNotEmpty()
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
@@ -1991,9 +2014,6 @@ private fun MessageInputBar(
val view = LocalView.current
val density = LocalDensity.current
// 🎯 Координатор плавных переходов клавиатуры (Telegram-style)
val coordinator = rememberKeyboardTransitionCoordinator()
// 🔥 Ссылка на EditText для программного фокуса
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 imeHeight = with(density) { imeInsets.getBottom(density).toDp() }
val isKeyboardVisible = imeHeight > 50.dp // 🔥 Согласованный порог с ChatDetailScreen
// 🔥 Флаг "клавиатура в процессе анимации"
var isKeyboardAnimating by remember { mutableStateOf(false) }
// 🔥 Флаг "клавиатура видна" - обновляется через snapshotFlow, НЕ вызывает рекомпозицию
var isKeyboardVisible by remember { mutableStateOf(false) }
var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) }
// 🔥 Логирование изменений высоты клавиатуры + обновление coordinator
LaunchedEffect(imeHeight) {
android.util.Log.d("KeyboardHeight", "═══════════════════════════════════════════════════════")
android.util.Log.d("KeyboardHeight", "📊 IME height changed: $imeHeight")
android.util.Log.d("KeyboardHeight", " isKeyboardVisible=$isKeyboardVisible")
android.util.Log.d("KeyboardHeight", " showEmojiPicker=$showEmojiPicker")
android.util.Log.d("KeyboardHeight", " isKeyboardAnimating=$isKeyboardAnimating")
// Обновляем coordinator с актуальной высотой клавиатуры
android.util.Log.d("KeyboardHeight", "🔄 Updating coordinator...")
coordinator.updateKeyboardHeight(imeHeight)
// Синхронизируем высоту emoji с клавиатурой
if (imeHeight > 100.dp) {
android.util.Log.d("KeyboardHeight", "🔄 Syncing heights...")
coordinator.syncHeights()
// 🔥 Обновляем coordinator через snapshotFlow (БЕЗ рекомпозиции!)
LaunchedEffect(Unit) {
snapshotFlow {
with(density) { imeInsets.getBottom(density).toDp() }
}.collect { currentImeHeight ->
// Обновляем флаг видимости (это НЕ вызывает рекомпозицию напрямую)
isKeyboardVisible = currentImeHeight > 50.dp
// Обновляем coordinator
coordinator.updateKeyboardHeight(currentImeHeight)
if (currentImeHeight > 100.dp) {
coordinator.syncHeights()
lastStableKeyboardHeight = currentImeHeight
}
}
android.util.Log.d("KeyboardHeight", "✅ Coordinator updated")
}
// 🔥 Запоминаем высоту клавиатуры когда она открыта (Telegram-style with SharedPreferences)
LaunchedEffect(Unit) {
// Загружаем сохранённую высоту при старте
val savedHeightPx = com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context)
android.util.Log.d("KeyboardHeight", "📱 MessageInputBar initialized, loaded height: ${savedHeightPx}px")
com.rosetta.messenger.ui.components.KeyboardHeightProvider.getSavedKeyboardHeight(context)
}
// 🔥 Сохраняем высоту ТОЛЬКО когда клавиатура стабильна
// Используем отдельный LaunchedEffect который НЕ реагирует на каждое изменение
LaunchedEffect(isKeyboardVisible, showEmojiPicker) {
// Если клавиатура стала видимой и emoji закрыт
if (isKeyboardVisible && !showEmojiPicker && !isKeyboardAnimating) {
if (isKeyboardVisible && !showEmojiPicker) {
// Ждем стабилизации
isKeyboardAnimating = true
kotlinx.coroutines.delay(300) // Анимация клавиатуры ~250ms
isKeyboardAnimating = false
kotlinx.coroutines.delay(350) // Анимация клавиатуры ~300ms
// Сохраняем только если всё еще видна и emoji закрыт
if (isKeyboardVisible && !showEmojiPicker && imeHeight > 300.dp) {
val heightPx = with(density) { imeHeight.toPx().toInt() }
if (isKeyboardVisible && !showEmojiPicker && lastStableKeyboardHeight > 300.dp) {
val heightPx = with(density) { lastStableKeyboardHeight.toPx().toInt() }
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", " 📞 Calling onToggleEmojiPicker(false)...")
onToggleEmojiPicker(false)
android.util.Log.d("EmojiPicker", " 📞 Setting coordinator.isEmojiVisible = false...")
coordinator.isEmojiVisible = false
android.util.Log.d("EmojiPicker", " ✅ Emoji close requested")
} else if (hasFocus && !showEmojiPicker) {
android.util.Log.d("EmojiPicker", "⌨️ TextField focused with emoji closed → normal keyboard behavior")