feat: Improve keyboard handling in selection mode for better user experience
This commit is contained in:
@@ -323,15 +323,22 @@ fun ChatDetailScreen(
|
||||
var selectedMessages by remember { mutableStateOf<Set<String>>(emptySet()) }
|
||||
val isSelectionMode = selectedMessages.isNotEmpty()
|
||||
|
||||
// 🔥 Закрываем клавиатуру и emoji picker когда открывается selection mode (action bar с Reply/Forward)
|
||||
// Логирование изменений selection mode
|
||||
LaunchedEffect(isSelectionMode, selectedMessages.size) {
|
||||
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
|
||||
android.util.Log.d("ChatDetailScreen", "📝 SELECTION MODE CHANGED")
|
||||
android.util.Log.d("ChatDetailScreen", " 📊 isSelectionMode: $isSelectionMode")
|
||||
android.util.Log.d("ChatDetailScreen", " 📊 selectedMessages.size: ${selectedMessages.size}")
|
||||
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
|
||||
}
|
||||
|
||||
// 🔥 Backup: если клавиатура ещё открыта когда selection mode активировался
|
||||
// (клавиатура уже должна быть закрыта в onLongClick, это только backup)
|
||||
LaunchedEffect(isSelectionMode) {
|
||||
if (isSelectionMode) {
|
||||
// Используем нативный InputMethodManager для НАДЁЖНОГО закрытия клавиатуры
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
android.util.Log.d("ChatDetailScreen", "⚠️ Backup keyboard hide triggered")
|
||||
// Backup закрытие клавиатуры (основное в onLongClick)
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
showEmojiPicker = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,164 +549,140 @@ fun ChatDetailScreen(
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
topBar = {
|
||||
// 🔥 SELECTION HEADER (появляется при выборе сообщений) - Telegram Style
|
||||
AnimatedVisibility(
|
||||
visible = isSelectionMode,
|
||||
enter = fadeIn(animationSpec = tween(200)) + slideInVertically(
|
||||
initialOffsetY = { -it },
|
||||
animationSpec = tween(250, easing = TelegramEasing)
|
||||
),
|
||||
exit = fadeOut(animationSpec = tween(150)) + slideOutVertically(
|
||||
targetOffsetY = { -it },
|
||||
animationSpec = tween(200, easing = TelegramEasing)
|
||||
)
|
||||
) {
|
||||
Box(modifier = Modifier
|
||||
// 🔥 UNIFIED HEADER - один контейнер, контент меняется внутри
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(if (isDarkTheme) Color(0xFF212121) else Color.White)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.height(56.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// Left: X (cancel) + Count - Telegram Style
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = { selectedMessages = emptySet() }) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = "Cancel",
|
||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
"${selectedMessages.size}",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (isDarkTheme) Color.White else Color.Black
|
||||
)
|
||||
}
|
||||
|
||||
// Right: Action buttons - Telegram Style
|
||||
.background(if (isSelectionMode) {
|
||||
if (isDarkTheme) Color(0xFF212121) else Color.White
|
||||
} else headerBackground)
|
||||
) {
|
||||
// Контент хедера с Crossfade для плавной смены
|
||||
Crossfade(
|
||||
targetState = isSelectionMode,
|
||||
animationSpec = tween(200),
|
||||
label = "headerContent"
|
||||
) { selectionMode ->
|
||||
if (selectionMode) {
|
||||
// SELECTION MODE CONTENT
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.height(56.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// Copy button
|
||||
IconButton(
|
||||
onClick = {
|
||||
val textToCopy = messages
|
||||
.filter { selectedMessages.contains("${it.id}_${it.timestamp.time}") }
|
||||
.sortedBy { it.timestamp }
|
||||
.joinToString("\n\n") { msg ->
|
||||
val time = SimpleDateFormat("HH:mm", Locale.getDefault())
|
||||
.format(msg.timestamp)
|
||||
"[${if (msg.isOutgoing) "You" else chatTitle}] $time\n${msg.text}"
|
||||
}
|
||||
clipboardManager.setText(androidx.compose.ui.text.AnnotatedString(textToCopy))
|
||||
selectedMessages = emptySet()
|
||||
// Left: X (cancel) + Count
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = { selectedMessages = emptySet() }) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = "Cancel",
|
||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy",
|
||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
||||
modifier = Modifier.size(22.dp)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
"${selectedMessages.size}",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (isDarkTheme) Color.White else Color.Black
|
||||
)
|
||||
}
|
||||
|
||||
// Delete button
|
||||
IconButton(
|
||||
onClick = {
|
||||
// Удаляем выбранные сообщения
|
||||
messages
|
||||
.filter { selectedMessages.contains("${it.id}_${it.timestamp.time}") }
|
||||
.forEach { msg -> viewModel.deleteMessage(msg.id) }
|
||||
selectedMessages = emptySet()
|
||||
// Right: Action buttons
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Copy button
|
||||
IconButton(
|
||||
onClick = {
|
||||
val textToCopy = messages
|
||||
.filter { selectedMessages.contains("${it.id}_${it.timestamp.time}") }
|
||||
.sortedBy { it.timestamp }
|
||||
.joinToString("\n\n") { msg ->
|
||||
val time = SimpleDateFormat("HH:mm", Locale.getDefault())
|
||||
.format(msg.timestamp)
|
||||
"[${if (msg.isOutgoing) "You" else chatTitle}] $time\n${msg.text}"
|
||||
}
|
||||
clipboardManager.setText(androidx.compose.ui.text.AnnotatedString(textToCopy))
|
||||
selectedMessages = emptySet()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy",
|
||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Delete button
|
||||
IconButton(
|
||||
onClick = {
|
||||
messages
|
||||
.filter { selectedMessages.contains("${it.id}_${it.timestamp.time}") }
|
||||
.forEach { msg -> viewModel.deleteMessage(msg.id) }
|
||||
selectedMessages = emptySet()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Delete",
|
||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = "Delete",
|
||||
tint = if (isDarkTheme) Color.White else Color.Black,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom line
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.height(0.5.dp)
|
||||
.background(
|
||||
if (isDarkTheme) Color.White.copy(alpha = 0.15f)
|
||||
else Color.Black.copy(alpha = 0.1f)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// NORMAL HEADER (скрывается при выборе)
|
||||
AnimatedVisibility(
|
||||
visible = !isSelectionMode,
|
||||
enter = fadeIn(animationSpec = tween(200)),
|
||||
exit = fadeOut(animationSpec = tween(150))
|
||||
) {
|
||||
// Telegram-style TopAppBar - solid background без blur
|
||||
Box(modifier = Modifier.fillMaxWidth().background(headerBackground)) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.height(56.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 🔔 Кнопка назад с badge непрочитанных сообщений
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = hideKeyboardAndBack,
|
||||
modifier = Modifier.size(40.dp)
|
||||
} else {
|
||||
// NORMAL HEADER CONTENT
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.height(56.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.KeyboardArrowLeft,
|
||||
contentDescription = "Back",
|
||||
tint = headerIconColor,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
// Badge с количеством непрочитанных из других чатов
|
||||
if (totalUnreadFromOthers > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.offset(x = (-4).dp, y = 6.dp)
|
||||
.size(if (totalUnreadFromOthers > 9) 20.dp else 18.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFFFF3B30)), // Красный цвет как в iOS
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = if (totalUnreadFromOthers > 99) "99+"
|
||||
else if (totalUnreadFromOthers > 9) "$totalUnreadFromOthers"
|
||||
else "$totalUnreadFromOthers",
|
||||
color = Color.White,
|
||||
fontSize = if (totalUnreadFromOthers > 9) 9.sp else 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1
|
||||
)
|
||||
// Back button with badge
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = hideKeyboardAndBack,
|
||||
modifier = Modifier.size(40.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.KeyboardArrowLeft,
|
||||
contentDescription = "Back",
|
||||
tint = headerIconColor,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
if (totalUnreadFromOthers > 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.offset(x = (-4).dp, y = 6.dp)
|
||||
.size(if (totalUnreadFromOthers > 9) 20.dp else 18.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFFFF3B30)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = if (totalUnreadFromOthers > 99) "99+"
|
||||
else "$totalUnreadFromOthers",
|
||||
color = Color.White,
|
||||
fontSize = if (totalUnreadFromOthers > 9) 9.sp else 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
@@ -962,20 +945,21 @@ fun ChatDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Нижняя линия для разделения
|
||||
}
|
||||
} // Закрытие Crossfade
|
||||
|
||||
// Bottom line для unified header
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.height(0.5.dp)
|
||||
.background(
|
||||
if (isDarkTheme)
|
||||
Color.White.copy(alpha = 0.1f)
|
||||
else Color.Black.copy(alpha = 0.08f)
|
||||
)
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.height(0.5.dp)
|
||||
.background(
|
||||
if (isDarkTheme) Color.White.copy(alpha = 0.15f)
|
||||
else Color.Black.copy(alpha = 0.1f)
|
||||
)
|
||||
)
|
||||
}
|
||||
} // Закрытие AnimatedVisibility для normal header
|
||||
} // Закрытие Box unified header
|
||||
},
|
||||
containerColor = backgroundColor, // Фон всего чата
|
||||
// 🔥 Bottom bar - инпут с умным padding:
|
||||
@@ -984,76 +968,38 @@ fun ChatDetailScreen(
|
||||
bottomBar = {
|
||||
// 🔥 Telegram-style: когда Box с эмодзи виден, НЕ используем imePadding
|
||||
// isEmojiBoxVisible учитывает анимацию fade-out (alpha > 0.01)
|
||||
val bottomModifier = if (coordinator.isEmojiBoxVisible) {
|
||||
Modifier // Без imePadding - Box с эмодзи заменяет клавиатуру
|
||||
} else {
|
||||
// 🔥 В selection mode НЕ используем imePadding (клавиатура закрыта)
|
||||
val useImePadding = !coordinator.isEmojiBoxVisible && !isSelectionMode
|
||||
val bottomModifier = if (useImePadding) {
|
||||
Modifier.imePadding() // С imePadding - клавиатура поднимает инпут
|
||||
}
|
||||
Column(modifier = bottomModifier) {
|
||||
// 🔥 FLOATING INPUT BAR - плавает поверх сообщений, поднимается с клавиатурой
|
||||
// Скрываем когда в режиме выбора
|
||||
AnimatedVisibility(
|
||||
visible = !isSelectionMode,
|
||||
enter = fadeIn(tween(200)) + slideInVertically(initialOffsetY = { it }),
|
||||
exit = fadeOut(tween(150)) + slideOutVertically(targetOffsetY = { it })
|
||||
) {
|
||||
Column {
|
||||
// Input bar с встроенным reply preview (как в React Native)
|
||||
MessageInputBar(
|
||||
value = inputText,
|
||||
onValueChange = {
|
||||
viewModel.updateInputText(it)
|
||||
// Отправляем индикатор печатания
|
||||
if (it.isNotEmpty() && !isSavedMessages) {
|
||||
viewModel.sendTypingIndicator()
|
||||
}
|
||||
},
|
||||
onSend = {
|
||||
// Скрываем кнопку scroll на время отправки
|
||||
isSendingMessage = true
|
||||
viewModel.sendMessage()
|
||||
// Скроллим к новому сообщению
|
||||
scope.launch {
|
||||
delay(100)
|
||||
listState.animateScrollToItem(0)
|
||||
delay(300) // Ждём завершения анимации
|
||||
isSendingMessage = false
|
||||
}
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
backgroundColor = backgroundColor, // Тот же цвет что и фон чата
|
||||
textColor = textColor,
|
||||
placeholderColor = secondaryTextColor,
|
||||
secondaryTextColor = secondaryTextColor,
|
||||
// Reply state
|
||||
replyMessages = replyMessages,
|
||||
isForwardMode = isForwardMode,
|
||||
onCloseReply = { viewModel.clearReplyMessages() },
|
||||
chatTitle = chatTitle,
|
||||
isBlocked = isBlocked,
|
||||
// Emoji picker state (поднят для KeyboardAvoidingView)
|
||||
showEmojiPicker = showEmojiPicker,
|
||||
onToggleEmojiPicker = { showEmojiPicker = it },
|
||||
// Focus requester для автофокуса при reply
|
||||
focusRequester = inputFocusRequester,
|
||||
// Coordinator для плавных переходов
|
||||
coordinator = coordinator,
|
||||
// 🔥 Для отображения reply preview и скролла
|
||||
displayReplyMessages = displayReplyMessages,
|
||||
onReplyClick = scrollToMessage
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Modifier // Без imePadding
|
||||
}
|
||||
|
||||
// 🔥 SELECTION ACTION BAR - Reply/Forward (появляется при выборе сообщений)
|
||||
// Плоский стиль как у инпута с border сверху
|
||||
AnimatedVisibility(
|
||||
visible = isSelectionMode,
|
||||
enter = fadeIn(tween(200)) + slideInVertically(initialOffsetY = { it }),
|
||||
exit = fadeOut(tween(150)) + slideOutVertically(targetOffsetY = { it })
|
||||
) {
|
||||
Column {
|
||||
// Плоский контейнер как у инпута
|
||||
// Логирование состояния
|
||||
LaunchedEffect(isSelectionMode, useImePadding, coordinator.isEmojiBoxVisible, coordinator.keyboardHeight) {
|
||||
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
|
||||
android.util.Log.d("ChatDetailScreen", "🔄 BOTTOM BAR STATE CHANGED")
|
||||
android.util.Log.d("ChatDetailScreen", " 📊 isSelectionMode: $isSelectionMode")
|
||||
android.util.Log.d("ChatDetailScreen", " 📊 useImePadding: $useImePadding")
|
||||
android.util.Log.d("ChatDetailScreen", " 📊 isEmojiBoxVisible: ${coordinator.isEmojiBoxVisible}")
|
||||
android.util.Log.d("ChatDetailScreen", " 📊 keyboardHeight: ${coordinator.keyboardHeight}")
|
||||
android.util.Log.d("ChatDetailScreen", " 📊 emojiHeight: ${coordinator.emojiHeight}")
|
||||
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
|
||||
}
|
||||
|
||||
Column(modifier = bottomModifier) {
|
||||
// 🔥 UNIFIED BOTTOM BAR - один контейнер, контент меняется внутри
|
||||
Crossfade(
|
||||
targetState = isSelectionMode,
|
||||
animationSpec = tween(200),
|
||||
label = "bottomBarContent"
|
||||
) { selectionMode ->
|
||||
android.util.Log.d("ChatDetailScreen", "🎬 Crossfade to selectionMode=$selectionMode")
|
||||
|
||||
if (selectionMode) {
|
||||
// SELECTION ACTION BAR - Reply/Forward
|
||||
// 🔥 Высота должна совпадать с MessageInputBar (~56dp content + nav bar)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -1067,12 +1013,12 @@ fun ChatDetailScreen(
|
||||
.background(if (isDarkTheme) Color.White.copy(alpha = 0.15f) else Color.Black.copy(alpha = 0.1f))
|
||||
)
|
||||
|
||||
// Кнопки Reply и Forward
|
||||
// Кнопки Reply и Forward - такие же отступы как у input row
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
.padding(bottom = 12.dp)
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
.navigationBarsPadding(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -1081,6 +1027,7 @@ fun ChatDetailScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(40.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(PrimaryBlue.copy(alpha = 0.1f))
|
||||
.clickable {
|
||||
@@ -1089,8 +1036,7 @@ fun ChatDetailScreen(
|
||||
.sortedBy { it.timestamp }
|
||||
viewModel.setReplyMessages(selectedMsgs)
|
||||
selectedMessages = emptySet()
|
||||
}
|
||||
.padding(vertical = 16.dp),
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
@@ -1117,6 +1063,7 @@ fun ChatDetailScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(40.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(PrimaryBlue.copy(alpha = 0.1f))
|
||||
.clickable {
|
||||
@@ -1125,8 +1072,7 @@ fun ChatDetailScreen(
|
||||
.sortedBy { it.timestamp }
|
||||
viewModel.setForwardMessages(selectedMsgs)
|
||||
selectedMessages = emptySet()
|
||||
}
|
||||
.padding(vertical = 16.dp),
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(
|
||||
@@ -1150,6 +1096,45 @@ fun ChatDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// INPUT BAR
|
||||
Column {
|
||||
MessageInputBar(
|
||||
value = inputText,
|
||||
onValueChange = {
|
||||
viewModel.updateInputText(it)
|
||||
if (it.isNotEmpty() && !isSavedMessages) {
|
||||
viewModel.sendTypingIndicator()
|
||||
}
|
||||
},
|
||||
onSend = {
|
||||
isSendingMessage = true
|
||||
viewModel.sendMessage()
|
||||
scope.launch {
|
||||
delay(100)
|
||||
listState.animateScrollToItem(0)
|
||||
delay(300)
|
||||
isSendingMessage = false
|
||||
}
|
||||
},
|
||||
isDarkTheme = isDarkTheme,
|
||||
backgroundColor = backgroundColor,
|
||||
textColor = textColor,
|
||||
placeholderColor = secondaryTextColor,
|
||||
secondaryTextColor = secondaryTextColor,
|
||||
replyMessages = replyMessages,
|
||||
isForwardMode = isForwardMode,
|
||||
onCloseReply = { viewModel.clearReplyMessages() },
|
||||
chatTitle = chatTitle,
|
||||
isBlocked = isBlocked,
|
||||
showEmojiPicker = showEmojiPicker,
|
||||
onToggleEmojiPicker = { showEmojiPicker = it },
|
||||
focusRequester = inputFocusRequester,
|
||||
coordinator = coordinator,
|
||||
displayReplyMessages = displayReplyMessages,
|
||||
onReplyClick = scrollToMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} // Закрытие Column с imePadding
|
||||
@@ -1288,12 +1273,29 @@ fun ChatDetailScreen(
|
||||
isSelected = selectedMessages.contains(selectionKey),
|
||||
isHighlighted = highlightedMessageId == message.id,
|
||||
onLongClick = {
|
||||
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
|
||||
android.util.Log.d("ChatDetailScreen", "👆 LONG CLICK on message")
|
||||
android.util.Log.d("ChatDetailScreen", " 📊 isSelectionMode BEFORE: $isSelectionMode")
|
||||
android.util.Log.d("ChatDetailScreen", " 📊 selectedMessages.size BEFORE: ${selectedMessages.size}")
|
||||
|
||||
// 🔥 СНАЧАЛА закрываем клавиатуру МГНОВЕННО (до изменения state)
|
||||
if (!isSelectionMode) {
|
||||
android.util.Log.d("ChatDetailScreen", " ⌨️ Closing keyboard...")
|
||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
focusManager.clearFocus()
|
||||
showEmojiPicker = false
|
||||
android.util.Log.d("ChatDetailScreen", " ✅ Keyboard closed")
|
||||
}
|
||||
// Toggle selection on long press
|
||||
selectedMessages = if (selectedMessages.contains(selectionKey)) {
|
||||
selectedMessages - selectionKey
|
||||
} else {
|
||||
selectedMessages + selectionKey
|
||||
}
|
||||
|
||||
android.util.Log.d("ChatDetailScreen", " 📊 selectedMessages.size AFTER: ${selectedMessages.size}")
|
||||
android.util.Log.d("ChatDetailScreen", "═══════════════════════════════════════")
|
||||
},
|
||||
onClick = {
|
||||
// If in selection mode, toggle selection
|
||||
@@ -2332,11 +2334,14 @@ private fun MessageInputBar(
|
||||
.background(if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f))
|
||||
)
|
||||
|
||||
// BLOCKED CHAT FOOTER - плоский стиль
|
||||
// 🔥 Высота должна совпадать с MessageInputBar и Selection Action Bar
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
.padding(bottom = 16.dp),
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
.navigationBarsPadding(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
@@ -2453,11 +2458,14 @@ private fun MessageInputBar(
|
||||
}
|
||||
|
||||
// INPUT ROW - Paperclip → TextField → Emoji → Send/Mic (ПЛОСКИЙ ДИЗАЙН)
|
||||
// 🔥 Высота должна совпадать с Selection Action Bar (padding + nav bar)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 48.dp)
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.padding(bottom = 16.dp) // 🔥 Такой же отступ как у Selection Action Bar
|
||||
.navigationBarsPadding(), // 🔥 Такой же navigationBarsPadding
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
// PAPERCLIP BUTTON (слева)
|
||||
|
||||
@@ -768,8 +768,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||
* 🔥 Очистить reply/forward
|
||||
*/
|
||||
fun clearReplyMessages() {
|
||||
_replyMessages.value = emptyList()
|
||||
_isForwardMode.value = false
|
||||
viewModelScope.launch {
|
||||
delay(350) // Задержка после закрытия панели (анимация fadeOut + shrinkVertically)
|
||||
_replyMessages.value = emptyList()
|
||||
_isForwardMode.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
|
||||
* ```
|
||||
*/
|
||||
object KeyboardHeightProvider {
|
||||
private const val TAG = "KeyboardHeight"
|
||||
private const val PREFS_NAME = "emoji_keyboard_prefs"
|
||||
private const val KEY_KEYBOARD_HEIGHT = "kbd_height"
|
||||
private const val DEFAULT_HEIGHT_DP = 280 // Telegram uses 200, we use 280
|
||||
@@ -35,7 +36,11 @@ object KeyboardHeightProvider {
|
||||
val savedPx = prefs.getInt(KEY_KEYBOARD_HEIGHT, defaultPx)
|
||||
val isDefault = savedPx == defaultPx
|
||||
|
||||
android.util.Log.d("KeyboardHeight", "📖 getSavedKeyboardHeight: ${savedPx}px (${pxToDp(context, savedPx)}dp) ${if (isDefault) "[DEFAULT]" else "[SAVED]"}")
|
||||
android.util.Log.d(TAG, "═══════════════════════════════════════")
|
||||
android.util.Log.d(TAG, "📖 getSavedKeyboardHeight()")
|
||||
android.util.Log.d(TAG, " 📏 Height: ${savedPx}px (${pxToDp(context, savedPx)}dp)")
|
||||
android.util.Log.d(TAG, " 📦 Source: ${if (isDefault) "DEFAULT" else "SAVED"}")
|
||||
android.util.Log.d(TAG, "═══════════════════════════════════════")
|
||||
return savedPx
|
||||
}
|
||||
|
||||
@@ -43,6 +48,9 @@ object KeyboardHeightProvider {
|
||||
* Сохранить высоту клавиатуры
|
||||
*/
|
||||
fun saveKeyboardHeight(context: Context, heightPx: Int) {
|
||||
android.util.Log.d(TAG, "═══════════════════════════════════════")
|
||||
android.util.Log.d(TAG, "💾 saveKeyboardHeight($heightPx px)")
|
||||
|
||||
if (heightPx > 0) {
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val oldHeight = prefs.getInt(KEY_KEYBOARD_HEIGHT, -1)
|
||||
@@ -50,21 +58,29 @@ object KeyboardHeightProvider {
|
||||
|
||||
prefs.edit().putInt(KEY_KEYBOARD_HEIGHT, heightPx).apply()
|
||||
|
||||
android.util.Log.d(TAG, " 📏 New: ${heightPx}px (${pxToDp(context, heightPx)}dp)")
|
||||
android.util.Log.d(TAG, " 📏 Old: ${oldHeight}px (${pxToDp(context, oldHeight)}dp)")
|
||||
android.util.Log.d(TAG, " 🔄 Changed: $changed")
|
||||
|
||||
if (changed) {
|
||||
android.util.Log.d("KeyboardHeight", "💾 SAVED keyboard height: ${heightPx}px (${pxToDp(context, heightPx)}dp) [WAS: ${oldHeight}px]")
|
||||
android.util.Log.d(TAG, " ✅ SAVED successfully!")
|
||||
} else {
|
||||
android.util.Log.v("KeyboardHeight", "💾 Same keyboard height: ${heightPx}px (no change)")
|
||||
android.util.Log.d(TAG, " ⏭️ Same height, no change")
|
||||
}
|
||||
} else {
|
||||
android.util.Log.w("KeyboardHeight", "⚠️ Attempted to save invalid height: ${heightPx}px")
|
||||
android.util.Log.w(TAG, " ⚠️ INVALID height: ${heightPx}px - NOT saved!")
|
||||
}
|
||||
android.util.Log.d(TAG, "═══════════════════════════════════════")
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить длительность анимации клавиатуры
|
||||
* Telegram использует: AdjustPanLayoutHelper.keyboardDuration (250ms обычно)
|
||||
*/
|
||||
fun getKeyboardAnimationDuration(): Long = 250L
|
||||
fun getKeyboardAnimationDuration(): Long {
|
||||
android.util.Log.d(TAG, "⏱️ getKeyboardAnimationDuration() = 250ms")
|
||||
return 250L
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
private fun dpToPx(context: Context, dp: Int): Int {
|
||||
@@ -91,7 +107,9 @@ fun rememberSavedKeyboardHeight(): Dp {
|
||||
val density = context.resources.displayMetrics.density
|
||||
val heightDp = (heightPx / density).dp
|
||||
|
||||
android.util.Log.d("KeyboardHeight", "🎯 rememberSavedKeyboardHeight: ${heightDp} (${heightPx}px, density=${density})")
|
||||
android.util.Log.d("KeyboardHeight", "🎯 rememberSavedKeyboardHeight()")
|
||||
android.util.Log.d("KeyboardHeight", " 📏 Result: $heightDp (${heightPx}px)")
|
||||
android.util.Log.d("KeyboardHeight", " 📱 Density: $density")
|
||||
|
||||
return heightDp
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user