feat: Add account verification status handling in MainScreen, ChatsListScreen, and ProfileScreen
This commit is contained in:
@@ -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() },
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user