feat: Optimize emoji picker performance by eliminating chunk loading, reducing animations, and improving emoji caching

This commit is contained in:
k1ngsterr1
2026-01-15 01:24:33 +05:00
parent 05fc6f61b7
commit 65094125f6
5 changed files with 300 additions and 102 deletions

View File

@@ -186,24 +186,28 @@ class MessageRepository private constructor(private val context: Context) {
// 🔒 Шифруем plainMessage с использованием приватного ключа
val encryptedPlainMessage = CryptoManager.encryptWithPassword(text.trim(), privateKey)
// Сохраняем в БД
val entity = MessageEntity(
account = account,
fromPublicKey = account,
toPublicKey = toPublicKey,
content = encryptedContent,
timestamp = timestamp,
chachaKey = encryptedKey,
read = if (account == toPublicKey) 1 else 0,
fromMe = 1,
delivered = DeliveryStatus.WAITING.value,
messageId = messageId,
plainMessage = encryptedPlainMessage, // 🔒 Зашифрованный текст
attachments = attachmentsJson,
replyToMessageId = replyToMessageId,
dialogKey = dialogKey
)
messageDao.insertMessage(entity)
// ✅ Проверяем существование - не дублируем сообщения
val exists = messageDao.messageExists(account, messageId)
if (!exists) {
// Сохраняем в БД только если сообщения нет
val entity = MessageEntity(
account = account,
fromPublicKey = account,
toPublicKey = toPublicKey,
content = encryptedContent,
timestamp = timestamp,
chachaKey = encryptedKey,
read = if (account == toPublicKey) 1 else 0,
fromMe = 1,
delivered = DeliveryStatus.WAITING.value,
messageId = messageId,
plainMessage = encryptedPlainMessage, // 🔒 Зашифрованный текст
attachments = attachmentsJson,
replyToMessageId = replyToMessageId,
dialogKey = dialogKey
)
messageDao.insertMessage(entity)
}
// Обновляем диалог
updateDialog(toPublicKey, text.trim(), timestamp)
@@ -283,7 +287,7 @@ class MessageRepository private constructor(private val context: Context) {
// 🔒 Шифруем plainMessage с использованием приватного ключа
val encryptedPlainMessage = CryptoManager.encryptWithPassword(plainText, privateKey)
// Сохраняем в БД
// Создаем entity для кэша и возможной вставки
val entity = MessageEntity(
account = account,
fromPublicKey = packet.fromPublicKey,
@@ -299,7 +303,13 @@ class MessageRepository private constructor(private val context: Context) {
attachments = attachmentsJson,
dialogKey = dialogKey
)
messageDao.insertMessage(entity)
// ✅ Проверяем существование перед вставкой (защита от дубликатов)
val stillExists = messageDao.messageExists(account, messageId)
if (!stillExists) {
// Сохраняем в БД только если сообщения нет
messageDao.insertMessage(entity)
}
// Обновляем диалог
updateDialog(packet.fromPublicKey, plainText, packet.timestamp, incrementUnread = true)
@@ -307,9 +317,11 @@ class MessageRepository private constructor(private val context: Context) {
// 🔥 Запрашиваем информацию о пользователе для отображения имени вместо ключа
requestUserInfo(packet.fromPublicKey)
// Обновляем кэш
val message = entity.toMessage()
updateMessageCache(dialogKey, message)
// Обновляем кэш только если сообщение новое
if (!stillExists) {
val message = entity.toMessage()
updateMessageCache(dialogKey, message)
}
} catch (e: Exception) {
e.printStackTrace()

View File

@@ -262,13 +262,8 @@ fun ChatDetailScreen(
// 🔥 Флаг видимости панели эмодзи (тот же что в MessageInputBar) - единый источник правды
val isEmojiPanelVisible = showEmojiPicker && !isKeyboardVisible
// 🔥 Анимированный отступ для списка сообщений когда emoji picker открыт
// Используем isEmojiPanelVisible для синхронизации с анимацией панели
val emojiPanelPadding by animateDpAsState(
targetValue = if (isEmojiPanelVisible) emojiPanelHeight else 0.dp,
animationSpec = tween(100, easing = FastOutSlowInEasing), // 100ms как exit анимация панели
label = "emojiPanelPadding"
)
// <EFBFBD> Простой отступ без анимации - AnimatedVisibility сама анимирует
val emojiPanelPadding = if (isEmojiPanelVisible) emojiPanelHeight else 0.dp
// 🔥 Reply/Forward state
val replyMessages by viewModel.replyMessages.collectAsState()
@@ -2322,11 +2317,11 @@ private fun MessageInputBar(
visible = showPanel,
enter = slideInVertically(
initialOffsetY = { it }, // Снизу вверх
animationSpec = tween(100, easing = FastOutSlowInEasing)
animationSpec = tween(150, easing = FastOutSlowInEasing) // 🚀 Быстрее и плавнее
),
exit = slideOutVertically(
targetOffsetY = { it }, // Сверху вниз
animationSpec = tween(100, easing = FastOutSlowInEasing) // 🔥 Синхронизировано с padding анимацией
animationSpec = tween(150, easing = FastOutSlowInEasing)
)
) {
AppleEmojiPickerPanel(

View File

@@ -280,32 +280,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
_messages.value = _messages.value + message
}
// 🔥 Сохраняем в БД здесь (в ChatViewModel)
// ProtocolManager.setupPacketHandlers() не вызывается, поэтому сохраняем сами
// Используем fromPublicKey как opponent для корректного dialogKey
// НЕ сохраняем в БД здесь - это делает MessageRepository.handleIncomingMessage()!
// Убираем дублирование: одно сообщение не должно сохраняться дважды
// 🔥 Обновляем диалог - используем fromPublicKey
val senderKey = packet.fromPublicKey
// 🔥 FIX: Если messageId пустой - генерируем новый UUID
val finalMessageId = if (packet.messageId.isNullOrEmpty()) {
UUID.randomUUID().toString().replace("-", "").take(32).also {
}
} else {
packet.messageId
}
saveMessageToDatabase(
messageId = finalMessageId,
text = decryptedText,
encryptedContent = packet.content,
encryptedKey = packet.chachaKey,
timestamp = packet.timestamp,
isFromMe = false, // Это входящее сообщение
delivered = DeliveryStatus.DELIVERED.value,
attachmentsJson = attachmentsJson,
opponentPublicKey = senderKey
)
// 🔥 Обновляем диалог - используем senderKey
updateDialog(senderKey, decryptedText, packet.timestamp, incrementUnread = !isDialogActive)
// 👁️ НЕ отправляем read receipt автоматически!
@@ -1059,8 +1038,12 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
// 🔒 Шифруем plainMessage с использованием приватного ключа
val encryptedPlainMessage = CryptoManager.encryptWithPassword(text, privateKey)
// Проверяем существует ли сообщение
// Проверяем существует ли сообщение - ИСПОЛЬЗУЕМ результат!
val exists = messageDao.messageExists(account, finalMessageId)
if (exists) {
// Сообщение уже есть в БД - не дублируем
return
}
val entity = MessageEntity(
account = account,

View File

@@ -432,41 +432,35 @@ object EmojiCache {
}
private fun groupEmojis(allEmojis: List<String>): Map<String, List<String>> {
val result = mutableMapOf<String, MutableList<String>>()
val usedEmojis = mutableSetOf<String>()
val emojiToCategory = mutableMapOf<String, EmojiCategory>()
// 🚀 Оптимизированная группировка - один проход вместо двух
val result = EMOJI_CATEGORIES.associate { it.key to mutableListOf<String>() }
val symbolsCategory = EMOJI_CATEGORIES.find { it.key == "Symbols" }!!
EMOJI_CATEGORIES.forEach { category ->
result[category.key] = mutableListOf()
}
// Сначала определяем категорию для каждого emoji
// Один проход по всем emoji - распределяем по категориям
for (emoji in allEmojis) {
var assigned = false
for (category in EMOJI_CATEGORIES) {
if (emojiMatchesCategory(emoji, category) && emoji !in usedEmojis) {
if (emojiMatchesCategory(emoji, category)) {
result[category.key]?.add(emoji)
usedEmojis.add(emoji)
emojiToCategory[emoji] = category
assigned = true
break
}
}
}
// Нераспределенные emoji идут в Symbols
for (emoji in allEmojis) {
if (emoji !in usedEmojis) {
// Нераспределенные идут в Symbols
if (!assigned) {
result["Symbols"]?.add(emoji)
emojiToCategory[emoji] = EMOJI_CATEGORIES.find { it.key == "Symbols" }!!
}
}
// Сортируем каждую категорию согласно Unicode порядку
for ((key, emojis) in result) {
// 🚀 Сортируем каждую категорию один раз
return result.mapValues { (key, emojis) ->
val category = EMOJI_CATEGORIES.find { it.key == key }
if (category != null) {
emojis.sortWith { a, b ->
if (category != null && emojis.size > 1) {
emojis.sortedWith { a, b ->
getEmojiSortIndex(a, category).compareTo(getEmojiSortIndex(b, category))
}
} else {
emojis
}
}
@@ -552,7 +546,7 @@ fun CategoryButton(
isDarkTheme: Boolean = true,
modifier: Modifier = Modifier
) {
// 🚀 Убрали анимацию scale для производительности
// 🚀 Без анимаций для максимальной производительности
val backgroundColor = if (isSelected) PrimaryBlue.copy(alpha = 0.2f) else Color.Transparent
val iconTint = if (isSelected) PrimaryBlue
else if (isDarkTheme) Color.White.copy(alpha = 0.6f)
@@ -563,7 +557,11 @@ fun CategoryButton(
.size(40.dp)
.clip(CircleShape)
.background(backgroundColor)
.clickable(onClick = onClick),
.clickable(
onClick = onClick,
indication = null, // 🚀 Убираем ripple для производительности
interactionSource = remember { MutableInteractionSource() }
),
contentAlignment = Alignment.Center
) {
Icon(
@@ -596,11 +594,9 @@ fun AppleEmojiPickerPanel(
}
}
// 🚀 Chunk loading: показываем эмодзи порциями для плавности
var loadedCount by remember { mutableStateOf(40) } // Начинаем с 40 (5 рядов)
// Текущие эмодзи для выбранной категории
val allEmojis = remember(selectedCategory, EmojiCache.isLoaded) {
// 🚀 Показываем все эмодзи сразу - LazyGrid сам виртуализирует рендеринг!
// Никаких chunk loading - это только добавляет recomposition
val displayedEmojis = remember(selectedCategory, EmojiCache.isLoaded) {
if (EmojiCache.isLoaded) {
EmojiCache.getEmojisForCategory(selectedCategory.key)
} else {
@@ -608,20 +604,9 @@ fun AppleEmojiPickerPanel(
}
}
// 🚀 При смене категории сбрасываем чанки и постепенно догружаем
// 🚀 При смене категории просто скроллим наверх
LaunchedEffect(selectedCategory) {
loadedCount = 40 // Сразу показываем 40 эмодзи
gridState.scrollToItem(0)
// Догружаем остальные чанками
while (loadedCount < allEmojis.size) {
delay(32) // 2 фрейма
loadedCount = minOf(loadedCount + 24, allEmojis.size)
}
}
// Отображаемые эмодзи (с chunk loading)
val displayedEmojis = remember(allEmojis, loadedCount) {
allEmojis.take(loadedCount)
}
val panelBackground = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7)
@@ -688,16 +673,17 @@ fun AppleEmojiPickerPanel(
)
}
} else {
// 🚀 Оптимизированная LazyVerticalGrid с chunk loading
// 🚀 Максимально оптимизированная LazyVerticalGrid
LazyVerticalGrid(
state = gridState,
columns = GridCells.Fixed(8),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
horizontalArrangement = Arrangement.spacedBy(1.dp),
verticalArrangement = Arrangement.spacedBy(1.dp),
contentPadding = PaddingValues(start = 12.dp, end = 12.dp, top = 4.dp, bottom = 4.dp)
// 🚀 Убираем spacing для производительности
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
// 🚀 Увеличиваем prefetch для плавной прокрутки
userScrollEnabled = true
) {
items(
items = displayedEmojis,