feat: Update ChatDetailScreen to use inputBackgroundColor for dropdown menus and menu items
This commit is contained in:
@@ -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) }
|
||||
// 🎯 Целевая высота: emojiHeight = видно, 0dp = скрыто
|
||||
val targetHeight = if (showEmojiPicker) {
|
||||
coordinator.emojiHeight
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
|
||||
// Отслеживаем изменения 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,
|
||||
// 🎬 Декларативная анимация высоты
|
||||
val animatedHeight by animateDpAsState(
|
||||
targetValue = targetHeight,
|
||||
animationSpec = tween(
|
||||
durationMillis = KeyboardTransitionCoordinator.TRANSITION_DURATION.toInt(),
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
label = "emojiPanelHeight"
|
||||
)
|
||||
)
|
||||
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}")
|
||||
}
|
||||
Log.d(tag, "🎨 Emoji panel: show=$showEmojiPicker, targetH=$targetHeight, currentH=$animatedHeight, emojiH=${coordinator.emojiHeight}")
|
||||
|
||||
} 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")
|
||||
// 🔥 Рендерим только если есть высота (показан или анимируется)
|
||||
if (animatedHeight > 0.dp) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(animatedHeight)
|
||||
.clipToBounds() // 🔥 Обрезаем контент по границам контейнера
|
||||
.graphicsLayer {
|
||||
// GPU-ускорение для плавности
|
||||
clip = true
|
||||
}
|
||||
|
||||
// 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 Рендерим контент ТОЛЬКО если showEmojiPicker = true
|
||||
// Не проверяем offsetY, чтобы избежать показа emoji при открытии клавиатуры
|
||||
if (showEmojiPicker) {
|
||||
Log.d(tag, "✅ Rendering emoji content (showEmojiPicker=true)")
|
||||
) {
|
||||
// Внутренний контейнер с полной высотой emoji
|
||||
// Он обрезается clipToBounds родителя
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(coordinator.emojiHeight)
|
||||
.offset(y = offsetY.value.dp)
|
||||
.alpha(alpha.value)
|
||||
// 🔥 Позиционируем снизу: когда 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
|
||||
AnimatedKeyboardTransition(
|
||||
coordinator = coordinator,
|
||||
showEmojiPicker = showEmojiPicker,
|
||||
content = content
|
||||
)
|
||||
)
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
// 🎯 Координатор плавных переходов клавиатуры (Telegram-style)
|
||||
val coordinator = rememberKeyboardTransitionCoordinator()
|
||||
|
||||
// 🔥 ОПТИМИЗАЦИЯ: НЕ читаем imeHeight напрямую в композиции!
|
||||
// Используем snapshotFlow чтобы избежать рекомпозиции на каждый пиксель анимации
|
||||
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
|
||||
// 🔥 Синхронизируем 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 через snapshotFlow (БЕЗ рекомпозиции!)
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow {
|
||||
with(density) { imeInsets.getBottom(density).toDp() }
|
||||
}.collect { currentImeHeight ->
|
||||
// Обновляем флаг видимости (это НЕ вызывает рекомпозицию напрямую)
|
||||
isKeyboardVisible = currentImeHeight > 50.dp
|
||||
|
||||
// Обновляем coordinator с актуальной высотой клавиатуры
|
||||
android.util.Log.d("KeyboardHeight", "🔄 Updating coordinator...")
|
||||
coordinator.updateKeyboardHeight(imeHeight)
|
||||
|
||||
// Синхронизируем высоту emoji с клавиатурой
|
||||
if (imeHeight > 100.dp) {
|
||||
android.util.Log.d("KeyboardHeight", "🔄 Syncing heights...")
|
||||
// Обновляем 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")
|
||||
|
||||
Reference in New Issue
Block a user