diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 479fca6..42f765e 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.1.4" -val rosettaVersionCode = 16 // Increment on each release +val rosettaVersionName = "1.1.5" +val rosettaVersionCode = 17 // Increment on each release android { namespace = "com.rosetta.messenger" 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 1b11aeb..567a7d7 100644 --- a/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt +++ b/app/src/main/java/com/rosetta/messenger/data/ReleaseNotes.kt @@ -17,20 +17,17 @@ object ReleaseNotes { val RELEASE_NOTICE = """ Update v$VERSION_PLACEHOLDER - Профиль и группы - - Фиксированные табы в профиле и группах - - Fast-scroll с отображением даты в медиа-галерее - - Поддержка Apple Emoji в аватарах и интерфейсе - - Восстановление ключей шифрования группы по инвайт-ссылке + Подключение + - Ускорен старт соединения и handshake при входе в аккаунт + - Логика reconnect синхронизирована с desktop-поведением + - Обновлён серверный endpoint на основной production (wss) - Аватары - - Улучшено отображение аватаров: поддержка текста с эмодзи - - Улучшена логика отображения в компоненте AvatarImage + Группы + - Добавлено предзагруженное кэширование участников группы + - Убран скачок "0 members" при повторном открытии группы - Исправления - - Исправлен переход по своему тэгу в группах - - Убрана лишняя подсветка в чатах - - Корректное отображение fast-scroll при изменении размера экрана + Интерфейс + - Исправлено вертикальное выравнивание verified-галочки в списке чатов """.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 46a7e62..554469b 100644 --- a/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/ProtocolManager.kt @@ -31,8 +31,8 @@ object ProtocolManager { private const val DEBUG_LOG_FLUSH_DELAY_MS = 60L private const val TYPING_INDICATOR_TIMEOUT_MS = 3_000L - // Server address - same as React Native version - private const val SERVER_ADDRESS = "ws://46.28.71.12:3000" + // Desktop parity: use the same primary WebSocket endpoint as desktop client. + private const val SERVER_ADDRESS = "wss://wss.rosetta.im" private const val DEVICE_PREFS = "rosetta_protocol" private const val DEVICE_ID_KEY = "device_id" private const val DEVICE_ID_LENGTH = 128 diff --git a/app/src/main/java/com/rosetta/messenger/providers/AuthState.kt b/app/src/main/java/com/rosetta/messenger/providers/AuthState.kt index 5632e3a..61449cd 100644 --- a/app/src/main/java/com/rosetta/messenger/providers/AuthState.kt +++ b/app/src/main/java/com/rosetta/messenger/providers/AuthState.kt @@ -163,11 +163,8 @@ class AuthStateManager( // Step 8: Connect and authenticate with protocol ProtocolManager.connect() - - // Give WebSocket time to connect before authenticating - kotlinx.coroutines.delay(500) - ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash) + ProtocolManager.reconnectNowIfNeeded("auth_state_create") Result.success(decryptedAccount) } catch (e: Exception) { @@ -210,11 +207,8 @@ class AuthStateManager( // Connect and authenticate with protocol ProtocolManager.connect() - - // Give WebSocket time to connect before authenticating - kotlinx.coroutines.delay(500) - ProtocolManager.authenticate(decryptedAccount.publicKey, decryptedAccount.privateKeyHash) + ProtocolManager.reconnectNowIfNeeded("auth_state_unlock") Result.success(decryptedAccount) } catch (e: Exception) { diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/AuthProtocolSync.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/AuthProtocolSync.kt index da4d999..805a591 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/AuthProtocolSync.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/AuthProtocolSync.kt @@ -2,20 +2,24 @@ package com.rosetta.messenger.ui.auth import com.rosetta.messenger.network.ProtocolManager import com.rosetta.messenger.network.ProtocolState -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeoutOrNull +internal fun startAuthHandshakeFast(publicKey: String, privateKeyHash: String) { + // Desktop parity: start connection+handshake immediately, without artificial waits. + ProtocolManager.connect() + ProtocolManager.authenticate(publicKey, privateKeyHash) + ProtocolManager.reconnectNowIfNeeded("auth_fast_start") +} + internal suspend fun awaitAuthHandshakeState( publicKey: String, privateKeyHash: String, attempts: Int = 2, timeoutMs: Long = 25_000L ): ProtocolState? { - repeat(attempts) { - ProtocolManager.disconnect() - delay(200) - ProtocolManager.authenticate(publicKey, privateKeyHash) + repeat(attempts) { attempt -> + startAuthHandshakeFast(publicKey, privateKeyHash) val state = withTimeoutOrNull(timeoutMs) { ProtocolManager.state.first { @@ -26,6 +30,7 @@ internal suspend fun awaitAuthHandshakeState( if (state != null) { return state } + ProtocolManager.reconnectNowIfNeeded("auth_handshake_retry_${attempt + 1}") } return null } diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt index f27dfca..c314f8b 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt @@ -527,17 +527,10 @@ fun SetPasswordScreen( keyPair.privateKey ) - val handshakeState = - awaitAuthHandshakeState( - keyPair.publicKey, - privateKeyHash - ) - if (handshakeState == null) { - error = - "Failed to connect to server. Please try again." - isCreating = false - return@launch - } + startAuthHandshakeFast( + keyPair.publicKey, + privateKeyHash + ) accountManager.setCurrentAccount(keyPair.publicKey) diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt index 234a297..fb5ef8d 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt @@ -116,12 +116,7 @@ val decryptedPrivateKey = CryptoManager.decryptWithPassword( name = selectedAccount.name ) - val handshakeState = awaitAuthHandshakeState(account.publicKey, privateKeyHash) - if (handshakeState == null) { - onError("Failed to connect to server") - onUnlocking(false) - return - } + startAuthHandshakeFast(account.publicKey, privateKeyHash) accountManager.setCurrentAccount(account.publicKey) onSuccess(decryptedAccount) 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 3394fcf..90141d7 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 @@ -3891,7 +3891,7 @@ fun DialogItemContent( VerifiedBadge( verified = if (dialog.verified > 0) dialog.verified else 1, size = 16, - modifier = Modifier.offset(y = (-1).dp) + modifier = Modifier.offset(y = (-2).dp) ) } // 🔒 Красная иконка замочка для заблокированных пользователей diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt index 7a354d3..00652f2 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/GroupInfoScreen.kt @@ -395,7 +395,7 @@ fun GroupInfoScreen( var showEncryptionPage by rememberSaveable(dialogPublicKey) { mutableStateOf(false) } var encryptionKey by rememberSaveable(dialogPublicKey) { mutableStateOf("") } var encryptionKeyLoading by remember { mutableStateOf(false) } - var membersLoading by remember { mutableStateOf(false) } + var membersLoading by remember(dialogPublicKey) { mutableStateOf(false) } var isMuted by remember { mutableStateOf(false) } var showGroupAvatarPicker by rememberSaveable(dialogPublicKey) { mutableStateOf(false) } var showGroupAvatarViewer by rememberSaveable(dialogPublicKey) { mutableStateOf(false) } @@ -414,18 +414,21 @@ fun GroupInfoScreen( val initialDiskMembersCache = remember(membersCacheKey) { GroupMembersDiskCache.getAny(context, membersCacheKey) } + val hasInitialMembersCache = remember(initialMemoryMembersCache, initialDiskMembersCache) { + initialMemoryMembersCache != null || initialDiskMembersCache != null + } val initialMembers = remember(initialMemoryMembersCache, initialDiskMembersCache) { initialMemoryMembersCache?.members ?: initialDiskMembersCache?.members.orEmpty() } - var members by remember(dialogPublicKey) { mutableStateOf(initialMembers) } + var members by remember(dialogPublicKey, membersCacheKey) { mutableStateOf(initialMembers) } val memberInfoByKey = - remember(dialogPublicKey) { + remember(dialogPublicKey, membersCacheKey) { mutableStateMapOf().apply { initialMemoryMembersCache?.memberInfoByKey?.let { putAll(it) } } } // Real online status from PacketOnlineState (0x05), NOT from SearchUser.online - val memberOnlineStatus = remember(dialogPublicKey) { mutableStateMapOf() } + val memberOnlineStatus = remember(dialogPublicKey, membersCacheKey) { mutableStateMapOf() } val groupEntity by produceState( initialValue = null, @@ -1089,7 +1092,7 @@ fun GroupInfoScreen( ) Text( - text = if (membersLoading) { + text = if (membersLoading || (members.isEmpty() && !hasInitialMembersCache)) { "Loading members..." } else { "${members.size} members, $onlineCount online"