feat: Optimize emoji picker with chunk loading and improve typing indicator in chat list

This commit is contained in:
k1ngsterr1
2026-01-14 02:00:43 +05:00
parent 43ef9ac87f
commit 37b7d6e613
4 changed files with 157 additions and 62 deletions

View File

@@ -2298,34 +2298,59 @@ private fun MessageInputBar(
} // Закрытие внешней Column с border
} // End of else (not blocked)
// 🔥 APPLE EMOJI PICKER - БЕЗ анимации когда клавиатура открывается
// <EFBFBD> APPLE EMOJI PICKER - ОПТИМИЗИРОВАННАЯ АНИМАЦИЯ
// Используем slide up + alpha вместо height анимации (нет relayout = супер плавно)
if (!isBlocked) {
// Высота панели: 0 если клавиатура видна или эмодзи закрыты, иначе emojiPanelHeight
// НЕ анимируем когда клавиатура открыта (чтобы не было прыжка)
val targetHeight = if (isKeyboardVisible || !showEmojiPicker) 0.dp else emojiPanelHeight
// Панель ВСЕГДА в DOM (pre-render), просто скрыта через offset/alpha
val shouldShow = showEmojiPicker && !isKeyboardVisible
// Анимируем только когда клавиатура закрыта
val animatedHeight by animateDpAsState(
targetValue = targetHeight,
animationSpec = if (isKeyboardVisible) {
// Мгновенно когда клавиатура открывается
snap()
// 🚀 Animatable для максимального контроля (быстрее чем animateDpAsState)
val slideProgress = remember { Animatable(0f) }
LaunchedEffect(shouldShow) {
if (shouldShow) {
// Открытие: быстрая spring анимация (critically damped = без пружинистости)
slideProgress.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = 1f, // Critically damped - без bounce
stiffness = 800f // Высокая жёсткость = быстро
)
)
} else {
// Быстрая анимация как обычная клавиатура (200ms)
tween(durationMillis = 200, easing = FastOutSlowInEasing)
},
label = "EmojiPanelHeight"
)
// Закрытие: мгновенно если клавиатура открылась, иначе быстрая анимация
if (isKeyboardVisible) {
slideProgress.snapTo(0f)
} else {
slideProgress.animateTo(
targetValue = 0f,
animationSpec = tween(120, easing = FastOutSlowInEasing)
)
}
}
}
val panelHeightPx = with(LocalDensity.current) { (emojiPanelHeight - 16.dp).toPx() }
Box(
modifier = Modifier
.fillMaxWidth()
.height(animatedHeight)
.padding(bottom = 16.dp) // 🔥 Отступ снизу для безопасной зоны
.height(emojiPanelHeight)
.padding(bottom = 16.dp)
.graphicsLayer {
// 🚀 Slide up анимация через translationY (НЕ вызывает relayout!)
translationY = panelHeightPx * (1f - slideProgress.value)
alpha = slideProgress.value
// Hardware layer для плавности
if (slideProgress.value > 0f && slideProgress.value < 1f) {
shadowElevation = 0f
}
}
.clipToBounds() // Обрезаем контент при slide
) {
// 🚀 Рендерим панель когда showEmojiPicker = true
// Высота контролируется через animatedHeight
if (showEmojiPicker) {
// 🚀 Панель ВСЕГДА рендерится (pre-render для instant open)
// Но скрыта через alpha = 0 когда не нужна
if (slideProgress.value > 0f || shouldShow) {
AppleEmojiPickerPanel(
isDarkTheme = isDarkTheme,
onEmojiSelected = { emoji ->
@@ -2336,7 +2361,7 @@ private fun MessageInputBar(
},
modifier = Modifier
.fillMaxWidth()
.height(emojiPanelHeight - 16.dp) // 🔥 Учитываем отступ
.height(emojiPanelHeight - 16.dp)
)
}
}

View File

@@ -27,6 +27,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.sp
import com.airbnb.lottie.compose.*
import com.rosetta.messenger.R
@@ -178,6 +179,9 @@ fun ChatsListScreen(
// Protocol connection state
val protocolState by ProtocolManager.state.collectAsState()
// 🔥 Пользователи, которые сейчас печатают
val typingUsers by ProtocolManager.typingUsers.collectAsState()
// Dialogs from database
val dialogsList by chatsViewModel.dialogs.collectAsState()
@@ -508,6 +512,7 @@ fun ChatsListScreen(
DialogItem(
dialog = dialog,
isDarkTheme = isDarkTheme,
isTyping = typingUsers.contains(dialog.opponentKey),
onClick = {
val user = chatsViewModel.dialogToSearchUser(dialog)
onUserSelect(user)
@@ -664,6 +669,8 @@ fun ChatItem(chat: Chat, isDarkTheme: Boolean, onClick: () -> Unit) {
text = chat.lastMessage,
fontSize = 14.sp,
color = secondaryTextColor,
maxLines = 1,
overflow = android.text.TextUtils.TruncateAt.END,
modifier = Modifier.weight(1f)
)
@@ -786,7 +793,7 @@ fun DrawerMenuItem(
/** Элемент диалога из базы данных - ОПТИМИЗИРОВАННЫЙ */
@Composable
fun DialogItem(dialog: DialogUiModel, isDarkTheme: Boolean, onClick: () -> Unit) {
fun DialogItem(dialog: DialogUiModel, isDarkTheme: Boolean, isTyping: Boolean = false, onClick: () -> Unit) {
// 🔥 ОПТИМИЗАЦИЯ: Кешируем цвета и строки
val textColor = remember(isDarkTheme) { if (isDarkTheme) Color.White else Color.Black }
val secondaryTextColor = remember(isDarkTheme) { if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) }
@@ -886,15 +893,22 @@ fun DialogItem(dialog: DialogUiModel, isDarkTheme: Boolean, onClick: () -> Unit)
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// 🔥 Используем AppleEmojiText для отображения эмодзи
// Если есть непрочитанные - текст темнее
AppleEmojiText(
text = dialog.lastMessage.ifEmpty { "No messages" },
fontSize = 14.sp,
color = if (dialog.unreadCount > 0) textColor.copy(alpha = 0.85f) else secondaryTextColor,
fontWeight = if (dialog.unreadCount > 0) FontWeight.Medium else FontWeight.Normal,
modifier = Modifier.weight(1f)
)
// 🔥 Показываем typing индикатор или последнее сообщение
if (isTyping) {
TypingIndicatorSmall()
} else {
// 🔥 Используем AppleEmojiText для отображения эмодзи
// Если есть непрочитанные - текст темнее
AppleEmojiText(
text = dialog.lastMessage.ifEmpty { "No messages" },
fontSize = 14.sp,
color = if (dialog.unreadCount > 0) textColor.copy(alpha = 0.85f) else secondaryTextColor,
fontWeight = if (dialog.unreadCount > 0) FontWeight.Medium else FontWeight.Normal,
maxLines = 1,
overflow = android.text.TextUtils.TruncateAt.END,
modifier = Modifier.weight(1f)
)
}
// Unread badge
if (dialog.unreadCount > 0) {
@@ -933,3 +947,50 @@ fun DialogItem(dialog: DialogUiModel, isDarkTheme: Boolean, onClick: () -> Unit)
)
}
}
/**
* 🔥 Компактный индикатор typing для списка чатов
* Голубой текст "typing" с анимированными точками
*/
@Composable
fun TypingIndicatorSmall() {
val infiniteTransition = rememberInfiniteTransition(label = "typing")
val typingColor = PrimaryBlue
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(1.dp)
) {
Text(
text = "typing",
fontSize = 14.sp,
color = typingColor,
fontWeight = FontWeight.Medium
)
// 3 анимированные точки
repeat(3) { index ->
val offsetY by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = -3f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 500,
delayMillis = index * 120,
easing = FastOutSlowInEasing
),
repeatMode = RepeatMode.Reverse
),
label = "dot$index"
)
Text(
text = ".",
fontSize = 14.sp,
color = typingColor,
fontWeight = FontWeight.Medium,
modifier = Modifier.offset(y = offsetY.dp)
)
}
}
}