401 lines
12 KiB
Markdown
401 lines
12 KiB
Markdown
# 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.
|