update home screen

This commit is contained in:
2026-05-06 08:39:19 +07:00
parent 109fe28d48
commit 06252ecbb4
9 changed files with 227 additions and 104 deletions

View File

@@ -26,6 +26,7 @@ kotlin {
implementation("org.jetbrains.compose.material3:material3:1.11.0-alpha07")
implementation("io.coil-kt.coil3:coil-compose:3.4.0")
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
implementation("su.reya:nostr-sdk-kmp:0.1.2")
}
commonMain.dependencies {
implementation(libs.compose.runtime)

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M234,684Q285,645 348,622.5Q411,600 480,600Q549,600 612,622.5Q675,645 726,684Q761,643 780.5,591Q800,539 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,539 179.5,591Q199,643 234,684ZM380.5,479.5Q340,439 340,380Q340,321 380.5,280.5Q421,240 480,240Q539,240 579.5,280.5Q620,321 620,380Q620,439 579.5,479.5Q539,520 480,520Q421,520 380.5,479.5ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM580,784.5Q627,769 666,740Q627,711 580,695.5Q533,680 480,680Q427,680 380,695.5Q333,711 294,740Q333,769 380,784.5Q427,800 480,800Q533,800 580,784.5ZM523,423Q540,406 540,380Q540,354 523,337Q506,320 480,320Q454,320 437,337Q420,354 420,380Q420,406 437,423Q454,440 480,440Q506,440 523,423ZM480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380ZM480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M784,840L532,588Q502,612 463,626Q424,640 380,640Q271,640 195.5,564.5Q120,489 120,380Q120,271 195.5,195.5Q271,120 380,120Q489,120 564.5,195.5Q640,271 640,380Q640,424 626,463Q612,502 588,532L840,784L784,840ZM380,560Q455,560 507.5,507.5Q560,455 560,380Q560,305 507.5,252.5Q455,200 380,200Q305,200 252.5,252.5Q200,305 200,380Q200,455 252.5,507.5Q305,560 380,560Z" />
</vector>

View File

@@ -6,13 +6,15 @@ import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.expressiveLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
@@ -26,6 +28,10 @@ import su.reya.coop.screens.ImportScreen
import su.reya.coop.screens.NewIdentityScreen
import su.reya.coop.screens.OnboardingScreen
val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
error("No NostrViewModel provided")
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun App(dbPath: String) {
@@ -44,7 +50,7 @@ fun App(dbPath: String) {
}
darkMode -> darkColorScheme()
else -> lightColorScheme()
else -> expressiveLightColorScheme()
}
LaunchedEffect(Unit) {
@@ -55,65 +61,67 @@ fun App(dbPath: String) {
MaterialExpressiveTheme(
colorScheme = colorScheme,
) {
rememberCoroutineScope()
val navController = rememberNavController()
val hasSecret by viewModel.hasSecret.collectAsState(initial = null)
CompositionLocalProvider(LocalNostrViewModel provides viewModel) {
rememberCoroutineScope()
val navController = rememberNavController()
val hasSecret by viewModel.hasSecret.collectAsState(initial = null)
LaunchedEffect(hasSecret) {
// Navigate to the home screen if the secret is already set
if (hasSecret == true) {
// Start a background notification handler
viewModel.startNotificationHandler()
// Get chat rooms
viewModel.getChatRooms()
// Navigate to the home screen
navController.navigate(Screen.Home) {
popUpTo(Screen.Onboarding) { inclusive = true }
LaunchedEffect(hasSecret) {
// Navigate to the home screen if the secret is already set
if (hasSecret == true) {
// Start a background notification handler
viewModel.startNotificationHandler()
// Get chat rooms
viewModel.getChatRooms()
// Navigate to the home screen
navController.navigate(Screen.Home) {
popUpTo(Screen.Onboarding) { inclusive = true }
}
}
}
}
// Show loading screen while initializing
if (hasSecret == null) return@MaterialExpressiveTheme
// Show loading screen while initializing
if (hasSecret == null) return@CompositionLocalProvider
NavHost(
navController = navController,
startDestination = if (hasSecret == true) Screen.Home else Screen.Onboarding
) {
composable<Screen.Onboarding> { backStackEntry ->
OnboardingScreen(
onOpenImport = { navController.navigate(Screen.Import) },
onOpenNew = { navController.navigate(Screen.NewIdentity) }
)
}
composable<Screen.Import> { backStackEntry ->
val isCreating by viewModel.isCreating.collectAsState()
NavHost(
navController = navController,
startDestination = if (hasSecret == true) Screen.Home else Screen.Onboarding
) {
composable<Screen.Onboarding> { backStackEntry ->
OnboardingScreen(
onOpenImport = { navController.navigate(Screen.Import) },
onOpenNew = { navController.navigate(Screen.NewIdentity) }
)
}
composable<Screen.Import> { backStackEntry ->
val isCreating by viewModel.isCreating.collectAsState()
ImportScreen(
isLoading = isCreating,
onSave = { secret ->
viewModel.import(secret)
}
)
}
composable<Screen.NewIdentity> { backStackEntry ->
val isCreating by viewModel.isCreating.collectAsState()
ImportScreen(
isLoading = isCreating,
onSave = { secret ->
viewModel.importIdentity(secret)
}
)
}
composable<Screen.NewIdentity> { backStackEntry ->
val isCreating by viewModel.isCreating.collectAsState()
NewIdentityScreen(
isLoading = isCreating,
onSave = { name, bio, uri ->
viewModel.createIdentity(name, bio, uri?.toString())
}
)
}
composable<Screen.Home> { backStackEntry ->
HomeScreen(
onOpenChat = { id -> navController.navigate(Screen.Chat(id)) }
)
}
composable<Screen.Chat> { backStackEntry ->
val chat: Screen.Chat = backStackEntry.toRoute()
ChatScreen(id = chat.id)
NewIdentityScreen(
isLoading = isCreating,
onSave = { name, bio, uri ->
viewModel.createIdentity(name, bio, uri?.toString())
}
)
}
composable<Screen.Home> { backStackEntry ->
HomeScreen(
onOpenChat = { id -> navController.navigate(Screen.Chat(id)) }
)
}
composable<Screen.Chat> { backStackEntry ->
val chat: Screen.Chat = backStackEntry.toRoute()
ChatScreen(id = chat.id)
}
}
}
}

View File

@@ -7,7 +7,7 @@ sealed interface Screen {
data object Home : Screen
@Serializable
data class Chat(val id: String) : Screen
data class Chat(val id: Long) : Screen
@Serializable
data object Onboarding : Screen

View File

@@ -8,7 +8,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun ChatScreen(id: String) {
fun ChatScreen(id: Long) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("Chat Screen (ID: $id)")
}

View File

@@ -1,76 +1,151 @@
package su.reya.coop.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.AppBarWithSearch
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberSearchBarState
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import coil3.compose.AsyncImage
import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_avatar
import coop.composeapp.generated.resources.ic_search
import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.Room
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(onOpenChat: (String) -> Unit) {
val scope = rememberCoroutineScope()
val searchState = rememberSearchBarState()
val textState = rememberTextFieldState()
val scrollBehavior = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior()
val inputField =
@Composable {
SearchBarDefaults.InputField(
textFieldState = textState,
searchBarState = searchState,
onSearch = { scope.launch { searchState.animateToCollapsed() } },
placeholder = {
Text(
modifier = Modifier.clearAndSetSemantics() {},
text = "Find or start a conversation"
)
},
)
}
fun HomeScreen(onOpenChat: (Long) -> Unit) {
val viewModel = LocalNostrViewModel.current
val userProfile by viewModel.getUserProfile().collectAsState(initial = null)
val chatRooms by viewModel.chatRooms.collectAsState(initial = emptyList())
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
containerColor = MaterialTheme.colorScheme.surfaceContainer,
topBar = {
AppBarWithSearch(
state = searchState,
inputField = inputField,
scrollBehavior = scrollBehavior,
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
),
title = {
Text(
text = "Coop",
style = MaterialTheme.typography.titleMediumEmphasized
)
},
actions = {
// Search
IconButton(onClick = { /* TODO: Open search */ }) {
Icon(
painter = painterResource(Res.drawable.ic_search),
contentDescription = "Search"
)
}
// User
IconButton(onClick = { /* TODO: Open profile */ }) {
if (userProfile?.asRecord()?.picture != null) {
AsyncImage(
model = userProfile?.asRecord()?.picture,
contentDescription = "User Avatar",
modifier = Modifier
.size(32.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
} else {
Icon(
painter = painterResource(Res.drawable.ic_avatar),
contentDescription = "User"
)
}
}
}
)
},
content = { innerPadding ->
LazyColumn(
Surface(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
.padding(top = innerPadding.calculateTopPadding()),
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
) {
items(count = 100) { index ->
if (chatRooms.isEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Chat $index")
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "No chats yet",
style = MaterialTheme.typography.titleLargeEmphasized,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = "Your conversations will appear here.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline
)
}
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(chatRooms.toList(), key = { it.id }) { room ->
ChatRoom(
room = room,
onClick = { onOpenChat(room.id) }
)
}
}
}
}
},
)
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ChatRoom(room: Room, onClick: () -> Unit) {
val title = room.subject ?: "Room"
ListItem(
modifier = Modifier.clickable { onClick },
headlineContent = {
Text(
text = title,
style = MaterialTheme.typography.titleMediumEmphasized
)
},
colors = ListItemDefaults.colors(
containerColor = MaterialTheme.colorScheme.surface
)
)
}