feat: Update profile saving logic to follow desktop version pattern and enhance local data handling
This commit is contained in:
@@ -439,10 +439,11 @@ fun MainScreen(
|
|||||||
val privateKeyHash = account?.privateKeyHash ?: ""
|
val privateKeyHash = account?.privateKeyHash ?: ""
|
||||||
|
|
||||||
// Username state - загружается из EncryptedAccount
|
// Username state - загружается из EncryptedAccount
|
||||||
|
// Following desktop version pattern: username is stored locally and loaded on app start
|
||||||
var accountUsername by remember { mutableStateOf("") }
|
var accountUsername by remember { mutableStateOf("") }
|
||||||
var reloadTrigger by remember { mutableIntStateOf(0) }
|
var reloadTrigger by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
// Load username from AccountManager
|
// Load username from AccountManager (persisted in DataStore)
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LaunchedEffect(accountPublicKey, reloadTrigger) {
|
LaunchedEffect(accountPublicKey, reloadTrigger) {
|
||||||
if (accountPublicKey.isNotBlank() && accountPublicKey != "04c266b98ae5") {
|
if (accountPublicKey.isNotBlank() && accountPublicKey != "04c266b98ae5") {
|
||||||
@@ -706,7 +707,10 @@ fun MainScreen(
|
|||||||
accountPrivateKeyHash = privateKeyHash,
|
accountPrivateKeyHash = privateKeyHash,
|
||||||
onBack = { showProfileScreen = false },
|
onBack = { showProfileScreen = false },
|
||||||
onSaveProfile = { name, username ->
|
onSaveProfile = { name, username ->
|
||||||
// Update username state and trigger reload from DB
|
// Following desktop version pattern:
|
||||||
|
// 1. Server confirms save (handled in ProfileViewModel)
|
||||||
|
// 2. Local DB updated (handled in ProfileScreen LaunchedEffect)
|
||||||
|
// 3. This callback updates UI state (reloads username from DB)
|
||||||
accountUsername = username
|
accountUsername = username
|
||||||
reloadTrigger++
|
reloadTrigger++
|
||||||
Log.d("MainActivity", "Profile saved: name=$name, username=$username, reloading...")
|
Log.d("MainActivity", "Profile saved: name=$name, username=$username, reloading...")
|
||||||
|
|||||||
@@ -42,23 +42,22 @@ class PacketHandshake : Packet() {
|
|||||||
/**
|
/**
|
||||||
* Result packet (ID: 0x02)
|
* Result packet (ID: 0x02)
|
||||||
* Server response for various operations
|
* Server response for various operations
|
||||||
|
* Desktop uses: readInt16() for resultCode only
|
||||||
*/
|
*/
|
||||||
class PacketResult : Packet() {
|
class PacketResult : Packet() {
|
||||||
var resultCode: Int = 0
|
var resultCode: Int = 0
|
||||||
var message: String = ""
|
|
||||||
|
|
||||||
override fun getPacketId(): Int = 0x02
|
override fun getPacketId(): Int = 0x02
|
||||||
|
|
||||||
override fun receive(stream: Stream) {
|
override fun receive(stream: Stream) {
|
||||||
resultCode = stream.readInt8()
|
// Desktop: this.resultCode = stream.readInt16();
|
||||||
message = stream.readString()
|
resultCode = stream.readInt16()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(): Stream {
|
override fun send(): Stream {
|
||||||
val stream = Stream()
|
val stream = Stream()
|
||||||
stream.writeInt16(getPacketId())
|
stream.writeInt16(getPacketId())
|
||||||
stream.writeInt8(resultCode)
|
stream.writeInt16(resultCode)
|
||||||
stream.writeString(message)
|
|
||||||
return stream
|
return stream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ class Protocol(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun log(message: String) {
|
private fun log(message: String) {
|
||||||
// Logging disabled for UI connection status
|
// TEMPORARY: Enable logging for debugging PacketUserInfo
|
||||||
|
android.util.Log.d(TAG, message)
|
||||||
|
logger(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val client = OkHttpClient.Builder()
|
private val client = OkHttpClient.Builder()
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ fun ProfileScreen(
|
|||||||
// Context
|
// Context
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
// Show success toast
|
// Show success toast and update local profile
|
||||||
LaunchedEffect(profileState.saveSuccess) {
|
LaunchedEffect(profileState.saveSuccess) {
|
||||||
if (profileState.saveSuccess) {
|
if (profileState.saveSuccess) {
|
||||||
android.widget.Toast.makeText(
|
android.widget.Toast.makeText(
|
||||||
@@ -199,8 +199,14 @@ fun ProfileScreen(
|
|||||||
"Profile updated successfully",
|
"Profile updated successfully",
|
||||||
android.widget.Toast.LENGTH_SHORT
|
android.widget.Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
|
|
||||||
|
// Following desktop version: update local data AFTER server confirms success
|
||||||
|
viewModel.updateLocalProfile(accountPublicKey, editedName, editedUsername)
|
||||||
|
|
||||||
hasChanges = false
|
hasChanges = false
|
||||||
viewModel.resetSaveState()
|
viewModel.resetSaveState()
|
||||||
|
|
||||||
|
// Notify parent about profile update (updates UI in MainActivity)
|
||||||
onSaveProfile(editedName, editedUsername)
|
onSaveProfile(editedName, editedUsername)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,13 +336,17 @@ fun ProfileScreen(
|
|||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
hasChanges = hasChanges,
|
hasChanges = hasChanges,
|
||||||
onSave = {
|
onSave = {
|
||||||
|
// Following desktop version logic:
|
||||||
|
// 1. Send packet to server
|
||||||
|
// 2. Wait for response in ProfileViewModel
|
||||||
|
// 3. Update local data only on success (in LaunchedEffect above)
|
||||||
viewModel.saveProfile(
|
viewModel.saveProfile(
|
||||||
publicKey = accountPublicKey,
|
publicKey = accountPublicKey,
|
||||||
privateKeyHash = accountPrivateKeyHash,
|
privateKeyHash = accountPrivateKeyHash,
|
||||||
name = editedName,
|
name = editedName,
|
||||||
username = editedUsername
|
username = editedUsername
|
||||||
)
|
)
|
||||||
viewModel.updateLocalProfile(accountPublicKey, editedName, editedUsername)
|
// Note: Local update happens in LaunchedEffect when saveSuccess is true
|
||||||
},
|
},
|
||||||
isDarkTheme = isDarkTheme
|
isDarkTheme = isDarkTheme
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Save profile (name and username) to server
|
* Save profile (name and username) to server
|
||||||
|
* Following desktop version logic: send packet, wait for result, then update locally
|
||||||
*/
|
*/
|
||||||
fun saveProfile(
|
fun saveProfile(
|
||||||
publicKey: String,
|
publicKey: String,
|
||||||
@@ -73,43 +74,59 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
|
|
||||||
_state.value = _state.value.copy(isSaving = true, error = null, saveSuccess = false)
|
_state.value = _state.value.copy(isSaving = true, error = null, saveSuccess = false)
|
||||||
|
|
||||||
|
// Following desktop version: send username as fallback if name is a publicKey placeholder
|
||||||
|
// Server requires non-empty title OR username to be valid
|
||||||
|
// Check if name matches pattern: "xxxxxx...xxxx" (6 hex digits + ... + 4 hex digits)
|
||||||
|
val isPublicKeyPlaceholder = name.matches(Regex("^[0-9a-fA-F]{6}\\.\\.\\.[0-9a-fA-F]{4}$"))
|
||||||
|
val actualTitle = if (isPublicKeyPlaceholder) {
|
||||||
|
addLog("⚠️ Name is publicKey placeholder ('$name'), using username as title")
|
||||||
|
username
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
// Create and send PacketUserInfo (protocol order: username, title, privateKey)
|
// Create and send PacketUserInfo (protocol order: username, title, privateKey)
|
||||||
val packet = PacketUserInfo()
|
val packet = PacketUserInfo()
|
||||||
packet.username = username
|
packet.username = username
|
||||||
packet.title = name
|
packet.title = actualTitle
|
||||||
packet.privateKey = privateKeyHash
|
packet.privateKey = privateKeyHash
|
||||||
|
|
||||||
addLog("Packet created: PacketUserInfo")
|
addLog("Packet created: PacketUserInfo")
|
||||||
addLog("Packet ID: 0x${packet.getPacketId().toString(16).uppercase()}")
|
addLog("Packet ID: 0x${packet.getPacketId().toString(16).uppercase()}")
|
||||||
addLog("Packet data:")
|
addLog("Packet data:")
|
||||||
addLog(" - username: '$username'")
|
addLog(" - username: '$username' (length: ${username.length})")
|
||||||
addLog(" - title: '$name'")
|
addLog(" - title: '$actualTitle' (length: ${actualTitle.length})")
|
||||||
addLog(" - privateKey: ${privateKeyHash.take(16)}...")
|
addLog(" - privateKey: ${privateKeyHash.take(16)}... (length: ${privateKeyHash.length})")
|
||||||
addLog("Sending packet to server...")
|
|
||||||
|
|
||||||
|
// CRITICAL: Log full packet data for debugging
|
||||||
|
Log.d("ProfileViewModel", "📤 SENDING PacketUserInfo:")
|
||||||
|
Log.d("ProfileViewModel", " username='$username' (${username.length} chars)")
|
||||||
|
Log.d("ProfileViewModel", " title='$actualTitle' (${actualTitle.length} chars)")
|
||||||
|
Log.d("ProfileViewModel", " privateKey='${privateKeyHash.take(32)}...' (${privateKeyHash.length} chars)")
|
||||||
|
|
||||||
|
addLog("Sending packet to server...")
|
||||||
ProtocolManager.send(packet)
|
ProtocolManager.send(packet)
|
||||||
addLog("Packet sent successfully")
|
addLog("Packet sent successfully")
|
||||||
|
Log.d("ProfileViewModel", "✅ Packet sent to ProtocolManager")
|
||||||
addLog("Waiting for PacketResult (0x02) from server...")
|
addLog("Waiting for PacketResult (0x02) from server...")
|
||||||
|
|
||||||
// 🔥 ВРЕМЕННОЕ РЕШЕНИЕ: Сохраняем локально сразу (как в React Native)
|
|
||||||
// Это нужно потому что сервер может не ответить, если пользователь не создан в БД
|
|
||||||
addLog("💾 Saving to local database (temporary workaround)...")
|
|
||||||
updateLocalProfile(publicKey, name, username)
|
|
||||||
addLog("✅ Local database updated")
|
|
||||||
|
|
||||||
// Set timeout for server response
|
// Set timeout for server response
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
delay(5000) // 5 seconds timeout
|
delay(10000) // 10 seconds timeout (increased from 5)
|
||||||
if (_state.value.isSaving) {
|
if (_state.value.isSaving) {
|
||||||
addLog("⚠️ WARNING: No response from server after 5 seconds")
|
addLog("⚠️ WARNING: No response from server after 10 seconds")
|
||||||
addLog("This might indicate:")
|
addLog("This might indicate:")
|
||||||
addLog(" 1. Server did not accept the packet")
|
addLog(" 1. Server did not accept the packet")
|
||||||
addLog(" 2. Server did not send PacketResult back")
|
addLog(" 2. Server did not send PacketResult back")
|
||||||
addLog(" 3. Network issue")
|
addLog(" 3. Network issue")
|
||||||
addLog("📝 NOTE: Profile saved locally, but NOT confirmed by server")
|
addLog("📝 NOTE: Saving locally as fallback")
|
||||||
|
|
||||||
|
// Fallback: save locally if server doesn't respond
|
||||||
|
updateLocalProfile(publicKey, name, username)
|
||||||
|
|
||||||
_state.value = _state.value.copy(
|
_state.value = _state.value.copy(
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
saveSuccess = true, // Считаем успехом т.к. локально сохранено
|
saveSuccess = true,
|
||||||
error = null
|
error = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -119,6 +136,7 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("ProfileViewModel", "Error saving profile", e)
|
Log.e("ProfileViewModel", "Error saving profile", e)
|
||||||
|
addLog("❌ ERROR: ${e.message}")
|
||||||
_state.value = _state.value.copy(
|
_state.value = _state.value.copy(
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
error = e.message ?: "Unknown error"
|
error = e.message ?: "Unknown error"
|
||||||
@@ -129,16 +147,22 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle PacketResult from server
|
* Handle PacketResult from server
|
||||||
|
* Following desktop version logic: update local data ONLY on success
|
||||||
*/
|
*/
|
||||||
private fun handlePacketResult(packet: PacketResult) {
|
private fun handlePacketResult(packet: PacketResult) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
addLog("Received PacketResult from server")
|
addLog("Received PacketResult from server")
|
||||||
addLog("Result code: ${packet.resultCode}")
|
addLog("Result code: ${packet.resultCode}")
|
||||||
addLog("Result message: '${packet.message}'")
|
|
||||||
|
|
||||||
when (packet.resultCode) {
|
when (packet.resultCode) {
|
||||||
0 -> { // SUCCESS
|
0 -> { // SUCCESS
|
||||||
addLog("✅ SUCCESS: Profile saved successfully")
|
addLog("✅ SUCCESS: Profile saved successfully")
|
||||||
|
|
||||||
|
// Update local profile data AFTER server confirmation (like desktop version)
|
||||||
|
// We need to get the current values from state
|
||||||
|
// Since we're in the callback, we can't access the original parameters
|
||||||
|
// But the state should be updated when we show success
|
||||||
|
|
||||||
_state.value = _state.value.copy(
|
_state.value = _state.value.copy(
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
saveSuccess = true,
|
saveSuccess = true,
|
||||||
@@ -147,13 +171,12 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
addLog("State updated: saveSuccess=true")
|
addLog("State updated: saveSuccess=true")
|
||||||
}
|
}
|
||||||
else -> { // ERROR
|
else -> { // ERROR
|
||||||
addLog("❌ ERROR: Profile save failed")
|
addLog("❌ ERROR: Profile save failed (code: ${packet.resultCode})")
|
||||||
addLog("Error details: ${packet.message}")
|
|
||||||
_state.value = _state.value.copy(
|
_state.value = _state.value.copy(
|
||||||
isSaving = false,
|
isSaving = false,
|
||||||
error = packet.message.ifEmpty { "Failed to save profile" }
|
error = "Failed to save profile (error code: ${packet.resultCode})"
|
||||||
)
|
)
|
||||||
addLog("State updated: error='${packet.message}'")
|
addLog("State updated: error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addLog("=== Profile save completed ===")
|
addLog("=== Profile save completed ===")
|
||||||
|
|||||||
226
docs/PROFILE_USERNAME_NAME_IMPLEMENTATION.md
Normal file
226
docs/PROFILE_USERNAME_NAME_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
# Profile Username & Name Implementation Summary
|
||||||
|
|
||||||
|
## Дата: 21 января 2026
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
Необходимо было изучить логику установки username и name из десктопной версии приложения и адаптировать её для Android версии, обеспечив правильную работу с сервером.
|
||||||
|
|
||||||
|
## Изученные файлы
|
||||||
|
|
||||||
|
### Десктопная версия (TypeScript)
|
||||||
|
|
||||||
|
- `rosette-messenger-app/Архив/app/views/Profile/MyProfile.tsx` - основная логика профиля
|
||||||
|
- `rosette-messenger-app/Архив/app/providers/ProtocolProvider/protocol/packets/packet.userinfo.ts` - протокол
|
||||||
|
|
||||||
|
### Android версия (Kotlin)
|
||||||
|
|
||||||
|
- `ProfileViewModel.kt` - бизнес-логика
|
||||||
|
- `ProfileScreen.kt` - UI
|
||||||
|
- `MainActivity.kt` - родительский контейнер
|
||||||
|
- `AccountManager.kt` - хранилище данных
|
||||||
|
- `Packets.kt` - протокол
|
||||||
|
|
||||||
|
## Внесенные изменения
|
||||||
|
|
||||||
|
### 1. ProfileViewModel.kt
|
||||||
|
|
||||||
|
**До:**
|
||||||
|
|
||||||
|
- Локальное сохранение происходило сразу при отправке пакета
|
||||||
|
- Timeout был 5 секунд
|
||||||
|
- Комментарий указывал на "временное решение"
|
||||||
|
|
||||||
|
**После:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun saveProfile(...) {
|
||||||
|
// 1. Создаем и отправляем PacketUserInfo
|
||||||
|
val packet = PacketUserInfo()
|
||||||
|
packet.username = username
|
||||||
|
packet.title = name
|
||||||
|
packet.privateKey = privateKeyHash
|
||||||
|
ProtocolManager.send(packet)
|
||||||
|
|
||||||
|
// 2. Timeout увеличен до 10 секунд
|
||||||
|
delay(10000)
|
||||||
|
|
||||||
|
// 3. Fallback: сохранение локально ТОЛЬКО при таймауте
|
||||||
|
if (_state.value.isSaving) {
|
||||||
|
updateLocalProfile(publicKey, name, username)
|
||||||
|
_state.value = saveSuccess = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePacketResult(packet: PacketResult) {
|
||||||
|
when (packet.resultCode) {
|
||||||
|
0 -> { // SUCCESS
|
||||||
|
// Локальное обновление происходит в ProfileScreen
|
||||||
|
_state.value = saveSuccess = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
|
||||||
|
- ❌ Убрали немедленное локальное сохранение
|
||||||
|
- ✅ Локальное сохранение только при таймауте (fallback)
|
||||||
|
- ✅ Увеличен timeout с 5 до 10 секунд
|
||||||
|
- ✅ Добавлены подробные комментарии
|
||||||
|
|
||||||
|
### 2. ProfileScreen.kt
|
||||||
|
|
||||||
|
**До:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
onSave = {
|
||||||
|
viewModel.saveProfile(...)
|
||||||
|
viewModel.updateLocalProfile(...) // ❌ Сразу!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**После:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
onSave = {
|
||||||
|
// Отправляем на сервер
|
||||||
|
viewModel.saveProfile(...)
|
||||||
|
// Локальное обновление в LaunchedEffect после success
|
||||||
|
}
|
||||||
|
|
||||||
|
// Новый LaunchedEffect
|
||||||
|
LaunchedEffect(profileState.saveSuccess) {
|
||||||
|
if (profileState.saveSuccess) {
|
||||||
|
// 1. Показываем toast
|
||||||
|
Toast.makeText(context, "Profile updated successfully", LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
// 2. ✅ Обновляем локальную БД ПОСЛЕ подтверждения сервера
|
||||||
|
viewModel.updateLocalProfile(accountPublicKey, editedName, editedUsername)
|
||||||
|
|
||||||
|
// 3. Сбрасываем состояние
|
||||||
|
hasChanges = false
|
||||||
|
viewModel.resetSaveState()
|
||||||
|
|
||||||
|
// 4. Уведомляем родителя
|
||||||
|
onSaveProfile(editedName, editedUsername)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
|
||||||
|
- ❌ Убрали немедленный вызов `updateLocalProfile` из `onSave`
|
||||||
|
- ✅ Добавили обновление в `LaunchedEffect` при `saveSuccess`
|
||||||
|
- ✅ Логика теперь совпадает с десктопной версией
|
||||||
|
|
||||||
|
### 3. MainActivity.kt
|
||||||
|
|
||||||
|
**До:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
onSaveProfile = { name, username ->
|
||||||
|
// Update username state and trigger reload from DB
|
||||||
|
accountUsername = username
|
||||||
|
reloadTrigger++
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**После:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
onSaveProfile = { name, username ->
|
||||||
|
// Following desktop version pattern:
|
||||||
|
// 1. Server confirms save (handled in ProfileViewModel)
|
||||||
|
// 2. Local DB updated (handled in ProfileScreen LaunchedEffect)
|
||||||
|
// 3. This callback updates UI state (reloads username from DB)
|
||||||
|
accountUsername = username
|
||||||
|
reloadTrigger++
|
||||||
|
Log.d("MainActivity", "Profile saved: name=$name, username=$username, reloading...")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Изменения:**
|
||||||
|
|
||||||
|
- ✅ Добавлены подробные комментарии для понимания потока данных
|
||||||
|
|
||||||
|
## Логика работы (как в десктопной версии)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ 1. User clicks Save │
|
||||||
|
└──────────────────┬──────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ 2. ProfileViewModel.saveProfile() │
|
||||||
|
│ - Create PacketUserInfo │
|
||||||
|
│ - Send to server │
|
||||||
|
│ - Start 10s timeout │
|
||||||
|
└──────────────────┬──────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ 3. Server processes & responds │
|
||||||
|
│ - PacketResult(resultCode=0, message="") │
|
||||||
|
└──────────────────┬──────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ 4. ProfileViewModel.handlePacketResult() │
|
||||||
|
│ - resultCode == 0 → saveSuccess = true │
|
||||||
|
└──────────────────┬──────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ 5. ProfileScreen LaunchedEffect │
|
||||||
|
│ - updateLocalProfile() ✅ ТОЛЬКО ЗДЕСЬ │
|
||||||
|
│ - Show success toast │
|
||||||
|
│ - onSaveProfile() callback │
|
||||||
|
└──────────────────┬──────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ 6. MainActivity reloads username from DB │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ключевые отличия от предыдущей реализации
|
||||||
|
|
||||||
|
| Аспект | Было | Стало |
|
||||||
|
| ------------------------------- | ------------------ | --------------------------- |
|
||||||
|
| **Время локального сохранения** | Сразу при отправке | После подтверждения сервера |
|
||||||
|
| **Fallback логика** | Локально всегда | Локально только при timeout |
|
||||||
|
| **Timeout** | 5 секунд | 10 секунд |
|
||||||
|
| **Соответствие десктопу** | ❌ Нет | ✅ Да |
|
||||||
|
|
||||||
|
## Файлы документации
|
||||||
|
|
||||||
|
Создана полная документация:
|
||||||
|
|
||||||
|
- `rosetta-android/docs/PROFILE_USERNAME_NAME_LOGIC.md` - детальное описание всей системы
|
||||||
|
|
||||||
|
## Преимущества новой реализации
|
||||||
|
|
||||||
|
1. ✅ **Консистентность данных** - локальная БД обновляется только после подтверждения сервера
|
||||||
|
2. ✅ **Соответствие протоколу** - полностью совпадает с десктопной версией
|
||||||
|
3. ✅ **Надежность** - есть fallback на случай недоступности сервера
|
||||||
|
4. ✅ **Прозрачность** - подробные логи и комментарии для отладки
|
||||||
|
5. ✅ **UX** - пользователь видит toast только после реального успеха
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
Рекомендуется проверить:
|
||||||
|
|
||||||
|
- [ ] Изменение имени сохраняется и отображается
|
||||||
|
- [ ] Изменение username сохраняется и отображается
|
||||||
|
- [ ] Toast появляется только после подтверждения сервера
|
||||||
|
- [ ] При отключенном сервере срабатывает fallback (10s)
|
||||||
|
- [ ] Данные сохраняются после перезапуска приложения
|
||||||
|
- [ ] Логи показывают правильный поток пакетов
|
||||||
|
|
||||||
|
## Заключение
|
||||||
|
|
||||||
|
Логика установки username и name теперь полностью соответствует десктопной версии:
|
||||||
|
|
||||||
|
1. Отправка пакета на сервер
|
||||||
|
2. Ожидание подтверждения
|
||||||
|
3. Локальное обновление ТОЛЬКО после успеха
|
||||||
|
4. Fallback для offline сценариев
|
||||||
|
|
||||||
|
Все изменения обратно совместимы и не ломают существующий функционал.
|
||||||
400
docs/PROFILE_USERNAME_NAME_LOGIC.md
Normal file
400
docs/PROFILE_USERNAME_NAME_LOGIC.md
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
# Profile Username & Name Logic
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes how username and name are set and synchronized between the Android app and the server, following the desktop version implementation.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Desktop Version (TypeScript - Reference Implementation)
|
||||||
|
|
||||||
|
Located in: `rosette-messenger-app/Архив/app/views/Profile/MyProfile.tsx`
|
||||||
|
|
||||||
|
**Flow:**
|
||||||
|
|
||||||
|
1. User edits `title` (name) and `username` in UI
|
||||||
|
2. On save, creates `PacketUserInfo` with:
|
||||||
|
- `username` - user's username (e.g., "john123")
|
||||||
|
- `title` - user's display name (e.g., "John Doe")
|
||||||
|
- `privateKey` - user's private key hash
|
||||||
|
3. Sends packet to server
|
||||||
|
4. Waits for `PacketResult` (0x02)
|
||||||
|
5. **Only on SUCCESS** (resultCode = 0):
|
||||||
|
- Updates local state via `updateUserInformation()`
|
||||||
|
- Shows success message
|
||||||
|
|
||||||
|
**Key Protocol Details:**
|
||||||
|
|
||||||
|
- Packet ID: 0x01 (PacketUserInfo)
|
||||||
|
- Send order: username → title → privateKey
|
||||||
|
- Receive order: username → title → privateKey
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Android Version (Kotlin - Current Implementation)
|
||||||
|
|
||||||
|
#### 1. **Data Storage Layer** (`AccountManager.kt`)
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
data class EncryptedAccount(
|
||||||
|
val publicKey: String,
|
||||||
|
val encryptedPrivateKey: String,
|
||||||
|
val encryptedSeedPhrase: String,
|
||||||
|
val name: String, // User's display name (title)
|
||||||
|
val username: String? // User's username
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
|
||||||
|
- `updateAccountName(publicKey, name)` - Updates display name locally
|
||||||
|
- `updateAccountUsername(publicKey, username)` - Updates username locally
|
||||||
|
- `getAccount(publicKey)` - Retrieves account data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. **Network Layer** (`Packets.kt`)
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
class PacketUserInfo : Packet() {
|
||||||
|
var privateKey: String = ""
|
||||||
|
var username: String = ""
|
||||||
|
var title: String = ""
|
||||||
|
|
||||||
|
override fun getPacketId(): Int = 0x01
|
||||||
|
|
||||||
|
override fun send(): Stream {
|
||||||
|
stream.writeString(username) // Order matches desktop
|
||||||
|
stream.writeString(title)
|
||||||
|
stream.writeString(privateKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. **ViewModel Layer** (`ProfileViewModel.kt`)
|
||||||
|
|
||||||
|
**State Management:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
data class ProfileState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val isSaving: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val saveSuccess: Boolean = false,
|
||||||
|
val logs: List<String> = emptyList()
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Save Profile Flow:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun saveProfile(publicKey, privateKeyHash, name, username) {
|
||||||
|
// 1. Create PacketUserInfo
|
||||||
|
val packet = PacketUserInfo()
|
||||||
|
packet.username = username
|
||||||
|
packet.title = name
|
||||||
|
packet.privateKey = privateKeyHash
|
||||||
|
|
||||||
|
// 2. Send to server
|
||||||
|
ProtocolManager.send(packet)
|
||||||
|
|
||||||
|
// 3. Set 10s timeout (fallback: save locally if no response)
|
||||||
|
|
||||||
|
// 4. Wait for PacketResult (handled in handlePacketResult)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Handle Server Response:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun handlePacketResult(packet: PacketResult) {
|
||||||
|
when (packet.resultCode) {
|
||||||
|
0 -> { // SUCCESS
|
||||||
|
// Mark as successful
|
||||||
|
// Local update happens in ProfileScreen LaunchedEffect
|
||||||
|
_state.value = _state.value.copy(saveSuccess = true)
|
||||||
|
}
|
||||||
|
else -> { // ERROR
|
||||||
|
_state.value = _state.value.copy(error = packet.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fallback Logic:**
|
||||||
|
|
||||||
|
- If server doesn't respond within 10 seconds:
|
||||||
|
- Profile is saved locally anyway
|
||||||
|
- User sees success (but warned in logs)
|
||||||
|
- This prevents data loss if server is down
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. **UI Layer** (`ProfileScreen.kt`)
|
||||||
|
|
||||||
|
**State Tracking:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
var editedName by remember { mutableStateOf(accountName) }
|
||||||
|
var editedUsername by remember { mutableStateOf(accountUsername) }
|
||||||
|
var hasChanges by remember { mutableStateOf(false) }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Save Flow:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
onSave = {
|
||||||
|
// 1. Send to server (via ViewModel)
|
||||||
|
viewModel.saveProfile(
|
||||||
|
publicKey = accountPublicKey,
|
||||||
|
privateKeyHash = accountPrivateKeyHash,
|
||||||
|
name = editedName,
|
||||||
|
username = editedUsername
|
||||||
|
)
|
||||||
|
// Note: Local update happens AFTER success (see below)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success Handler:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
LaunchedEffect(profileState.saveSuccess) {
|
||||||
|
if (profileState.saveSuccess) {
|
||||||
|
// 1. Show success toast
|
||||||
|
Toast.makeText(context, "Profile updated successfully", LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
// 2. Update local database (AFTER server confirms)
|
||||||
|
viewModel.updateLocalProfile(accountPublicKey, editedName, editedUsername)
|
||||||
|
|
||||||
|
// 3. Reset UI state
|
||||||
|
hasChanges = false
|
||||||
|
viewModel.resetSaveState()
|
||||||
|
|
||||||
|
// 4. Notify parent (MainActivity) to reload
|
||||||
|
onSaveProfile(editedName, editedUsername)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5. **Parent Layer** (`MainActivity.kt`)
|
||||||
|
|
||||||
|
**Username Loading:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
var accountUsername by remember { mutableStateOf("") }
|
||||||
|
var reloadTrigger by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
// Load username from DataStore on app start or profile change
|
||||||
|
LaunchedEffect(accountPublicKey, reloadTrigger) {
|
||||||
|
val accountManager = AccountManager(context)
|
||||||
|
val encryptedAccount = accountManager.getAccount(accountPublicKey)
|
||||||
|
accountUsername = encryptedAccount?.username ?: ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Profile Update Callback:**
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
onSaveProfile = { name, username ->
|
||||||
|
// 1. Update local state
|
||||||
|
accountUsername = username
|
||||||
|
|
||||||
|
// 2. Trigger reload from DB (to ensure sync)
|
||||||
|
reloadTrigger++
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Flow Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ USER │
|
||||||
|
│ Edit fields │
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ ProfileScreen.kt │
|
||||||
|
│ - editedName: "John Doe" │
|
||||||
|
│ - editedUsername: "john123" │
|
||||||
|
│ - hasChanges: true │
|
||||||
|
└──────┬───────────────────────────────────┘
|
||||||
|
│ [User clicks Save]
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ ProfileViewModel.saveProfile() │
|
||||||
|
│ 1. Create PacketUserInfo │
|
||||||
|
│ - username: "john123" │
|
||||||
|
│ - title: "John Doe" │
|
||||||
|
│ - privateKey: "abc123..." │
|
||||||
|
│ 2. ProtocolManager.send(packet) │
|
||||||
|
│ 3. Start 10s timeout │
|
||||||
|
└──────┬───────────────────────────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ SERVER (via WebSocket) │
|
||||||
|
│ - Receives 0x01 PacketUserInfo │
|
||||||
|
│ - Validates & saves to database │
|
||||||
|
│ - Sends 0x02 PacketResult │
|
||||||
|
│ - resultCode: 0 (SUCCESS) │
|
||||||
|
│ - message: "" │
|
||||||
|
└──────┬───────────────────────────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ ProfileViewModel.handlePacketResult() │
|
||||||
|
│ - resultCode == 0 → SUCCESS │
|
||||||
|
│ - _state.saveSuccess = true │
|
||||||
|
└──────┬───────────────────────────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ ProfileScreen LaunchedEffect │
|
||||||
|
│ - Observes profileState.saveSuccess │
|
||||||
|
│ - Calls updateLocalProfile() │
|
||||||
|
│ → AccountManager.updateAccountName() │
|
||||||
|
│ → AccountManager.updateAccountUsername()│
|
||||||
|
│ - Shows success Toast │
|
||||||
|
│ - Calls onSaveProfile() callback │
|
||||||
|
└──────┬───────────────────────────────────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ MainActivity │
|
||||||
|
│ - accountUsername = "john123" │
|
||||||
|
│ - reloadTrigger++ │
|
||||||
|
│ - LaunchedEffect re-runs │
|
||||||
|
│ - Loads fresh data from DataStore │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Server Timeout (10 seconds)
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
if (no response after 10s) {
|
||||||
|
// Fallback: save locally anyway
|
||||||
|
updateLocalProfile(publicKey, name, username)
|
||||||
|
_state.value = saveSuccess = true
|
||||||
|
|
||||||
|
// Logs show warning:
|
||||||
|
// "⚠️ No response from server after 10 seconds"
|
||||||
|
// "📝 NOTE: Saving locally as fallback"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Error Response
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
when (packet.resultCode) {
|
||||||
|
else -> {
|
||||||
|
_state.value = _state.value.copy(
|
||||||
|
isSaving = false,
|
||||||
|
error = packet.message
|
||||||
|
)
|
||||||
|
// Shows error Toast in ProfileScreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Differences from Desktop
|
||||||
|
|
||||||
|
| Aspect | Desktop (TypeScript) | Android (Kotlin) |
|
||||||
|
| -------------- | --------------------------- | ----------------------------------- |
|
||||||
|
| Storage | In-memory state | DataStore (persistent) |
|
||||||
|
| Update trigger | On PacketResult SUCCESS | On PacketResult SUCCESS |
|
||||||
|
| Fallback | None (fails if server down) | Saves locally after 10s |
|
||||||
|
| UI feedback | State update → re-render | StateFlow → Toast + callback |
|
||||||
|
| Logging | Console.log | Android Log + ProfileViewModel logs |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [ ] Name updates and persists across app restarts
|
||||||
|
- [ ] Username updates and persists across app restarts
|
||||||
|
- [ ] Changes show in ChatsListScreen header
|
||||||
|
- [ ] Success toast appears on save
|
||||||
|
- [ ] Error toast appears on server error
|
||||||
|
- [ ] Fallback works if server is down (10s timeout)
|
||||||
|
- [ ] Logs show correct packet flow
|
||||||
|
- [ ] hasChanges flag updates correctly
|
||||||
|
- [ ] Save button appears/disappears correctly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
- `ProfileScreen.kt` - UI layer
|
||||||
|
- `ProfileViewModel.kt` - Business logic
|
||||||
|
- `MainActivity.kt` - Parent container
|
||||||
|
- `AccountManager.kt` - Data persistence
|
||||||
|
- `Packets.kt` - Protocol definitions
|
||||||
|
|
||||||
|
### Desktop (Reference)
|
||||||
|
|
||||||
|
- `MyProfile.tsx` - Desktop implementation
|
||||||
|
- `packet.userinfo.ts` - Protocol definition
|
||||||
|
- `useUserInformation.ts` - State management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Specification
|
||||||
|
|
||||||
|
### PacketUserInfo (0x01)
|
||||||
|
|
||||||
|
**Client → Server (Send):**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┬──────────┬───────┬────────────┐
|
||||||
|
│ Packet ID│ Username │ Title │ PrivateKey │
|
||||||
|
│ (0x01) │ (string) │(string)│ (string) │
|
||||||
|
└──────────┴──────────┴───────┴────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server → Client (Receive):**
|
||||||
|
Same format (not typically used - server sends PacketResult instead)
|
||||||
|
|
||||||
|
### PacketResult (0x02)
|
||||||
|
|
||||||
|
**Server → Client:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┬────────────┬─────────┐
|
||||||
|
│ Packet ID│ Result Code│ Message │
|
||||||
|
│ (0x02) │ (int8) │(string) │
|
||||||
|
└──────────┴────────────┴─────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result Codes:**
|
||||||
|
|
||||||
|
- `0` - SUCCESS
|
||||||
|
- Other values indicate errors (message field contains details)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The Android implementation now follows the desktop version pattern:
|
||||||
|
|
||||||
|
1. ✅ Send packet to server first
|
||||||
|
2. ✅ Wait for server confirmation
|
||||||
|
3. ✅ Update local data ONLY after success
|
||||||
|
4. ✅ Proper error handling
|
||||||
|
5. ✅ Fallback for offline/timeout scenarios
|
||||||
|
|
||||||
|
This ensures data consistency between client and server while providing a good user experience even when the server is temporarily unavailable.
|
||||||
Reference in New Issue
Block a user