update home screen
This commit is contained in:
@@ -26,6 +26,7 @@ kotlin {
|
|||||||
implementation("org.jetbrains.compose.material3:material3:1.11.0-alpha07")
|
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-compose:3.4.0")
|
||||||
implementation("io.coil-kt.coil3:coil-network-okhttp: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 {
|
commonMain.dependencies {
|
||||||
implementation(libs.compose.runtime)
|
implementation(libs.compose.runtime)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -6,13 +6,15 @@ import androidx.compose.material3.MaterialExpressiveTheme
|
|||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.expressiveLightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.compose.NavHost
|
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.NewIdentityScreen
|
||||||
import su.reya.coop.screens.OnboardingScreen
|
import su.reya.coop.screens.OnboardingScreen
|
||||||
|
|
||||||
|
val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
|
||||||
|
error("No NostrViewModel provided")
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun App(dbPath: String) {
|
fun App(dbPath: String) {
|
||||||
@@ -44,7 +50,7 @@ fun App(dbPath: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
darkMode -> darkColorScheme()
|
darkMode -> darkColorScheme()
|
||||||
else -> lightColorScheme()
|
else -> expressiveLightColorScheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -55,6 +61,7 @@ fun App(dbPath: String) {
|
|||||||
MaterialExpressiveTheme(
|
MaterialExpressiveTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
) {
|
) {
|
||||||
|
CompositionLocalProvider(LocalNostrViewModel provides viewModel) {
|
||||||
rememberCoroutineScope()
|
rememberCoroutineScope()
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val hasSecret by viewModel.hasSecret.collectAsState(initial = null)
|
val hasSecret by viewModel.hasSecret.collectAsState(initial = null)
|
||||||
@@ -74,7 +81,7 @@ fun App(dbPath: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show loading screen while initializing
|
// Show loading screen while initializing
|
||||||
if (hasSecret == null) return@MaterialExpressiveTheme
|
if (hasSecret == null) return@CompositionLocalProvider
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
@@ -92,7 +99,7 @@ fun App(dbPath: String) {
|
|||||||
ImportScreen(
|
ImportScreen(
|
||||||
isLoading = isCreating,
|
isLoading = isCreating,
|
||||||
onSave = { secret ->
|
onSave = { secret ->
|
||||||
viewModel.import(secret)
|
viewModel.importIdentity(secret)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -117,4 +124,5 @@ fun App(dbPath: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ sealed interface Screen {
|
|||||||
data object Home : Screen
|
data object Home : Screen
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Chat(val id: String) : Screen
|
data class Chat(val id: Long) : Screen
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object Onboarding : Screen
|
data object Onboarding : Screen
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatScreen(id: String) {
|
fun ChatScreen(id: Long) {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text("Chat Screen (ID: $id)")
|
Text("Chat Screen (ID: $id)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,151 @@
|
|||||||
package su.reya.coop.screens
|
package su.reya.coop.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.AppBarWithSearch
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
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.Scaffold
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
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.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.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
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)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(onOpenChat: (String) -> Unit) {
|
fun HomeScreen(onOpenChat: (Long) -> Unit) {
|
||||||
val scope = rememberCoroutineScope()
|
val viewModel = LocalNostrViewModel.current
|
||||||
val searchState = rememberSearchBarState()
|
val userProfile by viewModel.getUserProfile().collectAsState(initial = null)
|
||||||
val textState = rememberTextFieldState()
|
val chatRooms by viewModel.chatRooms.collectAsState(initial = emptyList())
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
topBar = {
|
topBar = {
|
||||||
AppBarWithSearch(
|
TopAppBar(
|
||||||
state = searchState,
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
inputField = inputField,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
scrollBehavior = scrollBehavior,
|
),
|
||||||
|
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 ->
|
content = { innerPadding ->
|
||||||
LazyColumn(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxWidth()
|
contentAlignment = Alignment.Center
|
||||||
.height(50.dp)
|
|
||||||
) {
|
) {
|
||||||
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class Nostr {
|
|||||||
private set
|
private set
|
||||||
var deviceSigner: NostrSigner? = null
|
var deviceSigner: NostrSigner? = null
|
||||||
private set
|
private set
|
||||||
|
var userPubkey: PublicKey? = null
|
||||||
|
private set
|
||||||
var contactList: List<PublicKey> = emptyList()
|
var contactList: List<PublicKey> = emptyList()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -82,13 +84,23 @@ class Nostr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setKeySigner(keys: Keys) {
|
suspend fun setKeySigner(keys: Keys) {
|
||||||
|
try {
|
||||||
signer = NostrSigner.keys(keys)
|
signer = NostrSigner.keys(keys)
|
||||||
|
userPubkey = signer?.getPublicKey()
|
||||||
getUserMetadata()
|
getUserMetadata()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Failed to set signer: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setRemoteSigner(remote: NostrConnect) {
|
suspend fun setRemoteSigner(remote: NostrConnect) {
|
||||||
|
try {
|
||||||
signer = NostrSigner.nostrConnect(remote)
|
signer = NostrSigner.nostrConnect(remote)
|
||||||
|
userPubkey = signer?.getPublicKey()
|
||||||
getUserMetadata()
|
getUserMetadata()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Failed to set remote signer: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isSignedByUser(event: Event): Boolean {
|
suspend fun isSignedByUser(event: Event): Boolean {
|
||||||
|
|||||||
@@ -90,6 +90,14 @@ class NostrViewModel(
|
|||||||
_metadataStore.getOrPut(pubkey) { MutableStateFlow(null) }.value = metadata
|
_metadataStore.getOrPut(pubkey) { MutableStateFlow(null) }.value = metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getUserProfile(): StateFlow<Metadata?> {
|
||||||
|
return try {
|
||||||
|
getMetadata(nostr.userPubkey!!)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
MutableStateFlow(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun initAndConnect(dbPath: String) {
|
fun initAndConnect(dbPath: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
@@ -175,10 +183,11 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun import(secret: String) {
|
fun importIdentity(secret: String) {
|
||||||
// TODO: Implement import
|
// TODO: Implement import
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getChatRooms() {
|
fun getChatRooms() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user