From eb96d269f6a42e9692af6277c809b209ad16e1ed Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Mon, 2 Feb 2026 23:33:24 +0500 Subject: [PATCH] feat: add emoji picker with smooth transition and keyboard height management --- .../ui/keyboard/AnimatedKeyboardTransition.kt | 17 +- .../keyboard/KeyboardTransitionCoordinator.kt | 4 + .../ui/chats/components/ImageEditorScreen.kt | 98 ++--- .../components/MediaPickerBottomSheet.kt | 342 ++++++++++++++---- 4 files changed, 318 insertions(+), 143 deletions(-) diff --git a/app/src/main/java/app/rosette/android/ui/keyboard/AnimatedKeyboardTransition.kt b/app/src/main/java/app/rosette/android/ui/keyboard/AnimatedKeyboardTransition.kt index eaa0b2e..fa28bd4 100644 --- a/app/src/main/java/app/rosette/android/ui/keyboard/AnimatedKeyboardTransition.kt +++ b/app/src/main/java/app/rosette/android/ui/keyboard/AnimatedKeyboardTransition.kt @@ -43,10 +43,11 @@ fun AnimatedKeyboardTransition( // 📊 Последнее залогированное значение alpha (для фильтрации) val lastLoggedAlpha = remember { mutableStateOf(-1f) } - // 🔥 Клавиатура достигла ПОЛНОЙ высоты (разница не более 3dp) - val isKeyboardFullHeight = coordinator.emojiHeight > 0.dp && - coordinator.keyboardHeight >= coordinator.emojiHeight - 3.dp - + // 🔥 Клавиатура достигла высоты почти равной emoji (разница < 2dp) + // Это гарантирует плавный переход без прыжков и ререндеров + val isKeyboardNearFullHeight = coordinator.emojiHeight > 0.dp && + coordinator.keyboardHeight >= coordinator.emojiHeight - 2.dp + // Логика перехода if (showEmojiPicker && !wasEmojiShown) { wasEmojiShown = true @@ -55,10 +56,10 @@ fun AnimatedKeyboardTransition( // Emoji закрылся после того как был открыт = переход emoji→keyboard isTransitioningToKeyboard = true } - - // 🔥 КЛЮЧЕВОЕ: Сбрасываем флаг БЕЗ задержки когда клавиатура полностью открылась - // Проверяем прямо в composition для мгновенной реакции - if (isKeyboardFullHeight && isTransitioningToKeyboard) { + + // 🔥 КЛЮЧЕВОЕ: Сбрасываем флаг когда клавиатура достигла высоты близкой к emoji + // Это предотвращает прыжок инпута при переходе + if (isKeyboardNearFullHeight && isTransitioningToKeyboard && !showEmojiPicker) { isTransitioningToKeyboard = false wasEmojiShown = false } diff --git a/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt b/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt index 12f9ef8..865ed12 100644 --- a/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt +++ b/app/src/main/java/app/rosette/android/ui/keyboard/KeyboardTransitionCoordinator.kt @@ -89,6 +89,10 @@ class KeyboardTransitionCoordinator { emojiHeight = maxKeyboardHeight } + // 🔥 КРИТИЧНО: Устанавливаем isEmojiBoxVisible СРАЗУ, ДО showEmoji()! + // Это отключает imePadding на ПЕРВОМ же рендере, предотвращая "прыжок" + isEmojiBoxVisible = true + // 🔥 ПОКАЗЫВАЕМ EMOJI СРАЗУ! Не ждем закрытия клавиатуры showEmoji() isEmojiVisible = true // 🔥 ВАЖНО: Устанавливаем флаг видимости emoji! diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt index 6791f32..90229df 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri +import android.util.Log import android.view.inputmethod.InputMethodManager import android.widget.ImageView import androidx.activity.compose.BackHandler @@ -50,6 +51,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView +import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator import com.rosetta.messenger.ui.components.AppleEmojiTextField import com.rosetta.messenger.ui.components.KeyboardHeightProvider @@ -210,16 +212,20 @@ fun ImageEditorScreen( // Отслеживание высоты клавиатуры val imeInsets = WindowInsets.ime + var isKeyboardVisible by remember { mutableStateOf(false) } var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) } // Update coordinator through snapshotFlow LaunchedEffect(Unit) { snapshotFlow { with(density) { imeInsets.getBottom(density).toDp() } }.collect { currentImeHeight -> + isKeyboardVisible = currentImeHeight > 50.dp coordinator.updateKeyboardHeight(currentImeHeight) if (currentImeHeight > 100.dp) { coordinator.syncHeights() lastStableKeyboardHeight = currentImeHeight } + // 📊 Log IME height changes + Log.d(TAG, "IME height: ${currentImeHeight.value}dp, isKeyboardVisible: $isKeyboardVisible, emojiHeight: ${coordinator.emojiHeight.value}dp, isEmojiBoxVisible: ${coordinator.isEmojiBoxVisible}") } } @@ -248,19 +254,23 @@ fun ImageEditorScreen( val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + Log.d(TAG, "toggleEmojiPicker: isEmojiVisible=${coordinator.isEmojiVisible}, isEmojiBoxVisible=${coordinator.isEmojiBoxVisible}, showEmojiPicker=$showEmojiPicker") + if (coordinator.isEmojiVisible) { // EMOJI → KEYBOARD + Log.d(TAG, "TRANSITION: EMOJI → KEYBOARD") coordinator.requestShowKeyboard( showKeyboard = { editTextView?.let { editText -> editText.requestFocus() - imm.showSoftInput(editText, InputMethodManager.SHOW_FORCED) + imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) } }, hideEmoji = { showEmojiPicker = false } ) } else { // KEYBOARD → EMOJI + Log.d(TAG, "TRANSITION: KEYBOARD → EMOJI") coordinator.requestShowEmoji( hideKeyboard = { imm.hideSoftInputFromWindow(view.windowToken, 0) }, showEmoji = { showEmojiPicker = true } @@ -513,42 +523,22 @@ fun ImageEditorScreen( // ═══════════════════════════════════════════════════════════ // 📝 CAPTION INPUT + EMOJI PICKER - Telegram style - // Прячется с анимацией когда открыты инструменты (рисовалка, поворот и т.д.) + // Использует imePadding + AnimatedKeyboardTransition как в ChatDetailInput // ═══════════════════════════════════════════════════════════ - // ═══════════════════════════════════════════════════════════ - // 🔥 TELEGRAM-STYLE FIX: Единый spacer вместо imePadding - // Это ПОЛНОСТЬЮ устраняет прыжки при переходе keyboard ↔ emoji - // ═══════════════════════════════════════════════════════════ - val currentImeHeight = with(density) { WindowInsets.ime.getBottom(density).toDp() } - val isImeActuallyOpen = currentImeHeight > 50.dp - - // 🔥 КЛЮЧЕВОЕ: Единая высота spacer'а - ИЛИ keyboard ИЛИ emoji - // Когда keyboard открыта: spacerHeight = imeHeight - // Когда emoji открыта: spacerHeight = emojiHeight - // Это даёт плавный переход без прыжков! - - // Fallback высота emoji на случай если coordinator.emojiHeight = 0 - val effectiveEmojiHeight = when { - coordinator.emojiHeight > 0.dp -> coordinator.emojiHeight - lastStableKeyboardHeight > 0.dp -> lastStableKeyboardHeight - else -> 300.dp // Минимальная fallback высота - } - - val spacerHeight = when { - isImeActuallyOpen -> currentImeHeight // Keyboard открыта - её высота - showEmojiPicker || coordinator.isEmojiBoxVisible -> effectiveEmojiHeight // Emoji открыта - else -> 0.dp // Ничего не открыто - } - - // Нужен ли spacer вообще - val needsSpacer = spacerHeight > 0.dp - - // 🔥 Обновляем coordinator для правильной работы toolbar - coordinator.isEmojiBoxVisible = showEmojiPicker + // 🔥 КЛЮЧЕВОЕ: imePadding применяется ТОЛЬКО когда emoji НЕ показан + val shouldUseImePadding = !coordinator.isEmojiBoxVisible // Когда клавиатура/emoji закрыты - добавляем отступ снизу для toolbar (~100dp) - val bottomPaddingForCaption = if (!needsSpacer) 100.dp else 0.dp + // 🔥 Анимируем отступ для плавного перехода + val bottomPaddingForCaption by animateDpAsState( + targetValue = if (!isKeyboardVisible && !coordinator.isEmojiBoxVisible) 100.dp else 0.dp, + animationSpec = tween(200, easing = FastOutSlowInEasing), + label = "bottomPadding" + ) + + // 📊 Log render state + Log.d(TAG, "RENDER: showEmoji=$showEmojiPicker, isKeyboard=$isKeyboardVisible, isEmojiBoxVisible=${coordinator.isEmojiBoxVisible}, useImePadding=$shouldUseImePadding, emojiHeight=${coordinator.emojiHeight.value}dp, bottomPadding=${bottomPaddingForCaption.value}dp") AnimatedVisibility( visible = showCaptionInput && currentTool == EditorTool.NONE, @@ -562,13 +552,15 @@ fun ImageEditorScreen( modifier = Modifier .fillMaxWidth() .padding(bottom = bottomPaddingForCaption) - // 🔥 БЕЗ imePadding! Всё контролируется через spacer ниже + // 🔥 imePadding ТОЛЬКО когда emoji НЕ показан + .then(if (shouldUseImePadding) Modifier.imePadding() else Modifier) ) { TelegramCaptionBar( caption = caption, onCaptionChange = { caption = it }, isSaving = isSaving, - isKeyboardVisible = isImeActuallyOpen || showEmojiPicker, + // 🔥 Добавляем isEmojiBoxVisible чтобы во время перехода emoji→keyboard инпут не сжимался + isKeyboardVisible = isKeyboardVisible || showEmojiPicker || coordinator.isEmojiBoxVisible, showEmojiPicker = showEmojiPicker, onToggleEmojiPicker = { toggleEmojiPicker() }, onEditTextViewCreated = { editTextView = it }, @@ -590,30 +582,20 @@ fun ImageEditorScreen( ) // ═══════════════════════════════════════════════════════════ - // 🔥 UNIFIED SPACER: Один Box для keyboard И emoji - // Высота = imeHeight когда keyboard, = emojiHeight когда emoji + // 🔥 EMOJI PICKER - используем AnimatedKeyboardTransition + // Точно как в ChatDetailInput! // ═══════════════════════════════════════════════════════════ - if (needsSpacer) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(spacerHeight) - ) { - // Emoji picker рендерится ВНУТРИ spacer'а с fade анимацией - androidx.compose.animation.AnimatedVisibility( - visible = showEmojiPicker, - enter = fadeIn(animationSpec = tween(200)), - exit = fadeOut(animationSpec = tween(200)) - ) { - OptimizedEmojiPicker( - isVisible = true, - isDarkTheme = isDarkTheme, - onEmojiSelected = { emoji -> caption = caption + emoji }, - onClose = { toggleEmojiPicker() }, - modifier = Modifier.fillMaxSize() - ) - } - } + AnimatedKeyboardTransition( + coordinator = coordinator, + showEmojiPicker = showEmojiPicker + ) { + OptimizedEmojiPicker( + isVisible = true, + isDarkTheme = isDarkTheme, + onEmojiSelected = { emoji -> caption = caption + emoji }, + onClose = { toggleEmojiPicker() }, + modifier = Modifier.fillMaxWidth() + ) } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt index 9f0b80e..9282661 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/MediaPickerBottomSheet.kt @@ -3,11 +3,13 @@ package com.rosetta.messenger.ui.chats.components import android.Manifest import android.content.ContentUris import android.content.Context +import android.util.Log import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.provider.MediaStore import android.view.WindowManager +import android.view.inputmethod.InputMethodManager import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.* @@ -25,9 +27,11 @@ 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.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale @@ -36,6 +40,7 @@ import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.font.FontWeight @@ -59,7 +64,13 @@ import coil.request.ImageRequest import compose.icons.TablerIcons import compose.icons.tablericons.* import com.rosetta.messenger.ui.onboarding.PrimaryBlue +import com.rosetta.messenger.ui.components.AppleEmojiTextField +import com.rosetta.messenger.ui.components.KeyboardHeightProvider +import com.rosetta.messenger.ui.components.OptimizedEmojiPicker +import app.rosette.android.ui.keyboard.rememberKeyboardTransitionCoordinator +import app.rosette.android.ui.keyboard.AnimatedKeyboardTransition import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.math.roundToInt @@ -1383,6 +1394,11 @@ private fun formatDuration(durationMs: Long): String { /** * Экран предпросмотра фото с caption и кнопкой отправки (как в Telegram) + * С плавной анимацией перехода между клавиатурой и emoji picker + * + * Использует тот же подход что ChatDetailInput: + * - imePadding() применяется ТОЛЬКО когда emoji НЕ показан + * - AnimatedKeyboardTransition управляет emoji picker */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -1395,98 +1411,270 @@ fun PhotoPreviewWithCaptionScreen( isDarkTheme: Boolean ) { val backgroundColor = if (isDarkTheme) Color.Black else Color.White - val textColor = if (isDarkTheme) Color.White else Color.Black - + + val context = LocalContext.current + val view = LocalView.current + val focusManager = LocalFocusManager.current + val density = LocalDensity.current + + // ═══════════════════════════════════════════════════════════════ + // 😀 EMOJI PICKER STATE + // ═══════════════════════════════════════════════════════════════ + var showEmojiPicker by remember { mutableStateOf(false) } + val coordinator = rememberKeyboardTransitionCoordinator() + var editTextView by remember { mutableStateOf(null) } + var lastToggleTime by remember { mutableLongStateOf(0L) } + val toggleCooldownMs = 500L + + // Отслеживание высоты клавиатуры + val imeInsets = WindowInsets.ime + var isKeyboardVisible by remember { mutableStateOf(false) } + var lastStableKeyboardHeight by remember { mutableStateOf(0.dp) } + + // Update coordinator through snapshotFlow + LaunchedEffect(Unit) { + snapshotFlow { with(density) { imeInsets.getBottom(density).toDp() } }.collect { currentImeHeight -> + isKeyboardVisible = currentImeHeight > 50.dp + coordinator.updateKeyboardHeight(currentImeHeight) + if (currentImeHeight > 100.dp) { + coordinator.syncHeights() + lastStableKeyboardHeight = currentImeHeight + } + Log.d("PhotoPreview", "IME height: ${currentImeHeight.value}dp, isKeyboardVisible: $isKeyboardVisible, emojiHeight: ${coordinator.emojiHeight.value}dp, isEmojiBoxVisible: ${coordinator.isEmojiBoxVisible}") + } + } + + // Load saved keyboard height + LaunchedEffect(Unit) { + KeyboardHeightProvider.getSavedKeyboardHeight(context) + } + + // Save keyboard height when stable + LaunchedEffect(isKeyboardVisible, showEmojiPicker) { + if (isKeyboardVisible && !showEmojiPicker) { + delay(350) + if (isKeyboardVisible && !showEmojiPicker && lastStableKeyboardHeight > 300.dp) { + val heightPx = with(density) { lastStableKeyboardHeight.toPx().toInt() } + KeyboardHeightProvider.saveKeyboardHeight(context, heightPx) + } + } + } + + // Toggle emoji picker function + fun toggleEmojiPicker() { + val currentTime = System.currentTimeMillis() + if (currentTime - lastToggleTime < toggleCooldownMs) { + Log.d("PhotoPreview", "Toggle blocked by cooldown") + return + } + lastToggleTime = currentTime + + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + + if (coordinator.isEmojiVisible) { + // EMOJI → KEYBOARD + Log.d("PhotoPreview", "TOGGLE: Emoji → Keyboard") + coordinator.requestShowKeyboard( + showKeyboard = { + Log.d("PhotoPreview", "Showing keyboard...") + editTextView?.let { editText -> + editText.requestFocus() + imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) + } + }, + hideEmoji = { + Log.d("PhotoPreview", "Hiding emoji picker") + showEmojiPicker = false + } + ) + } else { + // KEYBOARD → EMOJI + Log.d("PhotoPreview", "TOGGLE: Keyboard → Emoji") + coordinator.requestShowEmoji( + hideKeyboard = { + Log.d("PhotoPreview", "Hiding keyboard...") + imm.hideSoftInputFromWindow(view.windowToken, 0) + }, + showEmoji = { + Log.d("PhotoPreview", "Showing emoji picker") + showEmojiPicker = true + } + ) + } + } + + // 🔥 КЛЮЧЕВОЕ: imePadding применяется ТОЛЬКО когда emoji box НЕ виден + val shouldUseImePadding = !coordinator.isEmojiBoxVisible + val shouldAddNavBarPadding = !isKeyboardVisible && !coordinator.isEmojiBoxVisible + + // Логируем состояние при каждой рекомпозиции + Log.d("PhotoPreview", "RENDER: showEmoji=$showEmojiPicker, isKeyboard=$isKeyboardVisible, isEmojiBoxVisible=${coordinator.isEmojiBoxVisible}, useImePadding=$shouldUseImePadding, emojiHeight=${coordinator.emojiHeight.value}dp") + Surface( color = backgroundColor, modifier = Modifier.fillMaxSize() ) { - Column( - modifier = Modifier - .fillMaxSize() - .systemBarsPadding() - .imePadding() - ) { - // Top bar with close button - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - IconButton(onClick = onDismiss) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Close", - tint = textColor - ) - } - } - - // Image preview + Box(modifier = Modifier.fillMaxSize()) { + // ═══════════════════════════════════════════════════════════ + // 📸 FULLSCREEN PHOTO - не реагирует на клавиатуру + // ═══════════════════════════════════════════════════════════ + AsyncImage( + model = ImageRequest.Builder(context) + .data(imageUri) + .crossfade(true) + .build(), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Fit + ) + + // ═══════════════════════════════════════════════════════════ + // 🎛️ TOP BAR - Transparent overlay + // ═══════════════════════════════════════════════════════════ Box( modifier = Modifier .fillMaxWidth() - .weight(1f), - contentAlignment = Alignment.Center + .align(Alignment.TopCenter) + .background( + Brush.verticalGradient( + colors = listOf( + Color.Black.copy(alpha = 0.5f), + Color.Transparent + ) + ) + ) + .statusBarsPadding() + .padding(horizontal = 4.dp, vertical = 8.dp) ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUri) - .crossfade(true) - .build(), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Fit - ) - } - - // Caption input - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedTextField( - value = caption, - onValueChange = onCaptionChange, - modifier = Modifier.weight(1f), - placeholder = { - Text( - "Add a caption...", - color = textColor.copy(alpha = 0.5f) - ) + IconButton( + onClick = { + showEmojiPicker = false + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(view.windowToken, 0) + focusManager.clearFocus() + onDismiss() }, - maxLines = 3, - colors = TextFieldDefaults.outlinedTextFieldColors( - focusedTextColor = textColor, - unfocusedTextColor = textColor, - cursorColor = PrimaryBlue, - focusedBorderColor = PrimaryBlue, - unfocusedBorderColor = textColor.copy(alpha = 0.3f) - ), - shape = RoundedCornerShape(20.dp) - ) - - // Send button - FloatingActionButton( - onClick = onSend, - containerColor = PrimaryBlue, - modifier = Modifier.size(56.dp) + modifier = Modifier.align(Alignment.CenterStart) ) { Icon( - imageVector = TablerIcons.Send, - contentDescription = "Send", + TablerIcons.X, + contentDescription = "Close", tint = Color.White, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(28.dp) + ) + } + } + + // ═══════════════════════════════════════════════════════════ + // 📝 CAPTION INPUT + EMOJI PICKER + // Как в ChatDetailInput: imePadding + AnimatedKeyboardTransition + // ═══════════════════════════════════════════════════════════ + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + // 🔥 imePadding ТОЛЬКО когда emoji НЕ показан + .then(if (shouldUseImePadding) Modifier.imePadding() else Modifier) + ) { + // Caption bar + Box( + modifier = Modifier + .fillMaxWidth() + .background(Color.Black.copy(alpha = 0.75f)) + .padding( + start = 12.dp, + end = 12.dp, + top = 10.dp, + bottom = if (isKeyboardVisible || coordinator.isEmojiBoxVisible) 10.dp else 16.dp + ) + .then(if (shouldAddNavBarPadding) Modifier.navigationBarsPadding() else Modifier) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + // Emoji/Keyboard toggle button + IconButton( + onClick = { toggleEmojiPicker() }, + modifier = Modifier.size(32.dp) + ) { + Crossfade( + targetState = showEmojiPicker, + animationSpec = tween(150), + label = "emojiIcon" + ) { isEmoji -> + Icon( + if (isEmoji) TablerIcons.Keyboard else TablerIcons.MoodSmile, + contentDescription = if (isEmoji) "Keyboard" else "Emoji", + tint = Color.White.copy(alpha = 0.7f), + modifier = Modifier.size(26.dp) + ) + } + } + + // Caption text field + Box( + modifier = Modifier + .weight(1f) + .heightIn(min = 24.dp, max = 100.dp) + ) { + AppleEmojiTextField( + value = caption, + onValueChange = onCaptionChange, + textColor = Color.White, + textSize = 16f, + hint = "Add a caption...", + hintColor = Color.White.copy(alpha = 0.5f), + modifier = Modifier.fillMaxWidth(), + requestFocus = false, + onViewCreated = { v -> editTextView = v }, + onFocusChanged = { hasFocus -> + if (hasFocus && showEmojiPicker) { + toggleEmojiPicker() + } + } + ) + } + + // Send button + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(PrimaryBlue) + .clickable { onSend() }, + contentAlignment = Alignment.Center + ) { + Icon( + TablerIcons.Send, + contentDescription = "Send", + tint = Color.White, + modifier = Modifier + .size(22.dp) + .offset(x = 1.dp) + ) + } + } + } + + // ═══════════════════════════════════════════════════════════ + // 🔥 EMOJI PICKER - используем AnimatedKeyboardTransition + // Точно как в ChatDetailInput! + // ═══════════════════════════════════════════════════════════ + AnimatedKeyboardTransition( + coordinator = coordinator, + showEmojiPicker = showEmojiPicker + ) { + OptimizedEmojiPicker( + isVisible = true, + isDarkTheme = isDarkTheme, + onEmojiSelected = { emoji -> onCaptionChange(caption + emoji) }, + onClose = { toggleEmojiPicker() }, + modifier = Modifier.fillMaxWidth() ) } } - - Spacer(modifier = Modifier.height(8.dp)) } } }