unify error handler

This commit is contained in:
2026-07-02 13:36:12 +07:00
parent 666f5c1795
commit e5f3078c9c
10 changed files with 396 additions and 297 deletions

View File

@@ -52,6 +52,10 @@ val LocalNostrViewModel = staticCompositionLocalOf<NostrViewModel> {
error("No NostrViewModel provided")
}
val LocalAuthViewModel = staticCompositionLocalOf<AuthViewModel> {
error("No AuthViewModel provided")
}
val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
error("No SnackbarHostState provided")
}
@@ -68,6 +72,7 @@ val LocalScanResult = staticCompositionLocalOf<QrScanResult> {
@Composable
fun App(
nostrViewModel: NostrViewModel,
authViewModel: AuthViewModel,
) {
val context = LocalContext.current
val activity = context as? ComponentActivity
@@ -76,7 +81,8 @@ fun App(
val qrScanResult = remember { QrScanResult() }
// Get the signer required state
val signerRequired by nostrViewModel.signerRequired.collectAsStateWithLifecycle()
val authState by authViewModel.state.collectAsStateWithLifecycle()
val signerRequired = authState.signerRequired
// Snackbar
val snackbarHostState = remember { SnackbarHostState() }
@@ -103,7 +109,7 @@ fun App(
}
LaunchedEffect(Unit) {
nostrViewModel.errorEvents.collect { message ->
ErrorManager.errors.collect { message ->
snackbarHostState.showSnackbar(message)
}
}
@@ -147,6 +153,7 @@ fun App(
) {
CompositionLocalProvider(
LocalNostrViewModel provides nostrViewModel,
LocalAuthViewModel provides authViewModel,
LocalSnackbarHostState provides snackbarHostState,
LocalNavigator provides navigator,
LocalScanResult provides qrScanResult,

View File

@@ -27,11 +27,14 @@ class MainActivity : ComponentActivity() {
AndroidExternalSigner(this@MainActivity, externalSignerLauncher)
private val secretStore = SecretStore(this@MainActivity)
private val nostrViewModel =
NostrViewModel(NostrManager.instance, secretStore, androidSigner)
NostrViewModel(NostrManager.instance)
private val authViewModel =
AuthViewModel(NostrManager.instance, secretStore, androidSigner)
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when {
modelClass.isAssignableFrom(NostrViewModel::class.java) -> nostrViewModel
modelClass.isAssignableFrom(AuthViewModel::class.java) -> authViewModel
else -> throw IllegalArgumentException("Unknown ViewModel class")
} as T
}
@@ -39,6 +42,7 @@ class MainActivity : ComponentActivity() {
}
private val nostrViewModel: NostrViewModel by viewModels { factory }
private val authViewModel: AuthViewModel by viewModels { factory }
override fun onCreate(savedInstanceState: Bundle?) {
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
@@ -77,12 +81,13 @@ class MainActivity : ComponentActivity() {
// Keep the splash screen visible until the signer check is complete
splashScreen.setKeepOnScreenCondition {
nostrViewModel.signerRequired.value == null
authViewModel.state.value.signerRequired == null
}
setContent {
App(
nostrViewModel = nostrViewModel,
authViewModel = authViewModel,
)
}
}

View File

@@ -90,6 +90,7 @@ 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.LocalAuthViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalScanResult
@@ -111,6 +112,7 @@ fun HomeScreen() {
val snackbarHostState = LocalSnackbarHostState.current
val clipboardManager = LocalClipboard.current
val nostrViewModel = LocalNostrViewModel.current
val authViewModel = LocalAuthViewModel.current
val scope = rememberCoroutineScope()
val sheetState = rememberModalBottomSheetState(true)
@@ -123,7 +125,9 @@ fun HomeScreen() {
val isRelayListEmpty by nostrViewModel.isRelayListEmpty.collectAsStateWithLifecycle()
val isSyncing by nostrViewModel.isSyncing.collectAsStateWithLifecycle()
val isPartialProcessedGiftWrap by nostrViewModel.isPartialProcessedGiftWrap.collectAsStateWithLifecycle()
val isBannerDismissed by nostrViewModel.isNotificationBannerDismissed.collectAsState()
val authState by authViewModel.state.collectAsStateWithLifecycle()
val isBannerDismissed = authState.isNotificationBannerDismissed
val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
var showBottomSheet by remember { mutableStateOf(false) }
@@ -275,7 +279,7 @@ fun HomeScreen() {
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TextButton(
onClick = { nostrViewModel.dismissNotificationBanner() },
onClick = { authViewModel.dismissNotificationBanner() },
modifier = Modifier.weight(1f),
) {
Text(text = "Maybe later")
@@ -615,6 +619,7 @@ fun HomeScreen() {
fun NewRequests(requests: List<Room>) {
val navigator = LocalNavigator.current
val nostrViewModel = LocalNostrViewModel.current
val authViewModel = LocalAuthViewModel.current
val total = requests.size
val firstRoom = requests.getOrNull(0)
@@ -693,6 +698,7 @@ fun NewRequests(requests: List<Room>) {
@Composable
fun ChatRoom(room: Room, onClick: () -> Unit) {
val nostrViewModel = LocalNostrViewModel.current
val authViewModel = LocalAuthViewModel.current
val roomState by room.rememberUiState(nostrViewModel)
ListItem(
@@ -740,6 +746,7 @@ fun BottomMenuList(
) {
val navigator = LocalNavigator.current
val nostrViewModel = LocalNostrViewModel.current
val authViewModel = LocalAuthViewModel.current
val defaultMenuList = listOf(
"Update Profile" to { navigator.navigate(Screen.UpdateProfile) },
@@ -769,7 +776,7 @@ fun BottomMenuList(
}
Spacer(modifier = Modifier.size(16.dp))
FilledTonalButton(
onClick = { onDismiss { nostrViewModel.logout() } },
onClick = { onDismiss { authViewModel.logout(onLogout = nostrViewModel::resetInternalState) } },
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError

View File

@@ -58,6 +58,7 @@ 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.LocalAuthViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalScanResult
@@ -74,10 +75,12 @@ fun ImportScreen() {
val qrScanResult = LocalScanResult.current
val focusManager = LocalFocusManager.current
val nostrViewModel = LocalNostrViewModel.current
val authViewModel = LocalAuthViewModel.current
val scope = rememberCoroutineScope()
val isBusy by nostrViewModel.isBusy.collectAsStateWithLifecycle(false)
val authState by authViewModel.state.collectAsStateWithLifecycle()
val isBusy = authState.isBusy
var secret by remember { mutableStateOf("") }
var pubkey by remember { mutableStateOf<PublicKey?>(null) }
@@ -240,10 +243,10 @@ fun ImportScreen() {
onClick = {
scope.launch {
if (pubkey == null) {
nostrViewModel.verifyIdentity(secret).let { pubkey = it }
authViewModel.verifyIdentity(secret).let { pubkey = it }
} else {
// Import the identity
nostrViewModel.importIdentity(secret)
authViewModel.importIdentity(secret)
// Navigate to the home screen
navigator.navigate(Screen.Home)
}

View File

@@ -5,6 +5,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import su.reya.coop.LocalAuthViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.Screen
@@ -13,9 +14,12 @@ import su.reya.coop.shared.ProfileEditor
@Composable
fun NewIdentityScreen() {
val nostrViewModel = LocalNostrViewModel.current
val authViewModel = LocalAuthViewModel.current
val navigator = LocalNavigator.current
val scope = rememberCoroutineScope()
val isBusy by nostrViewModel.isBusy.collectAsStateWithLifecycle(false)
val authState by authViewModel.state.collectAsStateWithLifecycle()
val isBusy = authState.isBusy
ProfileEditor(
title = "Create a new identity",
@@ -24,7 +28,7 @@ fun NewIdentityScreen() {
onBack = { navigator.goBack() },
onConfirm = { name, bio, bytes, type ->
scope.launch {
nostrViewModel.createIdentity(name, bio, bytes, type)
authViewModel.createIdentity(name, bio, bytes, type)
navigator.navigate(Screen.Home)
}
}

View File

@@ -45,6 +45,7 @@ import coop.composeapp.generated.resources.Res
import coop.composeapp.generated.resources.coop
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.painterResource
import su.reya.coop.LocalAuthViewModel
import su.reya.coop.LocalNavigator
import su.reya.coop.LocalNostrViewModel
import su.reya.coop.LocalSnackbarHostState
@@ -58,6 +59,7 @@ fun OnboardingScreen() {
val snackbarHostState = LocalSnackbarHostState.current
val navigator = LocalNavigator.current
val nostrViewModel = LocalNostrViewModel.current
val authViewModel = LocalAuthViewModel.current
val scope = rememberCoroutineScope()
val logoPainter = painterResource(Res.drawable.coop)
@@ -158,9 +160,9 @@ fun OnboardingScreen() {
FilledTonalButton(
onClick = {
scope.launch {
if (nostrViewModel.isExternalSignerAvailable()) {
if (authViewModel.isExternalSignerAvailable()) {
try {
nostrViewModel.connectExternalSigner()
authViewModel.connectExternalSigner()
navigator.navigate(Screen.Home)
} catch (e: Exception) {
e.message?.let { snackbarHostState.showSnackbar(it) }