feat: Enhance search functionality and user experience

- Added local account metadata handling in SearchScreen for improved "Saved Messages" search fallback.
- Updated search logic to include username and account name checks when searching for the user.
- Introduced search logging in SearchUsersViewModel for better debugging and tracking of search queries.
- Refactored image download process in AttachmentComponents to include detailed logging for debugging.
- Created AttachmentDownloadDebugLogger to manage and display download logs.
- Improved DeviceVerificationBanner UI for better user engagement during device verification.
- Adjusted OtherProfileScreen layout to enhance information visibility and user interaction.
- Updated network security configuration to include new Let's Encrypt certificate for CDN.
This commit is contained in:
2026-02-19 17:34:16 +05:00
parent cacd6dc029
commit 53d0e44ef8
26 changed files with 972 additions and 613 deletions

View File

@@ -0,0 +1,28 @@
package com.rosetta.messenger.data
private const val PLACEHOLDER_ACCOUNT_NAME = "Account"
fun resolveAccountDisplayName(publicKey: String, name: String?, username: String?): String {
val normalizedName = name?.trim().orEmpty()
if (normalizedName.isNotEmpty() && !normalizedName.equals(PLACEHOLDER_ACCOUNT_NAME, ignoreCase = true)) {
return normalizedName
}
val normalizedUsername = username?.trim().orEmpty()
if (normalizedUsername.isNotEmpty()) {
return normalizedUsername
}
if (publicKey.isBlank()) {
return PLACEHOLDER_ACCOUNT_NAME
}
return if (publicKey.length > 12) {
"${publicKey.take(6)}...${publicKey.takeLast(4)}"
} else {
publicKey
}
}
fun isPlaceholderAccountName(name: String?): Boolean =
name?.trim().orEmpty().equals(PLACEHOLDER_ACCOUNT_NAME, ignoreCase = true)

View File

@@ -612,6 +612,13 @@ class MessageRepository private constructor(private val context: Context) {
// 🔒 Шифруем plainMessage с использованием приватного ключа
val encryptedPlainMessage = CryptoManager.encryptWithPassword(plainText, privateKey)
val storedChachaKey =
if (isOwnMessage && packet.aesChachaKey.isNotBlank()) {
"sync:${packet.aesChachaKey}"
} else {
packet.chachaKey
}
// Создаем entity для кэша и возможной вставки
val entity =
MessageEntity(
@@ -620,7 +627,7 @@ class MessageRepository private constructor(private val context: Context) {
toPublicKey = packet.toPublicKey,
content = packet.content,
timestamp = packet.timestamp,
chachaKey = packet.chachaKey,
chachaKey = storedChachaKey,
read = 0,
fromMe = if (isOwnMessage) 1 else 0,
delivered = DeliveryStatus.DELIVERED.value,
@@ -711,27 +718,37 @@ class MessageRepository private constructor(private val context: Context) {
*/
suspend fun handleRead(packet: PacketRead) {
val account = currentAccount ?: return
val fromPublicKey = packet.fromPublicKey.trim()
val toPublicKey = packet.toPublicKey.trim()
if (fromPublicKey.isBlank() || toPublicKey.isBlank()) return
MessageLogger.debug("👁 READ PACKET from: ${packet.fromPublicKey.take(16)}...")
MessageLogger.debug("👁 READ PACKET from=${fromPublicKey.take(16)}..., to=${toPublicKey.take(16)}...")
// Синхронизация read может прийти как:
// 1) from=opponent, to=account (обычный read от собеседника)
// 2) from=account, to=opponent (read c другого устройства этого же аккаунта)
val opponentKey =
if (packet.fromPublicKey == account) packet.toPublicKey else packet.fromPublicKey
// Desktop parity:
// 1) from=opponent, to=account -> собеседник прочитал НАШИ сообщения (double check)
// 2) from=account, to=opponent -> sync с другого нашего устройства (мы прочитали входящие)
val isOwnReadSync = fromPublicKey == account
val opponentKey = if (isOwnReadSync) toPublicKey else fromPublicKey
if (opponentKey.isBlank()) return
// Проверяем последнее сообщение ДО обновления
val lastMsgBefore = messageDao.getLastMessageDebug(account, opponentKey)
val dialogKey = getDialogKey(opponentKey)
// Отмечаем все наши исходящие сообщения к этому собеседнику как прочитанные
if (isOwnReadSync) {
// Sync read from another own device: mark incoming messages as read.
messageDao.markDialogAsRead(account, dialogKey)
messageCache[dialogKey]?.let { flow ->
flow.value =
flow.value.map { msg ->
if (!msg.isFromMe && !msg.isRead) msg.copy(isRead = true) else msg
}
}
dialogDao.updateDialogFromMessages(account, opponentKey)
return
}
// Opponent read our outgoing messages.
messageDao.markAllAsRead(account, opponentKey)
// 🔥 DEBUG: Проверяем последнее сообщение ПОСЛЕ обновления
val lastMsgAfter = messageDao.getLastMessageDebug(account, opponentKey)
// Обновляем кэш - все исходящие сообщения помечаем как прочитанные
val dialogKey = getDialogKey(opponentKey)
val readCount = messageCache[dialogKey]?.value?.count { it.isFromMe && !it.isRead } ?: 0
messageCache[dialogKey]?.let { flow ->
flow.value =
@@ -740,17 +757,11 @@ class MessageRepository private constructor(private val context: Context) {
}
}
// 🔔 Уведомляем UI о прочтении (пустой messageId = все исходящие сообщения)
// Notify current dialog UI: all outgoing messages are now read.
_deliveryStatusEvents.tryEmit(DeliveryStatusUpdate(dialogKey, "", DeliveryStatus.READ))
// 📝 LOG: Статус прочтения
MessageLogger.logReadStatus(fromPublicKey = opponentKey, messagesCount = readCount)
// 🔥 КРИТИЧНО: Обновляем диалог чтобы lastMessageRead обновился
dialogDao.updateDialogFromMessages(account, opponentKey)
// Логируем что записалось в диалог
val dialog = dialogDao.getDialog(account, opponentKey)
}
/**