package com.garfiec.librechat import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kermit.Logger import com.garfiec.librechat.core.common.network.ConnectivityObserver import com.garfiec.librechat.core.data.datastore.ThemeDataStore import com.garfiec.librechat.core.data.datastore.ThemeMode import com.garfiec.librechat.core.ui.theme.LibreChatTheme import com.garfiec.librechat.feature.chat.ShareIntentConsumer import com.garfiec.librechat.feature.chat.SharedContent import com.garfiec.librechat.navigation.LibreChatNavHost import org.koin.android.ext.android.inject class MainActivity : ComponentActivity() { private val connectivityObserver: ConnectivityObserver by inject() private val themeDataStore: ThemeDataStore by inject() private var deepLinkUri by mutableStateOf(null) /** * Incremented each time a share intent arrives. * The NavDisplay observes this and either: * - Navigates to NewChat if the user is not on a chat screen, or * - Lets the active ChatViewModel consume the shared content in-place if already on a chat screen. */ private var shareNavigationTrigger by mutableStateOf(0) @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() handleIntent(intent) setContent { val windowSizeClass = calculateWindowSizeClass(this) val isConnected by connectivityObserver.isConnected.collectAsStateWithLifecycle(initialValue = true) val themeMode by themeDataStore.themeMode.collectAsStateWithLifecycle(initialValue = themeDataStore.initialThemeMode) val darkTheme = when (themeMode) { ThemeMode.LIGHT -> false ThemeMode.DARK -> true ThemeMode.SYSTEM -> isSystemInDarkTheme() } // Update system bar icon colors to match the app's resolved theme. // When light theme: dark icons on light background (isAppearanceLight = true). // When dark theme: light icons on dark background (isAppearanceLight = false). // This ensures correct visibility even when the user overrides the system theme. SideEffect { val insetsController = WindowCompat.getInsetsController(window, window.decorView) insetsController.isAppearanceLightStatusBars = !darkTheme insetsController.isAppearanceLightNavigationBars = !darkTheme } LibreChatTheme(darkTheme = darkTheme) { Surface(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) { AnimatedVisibility( visible = !isConnected, enter = expandVertically(), exit = shrinkVertically(), ) { Box( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.errorContainer) .padding(vertical = 6.dp, horizontal = 16.dp), contentAlignment = Alignment.Center, ) { Text( text = stringResource(R.string.no_connection), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onErrorContainer, ) } } LibreChatNavHost( windowSizeClass = windowSizeClass, deepLinkUri = deepLinkUri, onDeepLinkConsumed = { deepLinkUri = null }, shareNavigationTrigger = shareNavigationTrigger, modifier = Modifier.weight(1f), ) } } } } } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) handleIntent(intent) } private fun handleIntent(intent: Intent?) { if (intent == null) return when (intent.action) { Intent.ACTION_SEND -> handleShareIntent(intent) Intent.ACTION_SEND_MULTIPLE -> handleShareMultipleIntent(intent) else -> handleDeepLink(intent) } } private fun handleDeepLink(intent: Intent) { intent.data?.let { uri -> if (uri.scheme == "librechat" && uri.host in KNOWN_DEEP_LINK_HOSTS) { deepLinkUri = uri } else if (uri.scheme == "librechat") { Logger.w { "Ignoring deep link with unknown host: ${uri.host}" } } } } companion object { /** Hosts accepted by the librechat:// deep link scheme. */ private val KNOWN_DEEP_LINK_HOSTS = setOf("conversation", "oauth") /** MongoDB ObjectID: exactly 24 lowercase hex characters. */ val CONVERSATION_ID_REGEX = Regex("^[a-f0-9]{24}$") } private fun handleShareIntent(intent: Intent) { val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) @Suppress("DEPRECATION") val sharedUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) } else { intent.getParcelableExtra(Intent.EXTRA_STREAM) } val fileUris = if (sharedUri != null) listOf(sharedUri) else emptyList() if (sharedText != null || fileUris.isNotEmpty()) { Logger.d { "Share intent received: text=${sharedText != null}, uris=${fileUris.size}" } ShareIntentConsumer.setPendingShare( SharedContent(text = sharedText, fileUris = fileUris), ) shareNavigationTrigger++ } } private fun handleShareMultipleIntent(intent: Intent) { @Suppress("DEPRECATION") val sharedUris = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java) } else { intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) } if (!sharedUris.isNullOrEmpty()) { Logger.d { "Share multiple intent received: uris=${sharedUris.size}" } ShareIntentConsumer.setPendingShare( SharedContent(fileUris = sharedUris), ) shareNavigationTrigger++ } } }