12 KiB
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:
- User edits
title(name) andusernamein UI - On save, creates
PacketUserInfowith:username- user's username (e.g., "john123")title- user's display name (e.g., "John Doe")privateKey- user's private key hash
- Sends packet to server
- Waits for
PacketResult(0x02) - Only on SUCCESS (resultCode = 0):
- Updates local state via
updateUserInformation() - Shows success message
- Updates local state via
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)
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 locallyupdateAccountUsername(publicKey, username)- Updates username locallygetAccount(publicKey)- Retrieves account data
2. Network Layer (Packets.kt)
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:
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:
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:
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:
var editedName by remember { mutableStateOf(accountName) }
var editedUsername by remember { mutableStateOf(accountUsername) }
var hasChanges by remember { mutableStateOf(false) }
Save Flow:
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:
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:
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:
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)
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
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 layerProfileViewModel.kt- Business logicMainActivity.kt- Parent containerAccountManager.kt- Data persistencePackets.kt- Protocol definitions
Desktop (Reference)
MyProfile.tsx- Desktop implementationpacket.userinfo.ts- Protocol definitionuseUserInformation.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:
- ✅ Send packet to server first
- ✅ Wait for server confirmation
- ✅ Update local data ONLY after success
- ✅ Proper error handling
- ✅ 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.