feat: migrate to navigation3 (#7)

Reviewed-on: https://git.reya.su/reya/coop-mobile/pulls/7
This commit is contained in:
2026-05-31 01:28:09 +00:00
parent b88674d6e2
commit a3ab489d44
16 changed files with 318 additions and 285 deletions
@@ -54,7 +54,7 @@ import coop.composeapp.generated.resources.ic_send
import kotlinx.coroutines.flow.first
import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.UnsignedEvent
import su.reya.coop.LocalNavController
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
@@ -66,12 +66,9 @@ import su.reya.coop.shared.pictureFlow
import su.reya.coop.short
@Composable
fun ChatScreen(
id: Long,
onBack: () -> Unit,
) {
fun ChatScreen(id: Long) {
val snackbarHostState = LocalSnackbarHostState.current
val navController = LocalNavController.current
val navigator = LocalNavigator.current
val viewModel = LocalNostrViewModel.current
val listState = rememberLazyListState()
@@ -153,7 +150,7 @@ fun ChatScreen(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable {
room.members.firstOrNull()?.let { pubkey ->
navController.navigate(Screen.Profile(pubkey.toBech32()))
navigator.navigate(Screen.Profile(pubkey.toBech32()))
}
}
) {
@@ -185,7 +182,7 @@ fun ChatScreen(
}
}
) {
IconButton(onClick = onBack) {
IconButton(onClick = { navigator.goBack() }) {
Icon(
painter = painterResource(Res.drawable.ic_arrow_back),
contentDescription = "Back"
@@ -70,8 +70,9 @@ import coop.composeapp.generated.resources.ic_scanner
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.PublicKey
import su.reya.coop.LocalNavController
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalScanResult
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Room
import su.reya.coop.Screen
@@ -83,11 +84,9 @@ import su.reya.coop.short
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
onOpenChat: (Long) -> Unit,
onNewChat: () -> Unit,
) {
val navController = LocalNavController.current
fun HomeScreen() {
val navigator = LocalNavigator.current
val qrScanResult = LocalScanResult.current
val snackbarHostState = LocalSnackbarHostState.current
val clipboardManager = LocalClipboard.current
val viewModel = LocalNostrViewModel.current
@@ -107,33 +106,24 @@ fun HomeScreen(
var showBottomSheet 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) {
if (qrResult == null) {
viewModel.getChatRooms()
}
viewModel.getChatRooms()
}
LaunchedEffect(qrResult) {
qrResult?.let { result ->
LaunchedEffect(qrScanResult.content) {
qrScanResult.content?.let { result ->
runCatching { PublicKey.parse(result) }
.onSuccess { pubkey ->
try {
val roomId = viewModel.createChatRoom(listOf(pubkey))
navController.navigate(Screen.Chat(roomId))
navigator.navigate(Screen.Chat(roomId))
} catch (e: Exception) {
e.message?.let { snackbarHostState.showSnackbar(it) }
}
}
.onFailure { e -> println("Failed to parse QR: ${e.message}") }
// Clear the nav state
navController.currentBackStackEntry?.savedStateHandle?.remove<String>("qr_result")
qrScanResult.clear()
}
}
@@ -153,7 +143,7 @@ fun HomeScreen(
},
actions = {
// QR Scanner
IconButton(onClick = { navController.navigate(Screen.Scan) }) {
IconButton(onClick = { navigator.navigate(Screen.Scan) }) {
Icon(
painter = painterResource(Res.drawable.ic_scanner),
contentDescription = "Scanner"
@@ -184,7 +174,7 @@ fun HomeScreen(
state = rememberTooltipState(),
) {
ExtendedFloatingActionButton(
onClick = onNewChat,
onClick = { navigator.navigate(Screen.NewChat) },
expanded = expandedFab,
icon = {
Icon(
@@ -261,7 +251,7 @@ fun HomeScreen(
items(chatRooms.toList(), key = { it.id }) { room ->
ChatRoom(
room = room,
onClick = { onOpenChat(room.id) }
onClick = { navigator.navigate(Screen.Chat(room.id)) }
)
}
}
@@ -339,7 +329,7 @@ fun HomeScreen(
}
FilledIconButton(
onClick = {
dismissAndRun { navController.navigate(Screen.MyQr) }
dismissAndRun { navigator.navigate(Screen.MyQr) }
},
shape = MaterialShapes.Square.toShape()
) {
@@ -408,11 +398,11 @@ fun ChatRoom(room: Room, onClick: () -> Unit) {
fun BottomMenuList(
onDismiss: (suspend () -> Unit) -> Unit
) {
val navController = LocalNavController.current
val navigator = LocalNavigator.current
val viewModel = LocalNostrViewModel.current
val defaultMenuList = listOf(
"Relay Management" to { navController.navigate(Screen.Relay) },
"Relay Management" to { navigator.navigate(Screen.Relay) },
"Spams & Blocks" to { },
"Contacts" to { },
"Settings" to { }
@@ -58,8 +58,9 @@ import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.Keys
import rust.nostr.sdk.NostrConnectUri
import rust.nostr.sdk.PublicKey
import su.reya.coop.LocalNavController
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalScanResult
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
import su.reya.coop.shared.Avatar
@@ -69,12 +70,11 @@ import su.reya.coop.short
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ImportScreen(
isLoading: Boolean,
onBack: () -> Unit,
onSave: (secret: String) -> Unit
) {
val snackbarHostState = LocalSnackbarHostState.current
val navController = LocalNavController.current
val navigator = LocalNavigator.current
val qrScanResult = LocalScanResult.current
val focusManager = LocalFocusManager.current
val viewModel = LocalNostrViewModel.current
val scope = rememberCoroutineScope()
@@ -89,19 +89,14 @@ fun ImportScreen(
}
}.collectAsState(null)
val profile = metadata?.asRecord()
val displayName = profile?.displayName ?: profile?.name ?: pubkey?.short() ?: "Unknown"
val picture = profile?.picture
val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
val qrResult by savedStateHandle
?.getStateFlow<String?>("qr_result", null)
?.collectAsState()
?: remember { mutableStateOf(null) }
val isLoading by viewModel.isCreating.collectAsState()
LaunchedEffect(qrResult) {
qrResult?.let { result ->
LaunchedEffect(qrScanResult.content) {
qrScanResult.content?.let { result ->
runCatching {
if (result.startsWith("nsec")) {
Keys.parse(result)
@@ -113,8 +108,9 @@ fun ImportScreen(
}
.onSuccess { it -> secret = result }
.onFailure { e -> println("Failed to parse QR: ${e.message}") }
// Clear the nav state
navController.currentBackStackEntry?.savedStateHandle?.remove<String>("qr_result")
qrScanResult.clear()
}
}
@@ -133,7 +129,7 @@ fun ImportScreen(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
),
navigationIcon = {
IconButton(onClick = onBack) {
IconButton(onClick = { navigator.goBack() }) {
Icon(
painter = painterResource(Res.drawable.ic_arrow_back),
contentDescription = "Back"
@@ -141,7 +137,7 @@ fun ImportScreen(
}
},
actions = {
IconButton(onClick = { navController.navigate(Screen.Scan) }) {
IconButton(onClick = { navigator.navigate(Screen.Scan) }) {
Icon(
painter = painterResource(Res.drawable.ic_scanner),
contentDescription = "Scanner"
@@ -19,13 +19,13 @@ import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back
import io.github.alexzhirkevich.qrose.rememberQrCodePainter
import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
@Composable
fun MyQrScreen(
onBack: () -> Unit
) {
fun MyQrScreen() {
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val viewModel = LocalNostrViewModel.current
val currentUser = viewModel.currentUser() ?: return
@@ -41,7 +41,7 @@ fun MyQrScreen(
)
},
navigationIcon = {
IconButton(onClick = onBack) {
IconButton(onClick = { navigator.goBack() }) {
Icon(
painter = painterResource(Res.drawable.ic_arrow_back),
contentDescription = "Back"
@@ -54,8 +54,9 @@ import coop.composeapp.generated.resources.ic_scanner
import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.PublicKey
import su.reya.coop.LocalNavController
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalScanResult
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
import su.reya.coop.shared.Avatar
@@ -63,11 +64,10 @@ import su.reya.coop.short
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun NewChatScreen(
onBack: () -> Unit,
) {
fun NewChatScreen() {
val snackbarHostState = LocalSnackbarHostState.current
val navController = LocalNavController.current
val navigator = LocalNavigator.current
val qrScanResult = LocalScanResult.current
val viewModel = LocalNostrViewModel.current
val contactList by viewModel.contactList.collectAsState(initial = emptySet())
@@ -76,12 +76,6 @@ fun NewChatScreen(
val selectedReceivers = remember { mutableStateListOf<PublicKey>() }
var query by remember { mutableStateOf("") }
val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
val qrResult by savedStateHandle
?.getStateFlow<String?>("qr_result", null)
?.collectAsState()
?: remember { mutableStateOf(null) }
LaunchedEffect(query) {
if (query.length >= 3) {
delay(500) // 500ms debounce
@@ -111,13 +105,19 @@ fun NewChatScreen(
}
}
LaunchedEffect(qrResult) {
qrResult?.let { result ->
LaunchedEffect(qrScanResult.content) {
qrScanResult.content?.let { result ->
// Verify the content
runCatching { PublicKey.parse(result) }
.onSuccess { pubkey -> selectedReceivers.add(pubkey) }
.onFailure { e -> println("Failed to parse QR: ${e.message}") }
.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")
qrScanResult.clear()
}
}
@@ -136,7 +136,7 @@ fun NewChatScreen(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
),
navigationIcon = {
IconButton(onClick = onBack) {
IconButton(onClick = { navigator.goBack() }) {
Icon(
painter = painterResource(Res.drawable.ic_arrow_back),
contentDescription = "Back"
@@ -144,7 +144,7 @@ fun NewChatScreen(
}
},
actions = {
IconButton(onClick = { navController.navigate(Screen.Scan) }) {
IconButton(onClick = { navigator.navigate(Screen.Scan) }) {
Icon(
painter = painterResource(Res.drawable.ic_scanner),
contentDescription = "Scanner"
@@ -168,7 +168,7 @@ fun NewChatScreen(
ExtendedFloatingActionButton(
onClick = {
val roomId = viewModel.createChatRoom(selectedReceivers.toList())
navController.navigate(Screen.Chat(roomId))
navigator.navigate(Screen.Chat(roomId))
},
expanded = false,
icon = {
@@ -259,7 +259,7 @@ fun NewChatScreen(
selectedReceivers = selectedReceivers,
onContactClick = { pubkey ->
val roomId = viewModel.createChatRoom(listOf(pubkey))
navController.navigate(Screen.Chat(roomId))
navigator.navigate(Screen.Chat(roomId))
},
)
Spacer(modifier = Modifier.size(16.dp))
@@ -270,7 +270,7 @@ fun NewChatScreen(
selectedReceivers = selectedReceivers,
onContactClick = { pubkey ->
val roomId = viewModel.createChatRoom(listOf(pubkey))
navController.navigate(Screen.Chat(roomId))
navigator.navigate(Screen.Chat(roomId))
}
)
Spacer(modifier = Modifier.size(16.dp))
@@ -36,6 +36,7 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.toShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -54,22 +55,27 @@ import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.ic_arrow_back
import coop.composeapp.generated.resources.ic_plus
import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun NewIdentityScreen(
isLoading: Boolean,
onBack: () -> Unit,
onSave: (name: String, bio: String?, picture: Uri?) -> Unit
) {
val snackbarHostState = LocalSnackbarHostState.current
val focusManager = LocalFocusManager.current
val navigator = LocalNavigator.current
val viewModel = LocalNostrViewModel.current
var name by remember { mutableStateOf("") }
var bio by remember { mutableStateOf("") }
var picture by remember { mutableStateOf<Uri?>(null) }
val isLoading by viewModel.isCreating.collectAsState()
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
@@ -88,7 +94,7 @@ fun NewIdentityScreen(
)
},
navigationIcon = {
IconButton(onClick = onBack) {
IconButton(onClick = { navigator.goBack() }) {
Icon(
painter = painterResource(Res.drawable.ic_arrow_back),
contentDescription = "Back"
@@ -37,13 +37,17 @@ import androidx.compose.ui.unit.dp
import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.coop
import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
import su.reya.coop.shared.getExpressiveFontFamily
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun OnboardingScreen(onOpenImport: () -> Unit, onOpenNew: () -> Unit) {
fun OnboardingScreen() {
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val logoPainter = painterResource(Res.drawable.coop)
val expressiveFont = getExpressiveFontFamily()
val annotatedText = buildAnnotatedString {
@@ -127,7 +131,7 @@ fun OnboardingScreen(onOpenImport: () -> Unit, onOpenNew: () -> Unit) {
)
Spacer(modifier = Modifier.size(24.dp))
Button(
onClick = onOpenNew,
onClick = { navigator.navigate(Screen.NewIdentity) },
modifier = Modifier
.fillMaxWidth()
.size(ButtonDefaults.MediumContainerHeight),
@@ -139,7 +143,7 @@ fun OnboardingScreen(onOpenImport: () -> Unit, onOpenNew: () -> Unit) {
}
Spacer(modifier = Modifier.size(8.dp))
OutlinedButton(
onClick = onOpenImport,
onClick = { navigator.navigate(Screen.Import) },
modifier = Modifier
.fillMaxWidth()
.height(ButtonDefaults.MediumContainerHeight),
@@ -44,7 +44,7 @@ import coop.composeapp.generated.resources.ic_share
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.PublicKey
import su.reya.coop.LocalNavController
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
import su.reya.coop.Screen
@@ -54,15 +54,12 @@ import su.reya.coop.short
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ProfileScreen(
onBack: () -> Unit,
pubkey: String
) {
fun ProfileScreen(pubkey: String) {
val pubkey = runCatching { PublicKey.parse(pubkey) }.getOrNull() ?: return
val context = LocalContext.current
val snackbarHostState = LocalSnackbarHostState.current
val navController = LocalNavController.current
val navigator = LocalNavigator.current
val viewModel = LocalNostrViewModel.current
val scope = rememberCoroutineScope()
@@ -88,7 +85,7 @@ fun ProfileScreen(
TopAppBar(
title = { },
navigationIcon = {
IconButton(onClick = onBack) {
IconButton(onClick = { navigator.goBack() }) {
Icon(
painter = painterResource(Res.drawable.ic_arrow_back),
contentDescription = "Back"
@@ -162,7 +159,7 @@ fun ProfileScreen(
scope.launch {
try {
val roomId = viewModel.createChatRoom(listOf(pubkey))
navController.navigate(Screen.Chat(roomId))
navigator.navigate(Screen.Chat(roomId))
} catch (e: Exception) {
e.message?.let { snackbarHostState.showSnackbar(it) }
}
@@ -34,14 +34,14 @@ import coop.composeapp.generated.resources.ic_arrow_back
import org.jetbrains.compose.resources.painterResource
import rust.nostr.sdk.RelayMetadata
import rust.nostr.sdk.RelayUrl
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun RelayScreen(
onBack: () -> Unit
) {
fun RelayScreen() {
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val viewModel = LocalNostrViewModel.current
@@ -80,7 +80,7 @@ fun RelayScreen(
)
},
navigationIcon = {
IconButton(onClick = onBack) {
IconButton(onClick = { navigator.goBack() }) {
Icon(
painter = painterResource(Res.drawable.ic_arrow_back),
contentDescription = "Back"
@@ -30,21 +30,16 @@ import org.jetbrains.compose.resources.painterResource
import org.publicvalue.multiplatform.qrcode.CameraPosition
import org.publicvalue.multiplatform.qrcode.CodeType
import org.publicvalue.multiplatform.qrcode.ScannerWithPermissions
import su.reya.coop.LocalNavController
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalScanResult
import su.reya.coop.LocalSnackbarHostState
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun ScanScreen(
onBack: () -> Unit
) {
val navController = LocalNavController.current
fun ScanScreen() {
val navigator = LocalNavigator.current
val snackbarHostState = LocalSnackbarHostState.current
val onResult: (String) -> Unit = { result ->
navController.previousBackStackEntry?.savedStateHandle?.set("qr_result", result)
navController.popBackStack()
}
val qrScanResult = LocalScanResult.current
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
@@ -57,7 +52,7 @@ fun ScanScreen(
)
},
navigationIcon = {
IconButton(onClick = onBack) {
IconButton(onClick = { navigator.goBack() }) {
Icon(
painter = painterResource(Res.drawable.ic_arrow_back),
contentDescription = "Back"
@@ -76,7 +71,8 @@ fun ScanScreen(
ScannerWithPermissions(
modifier = Modifier.fillMaxSize(),
onScanned = {
onResult(it)
qrScanResult.content = it
navigator.goBack()
true
},
types = listOf(CodeType.QR),