# 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)