fix: close keyboard before dismissing ImageEditorScreen

This commit is contained in:
k1ngsterr1
2026-02-01 21:38:46 +05:00
parent b44fd3da29
commit b05f526b43
3 changed files with 181 additions and 62 deletions

View File

@@ -2030,6 +2030,18 @@ fun ChatDetailScreen(
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)

View File

@@ -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)
// Левая иконка: камера когда клавиатура закрыта, 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,10 +771,17 @@ private fun TelegramCaptionBar(
}
)
// Send button - голубой кружок с галочкой (как в Telegram)
// Кнопка отправки
AnimatedContent(
targetState = isKeyboardVisible,
transitionSpec = {
fadeIn(tween(150)) togetherWith fadeOut(tween(150))
},
label = "send_button"
) { keyboardOpen ->
Box(
modifier = Modifier
.size(32.dp)
.size(if (keyboardOpen) 32.dp else 36.dp)
.clip(CircleShape)
.background(PrimaryBlue)
.clickable(enabled = !isSaving) { onSend() },
@@ -733,21 +789,25 @@ private fun TelegramCaptionBar(
) {
if (isSaving) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
modifier = Modifier.size(if (keyboardOpen) 18.dp else 20.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
// Клавиатура открыта - галочка, закрыта - стрелка отправки
Icon(
TablerIcons.Check,
if (keyboardOpen) TablerIcons.Check else TablerIcons.Send,
contentDescription = "Send",
tint = Color.White,
modifier = Modifier.size(20.dp)
modifier = Modifier
.size(if (keyboardOpen) 20.dp else 22.dp)
.then(if (!keyboardOpen) Modifier.offset(x = 1.dp) else Modifier)
)
}
}
}
}
}
/** Save edited image and return the URI - crops black bars from FIT_CENTER */
private suspend fun saveEditedImage(

View File

@@ -76,11 +76,13 @@ fun MediaPickerBottomSheet(
onDismiss: () -> Unit,
isDarkTheme: Boolean,
onMediaSelected: (List<MediaItem>) -> 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) {
// Уже выбрана - открываем редактор (только для изображений)
// Telegram-style: клик на фото сразу открывает редактор с caption
if (!item.isVideo) {
editingItem = item
} else {
// Для видео - снимаем выделение
// Для видео - добавляем/убираем из selection
if (item.id in selectedItems) {
selectedItems = selectedItems - item.id
}
} else {
// Не выбрана - добавляем в selection
if (selectedItems.size < maxSelection) {
} else if (selectedItems.size < maxSelection) {
selectedItems = selectedItems + item.id
}
}
@@ -334,10 +330,35 @@ fun MediaPickerBottomSheet(
onDismiss = { editingItem = null },
onSave = { editedUri ->
editingItem = null
// После редактирования открываем предпросмотр с caption
// Если нет 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
)
}
@@ -350,10 +371,35 @@ fun MediaPickerBottomSheet(
},
onSave = { editedUri ->
pendingPhotoUri = null
// После редактирования открываем предпросмотр с caption
// Если нет 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
)
}