From b05f526b43cd1214a321bcf6bafbcd54352da781 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sun, 1 Feb 2026 21:38:46 +0500 Subject: [PATCH] fix: close keyboard before dismissing ImageEditorScreen --- .../messenger/ui/chats/ChatDetailScreen.kt | 19 ++- .../ui/chats/components/ImageEditorScreen.kt | 134 +++++++++++++----- .../components/MediaPickerBottomSheet.kt | 90 +++++++++--- 3 files changed, 181 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 705f5ba..7e150af 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -2020,16 +2020,28 @@ fun ChatDetailScreen( currentUserPublicKey = currentUserPublicKey, onMediaSelected = { selectedMedia -> // πŸ“Έ ΠžΡ‚ΠΊΡ€Ρ‹Π²Π°Π΅ΠΌ edit screen для Π²Ρ‹Π±Ρ€Π°Π½Π½Ρ‹Ρ… ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ - + // Π‘ΠΎΠ±ΠΈΡ€Π°Π΅ΠΌ URI ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ (ΠΏΠΎΠΊΠ° Π±Π΅Π· Π²ΠΈΠ΄Π΅ΠΎ) val imageUris = selectedMedia .filter { !it.isVideo } .map { it.uri } - + if (imageUris.isNotEmpty()) { pendingGalleryImages = imageUris } }, + onMediaSelectedWithCaption = { mediaItem, caption -> + // πŸ“Έ ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ Ρ„ΠΎΡ‚ΠΎ с caption Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ + showMediaPicker = false + scope.launch { + val base64 = MediaUtils.uriToBase64Image(context, mediaItem.uri) + val blurhash = MediaUtils.generateBlurhash(context, mediaItem.uri) + val (width, height) = MediaUtils.getImageDimensions(context, mediaItem.uri) + if (base64 != null) { + viewModel.sendImageMessage(base64, blurhash, caption, width, height) + } + } + }, onOpenCamera = { // πŸ“· Π‘ΠΎΠ·Π΄Π°Ρ‘ΠΌ Π²Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹ΠΉ Ρ„Π°ΠΉΠ» для Ρ„ΠΎΡ‚ΠΎ try { @@ -2054,7 +2066,8 @@ fun ChatDetailScreen( onAvatarClick = { // πŸ‘€ ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ свой Π°Π²Π°Ρ‚Π°Ρ€ (ΠΊΠ°ΠΊ Π² desktop) viewModel.sendAvatarMessage() - } + }, + recipientName = user.title ) // πŸ“· Image Editor для Ρ„ΠΎΡ‚ΠΎ с ΠΊΠ°ΠΌΠ΅Ρ€Ρ‹ (с caption ΠΊΠ°ΠΊ Π² Telegram) 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 5313d2b..812e488 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 @@ -12,6 +12,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.* import androidx.compose.animation.core.* +import androidx.compose.runtime.getValue import androidx.compose.foundation.* import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image @@ -38,6 +39,8 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -107,6 +110,8 @@ fun ImageEditorScreen( ) { val context = LocalContext.current val scope = rememberCoroutineScope() + val view = LocalView.current + val focusManager = LocalFocusManager.current // Editor state var currentTool by remember { mutableStateOf(EditorTool.NONE) } @@ -219,9 +224,22 @@ fun ImageEditorScreen( .statusBarsPadding() .padding(horizontal = 4.dp, vertical = 8.dp) ) { - // Close button (X) + // Close button (X) - сначала Π·Π°ΠΊΡ€Ρ‹Π²Π°Π΅Ρ‚ ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Ρƒ, ΠΏΠΎΡ‚ΠΎΠΌ экран IconButton( - onClick = onDismiss, + onClick = { + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚Π° Π»ΠΈ ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π° + val imm = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as android.view.inputmethod.InputMethodManager + val isKeyboardOpen = imm.isAcceptingText + + if (isKeyboardOpen) { + // Π—Π°ΠΊΡ€Ρ‹Π²Π°Π΅ΠΌ ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Ρƒ + imm.hideSoftInputFromWindow(view.windowToken, 0) + focusManager.clearFocus() + } else { + // Π—Π°ΠΊΡ€Ρ‹Π²Π°Π΅ΠΌ экран + onDismiss() + } + }, modifier = Modifier.align(Alignment.CenterStart) ) { Icon( @@ -340,6 +358,7 @@ fun ImageEditorScreen( caption = caption, onCaptionChange = { caption = it }, isSaving = isSaving, + isKeyboardVisible = isKeyboardVisibleForCaption, onSend = { scope.launch { isSaving = true @@ -673,32 +692,61 @@ private fun TelegramRotateBar( /** * Telegram-style caption input bar + * ΠœΠ΅Π½ΡΠ΅Ρ‚ внСшний Π²ΠΈΠ΄ Π² зависимости ΠΎΡ‚ состояния ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Ρ‹: + * - ΠšΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π° Π·Π°ΠΊΡ€Ρ‹Ρ‚Π°: ΠΌΠΈΠ½ΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ ΡΡ‚ΠΈΠ»ΡŒ (ΠΊΠ°ΠΌΠ΅Ρ€Π° + тСкст + синяя стрСлка) + * - ΠšΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π° ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚Π°: ΠΏΠΎΠ»Π½Ρ‹ΠΉ ΡΡ‚ΠΈΠ»ΡŒ (emoji + тСкст + Π³Π°Π»ΠΎΡ‡ΠΊΠ°) */ @Composable private fun TelegramCaptionBar( caption: String, onCaptionChange: (String) -> Unit, isSaving: Boolean, + isKeyboardVisible: Boolean, onSend: () -> Unit ) { - // Telegram-style: ΠΏΡ€ΡΠΌΠΎΡƒΠ³ΠΎΠ»ΡŒΠ½Ρ‹ΠΉ Ρ‚Π΅ΠΌΠ½Ρ‹ΠΉ Π±Π°Ρ€ с emoji слСва ΠΈ Π³Π°Π»ΠΎΡ‡ΠΊΠΎΠΉ справа + // Анимированный ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ ΠΌΠ΅ΠΆΠ΄Ρƒ стилями + val backgroundAlpha by animateFloatAsState( + targetValue = if (isKeyboardVisible) 0.75f else 0f, + animationSpec = tween(200, easing = TelegramEasing), + label = "background" + ) + Row( modifier = Modifier .fillMaxWidth() - .background(Color.Black.copy(alpha = 0.75f)) - .padding(horizontal = 8.dp, vertical = 10.dp), + .background(Color.Black.copy(alpha = backgroundAlpha)) + .padding(horizontal = 12.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(10.dp) ) { - // Emoji icon (ΠΊΠ°ΠΊ Π² Telegram) - Icon( - TablerIcons.MoodSmile, - contentDescription = "Emoji", - tint = Color.White.copy(alpha = 0.7f), - modifier = Modifier.size(26.dp) - ) + // ЛСвая ΠΈΠΊΠΎΠ½ΠΊΠ°: ΠΊΠ°ΠΌΠ΅Ρ€Π° ΠΊΠΎΠ³Π΄Π° ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π° Π·Π°ΠΊΡ€Ρ‹Ρ‚Π°, emoji ΠΊΠΎΠ³Π΄Π° ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚Π° + AnimatedContent( + targetState = isKeyboardVisible, + transitionSpec = { + fadeIn(tween(150)) togetherWith fadeOut(tween(150)) + }, + label = "left_icon" + ) { keyboardOpen -> + if (keyboardOpen) { + // ΠšΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π° ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚Π° - emoji ΠΈΠΊΠΎΠ½ΠΊΠ° + Icon( + TablerIcons.MoodSmile, + contentDescription = "Emoji", + tint = Color.White.copy(alpha = 0.7f), + modifier = Modifier.size(26.dp) + ) + } else { + // ΠšΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π° Π·Π°ΠΊΡ€Ρ‹Ρ‚Π° - ΠΊΠ°ΠΌΠ΅Ρ€Π° ΠΈΠΊΠΎΠ½ΠΊΠ° + Icon( + TablerIcons.Camera, + contentDescription = "Camera", + tint = Color.White.copy(alpha = 0.7f), + modifier = Modifier.size(26.dp) + ) + } + } - // Caption text field - простой Π±Π΅Π· Ρ„ΠΎΠ½Π° + // Caption text field BasicTextField( value = caption, onValueChange = onCaptionChange, @@ -707,7 +755,8 @@ private fun TelegramCaptionBar( color = Color.White, fontSize = 16.sp ), - maxLines = 4, + maxLines = if (isKeyboardVisible) 4 else 1, + singleLine = !isKeyboardVisible, decorationBox = { innerTextField -> Box { if (caption.isEmpty()) { @@ -722,28 +771,39 @@ private fun TelegramCaptionBar( } ) - // Send button - Π³ΠΎΠ»ΡƒΠ±ΠΎΠΉ ΠΊΡ€ΡƒΠΆΠΎΠΊ с Π³Π°Π»ΠΎΡ‡ΠΊΠΎΠΉ (ΠΊΠ°ΠΊ Π² Telegram) - Box( - modifier = Modifier - .size(32.dp) - .clip(CircleShape) - .background(PrimaryBlue) - .clickable(enabled = !isSaving) { onSend() }, - contentAlignment = Alignment.Center - ) { - if (isSaving) { - CircularProgressIndicator( - modifier = Modifier.size(18.dp), - color = Color.White, - strokeWidth = 2.dp - ) - } else { - Icon( - TablerIcons.Check, - contentDescription = "Send", - tint = Color.White, - modifier = Modifier.size(20.dp) - ) + // Кнопка ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ + AnimatedContent( + targetState = isKeyboardVisible, + transitionSpec = { + fadeIn(tween(150)) togetherWith fadeOut(tween(150)) + }, + label = "send_button" + ) { keyboardOpen -> + Box( + modifier = Modifier + .size(if (keyboardOpen) 32.dp else 36.dp) + .clip(CircleShape) + .background(PrimaryBlue) + .clickable(enabled = !isSaving) { onSend() }, + contentAlignment = Alignment.Center + ) { + if (isSaving) { + CircularProgressIndicator( + modifier = Modifier.size(if (keyboardOpen) 18.dp else 20.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + // ΠšΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π° ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚Π° - Π³Π°Π»ΠΎΡ‡ΠΊΠ°, Π·Π°ΠΊΡ€Ρ‹Ρ‚Π° - стрСлка ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ + Icon( + if (keyboardOpen) TablerIcons.Check else TablerIcons.Send, + contentDescription = "Send", + tint = Color.White, + modifier = Modifier + .size(if (keyboardOpen) 20.dp else 22.dp) + .then(if (!keyboardOpen) Modifier.offset(x = 1.dp) else Modifier) + ) + } } } } 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 bdda5b6..3feee53 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 @@ -76,11 +76,13 @@ fun MediaPickerBottomSheet( onDismiss: () -> Unit, isDarkTheme: Boolean, onMediaSelected: (List) -> Unit, + onMediaSelectedWithCaption: ((MediaItem, String) -> Unit)? = null, // Для ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ с caption onOpenCamera: () -> Unit = {}, onOpenFilePicker: () -> Unit = {}, onAvatarClick: () -> Unit = {}, currentUserPublicKey: String = "", - maxSelection: Int = 10 + maxSelection: Int = 10, + recipientName: String? = null // Имя получатСля для отобраТСния Π² Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΎΡ€Π΅ ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -290,20 +292,14 @@ fun MediaPickerBottomSheet( onOpenCamera() }, onItemClick = { item -> - // Telegram-style: - // - ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ ΠΊΠ»ΠΈΠΊ ΠΏΠΎ Π½Π΅Π²Ρ‹Π±Ρ€Π°Π½Π½ΠΎΠΉ Ρ„ΠΎΡ‚ΠΎ β†’ Π²Ρ‹Π±Ρ€Π°Ρ‚ΡŒ - // - Клик ΠΏΠΎ ΡƒΠΆΠ΅ Π²Ρ‹Π±Ρ€Π°Π½Π½ΠΎΠΉ Ρ„ΠΎΡ‚ΠΎ β†’ ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΎΡ€ - if (item.id in selectedItems) { - // Π£ΠΆΠ΅ Π²Ρ‹Π±Ρ€Π°Π½Π° - ΠΎΡ‚ΠΊΡ€Ρ‹Π²Π°Π΅ΠΌ Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΎΡ€ (Ρ‚ΠΎΠ»ΡŒΠΊΠΎ для ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ) - if (!item.isVideo) { - editingItem = item - } else { - // Для Π²ΠΈΠ΄Π΅ΠΎ - снимаСм Π²Ρ‹Π΄Π΅Π»Π΅Π½ΠΈΠ΅ - selectedItems = selectedItems - item.id - } + // Telegram-style: ΠΊΠ»ΠΈΠΊ Π½Π° Ρ„ΠΎΡ‚ΠΎ сразу ΠΎΡ‚ΠΊΡ€Ρ‹Π²Π°Π΅Ρ‚ Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΎΡ€ с caption + if (!item.isVideo) { + editingItem = item } else { - // НС Π²Ρ‹Π±Ρ€Π°Π½Π° - добавляСм Π² selection - if (selectedItems.size < maxSelection) { + // Для Π²ΠΈΠ΄Π΅ΠΎ - добавляСм/ΡƒΠ±ΠΈΡ€Π°Π΅ΠΌ ΠΈΠ· selection + if (item.id in selectedItems) { + selectedItems = selectedItems - item.id + } else if (selectedItems.size < maxSelection) { selectedItems = selectedItems + item.id } } @@ -334,26 +330,76 @@ fun MediaPickerBottomSheet( onDismiss = { editingItem = null }, onSave = { editedUri -> editingItem = null - // ПослС рСдактирования ΠΎΡ‚ΠΊΡ€Ρ‹Π²Π°Π΅ΠΌ прСдпросмотр с caption - previewPhotoUri = editedUri + // Если Π½Π΅Ρ‚ onMediaSelectedWithCaption - ΠΎΡ‚ΠΊΡ€Ρ‹Π²Π°Π΅ΠΌ preview + if (onMediaSelectedWithCaption == null) { + previewPhotoUri = editedUri + } else { + // ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ Π±Π΅Π· caption (Ссли Π½Π°ΠΆΠ°Π»ΠΈ Done вмСсто Send) + val mediaItem = MediaItem( + id = System.currentTimeMillis(), + uri = editedUri, + mimeType = "image/png", + dateModified = System.currentTimeMillis() + ) + onMediaSelected(listOf(mediaItem)) + onDismiss() + } }, - isDarkTheme = isDarkTheme + onSaveWithCaption = if (onMediaSelectedWithCaption != null) { editedUri, caption -> + editingItem = null + val mediaItem = MediaItem( + id = System.currentTimeMillis(), + uri = editedUri, + mimeType = "image/png", + dateModified = System.currentTimeMillis() + ) + onMediaSelectedWithCaption(mediaItem, caption) + onDismiss() + } else null, + isDarkTheme = isDarkTheme, + showCaptionInput = onMediaSelectedWithCaption != null, + recipientName = recipientName ) } - + // Image Editor overlay для Ρ„ΠΎΡ‚ΠΎ с ΠΊΠ°ΠΌΠ΅Ρ€Ρ‹ pendingPhotoUri?.let { uri -> ImageEditorScreen( imageUri = uri, - onDismiss = { + onDismiss = { pendingPhotoUri = null }, onSave = { editedUri -> pendingPhotoUri = null - // ПослС рСдактирования ΠΎΡ‚ΠΊΡ€Ρ‹Π²Π°Π΅ΠΌ прСдпросмотр с caption - previewPhotoUri = editedUri + // Если Π½Π΅Ρ‚ onMediaSelectedWithCaption - ΠΎΡ‚ΠΊΡ€Ρ‹Π²Π°Π΅ΠΌ preview + if (onMediaSelectedWithCaption == null) { + previewPhotoUri = editedUri + } else { + // ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ Π±Π΅Π· caption + val mediaItem = MediaItem( + id = System.currentTimeMillis(), + uri = editedUri, + mimeType = "image/png", + dateModified = System.currentTimeMillis() + ) + onMediaSelected(listOf(mediaItem)) + onDismiss() + } }, - isDarkTheme = isDarkTheme + onSaveWithCaption = if (onMediaSelectedWithCaption != null) { editedUri, caption -> + pendingPhotoUri = null + val mediaItem = MediaItem( + id = System.currentTimeMillis(), + uri = editedUri, + mimeType = "image/png", + dateModified = System.currentTimeMillis() + ) + onMediaSelectedWithCaption(mediaItem, caption) + onDismiss() + } else null, + isDarkTheme = isDarkTheme, + showCaptionInput = onMediaSelectedWithCaption != null, + recipientName = recipientName ) }