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>
This commit is contained in:
261
specs/agent-m-settings-polish.md
Normal file
261
specs/agent-m-settings-polish.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user