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>
348 lines
11 KiB
Markdown
348 lines
11 KiB
Markdown
# Agent B: Theme + Navigation
|
|
|
|
## Phase: 0 (Scaffolding)
|
|
## Depends on: Agent A (project structure must exist)
|
|
|
|
## Context
|
|
You are building the theme system and navigation graph for "Kecalek" — an encrypted chat Android app.
|
|
The app uses a Catppuccin Mocha dark theme (Signal-like appearance).
|
|
|
|
## Task
|
|
1. Implement Catppuccin Mocha dark theme in Jetpack Compose Material 3
|
|
2. Set up Compose Navigation with all app routes
|
|
3. Update MainActivity to use the theme and navigation
|
|
|
|
## Files to Create
|
|
|
|
### 1. ui/theme/Color.kt
|
|
```kotlin
|
|
package com.kecalek.chat.ui.theme
|
|
|
|
import androidx.compose.ui.graphics.Color
|
|
|
|
// Catppuccin Mocha palette
|
|
object CatppuccinMocha {
|
|
val Rosewater = Color(0xFFF5E0DC)
|
|
val Flamingo = Color(0xFFF2CDCD)
|
|
val Pink = Color(0xFFF5C2E7)
|
|
val Mauve = Color(0xFFCBA6F7)
|
|
val Red = Color(0xFFF38BA8)
|
|
val Maroon = Color(0xFFEBA0AC)
|
|
val Peach = Color(0xFFFAB387)
|
|
val Yellow = Color(0xFFF9E2AF)
|
|
val Green = Color(0xFFA6E3A1)
|
|
val Teal = Color(0xFF94E2D5)
|
|
val Sky = Color(0xFF89DCEB)
|
|
val Sapphire = Color(0xFF74C7EC)
|
|
val Blue = Color(0xFF89B4FA)
|
|
val Lavender = Color(0xFFB4BEFE)
|
|
|
|
val Text = Color(0xFFCDD6F4)
|
|
val Subtext1 = Color(0xFFBAC2DE)
|
|
val Subtext0 = Color(0xFFA6ADC8)
|
|
val Overlay2 = Color(0xFF9399B2)
|
|
val Overlay1 = Color(0xFF7F849C)
|
|
val Overlay0 = Color(0xFF6C7086)
|
|
val Surface2 = Color(0xFF585B70)
|
|
val Surface1 = Color(0xFF45475A)
|
|
val Surface0 = Color(0xFF313244)
|
|
val Base = Color(0xFF1E1E2E)
|
|
val Mantle = Color(0xFF181825)
|
|
val Crust = Color(0xFF11111B)
|
|
}
|
|
```
|
|
|
|
### 2. ui/theme/Type.kt
|
|
```kotlin
|
|
package com.kecalek.chat.ui.theme
|
|
|
|
import androidx.compose.material3.Typography
|
|
import androidx.compose.ui.text.TextStyle
|
|
import androidx.compose.ui.text.font.FontFamily
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.unit.sp
|
|
|
|
val KecalekTypography = Typography(
|
|
headlineLarge = TextStyle(
|
|
fontFamily = FontFamily.SansSerif,
|
|
fontWeight = FontWeight.Bold,
|
|
fontSize = 28.sp,
|
|
lineHeight = 36.sp,
|
|
),
|
|
headlineMedium = TextStyle(
|
|
fontFamily = FontFamily.SansSerif,
|
|
fontWeight = FontWeight.SemiBold,
|
|
fontSize = 22.sp,
|
|
lineHeight = 28.sp,
|
|
),
|
|
titleLarge = TextStyle(
|
|
fontFamily = FontFamily.SansSerif,
|
|
fontWeight = FontWeight.SemiBold,
|
|
fontSize = 18.sp,
|
|
lineHeight = 24.sp,
|
|
),
|
|
titleMedium = TextStyle(
|
|
fontFamily = FontFamily.SansSerif,
|
|
fontWeight = FontWeight.Medium,
|
|
fontSize = 16.sp,
|
|
lineHeight = 22.sp,
|
|
),
|
|
bodyLarge = TextStyle(
|
|
fontFamily = FontFamily.SansSerif,
|
|
fontWeight = FontWeight.Normal,
|
|
fontSize = 16.sp,
|
|
lineHeight = 24.sp,
|
|
),
|
|
bodyMedium = TextStyle(
|
|
fontFamily = FontFamily.SansSerif,
|
|
fontWeight = FontWeight.Normal,
|
|
fontSize = 14.sp,
|
|
lineHeight = 20.sp,
|
|
),
|
|
bodySmall = TextStyle(
|
|
fontFamily = FontFamily.SansSerif,
|
|
fontWeight = FontWeight.Normal,
|
|
fontSize = 12.sp,
|
|
lineHeight = 16.sp,
|
|
),
|
|
labelLarge = TextStyle(
|
|
fontFamily = FontFamily.SansSerif,
|
|
fontWeight = FontWeight.Medium,
|
|
fontSize = 14.sp,
|
|
lineHeight = 20.sp,
|
|
),
|
|
labelSmall = TextStyle(
|
|
fontFamily = FontFamily.SansSerif,
|
|
fontWeight = FontWeight.Medium,
|
|
fontSize = 11.sp,
|
|
lineHeight = 16.sp,
|
|
),
|
|
)
|
|
```
|
|
|
|
### 3. ui/theme/Theme.kt
|
|
```kotlin
|
|
package com.kecalek.chat.ui.theme
|
|
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.darkColorScheme
|
|
import androidx.compose.runtime.Composable
|
|
|
|
private val DarkColorScheme = darkColorScheme(
|
|
primary = CatppuccinMocha.Lavender,
|
|
onPrimary = CatppuccinMocha.Base,
|
|
primaryContainer = CatppuccinMocha.Lavender.copy(alpha = 0.3f),
|
|
onPrimaryContainer = CatppuccinMocha.Text,
|
|
secondary = CatppuccinMocha.Mauve,
|
|
onSecondary = CatppuccinMocha.Base,
|
|
secondaryContainer = CatppuccinMocha.Mauve.copy(alpha = 0.3f),
|
|
onSecondaryContainer = CatppuccinMocha.Text,
|
|
tertiary = CatppuccinMocha.Peach,
|
|
onTertiary = CatppuccinMocha.Base,
|
|
error = CatppuccinMocha.Red,
|
|
onError = CatppuccinMocha.Base,
|
|
errorContainer = CatppuccinMocha.Red.copy(alpha = 0.3f),
|
|
background = CatppuccinMocha.Base,
|
|
onBackground = CatppuccinMocha.Text,
|
|
surface = CatppuccinMocha.Surface0,
|
|
onSurface = CatppuccinMocha.Text,
|
|
surfaceVariant = CatppuccinMocha.Surface1,
|
|
onSurfaceVariant = CatppuccinMocha.Subtext1,
|
|
outline = CatppuccinMocha.Overlay0,
|
|
outlineVariant = CatppuccinMocha.Surface2,
|
|
inverseSurface = CatppuccinMocha.Text,
|
|
inverseOnSurface = CatppuccinMocha.Base,
|
|
)
|
|
|
|
@Composable
|
|
fun KecalekTheme(content: @Composable () -> Unit) {
|
|
MaterialTheme(
|
|
colorScheme = DarkColorScheme,
|
|
typography = KecalekTypography,
|
|
content = content,
|
|
)
|
|
}
|
|
```
|
|
|
|
### 4. ui/navigation/NavGraph.kt
|
|
```kotlin
|
|
package com.kecalek.chat.ui.navigation
|
|
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.navigation.NavHostController
|
|
import androidx.navigation.NavType
|
|
import androidx.navigation.compose.NavHost
|
|
import androidx.navigation.compose.composable
|
|
import androidx.navigation.compose.rememberNavController
|
|
import androidx.navigation.navArgument
|
|
|
|
/**
|
|
* Navigation routes for the app.
|
|
*/
|
|
object Routes {
|
|
const val LOGIN = "login"
|
|
const val REGISTER = "register"
|
|
const val PAIRING = "pairing"
|
|
const val CONVERSATION_LIST = "conversations"
|
|
const val CHAT = "chat/{conversationId}"
|
|
const val GROUP_INFO = "group_info/{conversationId}"
|
|
const val PROFILE = "profile/{userId}"
|
|
const val EDIT_PROFILE = "edit_profile"
|
|
const val VERIFICATION = "verification/{userId}"
|
|
const val DEVICE_LIST = "devices"
|
|
const val SETTINGS = "settings"
|
|
const val IMAGE_VIEWER = "image_viewer/{imageUrl}"
|
|
|
|
// Helper functions to build routes with arguments
|
|
fun chat(conversationId: String) = "chat/$conversationId"
|
|
fun groupInfo(conversationId: String) = "group_info/$conversationId"
|
|
fun profile(userId: String) = "profile/$userId"
|
|
fun verification(userId: String) = "verification/$userId"
|
|
fun imageViewer(imageUrl: String) = "image_viewer/$imageUrl"
|
|
}
|
|
|
|
@Composable
|
|
fun KecalekNavGraph(
|
|
navController: NavHostController = rememberNavController(),
|
|
startDestination: String = Routes.LOGIN,
|
|
) {
|
|
NavHost(
|
|
navController = navController,
|
|
startDestination = startDestination,
|
|
) {
|
|
composable(Routes.LOGIN) {
|
|
// TODO: LoginScreen(navController)
|
|
}
|
|
|
|
composable(Routes.REGISTER) {
|
|
// TODO: RegisterScreen(navController)
|
|
}
|
|
|
|
composable(Routes.PAIRING) {
|
|
// TODO: PairingScreen(navController)
|
|
}
|
|
|
|
composable(Routes.CONVERSATION_LIST) {
|
|
// TODO: ConversationListScreen(navController)
|
|
}
|
|
|
|
composable(
|
|
route = Routes.CHAT,
|
|
arguments = listOf(navArgument("conversationId") { type = NavType.StringType })
|
|
) { backStackEntry ->
|
|
val conversationId = backStackEntry.arguments?.getString("conversationId") ?: return@composable
|
|
// TODO: ChatScreen(conversationId, navController)
|
|
}
|
|
|
|
composable(
|
|
route = Routes.GROUP_INFO,
|
|
arguments = listOf(navArgument("conversationId") { type = NavType.StringType })
|
|
) { backStackEntry ->
|
|
val conversationId = backStackEntry.arguments?.getString("conversationId") ?: return@composable
|
|
// TODO: GroupInfoScreen(conversationId, navController)
|
|
}
|
|
|
|
composable(
|
|
route = Routes.PROFILE,
|
|
arguments = listOf(navArgument("userId") { type = NavType.StringType })
|
|
) { backStackEntry ->
|
|
val userId = backStackEntry.arguments?.getString("userId") ?: return@composable
|
|
// TODO: ProfileScreen(userId, navController)
|
|
}
|
|
|
|
composable(Routes.EDIT_PROFILE) {
|
|
// TODO: EditProfileScreen(navController)
|
|
}
|
|
|
|
composable(
|
|
route = Routes.VERIFICATION,
|
|
arguments = listOf(navArgument("userId") { type = NavType.StringType })
|
|
) { backStackEntry ->
|
|
val userId = backStackEntry.arguments?.getString("userId") ?: return@composable
|
|
// TODO: SafetyNumberScreen(userId, navController)
|
|
}
|
|
|
|
composable(Routes.DEVICE_LIST) {
|
|
// TODO: DeviceListScreen(navController)
|
|
}
|
|
|
|
composable(Routes.SETTINGS) {
|
|
// TODO: SettingsScreen(navController)
|
|
}
|
|
|
|
composable(
|
|
route = Routes.IMAGE_VIEWER,
|
|
arguments = listOf(navArgument("imageUrl") { type = NavType.StringType })
|
|
) { backStackEntry ->
|
|
val imageUrl = backStackEntry.arguments?.getString("imageUrl") ?: return@composable
|
|
// TODO: ImageViewer(imageUrl, navController)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. Update MainActivity.kt
|
|
```kotlin
|
|
package com.kecalek.chat
|
|
|
|
import android.os.Bundle
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.enableEdgeToEdge
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.material3.Surface
|
|
import androidx.compose.ui.Modifier
|
|
import com.kecalek.chat.ui.navigation.KecalekNavGraph
|
|
import com.kecalek.chat.ui.theme.KecalekTheme
|
|
import dagger.hilt.android.AndroidEntryPoint
|
|
|
|
@AndroidEntryPoint
|
|
class MainActivity : ComponentActivity() {
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
enableEdgeToEdge()
|
|
setContent {
|
|
KecalekTheme {
|
|
Surface(modifier = Modifier.fillMaxSize()) {
|
|
KecalekNavGraph()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Color Usage Guide
|
|
Use these mappings consistently across all screens:
|
|
|
|
| UI Element | Color |
|
|
|-----------|-------|
|
|
| Own message bubble background | `CatppuccinMocha.Lavender.copy(alpha = 0.15f)` |
|
|
| Other's message bubble background | `CatppuccinMocha.Surface0` |
|
|
| App background | `CatppuccinMocha.Base` |
|
|
| Card/surface background | `CatppuccinMocha.Surface0` |
|
|
| Primary text | `CatppuccinMocha.Text` |
|
|
| Secondary text | `CatppuccinMocha.Subtext1` |
|
|
| Muted text (timestamps) | `CatppuccinMocha.Overlay1` |
|
|
| Online indicator | `CatppuccinMocha.Green` |
|
|
| Error/delete | `CatppuccinMocha.Red` |
|
|
| Unread badge | `CatppuccinMocha.Lavender` |
|
|
| Links/@mentions | `CatppuccinMocha.Blue` |
|
|
| Verified checkmark | `CatppuccinMocha.Green` |
|
|
| Warning | `CatppuccinMocha.Peach` |
|
|
| Send button | `CatppuccinMocha.Lavender` |
|
|
| Input field background | `CatppuccinMocha.Surface1` |
|
|
| Dividers | `CatppuccinMocha.Surface2` |
|
|
|
|
## Constraints
|
|
- Dark theme only (no light theme for now)
|
|
- Use Material 3 components exclusively
|
|
- Edge-to-edge display (enableEdgeToEdge)
|
|
- All navigation routes defined with TODO placeholders for actual screens
|
|
|
|
## DO NOT
|
|
- Implement any actual screen UI (only TODO placeholders in NavGraph)
|
|
- Add business logic
|
|
- Implement any cryptographic operations
|
|
- Modify build.gradle files
|