package su.reya.coop import android.app.Activity import android.content.Intent import android.os.Build import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MotionScheme import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Typography import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.expressiveLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext import androidx.core.util.Consumer import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay import kotlinx.coroutines.launch import su.reya.coop.screens.ContactListScreen import su.reya.coop.screens.HomeScreen import su.reya.coop.screens.ImportScreen import su.reya.coop.screens.MyQrScreen import su.reya.coop.screens.NewChatScreen import su.reya.coop.screens.NewIdentityScreen import su.reya.coop.screens.OnboardingScreen import su.reya.coop.screens.ProfileScreen import su.reya.coop.screens.RelayScreen import su.reya.coop.screens.RequestListScreen import su.reya.coop.screens.ScanScreen import su.reya.coop.screens.UpdateProfileScreen import su.reya.coop.screens.chat.ChatScreen import su.reya.coop.viewmodel.ChatViewModel import su.reya.coop.viewmodel.NostrViewModel import su.reya.coop.viewmodel.account.AccountViewModel val LocalNostrViewModel = staticCompositionLocalOf { error("No NostrViewModel provided") } val LocalChatViewModel = staticCompositionLocalOf { error("No ChatViewModel provided") } val LocalAccountViewModel = staticCompositionLocalOf { error("No AccountViewModel provided") } val LocalSnackbarHostState = staticCompositionLocalOf { error("No SnackbarHostState provided") } val LocalNavigator = staticCompositionLocalOf { error("No Navigator provided") } val LocalScanResult = staticCompositionLocalOf { error("No QrScanResult provided") } @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable fun App( nostrViewModel: NostrViewModel, chatViewModel: ChatViewModel, accountViewModel: AccountViewModel, ) { val context = LocalContext.current val activity = context as? ComponentActivity val backStack = rememberNavBackStack(Screen.Home) val navigator = remember(backStack) { Navigator(backStack) } val qrScanResult = remember { QrScanResult() } // Get the signer required state val accountState by accountViewModel.state.collectAsStateWithLifecycle() val signerRequired = accountState.signerRequired // Snackbar val snackbarHostState = remember { SnackbarHostState() } // Check if dark theme enabled val darkMode = isSystemInDarkTheme() // Enabled the dynamic color scheme val colorScheme = when { // Enable the dynamic color scheme for Android 12+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { if (isSystemInDarkTheme()) dynamicDarkColorScheme(context) else dynamicLightColorScheme( context ) } // When dark mode is enabled, use the dark color scheme darkMode -> darkColorScheme() // Fallback to the light color scheme else -> expressiveLightColorScheme() } BackHandler(enabled = backStack.size > 1) { navigator.goBack() } LaunchedEffect(Unit) { launch { accountViewModel.errorEvents.collect { message -> snackbarHostState.showSnackbar(message) } } launch { chatViewModel.errorEvents.collect { message -> snackbarHostState.showSnackbar(message) } } launch { nostrViewModel.errorEvents.collect { message -> snackbarHostState.showSnackbar(message) } } } LaunchedEffect(activity) { activity?.let { fun handleIntent(intent: Intent) { val screen = Screen.fromIntent(intent) // Prevent pushing the same screen if (screen != null && backStack.lastOrNull() != screen) { navigator.navigate(screen) } } // Handle the intent that started the Activity handleIntent(it.intent) // Handle new intents while the Activity is running val listener = Consumer { intent -> handleIntent(intent) } it.addOnNewIntentListener(listener) } } LaunchedEffect(backStack.size) { if (backStack.isEmpty()) { (context as? Activity)?.finish() } } LaunchedEffect(signerRequired) { if (signerRequired == true && backStack.last() != Screen.Onboarding) { backStack.clear() backStack.add(Screen.Onboarding) } } MaterialExpressiveTheme( colorScheme = colorScheme, typography = Typography(), motionScheme = MotionScheme.expressive(), ) { CompositionLocalProvider( LocalNostrViewModel provides nostrViewModel, LocalChatViewModel provides chatViewModel, LocalAccountViewModel provides accountViewModel, LocalSnackbarHostState provides snackbarHostState, LocalNavigator provides navigator, LocalScanResult provides qrScanResult, ) { NavDisplay( backStack = backStack, onBack = { if (backStack.size > 1) { backStack.removeLastOrNull() } else { (context as? Activity)?.finish() } }, entryDecorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator() ), entryProvider = entryProvider { entry { HomeScreen() } entry { RequestListScreen() } entry { OnboardingScreen() } entry { ImportScreen() } entry { NewIdentityScreen() } entry { key -> ChatScreen(id = key.id, screening = key.screening) } entry { NewChatScreen() } entry { key -> ProfileScreen(pubkey = key.pubkey) } entry { UpdateProfileScreen() } entry { ScanScreen() } entry { MyQrScreen() } entry { ContactListScreen() } entry { RelayScreen() } } ) } } } class Navigator(private val backStack: NavBackStack) { fun navigate(route: NavKey) { backStack.add(route) } fun goBack() { if (backStack.size > 1) { backStack.removeAt(backStack.lastIndex) } } } class QrScanResult { var content by mutableStateOf(null) fun clear() { content = null } }