feat: Enhance ProtocolManager logging and error handling; optimize emoji picker performance

This commit is contained in:
k1ngsterr1
2026-01-13 20:46:46 +05:00
parent 7c911835ea
commit f155c4d36d
4 changed files with 116 additions and 80 deletions

View File

@@ -285,16 +285,16 @@ fun MainScreen(
) )
} }
// 🔍 Вход в поиск - slide сверху // 🔍 Вход в поиск - slide справа (как чаты)
isEnteringSearch -> { isEnteringSearch -> {
slideInVertically( slideInHorizontally(
initialOffsetY = { -it }, initialOffsetX = { fullWidth -> fullWidth }, // Начинаем за экраном справа
animationSpec = spring( animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy, dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMediumLow stiffness = Spring.StiffnessMediumLow
) )
) togetherWith slideOutVertically( ) togetherWith slideOutHorizontally(
targetOffsetY = { it / 4 }, targetOffsetX = { fullWidth -> -fullWidth / 4 }, // Список уходит влево на 25%
animationSpec = spring( animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy, dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMediumLow stiffness = Spring.StiffnessMediumLow
@@ -302,16 +302,16 @@ fun MainScreen(
) )
} }
// ❌ Выход из поиска // ❌ Выход из поиска - обратный slide
isExitingSearch -> { isExitingSearch -> {
slideInVertically( slideInHorizontally(
initialOffsetY = { it / 4 }, initialOffsetX = { fullWidth -> -fullWidth / 4 }, // Список возвращается слева
animationSpec = spring( animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy, dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium stiffness = Spring.StiffnessMedium
) )
) togetherWith slideOutVertically( ) togetherWith slideOutHorizontally(
targetOffsetY = { -it }, targetOffsetX = { fullWidth -> fullWidth }, // Поиск уходит за экран вправо
animationSpec = spring( animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy, dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium stiffness = Spring.StiffnessMedium

View File

@@ -103,10 +103,17 @@ object ProtocolManager {
addLog("🟢 Online status received: ${onlinePacket.publicKeysState.size} entries") addLog("🟢 Online status received: ${onlinePacket.publicKeysState.size} entries")
scope.launch { scope.launch {
if (messageRepository == null) {
addLog("❌ ERROR: messageRepository is NULL!")
} else {
addLog("✅ messageRepository is initialized")
onlinePacket.publicKeysState.forEach { item -> onlinePacket.publicKeysState.forEach { item ->
val isOnline = item.state == OnlineState.ONLINE val isOnline = item.state == OnlineState.ONLINE
addLog(" ${item.publicKey.take(16)}... -> ${if (isOnline) "ONLINE" else "OFFLINE"}") addLog(" ${item.publicKey.take(16)}... -> ${if (isOnline) "ONLINE" else "OFFLINE"}")
addLog(" Calling updateOnlineStatus...")
messageRepository?.updateOnlineStatus(item.publicKey, isOnline) messageRepository?.updateOnlineStatus(item.publicKey, isOnline)
addLog(" updateOnlineStatus called")
}
} }
} }
} }

View File

@@ -639,27 +639,32 @@ fun ChatDetailScreen(
keyboardController?.hide() keyboardController?.hide()
focusManager.clearFocus() focusManager.clearFocus()
showMenu = true showMenu = true
} },
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
) { ) {
Icon( Icon(
Icons.Default.MoreVert, Icons.Default.MoreVert,
contentDescription = "More", contentDescription = "More",
tint = headerIconColor tint = headerIconColor,
modifier = Modifier.size(26.dp)
) )
} }
// Выпадающее меню - чистый дизайн без артефактов // Выпадающее меню - чистый дизайн без артефактов
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(
surface = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White
)
) {
DropdownMenu( DropdownMenu(
expanded = showMenu, expanded = showMenu,
onDismissRequest = { showMenu = false }, onDismissRequest = { showMenu = false },
modifier = Modifier modifier = Modifier
.width(220.dp) .width(220.dp)
.clip(RoundedCornerShape(16.dp))
.background( .background(
color = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White color = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White,
)
.shadow(
elevation = 8.dp,
shape = RoundedCornerShape(16.dp) shape = RoundedCornerShape(16.dp)
) )
) { ) {
@@ -689,7 +694,10 @@ fun ChatDetailScreen(
showMenu = false showMenu = false
showDeleteConfirm = true showDeleteConfirm = true
}, },
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 8.dp),
colors = MenuDefaults.itemColors(
textColor = Color(0xFFE53935)
)
) )
// Разделитель // Разделитель
@@ -733,7 +741,10 @@ fun ChatDetailScreen(
showBlockConfirm = true showBlockConfirm = true
} }
}, },
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 8.dp),
colors = MenuDefaults.itemColors(
textColor = PrimaryBlue
)
) )
// Разделитель // Разделитель
@@ -771,8 +782,12 @@ fun ChatDetailScreen(
showMenu = false showMenu = false
showLogs = true showLogs = true
}, },
modifier = Modifier.padding(horizontal = 8.dp) modifier = Modifier.padding(horizontal = 8.dp),
colors = MenuDefaults.itemColors(
textColor = textColor
) )
)
}
} }
} }
} }
@@ -797,10 +812,16 @@ fun ChatDetailScreen(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.graphicsLayer { clip = false }
.padding(paddingValues) .padding(paddingValues)
) { ) {
// Список сообщений - динамический padding для клавиатуры/эмодзи // Список сообщений - динамический padding для клавиатуры/эмодзи
Box(modifier = Modifier.fillMaxSize().padding(bottom = listBottomPadding)) { // 🔥 graphicsLayer(clip = false) - позволяет пузырькам выходить за границы padding
Box(modifier = Modifier
.fillMaxSize()
.graphicsLayer { clip = false }
.padding(bottom = listBottomPadding)
) {
when { when {
// 🔥 СКЕЛЕТОН - показываем пока загружаются сообщения // 🔥 СКЕЛЕТОН - показываем пока загружаются сообщения
isLoading -> { isLoading -> {
@@ -1483,11 +1504,12 @@ private fun MessageBubble(
label = "selectionAlpha" label = "selectionAlpha"
) )
// 🔥 ОПТИМИЗАЦИЯ: Кешируем цвета - они не меняются для одного сообщения // 🔥 Цвета - НАШИ ОРИГИНАЛЬНЫЕ
val bubbleColor = remember(message.isOutgoing, isDarkTheme) { val bubbleColor = remember(message.isOutgoing, isDarkTheme) {
if (message.isOutgoing) { if (message.isOutgoing) {
PrimaryBlue PrimaryBlue // Исходящие - наш синий
} else { } else {
// Входящие - наши цвета
if (isDarkTheme) Color(0xFF212121) else Color(0xFFF5F5F5) if (isDarkTheme) Color(0xFF212121) else Color(0xFFF5F5F5)
} }
} }
@@ -1495,18 +1517,20 @@ private fun MessageBubble(
if (message.isOutgoing) Color.White if (message.isOutgoing) Color.White
else if (isDarkTheme) Color.White else Color(0xFF000000) else if (isDarkTheme) Color.White else Color(0xFF000000)
} }
// Время - наши оригинальные цвета
val timeColor = remember(message.isOutgoing, isDarkTheme) { val timeColor = remember(message.isOutgoing, isDarkTheme) {
if (message.isOutgoing) Color.White.copy(alpha = 0.7f) if (message.isOutgoing) Color.White.copy(alpha = 0.7f)
else if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) else if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
} }
// 🔥 ОПТИМИЗАЦИЯ: Кешируем форму bubble // 🔥 TELEGRAM STYLE: Форма пузырька - более мягкие углы
val bubbleShape = remember(message.isOutgoing, showTail) { val bubbleShape = remember(message.isOutgoing, showTail) {
RoundedCornerShape( RoundedCornerShape(
topStart = 18.dp, topStart = 16.dp,
topEnd = 18.dp, topEnd = 16.dp,
bottomStart = if (message.isOutgoing) 18.dp else (if (showTail) 4.dp else 18.dp), // Хвостик: маленький радиус (4dp) только у нижнего угла со стороны отправителя
bottomEnd = if (message.isOutgoing) (if (showTail) 4.dp else 18.dp) else 18.dp bottomStart = if (message.isOutgoing) 16.dp else (if (showTail) 4.dp else 16.dp),
bottomEnd = if (message.isOutgoing) (if (showTail) 4.dp else 16.dp) else 16.dp
) )
} }
@@ -1574,11 +1598,10 @@ private fun MessageBubble(
Row( Row(
modifier = modifier =
Modifier.fillMaxWidth() Modifier.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 1.dp) // 🔥 TELEGRAM: horizontal 4dp (меньше), vertical 2dp (компактнее)
.padding(horizontal = 4.dp, vertical = 2.dp)
.offset { IntOffset(animatedOffset.toInt(), 0) } .offset { IntOffset(animatedOffset.toInt(), 0) }
.graphicsLayer { .graphicsLayer {
// ❌ УБРАЛИ: alpha = alpha * selectionAlpha и translationY
// Оставляем только selection анимацию
this.alpha = selectionAlpha this.alpha = selectionAlpha
this.scaleX = selectionScale this.scaleX = selectionScale
this.scaleY = selectionScale this.scaleY = selectionScale
@@ -1607,7 +1630,7 @@ private fun MessageBubble(
Box( Box(
modifier = modifier =
Modifier.widthIn(max = 300.dp) Modifier.widthIn(max = 280.dp) // 🔥 TELEGRAM: чуть уже (280dp)
.combinedClickable( .combinedClickable(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
@@ -1616,9 +1639,10 @@ private fun MessageBubble(
) )
.clip(bubbleShape) .clip(bubbleShape)
.background(bubbleColor) .background(bubbleColor)
.padding(horizontal = 12.dp, vertical = 7.dp) // 🔥 TELEGRAM: padding 10-12dp horizontal, 8dp vertical
.padding(horizontal = 10.dp, vertical = 8.dp)
) { ) {
// 🔥 Telegram-style: текст и время на одной строке, выровнены по нижней границе // 🔥 TELEGRAM STYLE: текст и время на одной строке
Column { Column {
// Reply bubble (цитата) // Reply bubble (цитата)
message.replyData?.let { reply -> message.replyData?.let { reply ->
@@ -1627,32 +1651,34 @@ private fun MessageBubble(
isOutgoing = message.isOutgoing, isOutgoing = message.isOutgoing,
isDarkTheme = isDarkTheme isDarkTheme = isDarkTheme
) )
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(4.dp)) // Меньше отступ
} }
// Текст и время в одной строке (Row) // Текст и время в одной строке (Row)
Row( Row(
verticalAlignment = Alignment.Bottom, // Выравнивание по нижней границе (baseline) verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(6.dp) horizontalArrangement = Arrangement.spacedBy(8.dp) // Чуть больше отступ до времени
) { ) {
// Текст (не растягивается на всю ширину) // 🔥 TELEGRAM: Текст 17sp, lineHeight 22sp, letterSpacing -0.4sp
AppleEmojiText( AppleEmojiText(
text = message.text, text = message.text,
color = textColor, color = textColor,
fontSize = 16.sp, fontSize = 17.sp,
modifier = Modifier.weight(1f, fill = false) modifier = Modifier.weight(1f, fill = false)
) )
// Время и статус справа // Время и статус справа
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(3.dp), horizontalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.padding(bottom = 1.dp) // Небольшая коррекция для выравнивания modifier = Modifier.padding(bottom = 2.dp)
) { ) {
// 🔥 TELEGRAM: Время 11sp, italic style
Text( Text(
text = timeFormat.format(message.timestamp), text = timeFormat.format(message.timestamp),
color = timeColor, color = timeColor,
fontSize = 11.sp fontSize = 11.sp,
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
) )
if (message.isOutgoing) { if (message.isOutgoing) {
AnimatedMessageStatus( AnimatedMessageStatus(
@@ -1781,7 +1807,7 @@ private fun AnimatedMessageStatus(
} }
/** /**
* 🔥 Reply bubble (цитата) внутри сообщения - как в React Native * 🔥 Reply bubble (цитата) внутри сообщения
* Стиль: вертикальная линия слева + имя + текст * Стиль: вертикальная линия слева + имя + текст
*/ */
@Composable @Composable
@@ -1790,7 +1816,7 @@ private fun ReplyBubble(
isOutgoing: Boolean, isOutgoing: Boolean,
isDarkTheme: Boolean isDarkTheme: Boolean
) { ) {
// Цвета как в React Native // НАШИ ОРИГИНАЛЬНЫЕ ЦВЕТА
val backgroundColor = if (isOutgoing) { val backgroundColor = if (isOutgoing) {
Color.Black.copy(alpha = 0.15f) Color.Black.copy(alpha = 0.15f)
} else { } else {
@@ -1809,7 +1835,7 @@ private fun ReplyBubble(
PrimaryBlue PrimaryBlue
} }
val textColor = if (isOutgoing) { val replyTextColor = if (isOutgoing) {
Color.White.copy(alpha = 0.85f) Color.White.copy(alpha = 0.85f)
} else { } else {
if (isDarkTheme) Color.White else Color.Black if (isDarkTheme) Color.White else Color.Black
@@ -1817,12 +1843,12 @@ private fun ReplyBubble(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .wrapContentWidth()
.height(IntrinsicSize.Min) .height(IntrinsicSize.Min)
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp))
.background(backgroundColor) .background(backgroundColor)
) { ) {
// Вертикальная линия слева (как borderLeft в React Native) // 🔥 TELEGRAM: Вертикальная линия слева 3dp
Box( Box(
modifier = Modifier modifier = Modifier
.width(3.dp) .width(3.dp)
@@ -1833,22 +1859,24 @@ private fun ReplyBubble(
// Контент // Контент
Column( Column(
modifier = Modifier modifier = Modifier
.padding(start = 10.dp, end = 10.dp, top = 6.dp, bottom = 6.dp) // 🔥 TELEGRAM: padding как в дизайне
.padding(start = 8.dp, end = 10.dp, top = 4.dp, bottom = 4.dp)
.widthIn(max = 220.dp)
) { ) {
// Имя отправителя цитируемого сообщения // 🔥 TELEGRAM: Имя 14sp, Medium weight
Text( Text(
text = replyData.senderName, text = replyData.senderName,
color = nameColor, color = nameColor,
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.Medium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
// Текст цитируемого сообщения // 🔥 TELEGRAM: Текст цитаты 14sp, Regular
Text( Text(
text = replyData.text.ifEmpty { "..." }, text = replyData.text.ifEmpty { "..." },
color = textColor, color = replyTextColor,
fontSize = 14.sp, fontSize = 14.sp,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
@@ -2007,6 +2035,7 @@ private fun MessageInputBar(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.graphicsLayer { clip = false }
.windowInsetsPadding(WindowInsets.ime.only(WindowInsetsSides.Bottom)) .windowInsetsPadding(WindowInsets.ime.only(WindowInsetsSides.Bottom))
) { ) {
// Если пользователь заблокирован - показываем BlockedChatFooter (плоский как инпут) // Если пользователь заблокирован - показываем BlockedChatFooter (плоский как инпут)
@@ -2053,6 +2082,7 @@ private fun MessageInputBar(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.graphicsLayer { clip = false }
) { ) {
// Верхний border (как в архиве) // Верхний border (как в архиве)
Box( Box(
@@ -2240,8 +2270,8 @@ private fun MessageInputBar(
// Мгновенно когда клавиатура открывается // Мгновенно когда клавиатура открывается
snap() snap()
} else { } else {
// Быстрая анимация для мгновенного отклика (как в Telegram) // Быстрая анимация как обычная клавиатура (200ms)
tween(durationMillis = 150, easing = TelegramEasing) tween(durationMillis = 200, easing = FastOutSlowInEasing)
}, },
label = "EmojiPanelHeight" label = "EmojiPanelHeight"
) )
@@ -2250,10 +2280,9 @@ private fun MessageInputBar(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(animatedHeight) .height(animatedHeight)
.clipToBounds()
) { ) {
// 🚀 Рендерим панель только когда нужно // 🚀 Рендерим панель только когда нужно
if (showEmojiPicker && !isKeyboardVisible && animatedHeight > 0.dp) { if (showEmojiPicker && !isKeyboardVisible) {
AppleEmojiPickerPanel( AppleEmojiPickerPanel(
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
onEmojiSelected = { emoji -> onEmojiSelected = { emoji ->

View File

@@ -36,6 +36,7 @@ import coil.request.ImageRequest
import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
/** /**
@@ -505,14 +506,8 @@ fun EmojiButton(
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState() val isPressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState( // 🚀 Убираем анимацию scale для производительности
targetValue = if (isPressed) 0.85f else 1f, val scale = if (isPressed) 0.85f else 1f
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessHigh
),
label = "emojiScale"
)
val imageRequest = remember(unified) { val imageRequest = remember(unified) {
ImageRequest.Builder(context) ImageRequest.Builder(context)
@@ -613,12 +608,16 @@ fun AppleEmojiPickerPanel(
var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) } var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) }
val gridState = rememberLazyGridState() val gridState = rememberLazyGridState()
// Загружаем эмодзи если еще не загружены (синхронно из кеша если уже загружено) // 🚀 Предзагружаем эмодзи СИНХРОННО при создании компонента
LaunchedEffect(Unit) { val emojisReady = remember {
if (!EmojiCache.isLoaded) { if (!EmojiCache.isLoaded) {
// Загружаем синхронно для мгновенного отображения
runBlocking {
EmojiCache.loadEmojis(context) EmojiCache.loadEmojis(context)
} }
} }
true
}
// Текущие эмодзи для выбранной категории - используем derivedStateOf для оптимизации // Текущие эмодзи для выбранной категории - используем derivedStateOf для оптимизации
val currentEmojis by remember { val currentEmojis by remember {
@@ -631,7 +630,7 @@ fun AppleEmojiPickerPanel(
} }
} }
// Сбрасываем скролл при смене категории // 🚀 Убираем анимацию скролла для мгновенного переключения категорий
LaunchedEffect(selectedCategory) { LaunchedEffect(selectedCategory) {
gridState.scrollToItem(0) gridState.scrollToItem(0)
} }
@@ -700,6 +699,7 @@ fun AppleEmojiPickerPanel(
) )
} }
} else { } else {
// 🚀 Оптимизированная LazyVerticalGrid для быстрого рендеринга
LazyVerticalGrid( LazyVerticalGrid(
state = gridState, state = gridState,
columns = GridCells.Fixed(8), columns = GridCells.Fixed(8),