feat: Enhance ChatDetailScreen with dynamic keyboard handling and new animations

- Implemented dynamic bottom padding for the message list based on keyboard visibility.
- Added Lottie animation for the speech icon when no messages are present.
- Adjusted input bar design to match chat background color and improved spacing.
- Integrated emoji picker behavior to close when the keyboard opens.
- Updated message input row with attach file functionality and refined button sizes.
- Introduced a new Lottie animation resource for the speech icon.
This commit is contained in:
k1ngsterr1
2026-01-13 02:46:16 +05:00
parent 8720d02701
commit b26c25629a
3 changed files with 131 additions and 65 deletions

File diff suppressed because one or more lines are too long

View File

@@ -53,6 +53,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.rosetta.messenger.R
import com.rosetta.messenger.data.Message
import com.rosetta.messenger.network.DeliveryStatus
import com.rosetta.messenger.network.SearchUser
@@ -65,6 +66,7 @@ import android.view.inputmethod.InputMethodManager
import android.content.Context
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import com.airbnb.lottie.compose.*
import java.text.SimpleDateFormat
import java.util.*
import kotlinx.coroutines.delay
@@ -202,6 +204,15 @@ fun ChatDetailScreen(
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
val density = LocalDensity.current
// 🔥 Отслеживаем высоту клавиатуры для поднятия контента
val imeInsets = WindowInsets.ime
val imeHeight = with(density) { imeInsets.getBottom(density).toDp() }
val isKeyboardVisible = imeHeight > 0.dp
// Динамический bottom padding для списка: инпут (~70dp) + клавиатура
val listBottomPadding = if (isKeyboardVisible) 70.dp + imeHeight else 100.dp
// Telegram-style scroll tracking
var wasManualScroll by remember { mutableStateOf(false) }
@@ -675,9 +686,10 @@ fun ChatDetailScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.imePadding() // KeyboardAvoidingView equivalent
) {
// Список сообщений - занимает весь экран
Box(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.fillMaxSize().padding(bottom = 70.dp)) {
if (messages.isEmpty()) {
// Пустое состояние
Column(
@@ -685,13 +697,25 @@ fun ChatDetailScreen(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
if (isSavedMessages) Icons.Default.Bookmark
else Icons.Default.Chat,
if (isSavedMessages) {
Icon(
Icons.Default.Bookmark,
contentDescription = null,
tint = secondaryTextColor.copy(alpha = 0.5f),
modifier = Modifier.size(64.dp)
)
)
} else {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.speech))
val progress by animateLottieCompositionAsState(
composition = composition,
iterations = LottieConstants.IterateForever
)
LottieAnimation(
composition = composition,
progress = { progress },
modifier = Modifier.size(120.dp)
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text =
@@ -737,13 +761,13 @@ fun ChatDetailScreen(
}
}
),
// padding для контента списка - добавляем снизу место для инпута
// padding для контента списка - фиксированный для инпута
contentPadding =
PaddingValues(
start = 8.dp,
end = 8.dp,
top = 8.dp,
bottom = 100.dp // Место для floating input (с запасом)
bottom = 16.dp // Небольшой отступ снизу
),
reverseLayout = true
) {
@@ -897,7 +921,7 @@ fun ChatDetailScreen(
}
},
isDarkTheme = isDarkTheme,
backgroundColor = inputBackgroundColor,
backgroundColor = backgroundColor, // Тот же цвет что и фон чата
textColor = textColor,
placeholderColor = secondaryTextColor,
secondaryTextColor = secondaryTextColor,
@@ -1538,6 +1562,13 @@ private fun MessageInputBar(
// Состояние отправки
val canSend = remember(value) { value.isNotBlank() }
// 🔥 Закрываем эмодзи панель когда клавиатура открывается
LaunchedEffect(isKeyboardVisible) {
if (isKeyboardVisible && showEmojiPicker) {
showEmojiPicker = false
}
}
// Функция для гарантированного закрытия клавиатуры через InputMethodManager
fun hideKeyboardCompletely() {
@@ -1612,15 +1643,30 @@ private fun MessageInputBar(
)
}
} else {
// 🔥 TELEGRAM STYLE: простой фон, все кнопки внутри
// 🔥 TELEGRAM STYLE: фон как у чата, верхний border
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7)
)
.padding(bottom = if (isKeyboardVisible) 0.dp else 16.dp)
) {
// Верхний border (как в архиве)
Box(
modifier = Modifier
.fillMaxWidth()
.height(0.5.dp)
.background(
if (isDarkTheme) Color.White.copy(alpha = 0.1f)
else Color.Black.copy(alpha = 0.08f)
)
)
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = backgroundColor // Тот же цвет что и фон чата
)
.padding(bottom = if (isKeyboardVisible) 0.dp else 16.dp)
) {
// REPLY PANEL
AnimatedVisibility(
visible = hasReply,
@@ -1630,7 +1676,7 @@ private fun MessageInputBar(
Row(
modifier = Modifier
.fillMaxWidth()
.background(if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFFFFFFF))
.background(backgroundColor) // Тот же цвет что и фон чата
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
@@ -1679,38 +1725,47 @@ private fun MessageInputBar(
}
}
// INPUT ROW - как в Telegram
// INPUT ROW - Paperclip → TextField → Emoji → Send/Mic (ПЛОСКИЙ ДИЗАЙН)
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 48.dp)
.padding(horizontal = 8.dp, vertical = 8.dp),
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.Bottom
) {
// EMOJI BUTTON (слева)
// PAPERCLIP BUTTON (слева)
IconButton(
onClick = { toggleEmojiPicker() },
modifier = Modifier.size(48.dp)
onClick = { /* TODO: Attach file/image */ },
modifier = Modifier.size(40.dp)
) {
Icon(
if (showEmojiPicker) Icons.Default.Keyboard
else Icons.Default.SentimentSatisfiedAlt,
contentDescription = "Emoji",
Icons.Default.AttachFile,
contentDescription = "Attach",
tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
modifier = Modifier.size(26.dp)
modifier = Modifier.size(24.dp)
)
}
// TEXT INPUT
Spacer(modifier = Modifier.width(4.dp))
// TEXT INPUT - ПЛОСКИЙ (тот же цвет что и фон чата)
Box(
modifier = Modifier
.weight(1f)
.heightIn(min = 36.dp)
.heightIn(min = 40.dp)
.background(
color = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFE5E5EA),
shape = RoundedCornerShape(20.dp)
color = backgroundColor // Тот же цвет что и фон чата
)
.padding(horizontal = 12.dp, vertical = 10.dp),
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
// При клике на инпут - закрываем эмодзи панель, клавиатура откроется автоматически
if (showEmojiPicker) {
showEmojiPicker = false
}
}
.padding(horizontal = 12.dp, vertical = 8.dp),
contentAlignment = Alignment.CenterStart
) {
AppleEmojiTextField(
@@ -1718,60 +1773,69 @@ private fun MessageInputBar(
onValueChange = { newValue -> onValueChange(newValue) },
textColor = textColor,
textSize = 16f,
hint = "Message",
hint = "Type message...",
hintColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
modifier = Modifier.fillMaxWidth()
)
}
Spacer(modifier = Modifier.width(4.dp))
Spacer(modifier = Modifier.width(6.dp))
// ATTACH / MIC / SEND BUTTON (справа)
if (canSend) {
// SEND BUTTON
// EMOJI BUTTON (между input и send)
IconButton(
onClick = { toggleEmojiPicker() },
modifier = Modifier.size(40.dp)
) {
Icon(
if (showEmojiPicker) Icons.Default.Keyboard
else Icons.Default.SentimentSatisfiedAlt,
contentDescription = "Emoji",
tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.width(2.dp))
// SEND BUTTON (всегда справа) - с анимацией
AnimatedVisibility(
visible = canSend,
enter = scaleIn(tween(150)) + fadeIn(tween(150)),
exit = scaleOut(tween(100)) + fadeOut(tween(100))
) {
IconButton(
onClick = { handleSend() },
modifier = Modifier.size(48.dp)
) {
Box(
modifier = Modifier
.size(34.dp)
.clip(CircleShape)
.background(PrimaryBlue),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = TelegramSendIcon,
contentDescription = "Send",
tint = Color.White,
modifier = Modifier.size(18.dp)
)
}
}
} else {
// MIC BUTTON
IconButton(
onClick = { /* TODO: Voice recording */ },
modifier = Modifier.size(48.dp)
modifier = Modifier.size(40.dp)
) {
Icon(
Icons.Default.Mic,
contentDescription = "Voice",
tint = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF8E8E93),
modifier = Modifier.size(26.dp)
imageVector = TelegramSendIcon,
contentDescription = "Send",
tint = PrimaryBlue,
modifier = Modifier.size(24.dp)
)
}
}
}
}
} // Закрытие внутренней Column с padding
} // Закрытие внешней Column с border
} // End of else (not blocked)
// 🔥 APPLE EMOJI PICKER - плавная анимация высоты
// 🔥 APPLE EMOJI PICKER - БЕЗ анимации когда клавиатура открывается
if (!isBlocked) {
// Анимируем высоту панели
// Высота панели: 0 если клавиатура видна или эмодзи закрыты, иначе emojiPanelHeight
// НЕ анимируем когда клавиатура открыта (чтобы не было прыжка)
val targetHeight = if (isKeyboardVisible || !showEmojiPicker) 0.dp else emojiPanelHeight
// Анимируем только когда клавиатура закрыта
val animatedHeight by animateDpAsState(
targetValue = if (showEmojiPicker) emojiPanelHeight else 0.dp,
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
targetValue = targetHeight,
animationSpec = if (isKeyboardVisible) {
// Мгновенно когда клавиатура открывается
snap()
} else {
// Плавно когда открываем/закрываем эмодзи без клавиатуры
tween(durationMillis = 200, easing = FastOutSlowInEasing)
},
label = "EmojiPanelHeight"
)
@@ -1781,7 +1845,7 @@ private fun MessageInputBar(
.height(animatedHeight)
.clipToBounds()
) {
if (showEmojiPicker) {
if (showEmojiPicker && !isKeyboardVisible) {
AppleEmojiPickerPanel(
isDarkTheme = isDarkTheme,
onEmojiSelected = { emoji ->

File diff suppressed because one or more lines are too long