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>
11 KiB
11 KiB
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
- Implement Catppuccin Mocha dark theme in Jetpack Compose Material 3
- Set up Compose Navigation with all app routes
- Update MainActivity to use the theme and navigation
Files to Create
1. ui/theme/Color.kt
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
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
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
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
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