feat: Add account verification status handling in MainScreen, ChatsListScreen, and ProfileScreen

This commit is contained in:
2026-02-26 20:32:05 +05:00
parent 388d279ea9
commit 829e19364a
6 changed files with 64 additions and 46 deletions

View File

@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
// Rosetta versioning — bump here on each release // Rosetta versioning — bump here on each release
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
val rosettaVersionName = "1.0.9" val rosettaVersionName = "1.0.10"
val rosettaVersionCode = 9 // Increment on each release val rosettaVersionCode = 10 // Increment on each release
android { android {
namespace = "com.rosetta.messenger" namespace = "com.rosetta.messenger"

View File

@@ -539,6 +539,7 @@ fun MainScreen(
// Username state - загружается из EncryptedAccount // Username state - загружается из EncryptedAccount
// Following desktop version pattern: username is stored locally and loaded on app start // Following desktop version pattern: username is stored locally and loaded on app start
var accountUsername by remember { mutableStateOf("") } var accountUsername by remember { mutableStateOf("") }
var accountVerified by remember(accountPublicKey) { mutableIntStateOf(0) }
var reloadTrigger by remember { mutableIntStateOf(0) } var reloadTrigger by remember { mutableIntStateOf(0) }
// Load username AND name from AccountManager (persisted in DataStore) // Load username AND name from AccountManager (persisted in DataStore)
@@ -549,12 +550,15 @@ fun MainScreen(
val encryptedAccount = accountManager.getAccount(accountPublicKey) val encryptedAccount = accountManager.getAccount(accountPublicKey)
val username = encryptedAccount?.username val username = encryptedAccount?.username
accountUsername = username.orEmpty() accountUsername = username.orEmpty()
accountVerified = ProtocolManager.getCachedUserInfo(accountPublicKey)?.verified ?: 0
accountName = accountName =
resolveAccountDisplayName( resolveAccountDisplayName(
accountPublicKey, accountPublicKey,
encryptedAccount?.name ?: accountName, encryptedAccount?.name ?: accountName,
username username
) )
} else {
accountVerified = 0
} }
} }
@@ -569,6 +573,7 @@ fun MainScreen(
val encryptedAccount = accountManager.getAccount(accountPublicKey) val encryptedAccount = accountManager.getAccount(accountPublicKey)
val username = encryptedAccount?.username val username = encryptedAccount?.username
accountUsername = username.orEmpty() accountUsername = username.orEmpty()
accountVerified = ProtocolManager.getCachedUserInfo(accountPublicKey)?.verified ?: 0
accountName = accountName =
resolveAccountDisplayName( resolveAccountDisplayName(
accountPublicKey, accountPublicKey,
@@ -682,6 +687,7 @@ fun MainScreen(
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
accountName = accountName, accountName = accountName,
accountUsername = accountUsername, accountUsername = accountUsername,
accountVerified = accountVerified,
accountPhone = accountPhone, accountPhone = accountPhone,
accountPublicKey = accountPublicKey, accountPublicKey = accountPublicKey,
accountPrivateKey = accountPrivateKey, accountPrivateKey = accountPrivateKey,
@@ -776,6 +782,7 @@ fun MainScreen(
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
accountName = accountName, accountName = accountName,
accountUsername = accountUsername, accountUsername = accountUsername,
accountVerified = accountVerified,
accountPublicKey = accountPublicKey, accountPublicKey = accountPublicKey,
accountPrivateKeyHash = privateKeyHash, accountPrivateKeyHash = privateKeyHash,
onBack = { popProfileAndChildren() }, onBack = { popProfileAndChildren() },

View File

@@ -17,27 +17,15 @@ object ReleaseNotes {
val RELEASE_NOTICE = """ val RELEASE_NOTICE = """
Update v$VERSION_PLACEHOLDER Update v$VERSION_PLACEHOLDER
Стабильность и производительность Верификация аккаунта
- Фильтрация неподдерживаемых пакетов (групповые чаты) — исключение крэшей при обработке - Бейдж верификации отображается в боковом меню рядом с именем
- Таймаут очереди входящих сообщений (20 сек) — защита от зависания синхронизации - Бейдж верификации отображается в экране профиля
- Повторный запрос синхронизации при таймауте без потери сообщений - Статус загружается из кэша пользователей при старте
- Отключено накопление отладочных логов в памяти — снижение расхода RAM
Индикаторы прочтения Стабильность
- Исправлена логика отображения статуса «прочитано» в чате - Фильтрация неподдерживаемых пакетов (группы, conversations)
- Добавлена повторная отправка read receipt при сбое - Добавлена фильтрация delivery-пакетов для неподдерживаемых диалогов
- Автоматическая отправка read receipt при обновлении сообщений из БД - Улучшена обработка ошибок в очереди входящих пакетов
Верификация
- Бейдж верификации корректно сохраняется при обновлении имени собеседника
- Статус верификации передаётся при открытии диалога
FCM Push-уведомления
- Дедупликация подписки FCM-токена — устранены повторные регистрации
- Автоматическая отписка старого токена перед регистрацией нового
Интерфейс
- Убран отладочный интерфейс (Debug Logs) из бокового меню и экрана чата
""".trimIndent() """.trimIndent()
fun getNotice(version: String): String = fun getNotice(version: String): String =

View File

@@ -21,7 +21,6 @@ import kotlin.coroutines.resume
*/ */
object ProtocolManager { object ProtocolManager {
private const val TAG = "ProtocolManager" private const val TAG = "ProtocolManager"
private const val INBOUND_QUEUE_WAIT_TIMEOUT_MS = 20_000L
// Server address - same as React Native version // Server address - same as React Native version
private const val SERVER_ADDRESS = "ws://46.28.71.12:3000" 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") requireResyncAfterAccountInit("⏳ Delivery status before account init, scheduling re-sync")
return@launchInboundPacketTask 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) repository.handleDelivery(deliveryPacket)
if (!syncBatchInProgress) { if (!syncBatchInProgress) {
repository.updateLastSyncTimestamp(System.currentTimeMillis()) repository.updateLastSyncTimestamp(System.currentTimeMillis())
@@ -375,8 +381,8 @@ object ProtocolManager {
for (task in inboundTaskChannel) { for (task in inboundTaskChannel) {
try { try {
task() task()
} catch (e: Exception) { } catch (t: Throwable) {
android.util.Log.e(TAG, "Inbound packet task error", e) 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 * Since the queue is strictly FIFO, when the sentinel runs, all previously
* submitted tasks are guaranteed to have completed. * 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<Unit>() val done = CompletableDeferred<Unit>()
if (!launchInboundPacketTask { done.complete(Unit) }) { if (!launchInboundPacketTask { done.complete(Unit) }) {
return false return false
} }
return try { done.await()
withTimeout(timeoutMs) { done.await() } return true
true
} catch (_: TimeoutCancellationException) {
false
}
} }
private fun isLikelyUserPublicKey(value: String): Boolean { private fun isUnsupportedDialogKey(value: String): Boolean {
val normalized = value.removePrefix("0x") val normalized = value.trim().lowercase(Locale.ROOT)
if (normalized.length != 64 && normalized.length != 66 && normalized.length != 128 && normalized.length != 130) { if (normalized.isBlank()) return true
return false return normalized.startsWith("#") ||
} normalized.startsWith("group:") ||
return normalized.all { it.isDigit() || it in 'a'..'f' || it in 'A'..'F' } normalized.startsWith("conversation:")
} }
private fun isSupportedDirectPeerKey(peerKey: String, ownKey: String): Boolean { private fun isSupportedDirectPeerKey(peerKey: String, ownKey: String): Boolean {
return peerKey == ownKey || val normalized = peerKey.trim()
MessageRepository.isSystemAccount(peerKey) || if (normalized.isBlank()) return false
isLikelyUserPublicKey(peerKey) if (normalized == ownKey) return true
if (MessageRepository.isSystemAccount(normalized)) return true
return !isUnsupportedDialogKey(normalized)
} }
private fun isSupportedDirectMessagePacket(packet: PacketMessage, ownKey: String): Boolean { private fun isSupportedDirectMessagePacket(packet: PacketMessage, ownKey: String): Boolean {
@@ -568,7 +572,7 @@ object ProtocolManager {
if (!tasksFinished) { if (!tasksFinished) {
android.util.Log.w( android.util.Log.w(
TAG, 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 { val fallbackTimestamp = try {
messageRepository?.getLastSyncTimestamp() ?: packet.timestamp messageRepository?.getLastSyncTimestamp() ?: packet.timestamp

View File

@@ -212,6 +212,7 @@ fun ChatsListScreen(
isDarkTheme: Boolean, isDarkTheme: Boolean,
accountName: String, accountName: String,
accountUsername: String, accountUsername: String,
accountVerified: Int = 0,
accountPhone: String, accountPhone: String,
accountPublicKey: String, accountPublicKey: String,
accountPrivateKey: String = "", accountPrivateKey: String = "",
@@ -864,7 +865,7 @@ fun ChatsListScreen(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = Color.White color = Color.White
) )
if (isRosettaOfficial) { if (accountVerified > 0 || isRosettaOfficial) {
Spacer( Spacer(
modifier = modifier =
Modifier.width( Modifier.width(
@@ -872,7 +873,7 @@ fun ChatsListScreen(
) )
) )
VerifiedBadge( VerifiedBadge(
verified = 1, verified = if (accountVerified > 0) accountVerified else 1,
size = 15, size = 15,
badgeTint = Color(0xFFACD2F9) 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
}
}
)
} }
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════

View File

@@ -270,6 +270,7 @@ fun ProfileScreen(
isDarkTheme: Boolean, isDarkTheme: Boolean,
accountName: String, accountName: String,
accountUsername: String, accountUsername: String,
accountVerified: Int = 0,
accountPublicKey: String, accountPublicKey: String,
accountPrivateKeyHash: String, accountPrivateKeyHash: String,
onBack: () -> Unit, onBack: () -> Unit,
@@ -921,6 +922,7 @@ fun ProfileScreen(
CollapsingProfileHeader( CollapsingProfileHeader(
name = editedName.ifBlank { accountPublicKey.take(10) }, name = editedName.ifBlank { accountPublicKey.take(10) },
username = editedUsername, username = editedUsername,
verified = accountVerified,
publicKey = accountPublicKey, publicKey = accountPublicKey,
avatarColors = avatarColors, avatarColors = avatarColors,
collapseProgress = collapseProgress, collapseProgress = collapseProgress,
@@ -1069,6 +1071,7 @@ fun ProfileScreen(
private fun CollapsingProfileHeader( private fun CollapsingProfileHeader(
name: String, name: String,
username: String, username: String,
verified: Int = 0,
publicKey: String, publicKey: String,
avatarColors: AvatarColors, avatarColors: AvatarColors,
collapseProgress: Float, collapseProgress: Float,
@@ -1397,10 +1400,10 @@ private fun CollapsingProfileHeader(
modifier = Modifier.widthIn(max = 220.dp), modifier = Modifier.widthIn(max = 220.dp),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
if (isRosettaOfficial) { if (verified > 0 || isRosettaOfficial) {
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
VerifiedBadge( VerifiedBadge(
verified = 2, verified = if (verified > 0) verified else 2,
size = (nameFontSize.value * 0.8f).toInt(), size = (nameFontSize.value * 0.8f).toInt(),
isDarkTheme = isDarkTheme isDarkTheme = isDarkTheme
) )