Files
mobile-android/docs/PROFILE_USERNAME_NAME_LOGIC.md

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:

  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)

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)

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

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.