Merge remote-tracking branch 'refs/remotes/origin/master'

This commit is contained in:
senseiGai
2026-01-11 17:14:22 +05:00
6 changed files with 141 additions and 8 deletions

69
FIX_WEBSOCKET_CONNECT.md Normal file
View File

@@ -0,0 +1,69 @@
# Fix: WebSocket Connection After Registration
## Проблема
После первой регистрации аккаунта в Android приложении не работали запросы на сервер до перезапуска приложения.
## Причина
При регистрации или разблокировке аккаунта вызывался `ProtocolManager.authenticate()` до того, как WebSocket соединение было установлено.
Последовательность была следующая:
1. Создание/разблокировка аккаунта
2. Вызов `ProtocolManager.authenticate(publicKey, privateKeyHash)`
3. `authenticate()` вызывает `getProtocol().startHandshake()`
4. `startHandshake()` проверяет что не подключено и вызывает `connect()`, но затем сразу возвращается
5. Соединение устанавливается асинхронно, но handshake не происходит вовремя
6. Приложение переходит к экрану чатов, запросы не проходят
После перезапуска приложения `ProtocolManager.connect()` вызывался в `ChatsListScreen`, поэтому все работало нормально.
## Решение
Добавлен явный вызов `ProtocolManager.connect()` перед `authenticate()` в следующих местах:
### 1. AuthState.kt
- Метод `createAccount()` - после создания аккаунта
- Метод `unlock()` - после разблокировки
### 2. SetPasswordScreen.kt
- При создании нового аккаунта после установки пароля
### 3. UnlockScreen.kt
- При разблокировке существующего аккаунта
Добавлена задержка 500ms после `connect()` для того, чтобы WebSocket успел установить соединение до попытки аутентификации.
## Измененные файлы
- `app/src/main/java/com/rosetta/messenger/providers/AuthState.kt`
- `app/src/main/java/com/rosetta/messenger/ui/auth/SetPasswordScreen.kt`
- `app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt`
## Код изменений
```kotlin
// Было:
ProtocolManager.authenticate(publicKey, privateKeyHash)
// Стало:
ProtocolManager.connect()
kotlinx.coroutines.delay(500) // Даём время на установку соединения
ProtocolManager.authenticate(publicKey, privateKeyHash)
```
## Тестирование
После этого исправления:
1. Зарегистрируйте новый аккаунт
2. Сразу после регистрации попробуйте отправить сообщение или выполнить другой запрос
3. Запросы должны проходить без перезапуска приложения
## Дата
11 января 2026

View File

@@ -69,6 +69,9 @@ class Protocol(
private var lastPublicKey: String? = null private var lastPublicKey: String? = null
private var lastPrivateHash: String? = null private var lastPrivateHash: String? = null
// Heartbeat
private var heartbeatJob: Job? = null
// Supported packets // Supported packets
private val supportedPackets = mapOf( private val supportedPackets = mapOf(
0x00 to { PacketHandshake() }, 0x00 to { PacketHandshake() },
@@ -92,6 +95,30 @@ class Protocol(
handshakeComplete = true handshakeComplete = true
_state.value = ProtocolState.AUTHENTICATED _state.value = ProtocolState.AUTHENTICATED
flushPacketQueue() flushPacketQueue()
// Start heartbeat with interval from server
startHeartbeat(packet.heartbeatInterval)
}
}
}
/**
* Start heartbeat to keep connection alive
*/
private fun startHeartbeat(intervalSeconds: Int) {
heartbeatJob?.cancel()
val intervalMs = (intervalSeconds * 1000L) / 2 // Send at half the interval
log("💓 Starting heartbeat with interval: ${intervalSeconds}s (sending every ${intervalMs}ms)")
heartbeatJob = scope.launch {
while (isActive) {
delay(intervalMs)
if (webSocket?.send("heartbeat") == true) {
log("💓 Heartbeat sent")
} else {
log("💔 Heartbeat failed to send")
}
} }
} }
} }
@@ -100,13 +127,19 @@ class Protocol(
* Initialize connection to server * Initialize connection to server
*/ */
fun connect() { fun connect() {
if (_state.value == ProtocolState.CONNECTING || _state.value == ProtocolState.CONNECTED) { if (_state.value == ProtocolState.CONNECTING) {
log("Already connecting or connected") log("Already connecting, skipping...")
return
}
// Allow reconnection even if connected (for manual reconnect)
if (_state.value == ProtocolState.CONNECTED || _state.value == ProtocolState.AUTHENTICATED) {
log("Already connected/authenticated, skipping...")
return return
} }
isManuallyClosed = false isManuallyClosed = false
reconnectAttempts = 0 // Reset reconnect attempts on new connection // Don't reset reconnectAttempts here - it's reset on successful connection in onOpen
_state.value = ProtocolState.CONNECTING _state.value = ProtocolState.CONNECTING
_lastError.value = null _lastError.value = null
@@ -266,6 +299,7 @@ class Protocol(
_state.value = ProtocolState.DISCONNECTED _state.value = ProtocolState.DISCONNECTED
handshakeComplete = false handshakeComplete = false
handshakeJob?.cancel() handshakeJob?.cancel()
heartbeatJob?.cancel()
if (!isManuallyClosed && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { if (!isManuallyClosed && reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++ reconnectAttempts++
@@ -302,6 +336,7 @@ class Protocol(
log("Disconnecting...") log("Disconnecting...")
isManuallyClosed = true isManuallyClosed = true
handshakeJob?.cancel() handshakeJob?.cancel()
heartbeatJob?.cancel()
webSocket?.close(1000, "User disconnected") webSocket?.close(1000, "User disconnected")
webSocket = null webSocket = null
_state.value = ProtocolState.DISCONNECTED _state.value = ProtocolState.DISCONNECTED
@@ -332,6 +367,7 @@ class Protocol(
*/ */
fun destroy() { fun destroy() {
disconnect() disconnect()
heartbeatJob?.cancel()
scope.cancel() scope.cancel()
} }
} }

View File

@@ -175,7 +175,13 @@ class AuthStateManager(
loadAccounts() loadAccounts()
// Step 8: Authenticate with protocol // Step 8: Connect and authenticate with protocol
Log.d(TAG, "🌐 Connecting to protocol server...")
ProtocolManager.connect()
// Give WebSocket time to connect before authenticating
kotlinx.coroutines.delay(500)
Log.d(TAG, "🌐 Authenticating with protocol server...") Log.d(TAG, "🌐 Authenticating with protocol server...")
ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash) ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash)
@@ -236,7 +242,13 @@ class AuthStateManager(
status = AuthStatus.Authenticated(decryptedAccount) status = AuthStatus.Authenticated(decryptedAccount)
)} )}
// Authenticate with protocol // Connect and authenticate with protocol
Log.d(TAG, "🌐 Connecting to protocol server...")
ProtocolManager.connect()
// Give WebSocket time to connect before authenticating
kotlinx.coroutines.delay(500)
Log.d(TAG, "🌐 Authenticating with protocol server...") Log.d(TAG, "🌐 Authenticating with protocol server...")
ProtocolManager.authenticate(decryptedAccount.publicKey, decryptedAccount.privateKeyHash) ProtocolManager.authenticate(decryptedAccount.publicKey, decryptedAccount.privateKeyHash)

View File

@@ -130,7 +130,7 @@ fun AuthFlow(
currentScreen = AuthScreen.CONFIRM_SEED currentScreen = AuthScreen.CONFIRM_SEED
} }
}, },
onAccountCreated = { onAuthComplete(null) } onAccountCreated = { account -> onAuthComplete(account) }
) )
} }

