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