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>
262 lines
8.4 KiB
Markdown
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)
|