chore: merge the develop branch into master #1

Merged
reya merged 43 commits from develop into master 2026-05-23 00:50:13 +00:00
4 changed files with 72 additions and 25 deletions
Showing only changes of commit 0104cf453d - Show all commits

View File

@@ -15,9 +15,11 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
@@ -61,12 +63,14 @@ import androidx.compose.ui.unit.dp
import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_new_chat import coop.composeapp.generated.resources.ic_new_chat
import coop.composeapp.generated.resources.ic_scanner import coop.composeapp.generated.resources.ic_scanner
import coop.composeapp.generated.resources.ic_search
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.PublicKey
import su.reya.coop.LocalNavController
import su.reya.coop.LocalNostrViewModel import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Room import su.reya.coop.Room
import su.reya.coop.Screen
import su.reya.coop.ago import su.reya.coop.ago
import su.reya.coop.shared.Avatar import su.reya.coop.shared.Avatar
import su.reya.coop.shared.displayNameFlow import su.reya.coop.shared.displayNameFlow
@@ -80,6 +84,7 @@ fun HomeScreen(
onNewChat: () -> Unit, onNewChat: () -> Unit,
) { ) {
val clipboard = LocalClipboard.current val clipboard = LocalClipboard.current
val navController = LocalNavController.current
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
val viewModel = LocalNostrViewModel.current val viewModel = LocalNostrViewModel.current
@@ -97,10 +102,30 @@ fun HomeScreen(
var showBottomSheet by remember { mutableStateOf(false) } var showBottomSheet by remember { mutableStateOf(false) }
var isRefreshing by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) }
val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
val qrResult by savedStateHandle
?.getStateFlow<String?>("qr_result", null)
?.collectAsState()
?: remember { mutableStateOf(null) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.getChatRooms() viewModel.getChatRooms()
} }
LaunchedEffect(qrResult) {
qrResult?.let { result ->
runCatching { PublicKey.parse(result) }
.onSuccess { pubkey ->
val roomId = viewModel.createChatRoom(listOf(pubkey))
navController.navigate(Screen.Chat(roomId))
}
.onFailure { e -> println("Failed to parse QR: ${e.message}") }
// Clear the nav state
navController.currentBackStackEntry?.savedStateHandle?.remove<String>("qr_result")
}
}
Scaffold( Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
containerColor = MaterialTheme.colorScheme.surfaceContainer, containerColor = MaterialTheme.colorScheme.surfaceContainer,
@@ -116,15 +141,8 @@ fun HomeScreen(
) )
}, },
actions = { actions = {
// Search
IconButton(onClick = { /* TODO: Open search */ }) {
Icon(
painter = painterResource(Res.drawable.ic_search),
contentDescription = "Search"
)
}
// QR Scanner // QR Scanner
IconButton(onClick = { /* TODO: Open search */ }) { IconButton(onClick = { navController.navigate(Screen.Scan) }) {
Icon( Icon(
painter = painterResource(Res.drawable.ic_scanner), painter = painterResource(Res.drawable.ic_scanner),
contentDescription = "Scanner" contentDescription = "Scanner"
@@ -353,20 +371,37 @@ val defaultMenuList = listOf(
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun BottomMenuList() { fun BottomMenuList() {
val viewModel = LocalNostrViewModel.current
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap), horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
defaultMenuList.forEachIndexed { index, item -> Column(
SegmentedListItem( modifier = Modifier.fillMaxWidth(),
checked = false, verticalArrangement = Arrangement.spacedBy(ListItemDefaults.SegmentedGap),
onCheckedChange = { }, ) {
shapes = ListItemDefaults.segmentedShapes( defaultMenuList.forEachIndexed { index, item ->
index = index, SegmentedListItem(
count = defaultMenuList.size checked = false,
), onCheckedChange = { },
content = { Text(text = item) }, shapes = ListItemDefaults.segmentedShapes(
index = index,
count = defaultMenuList.size
),
content = { Text(text = item) },
)
}
}
Spacer(modifier = Modifier.size(16.dp))
FilledTonalButton(
onClick = { viewModel.logout() },
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError
) )
) {
Text(text = "Logout")
} }
} }
} }

View File

@@ -77,7 +77,9 @@ fun NewChatScreen(
var query by remember { mutableStateOf("") } var query by remember { mutableStateOf("") }
val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
val qrResult by savedStateHandle?.getStateFlow<String?>("qr_result", null)?.collectAsState() val qrResult by savedStateHandle
?.getStateFlow<String?>("qr_result", null)
?.collectAsState()
?: remember { mutableStateOf(null) } ?: remember { mutableStateOf(null) }
LaunchedEffect(query) { LaunchedEffect(query) {
@@ -88,6 +90,7 @@ fun NewChatScreen(
val pubkey = try { val pubkey = try {
PublicKey.parse(query) PublicKey.parse(query)
} catch (e: Exception) { } catch (e: Exception) {
println("Failed to parse npub: ${e.message}")
null null
} }
if (pubkey != null) { if (pubkey != null) {
@@ -109,8 +112,11 @@ fun NewChatScreen(
} }
LaunchedEffect(qrResult) { LaunchedEffect(qrResult) {
qrResult?.let { qrResult?.let { result ->
println("QR result: $it") runCatching { PublicKey.parse(result) }
.onSuccess { pubkey -> selectedReceivers.add(pubkey) }
.onFailure { e -> println("Failed to parse QR: ${e.message}") }
// Clear the nav state
navController.currentBackStackEntry?.savedStateHandle?.remove<String>("qr_result") navController.currentBackStackEntry?.savedStateHandle?.remove<String>("qr_result")
} }
} }

View File

@@ -42,9 +42,7 @@ fun ScanScreen(
val snackbarHostState = LocalSnackbarHostState.current val snackbarHostState = LocalSnackbarHostState.current
val onResult: (String) -> Unit = { result -> val onResult: (String) -> Unit = { result ->
navController.previousBackStackEntry navController.previousBackStackEntry?.savedStateHandle?.set("qr_result", result)
?.savedStateHandle
?.set("qr_result", result)
navController.popBackStack() navController.popBackStack()
} }

View File

@@ -219,6 +219,14 @@ class NostrViewModel(
return nostr.signer.currentUser return nostr.signer.currentUser
} }
fun logout() {
viewModelScope.launch {
secretStore.clear("user_signer")
nostr.signer.switch(Keys.generate())
_emptySecret.value = true
}
}
private suspend fun getOrInitAppKeys(): Keys { private suspend fun getOrInitAppKeys(): Keys {
val secret = secretStore.get("app_keys") val secret = secretStore.get("app_keys")