diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0d55d44..675523e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown" // ═══════════════════════════════════════════════════════════ // Rosetta versioning — bump here on each release // ═══════════════════════════════════════════════════════════ -val rosettaVersionName = "1.0.9" -val rosettaVersionCode = 9 // Increment on each release +val rosettaVersionName = "1.0.10" +val rosettaVersionCode = 10 // Increment on each release android { namespace = "com.rosetta.messenger" diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 1037fc4..a4a51cf 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -539,6 +539,7 @@ fun MainScreen( // Username state - загружается из EncryptedAccount // Following desktop version pattern: username is stored locally and loaded on app start var accountUsername by remember { mutableStateOf("") } + var accountVerified by remember(accountPublicKey) { mutableIntStateOf(0) } var reloadTrigger by remember { mutableIntStateOf(0) } // Load username AND name from AccountManager (persisted in DataStore) @@ -549,12 +550,15 @@ fun MainScreen( val encryptedAccount = accountManager.getAccount(accountPublicKey) val username = encryptedAccount?.username accountUsername = username.orEmpty() + accountVerified = ProtocolManager.getCachedUserInfo(accountPublicKey)?.verified ?: 0 accountName = resolveAccountDisplayName( accountPublicKey, encryptedAccount?.name ?: accountName, username ) + } else { + accountVerified = 0 } } @@ -569,6 +573,7 @@ fun MainScreen( val encryptedAccount = accountManager.getAccount(accountPublicKey) val username = encryptedAccount?.username accountUsername = username.orEmpty() + accountVerified = ProtocolManager.getCachedUserInfo(accountPublicKey)?.verified ?: 0 accountName = resolveAccountDisplayName( accountPublicKey, @@ -682,6 +687,7 @@ fun MainScreen( isDarkTheme = isDarkTheme, accountName = accountName, accountUsername = accountUsername, + accountVerified = accountVerified, accountPhone = accountPhone, accountPublicKey = accountPublicKey, accountPrivateKey = accountPrivateKey, @@ -776,6 +782,7 @@ fun MainScreen( isDarkTheme = isDarkTheme, accountName = accountName, accountUsername = accountUsername, + accountVerified = accountVerified, accountPublicKey = accountPublicKey, accountPrivateKeyHash = privateKeyHash, onBack = { popProfileAndChildren() }, diff --git a/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt b/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt index ec5a0cf..eb4980a 100644 --- a/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt +++ b/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt @@ -17,27 +17,15 @@ object ReleaseNotes { val RELEASE_NOTICE = """ Update v$VERSION_PLACEHOLDER - Стабильность и производительность - - Фильтрация неподдерживаемых пакетов (групповые чаты) — исключение крэшей при обработке - - Таймаут очереди входящих сообщений (20 сек) — защита от зависания синхронизации - - Повторный запрос синхронизации при таймауте без потери сообщений - - Отключено накопление отладочных логов в памяти — снижение расхода RAM + Верификация аккаунта + - Бейдж верификации отображается в боковом меню рядом с именем + - Бейдж верификации отображается в экране профиля + - Статус загружается из кэша пользователей при старте - Индикаторы прочтения - - Исправлена логика отображения статуса «прочитано» в чате - - Добавлена повторная отправка read receipt при сбое - - Автоматическая отправка read receipt при обновлении сообщений из БД - - Верификация - - Бейдж верификации корректно сохраняется при обновлении имени собеседника - - Статус верификации передаётся при открытии диалога - - FCM Push-уведомления - - Дедупликация подписки FCM-токена — устранены повторные регистрации - - Автоматическая отписка старого токена перед регистрацией нового - - Интерфейс - - Убран отладочный интерфейс (Debug Logs) из бокового меню и экрана чата + Стабильность + - Фильтрация неподдерживаемых пакетов (группы, conversations) + - Добавлена фильтрация delivery-пакетов для неподдерживаемых диалогов + - Улучшена обработка ошибок в очереди входящих пакетов """.trimIndent() fun getNotice(version: String): String = diff --git a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt index 74e97c8..127b191 100644 --- a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt @@ -21,7 +21,6 @@ import kotlin.coroutines.resume */ object ProtocolManager { private const val TAG = "ProtocolManager" - private const val INBOUND_QUEUE_WAIT_TIMEOUT_MS = 20_000L // Server address - same as React Native version private const val SERVER_ADDRESS = "ws://46.28.71.12:3000" @@ -206,6 +205,13 @@ object ProtocolManager { requireResyncAfterAccountInit("⏳ Delivery status before account init, scheduling re-sync") return@launchInboundPacketTask } + if (isUnsupportedDialogKey(deliveryPacket.toPublicKey)) { + android.util.Log.w( + TAG, + "Skipping unsupported delivery packet (conversation/group): to=${deliveryPacket.toPublicKey.take(24)}" + ) + return@launchInboundPacketTask + } repository.handleDelivery(deliveryPacket) if (!syncBatchInProgress) { repository.updateLastSyncTimestamp(System.currentTimeMillis()) @@ -375,8 +381,8 @@ object ProtocolManager { for (task in inboundTaskChannel) { try { task() - } catch (e: Exception) { - android.util.Log.e(TAG, "Inbound packet task error", e) + } catch (t: Throwable) { + android.util.Log.e(TAG, "Dialog queue error", t) } } } @@ -405,31 +411,29 @@ object ProtocolManager { * Since the queue is strictly FIFO, when the sentinel runs, all previously * submitted tasks are guaranteed to have completed. */ - private suspend fun whenInboundTasksFinish(timeoutMs: Long = INBOUND_QUEUE_WAIT_TIMEOUT_MS): Boolean { + private suspend fun whenInboundTasksFinish(): Boolean { val done = CompletableDeferred() if (!launchInboundPacketTask { done.complete(Unit) }) { return false } - return try { - withTimeout(timeoutMs) { done.await() } - true - } catch (_: TimeoutCancellationException) { - false - } + done.await() + return true } - private fun isLikelyUserPublicKey(value: String): Boolean { - val normalized = value.removePrefix("0x") - if (normalized.length != 64 && normalized.length != 66 && normalized.length != 128 && normalized.length != 130) { - return false - } - return normalized.all { it.isDigit() || it in 'a'..'f' || it in 'A'..'F' } + private fun isUnsupportedDialogKey(value: String): Boolean { + val normalized = value.trim().lowercase(Locale.ROOT) + if (normalized.isBlank()) return true + return normalized.startsWith("#") || + normalized.startsWith("group:") || + normalized.startsWith("conversation:") } private fun isSupportedDirectPeerKey(peerKey: String, ownKey: String): Boolean { - return peerKey == ownKey || - MessageRepository.isSystemAccount(peerKey) || - isLikelyUserPublicKey(peerKey) + val normalized = peerKey.trim() + if (normalized.isBlank()) return false + if (normalized == ownKey) return true + if (MessageRepository.isSystemAccount(normalized)) return true + return !isUnsupportedDialogKey(normalized) } private fun isSupportedDirectMessagePacket(packet: PacketMessage, ownKey: String): Boolean { @@ -568,7 +572,7 @@ object ProtocolManager { if (!tasksFinished) { android.util.Log.w( TAG, - "SYNC BATCH_END: inbound queue did not drain in time, requesting re-sync without advancing cursor" + "SYNC BATCH_END: queue unavailable, requesting re-sync without advancing cursor" ) val fallbackTimestamp = try { messageRepository?.getLastSyncTimestamp() ?: packet.timestamp diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index d6ea31b..1cc75e2 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -212,6 +212,7 @@ fun ChatsListScreen( isDarkTheme: Boolean, accountName: String, accountUsername: String, + accountVerified: Int = 0, accountPhone: String, accountPublicKey: String, accountPrivateKey: String = "", @@ -864,7 +865,7 @@ fun ChatsListScreen( fontWeight = FontWeight.Bold, color = Color.White ) - if (isRosettaOfficial) { + if (accountVerified > 0 || isRosettaOfficial) { Spacer( modifier = Modifier.width( @@ -872,7 +873,7 @@ fun ChatsListScreen( ) ) VerifiedBadge( - verified = 1, + verified = if (accountVerified > 0) accountVerified else 1, size = 15, badgeTint = Color(0xFFACD2F9) ) @@ -1148,6 +1149,21 @@ fun ChatsListScreen( } ) + // 🔄 Sync logs + DrawerMenuItemEnhanced( + painter = painterResource(id = R.drawable.files_document), + text = "Sync Logs", + iconColor = menuIconColor, + textColor = menuTextColor, + onClick = { + scope.launch { + drawerState.close() + kotlinx.coroutines.delay(100) + showSduLogs = true + } + } + ) + } // ═══════════════════════════════════════════════════════════ diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt index d2d1eae..8b9af63 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt @@ -270,6 +270,7 @@ fun ProfileScreen( isDarkTheme: Boolean, accountName: String, accountUsername: String, + accountVerified: Int = 0, accountPublicKey: String, accountPrivateKeyHash: String, onBack: () -> Unit, @@ -921,6 +922,7 @@ fun ProfileScreen( CollapsingProfileHeader( name = editedName.ifBlank { accountPublicKey.take(10) }, username = editedUsername, + verified = accountVerified, publicKey = accountPublicKey, avatarColors = avatarColors, collapseProgress = collapseProgress, @@ -1069,6 +1071,7 @@ fun ProfileScreen( private fun CollapsingProfileHeader( name: String, username: String, + verified: Int = 0, publicKey: String, avatarColors: AvatarColors, collapseProgress: Float, @@ -1397,10 +1400,10 @@ private fun CollapsingProfileHeader( modifier = Modifier.widthIn(max = 220.dp), textAlign = TextAlign.Center ) - if (isRosettaOfficial) { + if (verified > 0 || isRosettaOfficial) { Spacer(modifier = Modifier.width(4.dp)) VerifiedBadge( - verified = 2, + verified = if (verified > 0) verified else 2, size = (nameFontSize.value * 0.8f).toInt(), isDarkTheme = isDarkTheme )