Files
Kecalek/specs/agent-m-settings-polish.md
filip fe861cfafa Initial commit: Kecalek Android client
Complete Android client for encrypted chat platform.
78+ Kotlin files: crypto (X3DH, Double Ratchet, AES-GCM, Ed25519, X25519,
RSA-PSS), network (TCP/TLS, 50 endpoints), Hilt DI, Room+SQLCipher DB,
Jetpack Compose UI with Catppuccin Mocha theme.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 01:19:17 +01:00

262 lines
8.4 KiB
Markdown

# Agent M: Settings + Polish
## Phase: 4 (Feature Completion)
## Depends on: Agent B (Theme + Navigation), Agent D (Auth for server config)
## Context
Final polish: settings screen, connection indicator, privacy lock screen, error handling UI.
## Task
Create settings screen, reusable UI components for connection status, error handling, and privacy lock.
## Files to Create
### 1. ui/settings/SettingsScreen.kt
Jetpack Compose settings screen:
- **Top bar**: Back arrow + "Settings" title
- **Server Configuration section**:
- Host text field (current value displayed)
- Port text field
- TLS toggle switch
- "Save" button
- **Account section**:
- "Change Username" button
- "Change Password" button
- "Key Rotation" button (Peach/warning color with info text)
- "My Devices" button (navigates to DeviceList)
- **Privacy section**:
- "Privacy Lock" toggle (enables biometric/PIN lock)
- "Lock Timeout" selector (30s, 1min, 5min)
- **About section**:
- App version ("Kecalek v0.8.5")
- "End-to-end encrypted" info text
- "Signal Protocol (X3DH + Double Ratchet)"
- **Danger zone**:
- "Delete Account" button (Red, with confirmation dialog)
- "Logout" button (Red outlined)
### 2. ui/components/ConnectionIndicator.kt
```kotlin
package com.kecalek.chat.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.kecalek.chat.ui.theme.CatppuccinMocha
enum class ConnectionStatus {
CONNECTED,
DISCONNECTED,
RECONNECTING,
}
/**
* Small dot indicator showing connection status.
* Green = connected, Red = disconnected, Orange = reconnecting.
*/
@Composable
fun ConnectionIndicator(
status: ConnectionStatus,
showLabel: Boolean = false,
modifier: Modifier = Modifier,
) {
val color = when (status) {
ConnectionStatus.CONNECTED -> CatppuccinMocha.Green
ConnectionStatus.DISCONNECTED -> CatppuccinMocha.Red
ConnectionStatus.RECONNECTING -> CatppuccinMocha.Peach
}
val label = when (status) {
ConnectionStatus.CONNECTED -> "Connected"
ConnectionStatus.DISCONNECTED -> "Disconnected"
ConnectionStatus.RECONNECTING -> "Reconnecting..."
}
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
Box(
modifier = Modifier
.size(8.dp)
.background(color, CircleShape)
)
if (showLabel) {
Spacer(Modifier.width(4.dp))
Text(
text = label,
color = color,
fontSize = 11.sp,
)
}
}
}
```
### 3. ui/components/SearchBar.kt
```kotlin
package com.kecalek.chat.ui.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.kecalek.chat.ui.theme.CatppuccinMocha
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
onClose: () -> Unit,
placeholder: String = "Search...",
modifier: Modifier = Modifier,
) {
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
placeholder = { Text(placeholder, color = CatppuccinMocha.Overlay1) },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = "Search") },
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = { onQueryChange("") }) {
Icon(Icons.Default.Close, contentDescription = "Clear")
}
}
},
singleLine = true,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = CatppuccinMocha.Lavender,
unfocusedBorderColor = CatppuccinMocha.Surface2,
focusedContainerColor = CatppuccinMocha.Surface1,
unfocusedContainerColor = CatppuccinMocha.Surface1,
),
)
}
```
### 4. ui/auth/PrivacyLockScreen.kt
Privacy lock overlay (anti-forensic feature):
- **Full-screen overlay** covering all content
- **Dark background** (Base color, 98% opacity)
- **Lock icon** centered (large, 64dp)
- **Password field** (or biometric prompt)
- **"Unlock" button** (Lavender)
- **Behavior**:
- Shown when app loses focus for > 30 seconds (configurable)
- Immediate dark overlay on app background (hides content)
- After timeout: requires password/biometric to unlock
- Password verified by decrypting identity key (ECP1 format)
### 5. ui/components/ErrorSnackbar.kt
```kotlin
package com.kecalek.chat.ui.components
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.kecalek.chat.ui.theme.CatppuccinMocha
@Composable
fun ErrorSnackbar(
snackbarHostState: SnackbarHostState,
modifier: Modifier = Modifier,
) {
SnackbarHost(
hostState = snackbarHostState,
modifier = modifier,
) { data ->
Snackbar(
containerColor = CatppuccinMocha.Red.copy(alpha = 0.9f),
contentColor = CatppuccinMocha.Text,
actionColor = CatppuccinMocha.Rosewater,
snackbarData = data,
)
}
}
```
### 6. ui/components/ConfirmationDialog.kt
```kotlin
package com.kecalek.chat.ui.components
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import com.kecalek.chat.ui.theme.CatppuccinMocha
@Composable
fun ConfirmationDialog(
title: String,
message: String,
confirmText: String = "Confirm",
dismissText: String = "Cancel",
isDestructive: Boolean = false,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
containerColor = CatppuccinMocha.Surface0,
titleContentColor = CatppuccinMocha.Text,
textContentColor = CatppuccinMocha.Subtext1,
title = { Text(title) },
text = { Text(message) },
confirmButton = {
FilledTonalButton(
onClick = onConfirm,
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = if (isDestructive) CatppuccinMocha.Red else CatppuccinMocha.Lavender,
contentColor = if (isDestructive) CatppuccinMocha.Text else CatppuccinMocha.Base,
),
) {
Text(confirmText)
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(dismissText, color = CatppuccinMocha.Subtext1)
}
},
)
}
```
## Constraints
- Settings persist via DataStore or SharedPreferences
- Server config changes require reconnection
- Privacy lock uses BiometricPrompt API
- Connection indicator should be lightweight (placed in top bars)
- Confirmation dialogs for all destructive actions
- Error snackbar theming matches Catppuccin Mocha
## DO NOT
- Implement actual server reconnection logic
- Handle biometric authentication implementation (just the UI shell)
- Implement password verification against ECP1 keys
- Implement actual account deletion (server endpoint doesn't exist yet)