View File

@@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.rosetta.messenger.crypto.CryptoManager import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.data.AccountManager import com.rosetta.messenger.data.AccountManager
import com.rosetta.messenger.data.DecryptedAccount
import com.rosetta.messenger.data.EncryptedAccount import com.rosetta.messenger.data.EncryptedAccount
import com.rosetta.messenger.network.ProtocolManager import com.rosetta.messenger.network.ProtocolManager
import com.rosetta.messenger.ui.onboarding.PrimaryBlue import com.rosetta.messenger.ui.onboarding.PrimaryBlue
@@ -38,7 +39,7 @@ fun SetPasswordScreen(
seedPhrase: List<String>, seedPhrase: List<String>,
isDarkTheme: Boolean, isDarkTheme: Boolean,
onBack: () -> Unit, onBack: () -> Unit,
onAccountCreated: () -> Unit onAccountCreated: (DecryptedAccount) -> Unit
) { ) {
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f)) val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec) val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec)
@@ -497,9 +498,21 @@ fun SetPasswordScreen(
// 🔌 Connect to server and authenticate // 🔌 Connect to server and authenticate
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey) val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
Log.d("SetPasswordScreen", "🔌 Connecting to server...") Log.d("SetPasswordScreen", "🔌 Connecting to server...")
ProtocolManager.connect()
// Give WebSocket time to connect before authenticating
kotlinx.coroutines.delay(500)
ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash) ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash)
onAccountCreated() // Create DecryptedAccount to pass to callback
val decryptedAccount = DecryptedAccount(
publicKey = keyPair.publicKey,
privateKey = keyPair.privateKey,
seedPhrase = seedPhrase,
privateKeyHash = privateKeyHash,
name = truncatedKey
)
onAccountCreated(decryptedAccount)
} catch (e: Exception) { } catch (e: Exception) {
error = "Failed to create account: ${e.message}" error = "Failed to create account: ${e.message}"
isCreating = false isCreating = false

View File

@@ -581,6 +581,9 @@ fun UnlockScreen(
// Connect to server and authenticate // Connect to server and authenticate
Log.d("UnlockScreen", "Connecting to server...") Log.d("UnlockScreen", "Connecting to server...")
ProtocolManager.connect()
// Give WebSocket time to connect before authenticating
kotlinx.coroutines.delay(500)
ProtocolManager.authenticate(account.publicKey, privateKeyHash) ProtocolManager.authenticate(account.publicKey, privateKeyHash)
accountManager.setCurrentAccount(account.publicKey) accountManager.setCurrentAccount(account.publicKey)