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

@@ -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() },

View File

@@ -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 =

View File

@@ -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<Unit>()
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

View File

@@ -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
}
}
)
}
// ═══════════════════════════════════════════════════════════

View File

@@ -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
)