diff --git a/.gitignore b/.gitignore index 0d4e89b3..96043124 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ .cxx local.properties peptidebuild.properties -sentry.properties /.kotlin/sessions app/src/main/assets/embedded pep-api-postman.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 75514cb9..d8a6e111 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,4 @@ import com.mikepenz.aboutlibraries.plugin.StrictMode -import io.sentry.android.gradle.instrumentation.logcat.LogcatLevel import java.io.FileInputStream import java.util.Properties @@ -12,7 +11,6 @@ plugins { id("com.google.dagger.hilt.android") id("com.google.devtools.ksp") id("org.jmailen.kotlinter") - id("io.sentry.android.gradle") version "4.12.0" id("app.cash.sqldelight") version "2.0.1" id("kotlin-kapt") id("kotlin-parcelize") @@ -97,11 +95,6 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) - buildConfigField( - "String", - "SENTRY_DSN", - "\"${buildproperty("sentry.dsn", "RVX_SENTRY_DSN")}\"" - ) buildConfigField( "String", "FLAVOUR_ID", @@ -120,11 +113,11 @@ android { buildproperty("build.debug.app_name", "RVX_DEBUG_APP_NAME")!! ) - buildConfigField( - "String", - "SENTRY_DSN", - "\"${buildproperty("sentry.dsn", "RVX_SENTRY_DSN")}\"" - ) +// buildConfigField( +// "String", +// "SENTRY_DSN", +// "\"${buildproperty("sentry.dsn", "RVX_SENTRY_DSN")}\"" +// ) buildConfigField( "String", "FLAVOUR_ID", @@ -164,23 +157,23 @@ android { abortOnError = false disable += "MissingTranslation" disable += "NullSafeMutableLiveData" -// checkReleaseBuilds false + checkReleaseBuilds = false } } -sentry { - autoUploadProguardMapping = - buildproperty("sentry.upload_mappings", "RVX_SENTRY_UPLOAD_MAPPINGS") == "true" - - tracingInstrumentation { - enabled = true - - logcat { - enabled = true - minLevel = LogcatLevel.WARNING - } - } -} +//sentry { +// autoUploadProguardMapping = +// buildproperty("sentry.upload_mappings", "RVX_SENTRY_UPLOAD_MAPPINGS") == "true" +// +// tracingInstrumentation { +// enabled = true +// +// logcat { +// enabled = true +// minLevel = LogcatLevel.WARNING +// } +// } +//} dependencies { // Android/Kotlin Core @@ -247,8 +240,8 @@ dependencies { implementation("com.mikepenz:aboutlibraries-core:11.3.0-rc02") // Sentry - crash reporting - implementation("io.sentry:sentry-android:8.13.2") - implementation("io.sentry:sentry-compose-android:8.13.2") +// implementation("io.sentry:sentry-android:8.19.1") +// implementation("io.sentry:sentry-compose-android:8.19.1") // Other AndroidX libraries implementation("androidx.documentfile:documentfile:1.1.0") @@ -346,4 +339,4 @@ sqldelight { packageName.set("chat.peptide.persistence") } } -} \ No newline at end of file +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index befd34c8..8d2b2979 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -48,9 +48,9 @@ android:resource="@xml/file_paths" /> - + + + (null) - var isConnected = MutableStateFlow(false) - val isReady = MutableStateFlow(false) - val couldNotLogIn = MutableStateFlow(false) - - private fun hasInternetConnection(): Boolean { - val connectivityManager = - context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - - val network = connectivityManager.activeNetwork ?: return false - val capabilities = - connectivityManager.getNetworkCapabilities(network) ?: return false - - return when { - capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true - capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true - capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true - capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> true - else -> false - } - } - - private suspend fun canReachPeptide(): Boolean { - try { - val res = PeptideHttp.get("/".api()) - return res.status.value == 200 - } catch (_: Exception) { - return false - } - } - - private suspend fun startWithDestination(destination: String) { - nextDestination.emit(destination) - isReady.emit(true) - } - - private suspend fun startWithoutDestination() { - isReady.emit(true) - } - - private fun doPreStartupTasks() { - Log.d("MainActivity", "Performing pre-startup tasks") - viewModelScope.launch { - Log.d("MainActivity", "Hydrating Experiments from KV") - Experiments.hydrateWithKv() - Log.d("MainActivity", "Performing health check") - doHealthCheck() - Log.d("MainActivity", "Performing update geo state") - updateGeoState() - } - } - - /** - * Sets the default platform on application creation. - * It retrieves the platform from key-value storage and sets it in the PeptideAPI. - * If the platform is not found or invalid, it logs the user out. - */ - fun setDefaultPlatformOnCreate() { - viewModelScope.launch { - ApplicationPlatform.fromName( - name = kvStorage.get(UrlsStorageKeys.PLATFORM) ?: "---" - )?.let { platform -> - setPlatform(platform) - } ?: run { - logOut() - } - } - } - - private suspend fun updateGeoState() { - try { - Log.d("MainActivity", "Querying geo state") - GeoStateProvider.updateGeoState(queryGeo()) - } catch (e: Exception) { - Log.e("MainActivity", "Failed to query geo state", e) - } - } - - fun checkLoggedInState() { - viewModelScope.launch { - Log.d("MainActivity", "Checking logged in state") - - isConnected.emit(hasInternetConnection()) - - Log.d("MainActivity", "Checking if we can reach Peptide") - - if (!isConnected.value) return@launch startWithoutDestination() - - Log.d("MainActivity", "We can reach Peptide, checking if we're logged in") - - val token = kvStorage.get("sessionToken") - ?: return@launch startWithDestination("choose-platform") - val id = kvStorage.get("sessionId") ?: "" - - Log.d( - "MainActivity", - "We have a session token, checking if it's valid and if we can still reach Peptide" - ) - - val canReachPeptide = canReachPeptide() - val valid = try { - PeptideAPI.checkSessionToken(token) - } catch (_: Throwable) { - false - } - - if (canReachPeptide && !valid) { - Log.d("MainActivity", "Session token is invalid, could not log in") - couldNotLogIn.emit(true) - } else { - try { - Log.d("MainActivity", "Session token is valid, checking onboarding state") - val onboard = needsOnboarding(token) - if (onboard) { - Log.d("MainActivity", "Onboarding state is incomplete, starting onboarding") - startWithDestination("register/onboarding") - return@launch - } - } catch (e: HitRateLimitException) { - Log.e("MainActivity", "Rate limited while checking onboarding state", e) - Toast.makeText( - context, - context.getString(R.string.rate_limit_toast), - Toast.LENGTH_SHORT - ).show() - return@launch startWithoutDestination() - } catch (e: Exception) { - Log.e("MainActivity", "Failed to check onboarding state, could not log in", e) - couldNotLogIn.emit(true) - } - - try { - Log.d("MainActivity", "Onboarding state is complete, logging in") - PeptideAPI.loginAs(token) - PeptideAPI.setSessionId(id) - if (Experiments.usePolar.isEnabled) { - startWithDestination("main") - } else { - startWithDestination("chat") - } - } catch (e: Exception) { - Log.e("MainActivity", "Failed to login, could not log in", e) - couldNotLogIn.emit(true) - } - } - } - } - - fun logOut() { - viewModelScope.launch { - kvStorage.remove("sessionToken") - kvStorage.remove("sessionId") - startWithDestination("choose-platform") - } - } - - fun updateNextDestination(destination: String) { - viewModelScope.launch { - nextDestination.emit(null) - nextDestination.emit(destination) - } - } - - val activeAlert = MutableStateFlow(null) - val isAlertActive = MutableStateFlow(false) - - private fun doHealthCheck() { - viewModelScope.launch { - try { - val health = healthCheck() - if (health.alert != null) { - activeAlert.emit(health) - isAlertActive.emit(true) - } - } catch (e: Exception) { - Log.e("MainActivity", "Failed to perform health check", e) - } - } - } - - fun onDismissHealthAlert() { - viewModelScope.launch { - activeAlert.emit(null) - isAlertActive.emit(false) - } - } - - fun onDismissLoginError() { - viewModelScope.launch { - couldNotLogIn.emit(false) - } - } - - init { - Log.d("MainActivity", "Starting up") - doPreStartupTasks() - checkLoggedInState() - } -} - -@AndroidEntryPoint -class MainActivity : AppCompatActivity() { - private val viewModel by viewModels() - - // Fix for SDK >=31, where core-splashscreen accidentally removes dynamic colours - // See the other one in DefaultDestinationScreen.kt - override fun onResume() { - super.onResume() - DynamicColors.applyToActivityIfAvailable(this) - DynamicColors.applyToActivitiesIfAvailable(PeptideApplication.instance) - @Suppress("DEPRECATION") // We are fixing a bug in the splash screen - window.statusBarColor = Color.Transparent.toArgb() - } - - // Same as above for configuration changes (rotation, dark mode, etc.) - override fun onConfigurationChanged(newConfig: android.content.res.Configuration) { - super.onConfigurationChanged(newConfig) - DynamicColors.applyToActivityIfAvailable(this) - DynamicColors.applyToActivitiesIfAvailable(PeptideApplication.instance) - @Suppress("DEPRECATION") // We are fixing a bug in the splash screen - window.statusBarColor = Color.Transparent.toArgb() - } - - @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) - override fun onCreate(savedInstanceState: Bundle?) { +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Bundle +import android.util.Log +import android.view.KeyEvent +import android.view.KeyboardShortcutGroup +import android.view.KeyboardShortcutInfo +import android.view.Menu +import android.view.View +import android.view.ViewTreeObserver +import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.EaseInOutExpo +import androidx.compose.animation.core.FiniteAnimationSpec +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.core.view.WindowCompat +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import chat.peptide.BuildConfig +import chat.peptide.R +import chat.peptide.PeptideApplication +import chat.peptide.api.ApplicationPlatform +import chat.peptide.api.HitRateLimitException +import chat.peptide.api.PeptideAPI +import chat.peptide.api.PeptideAPI.setPlatform +import chat.peptide.api.PeptideHttp +import chat.peptide.api.UrlsStorageKeys +import chat.peptide.api.api +import chat.peptide.api.routes.microservices.geo.queryGeo +import chat.peptide.api.routes.microservices.health.healthCheck +import chat.peptide.api.routes.onboard.needsOnboarding +import chat.peptide.api.schemas.HealthNotice +import chat.peptide.api.settings.Experiments +import chat.peptide.api.settings.GeoStateProvider +import chat.peptide.api.settings.LoadedSettings +import chat.peptide.api.settings.SyncedSettings +import chat.peptide.composables.generic.HealthAlert +import chat.peptide.composables.generic.PepTextButton +import chat.peptide.composables.voice.VoicePermissionSwitch +import chat.peptide.composables.voice.VoiceSheet +import chat.peptide.material.EasingTokens +import chat.peptide.ndk.NativeLibraries +import chat.peptide.persistence.KVStorage +import chat.peptide.screens.DefaultDestinationScreen +import chat.peptide.screens.about.AboutScreen +import chat.peptide.screens.about.AttributionScreen +import chat.peptide.screens.chat.ChatRouterScreen +import chat.peptide.screens.chat.views.channel.ChannelScreen +import chat.peptide.screens.chooseplatform.ChoosePlatformScreen +import chat.peptide.screens.create.CreateGroupScreen +import chat.peptide.screens.labs.LabsRootScreen +import chat.peptide.screens.login.LoginGreetingScreen +import chat.peptide.screens.login.LoginScreen +import chat.peptide.screens.login.MfaScreen +import chat.peptide.screens.login2.InitScreen +import chat.peptide.screens.main.MainScreen +import chat.peptide.screens.register.OnboardingScreen +import chat.peptide.screens.register.RegisterDetailsScreen +import chat.peptide.screens.register.RegisterGreetingScreen +import chat.peptide.screens.register.RegisterVerifyScreen +import chat.peptide.screens.services.DiscoverScreen +import chat.peptide.screens.settings.AppearanceSettingsScreen +import chat.peptide.screens.settings.ChangelogsSettingsScreen +import chat.peptide.screens.settings.ChatSettingsScreen +import chat.peptide.screens.settings.DebugSettingsScreen +import chat.peptide.screens.settings.ExperimentsSettingsScreen +import chat.peptide.screens.settings.LanguagePickerSettingsScreen +import chat.peptide.screens.settings.ProfileSettingsScreen +import chat.peptide.screens.settings.SessionSettingsScreen +import chat.peptide.screens.settings.SettingsScreen +import chat.peptide.screens.settings.channel.ChannelSettingsHome +import chat.peptide.screens.settings.channel.ChannelSettingsOverview +import chat.peptide.screens.settings.channel.ChannelSettingsPermissions +import chat.peptide.ui.theme.PeptideTheme +import chat.peptide.utils.DeepLinkHandler +import com.google.android.material.color.DynamicColors +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import io.ktor.client.request.get +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel +@SuppressLint("StaticFieldLeak") +class MainActivityViewModel @Inject constructor( + private val kvStorage: KVStorage, + @ApplicationContext private val context: Context +) : ViewModel() { + val nextDestination = MutableStateFlow(null) + var isConnected = MutableStateFlow(false) + val isReady = MutableStateFlow(false) + val couldNotLogIn = MutableStateFlow(false) + + private fun hasInternetConnection(): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + val network = connectivityManager.activeNetwork ?: return false + val capabilities = + connectivityManager.getNetworkCapabilities(network) ?: return false + + return when { + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> true + else -> false + } + } + + private suspend fun canReachPeptide(): Boolean { + try { + val res = PeptideHttp.get("/".api()) + return res.status.value == 200 + } catch (_: Exception) { + return false + } + } + + private suspend fun startWithDestination(destination: String) { + nextDestination.emit(destination) + isReady.emit(true) + } + + private suspend fun startWithoutDestination() { + isReady.emit(true) + } + + private fun doPreStartupTasks() { + Log.d("MainActivity", "Performing pre-startup tasks") + viewModelScope.launch { + Log.d("MainActivity", "Hydrating Experiments from KV") + Experiments.hydrateWithKv() + Log.d("MainActivity", "Performing health check") + doHealthCheck() + Log.d("MainActivity", "Performing update geo state") + updateGeoState() + } + } + + /** + * Sets the default platform on application creation. + * It retrieves the platform from key-value storage and sets it in the PeptideAPI. + * If the platform is not found or invalid, it logs the user out. + */ + fun setDefaultPlatformOnCreate() { + viewModelScope.launch { + ApplicationPlatform.fromName( + name = kvStorage.get(UrlsStorageKeys.PLATFORM) ?: "---" + )?.let { platform -> + setPlatform(platform) + } ?: run { + logOut() + } + } + } + + private suspend fun updateGeoState() { + try { + Log.d("MainActivity", "Querying geo state") + GeoStateProvider.updateGeoState(queryGeo()) + } catch (e: Exception) { + Log.e("MainActivity", "Failed to query geo state", e) + } + } + + fun checkLoggedInState() { + viewModelScope.launch { + Log.d("MainActivity", "Checking logged in state") + + isConnected.emit(hasInternetConnection()) + + Log.d("MainActivity", "Checking if we can reach Peptide") + + if (!isConnected.value) return@launch startWithoutDestination() + + Log.d("MainActivity", "We can reach Peptide, checking if we're logged in") + + val token = kvStorage.get("sessionToken") + ?: return@launch startWithDestination("choose-platform") + val id = kvStorage.get("sessionId") ?: "" + + Log.d( + "MainActivity", + "We have a session token, checking if it's valid and if we can still reach Peptide" + ) + + val canReachPeptide = canReachPeptide() + val valid = try { + PeptideAPI.checkSessionToken(token) + } catch (_: Throwable) { + false + } + + if (canReachPeptide && !valid) { + Log.d("MainActivity", "Session token is invalid, could not log in") + couldNotLogIn.emit(true) + } else { + try { + Log.d("MainActivity", "Session token is valid, checking onboarding state") + val onboard = needsOnboarding(token) + if (onboard) { + Log.d("MainActivity", "Onboarding state is incomplete, starting onboarding") + startWithDestination("register/onboarding") + return@launch + } + } catch (e: HitRateLimitException) { + Log.e("MainActivity", "Rate limited while checking onboarding state", e) + Toast.makeText( + context, + context.getString(R.string.rate_limit_toast), + Toast.LENGTH_SHORT + ).show() + return@launch startWithoutDestination() + } catch (e: Exception) { + Log.e("MainActivity", "Failed to check onboarding state, could not log in", e) + couldNotLogIn.emit(true) + } + + try { + Log.d("MainActivity", "Onboarding state is complete, logging in") + PeptideAPI.loginAs(token) + PeptideAPI.setSessionId(id) + if (Experiments.usePolar.isEnabled) { + startWithDestination("main") + } else { + startWithDestination("chat") + } + } catch (e: Exception) { + Log.e("MainActivity", "Failed to login, could not log in", e) + couldNotLogIn.emit(true) + } + } + } + } + + fun logOut() { + viewModelScope.launch { + kvStorage.remove("sessionToken") + kvStorage.remove("sessionId") + startWithDestination("choose-platform") + } + } + + fun updateNextDestination(destination: String) { + viewModelScope.launch { + nextDestination.emit(null) + nextDestination.emit(destination) + } + } + + val activeAlert = MutableStateFlow(null) + val isAlertActive = MutableStateFlow(false) + + private fun doHealthCheck() { + viewModelScope.launch { + try { + val health = healthCheck() + if (health.alert != null) { + activeAlert.emit(health) + isAlertActive.emit(true) + } + } catch (e: Exception) { + Log.e("MainActivity", "Failed to perform health check", e) + } + } + } + + fun onDismissHealthAlert() { + viewModelScope.launch { + activeAlert.emit(null) + isAlertActive.emit(false) + } + } + + fun onDismissLoginError() { + viewModelScope.launch { + couldNotLogIn.emit(false) + } + } + + init { + Log.d("MainActivity", "Starting up") + doPreStartupTasks() + checkLoggedInState() + } +} + +@AndroidEntryPoint +class MainActivity : AppCompatActivity() { + private val viewModel by viewModels() + + // Fix for SDK >=31, where core-splashscreen accidentally removes dynamic colours + // See the other one in DefaultDestinationScreen.kt + override fun onResume() { + super.onResume() + DynamicColors.applyToActivityIfAvailable(this) + DynamicColors.applyToActivitiesIfAvailable(PeptideApplication.instance) + @Suppress("DEPRECATION") // We are fixing a bug in the splash screen + window.statusBarColor = Color.Transparent.toArgb() + } + + // Same as above for configuration changes (rotation, dark mode, etc.) + override fun onConfigurationChanged(newConfig: android.content.res.Configuration) { + super.onConfigurationChanged(newConfig) + DynamicColors.applyToActivityIfAvailable(this) + DynamicColors.applyToActivitiesIfAvailable(PeptideApplication.instance) + @Suppress("DEPRECATION") // We are fixing a bug in the splash screen + window.statusBarColor = Color.Transparent.toArgb() + } + + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // waiting for view to draw to better represent a captured error with a screenshot findViewById(android.R.id.content).viewTreeObserver.addOnGlobalLayoutListener { try { throw Exception("This app uses Sentry! :)") } catch (e: Exception) { - Sentry.captureException(e) +// Sentry.captureException(e) } } - - - // Set the default platform for the Platform API. - viewModel.setDefaultPlatformOnCreate() - - SentryAndroid.init(this) { options -> - options.dsn = BuildConfig.SENTRY_DSN - options.release = BuildConfig.VERSION_NAME - } - - @Suppress("DEPRECATION") // We are fixing a bug in the splash screen - window.statusBarColor = Color.Transparent.toArgb() - WindowCompat.setDecorFitsSystemWindows(window, false) - - PeptideAPI.hydrateFromPersistentCache() - - // Handle deep link data if available - handleDeepLinkData(intent) - - setContent { - val windowSizeClass = calculateWindowSizeClass(this) - AppEntrypoint( - windowSizeClass, - viewModel.nextDestination.collectAsState().value, - viewModel.isConnected.collectAsState().value, - viewModel.activeAlert.collectAsState().value, - viewModel.isAlertActive.collectAsState().value, - viewModel.couldNotLogIn.collectAsState().value, - viewModel::logOut, - viewModel::onDismissHealthAlert, - viewModel::onDismissLoginError, - viewModel::checkLoggedInState, - viewModel::updateNextDestination - ) - } - - val content: View = findViewById(android.R.id.content) - content.viewTreeObserver.addOnPreDrawListener( - object : ViewTreeObserver.OnPreDrawListener { - override fun onPreDraw(): Boolean { - // Check whether the initial data is ready. - return if (viewModel.isReady.value) { - // The content is ready. Start drawing. - content.viewTreeObserver.removeOnPreDrawListener(this) - true - } else { - // The content isn't ready. Suspend. - false - } - } - } - ) - } - - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - handleDeepLinkData(intent) - } - - private fun handleDeepLinkData(intent: Intent) { - // Check for deep link data from DeepLinkActivity - val channelId = intent.getStringExtra(DeepLinkActivity.PATH_CHANNEL) - val serverId = intent.getStringExtra(DeepLinkActivity.PATH_SERVER) - val userId = intent.getStringExtra(DeepLinkActivity.PATH_USER) - val messageId = intent.getStringExtra(DeepLinkActivity.PATH_MESSAGE) - val nextDestination = intent.getStringExtra("next_destination") - - if (nextDestination != null) { - Log.d(TAG, "Setting next destination from deep link: $nextDestination") - viewModel.updateNextDestination(nextDestination) - return - } - - // Store deep link data in a static companion object for ChatRouterScreen to access - DeepLinkHandler.channelId = channelId - DeepLinkHandler.serverId = serverId - DeepLinkHandler.userId = userId - DeepLinkHandler.messageId = messageId - DeepLinkHandler.hasDeepLink = channelId != null || serverId != null || userId != null || messageId != null - - // Wait for the app to be ready before navigating to the deep link destination - viewModel.viewModelScope.launch { - viewModel.isReady.collect { isReady -> - if (isReady) { - when { - channelId != null -> { - Log.d(TAG, "Navigating to channel from deep link: $channelId") - // Check if Polar mode is enabled - if (Experiments.usePolar.isEnabled) { - viewModel.updateNextDestination("main/conversation/$channelId") - } else { - // For non-Polar mode, we'll use the DeepLinkHandler to communicate with ChatRouterScreen - viewModel.updateNextDestination("chat") - } - } - serverId != null -> { - Log.d(TAG, "Navigating to server from deep link: $serverId") - // Navigate to chat screen, the ChatRouterScreen will handle the server navigation - viewModel.updateNextDestination("chat") - } - userId != null -> { - Log.d(TAG, "Navigating to user from deep link: $userId") - // Navigate to chat screen, the ChatRouterScreen will handle the user navigation - viewModel.updateNextDestination("chat") - } - messageId != null -> { - Log.d(TAG, "Navigating to message from deep link: $messageId") - // Navigate to chat screen, the ChatRouterScreen will handle the message navigation - viewModel.updateNextDestination("chat") - } - } - // Only collect once - return@collect - } - } - } - } - - override fun onProvideKeyboardShortcuts( - data: MutableList?, - menu: Menu?, - deviceId: Int - ) { - val messaging = KeyboardShortcutGroup( - getString(R.string.keyboard_shortcut_messaging), - listOf( - KeyboardShortcutInfo( - getString(R.string.keyboard_shortcut_messaging_new_line), - KeyEvent.KEYCODE_ENTER, - 0 - ), - KeyboardShortcutInfo( - getString(R.string.keyboard_shortcut_messaging_send_message), - KeyEvent.KEYCODE_ENTER, - KeyEvent.META_CTRL_ON - ) - ) - ) - - data?.add(messaging) - } - - companion object { - private const val TAG = "MainActivity" - init { - NativeLibraries.init() - } - } -} - -val PeptideTweenInt: FiniteAnimationSpec = tween(400, easing = EaseInOutExpo) -val PeptideTweenFloat: FiniteAnimationSpec = tween(400, easing = EaseInOutExpo) -val PeptideTweenDp: FiniteAnimationSpec = tween(400, easing = EaseInOutExpo) - -val NavTweenInt: FiniteAnimationSpec = tween(350, easing = EaseInOutExpo) -val NavTweenFloat: FiniteAnimationSpec = tween(350, easing = EaseInOutExpo) - -// This composable handles the main compose entrypoint of the app, provides the main navigation -// graph, and handles the animation and layout for the voice chat UI. -@Composable -fun AppEntrypoint( - windowSizeClass: WindowSizeClass, - nextDestination: String?, - isConnected: Boolean, - healthNotice: HealthNotice?, - isHealthAlertActive: Boolean, - couldNotLogIn: Boolean, - onLogout: () -> Unit = {}, - onDismissHealthAlert: () -> Unit = {}, - onDismissLoginError: () -> Unit = {}, - onRetryConnection: () -> Unit, - onUpdateNextDestination: (String) -> Unit = {} -) { - var showVoiceUI by rememberSaveable { mutableStateOf(false) } - var voiceChannelId by rememberSaveable { mutableStateOf(null) } - - val chatUIScale by animateFloatAsState( - if (showVoiceUI) 0.8f else 1.0f, - animationSpec = tween( - durationMillis = 300, - easing = EasingTokens.EmphasizedDecelerate - ) - ) - val chatUIOpacity by animateFloatAsState( - if (showVoiceUI) 0.8f else 1.0f, - animationSpec = tween( - durationMillis = 300, - easing = EasingTokens.EmphasizedDecelerate - ) - ) - - BackHandler(showVoiceUI) { - showVoiceUI = false - } - - val keyboardController = LocalSoftwareKeyboardController.current - LaunchedEffect(showVoiceUI) { - if (showVoiceUI) keyboardController?.hide() - } - - val navController = rememberNavController() - - PeptideTheme( - requestedTheme = LoadedSettings.theme, - colourOverrides = SyncedSettings.android.colourOverrides, - ) { - Box( - Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceContainerLowest) - ) { - Surface( - modifier = Modifier - .fillMaxSize() - .scale(chatUIScale) - .alpha(chatUIOpacity), - color = MaterialTheme.colorScheme.background - ) { - if (isHealthAlertActive) { - healthNotice?.let { - HealthAlert(notice = healthNotice, onDismiss = onDismissHealthAlert) - } - } - - if (couldNotLogIn) { - AlertDialog( - onDismissRequest = { - // no-op - }, - title = { - Text(stringResource(R.string.could_not_log_in_heading)) - }, - text = { - Text(stringResource(R.string.could_not_log_in_body)) - }, - confirmButton = { - PepTextButton( - onClick = { - onDismissLoginError() - onRetryConnection() - } - ) { - Text(stringResource(R.string.could_not_log_in_cta_try_again)) - } - }, - dismissButton = { - PepTextButton( - onClick = { - onDismissLoginError() - onLogout() - } - ) { - Text(stringResource(R.string.could_not_log_in_cta_logout)) - } - } - ) - } - - NavHost( - navController = navController, - startDestination = "default", - enterTransition = { - slideIntoContainer( - AnimatedContentTransitionScope.SlideDirection.Left, - animationSpec = NavTweenInt, - initialOffset = { it / 3 } - ) + fadeIn(animationSpec = NavTweenFloat) - }, - exitTransition = { - slideOutOfContainer( - AnimatedContentTransitionScope.SlideDirection.Left, - animationSpec = NavTweenInt, - targetOffset = { it / 3 } - ) + fadeOut(animationSpec = NavTweenFloat) - }, - popEnterTransition = { - slideIntoContainer( - AnimatedContentTransitionScope.SlideDirection.Right, - animationSpec = NavTweenInt, - initialOffset = { it / 3 } - ) + fadeIn(animationSpec = NavTweenFloat) - }, - popExitTransition = { - slideOutOfContainer( - AnimatedContentTransitionScope.SlideDirection.Right, - animationSpec = NavTweenInt, - targetOffset = { it / 2 } - ) + fadeOut(animationSpec = NavTweenFloat) - } - ) { - composable("default") { - DefaultDestinationScreen( - navController, - nextDestination, - isConnected, - onRetryConnection - ) - } - - composable("choose-platform") { ChoosePlatformScreen(navController) } - composable("login/greeting") { LoginGreetingScreen(navController) } - composable("login/login") { LoginScreen(navController) } - composable("login/mfa/{mfaTicket}/{allowedAuthTypes}") { backStackEntry -> - val mfaTicket = backStackEntry.arguments?.getString("mfaTicket") ?: "" - val allowedAuthTypes = - backStackEntry.arguments?.getString("allowedAuthTypes") ?: "" - - MfaScreen(navController, allowedAuthTypes, mfaTicket) - } - - composable("register/greeting") { RegisterGreetingScreen(navController) } - composable("register/details") { RegisterDetailsScreen(navController) } - composable("register/verify/{email}") { backStackEntry -> - val email = backStackEntry.arguments?.getString("email") ?: "" - - RegisterVerifyScreen(navController, email) - } - composable("register/onboarding") { - OnboardingScreen( - navController, - onOnboardingComplete = { - onUpdateNextDestination("chat") - navController.popBackStack( - navController.graph.startDestinationRoute!!, - inclusive = true - ) - navController.navigate("default") - } - ) - } - - composable("login2/init") { InitScreen(navController, windowSizeClass) } - - // This is only used outside of Polar mode - // Otherwise you may be looking for "main" right below - composable( - "chat", - enterTransition = { - slideIntoContainer( - AnimatedContentTransitionScope.SlideDirection.Up, - animationSpec = tween( - 400, - easing = EasingTokens.EmphasizedDecelerate - ), - initialOffset = { it / 3 } - ) + fadeIn(animationSpec = PeptideTweenFloat) - } - ) { - ChatRouterScreen( - navController, - windowSizeClass, - onNullifiedUser = { - onRetryConnection() - navController.popBackStack( - navController.graph.startDestinationRoute!!, - inclusive = true - ) - navController.navigate("default") - }, - onEnterVoiceUI = { channelId -> - showVoiceUI = true - voiceChannelId = channelId - }, - ) - } - - // This is only the main screen in Polar mode - // Otherwise you may be looking for "chat" right above - composable( - "main", - enterTransition = { - slideIntoContainer( - AnimatedContentTransitionScope.SlideDirection.Up, - animationSpec = tween( - 400, - easing = EasingTokens.EmphasizedDecelerate - ), - initialOffset = { it / 3 } - ) + fadeIn(animationSpec = PeptideTweenFloat) + scaleIn( - animationSpec = tween( - 400, - easing = EasingTokens.EmphasizedDecelerate - ), - initialScale = 0.8f, - transformOrigin = TransformOrigin.Center - ) - } - ) { - MainScreen(navController) - } - composable( - "main/conversation/{channelId}", - enterTransition = { - slideIntoContainer( - AnimatedContentTransitionScope.SlideDirection.Left, - animationSpec = tween( - 600, - easing = EasingTokens.EmphasizedDecelerate - ), - initialOffset = { it } - ) + fadeIn(animationSpec = PeptideTweenFloat) - }, - exitTransition = { - slideOutOfContainer( - AnimatedContentTransitionScope.SlideDirection.Right, - animationSpec = tween( - 600, - easing = EasingTokens.EmphasizedDecelerate - ), - targetOffset = { it } - ) + fadeOut(animationSpec = PeptideTweenFloat) - } - ) { backStackEntry -> - val channelId = backStackEntry.arguments?.getString("channelId") ?: "" - ChannelScreen( - channelId = channelId, - useDrawer = false, - useBackButton = true, - backToChannelsScreen = { - navController.navigate("main") - }, - backButtonAction = { - navController.popBackStack() - }, - useChatUI = true - ) - } - - composable("create/group") { CreateGroupScreen(navController) } - - composable("discover") { DiscoverScreen(navController) } - - composable("settings") { SettingsScreen(navController) } - composable("settings/profile") { ProfileSettingsScreen(navController) } - composable("settings/sessions") { SessionSettingsScreen(navController) } - composable("settings/appearance") { AppearanceSettingsScreen(navController) } - composable("settings/chat") { ChatSettingsScreen(navController) } - composable("settings/debug") { DebugSettingsScreen(navController) } - composable("settings/experiments") { ExperimentsSettingsScreen(navController) } - composable("settings/changelogs") { ChangelogsSettingsScreen(navController) } - composable("settings/language") { LanguagePickerSettingsScreen(navController) } - - composable("settings/channel/{channelId}") { backStackEntry -> - val channelId = backStackEntry.arguments?.getString("channelId") ?: "" - ChannelSettingsHome(navController, channelId) - } - composable("settings/channel/{channelId}/overview") { backStackEntry -> - val channelId = backStackEntry.arguments?.getString("channelId") ?: "" - ChannelSettingsOverview(navController, channelId) - } - composable("settings/channel/{channelId}/permissions") { backStackEntry -> - val channelId = backStackEntry.arguments?.getString("channelId") ?: "" - ChannelSettingsPermissions(navController, channelId) - } - - composable("about") { AboutScreen(navController) } - composable("about/oss") { AttributionScreen(navController) } - - composable("labs") { LabsRootScreen(navController) } - } - } - - if (showVoiceUI) { // if tapped outside the voice UI, close it - Box( - Modifier - .fillMaxSize() - .clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - showVoiceUI = false - } - ) - } - - AnimatedVisibility( - visible = showVoiceUI, - modifier = Modifier.align(Alignment.BottomCenter), - enter = slideInVertically( - initialOffsetY = { it -> it }, - animationSpec = tween( - durationMillis = 300, - easing = EasingTokens.EmphasizedDecelerate - ) - ), - exit = slideOutVertically( - targetOffsetY = { it -> it }, - animationSpec = tween( - durationMillis = 300, - easing = EasingTokens.EmphasizedDecelerate - ) - ) - ) { - // We need a box as applying the padding elsewhere leads to either - // janky animation or layout - Box(Modifier.safeDrawingPadding()) { - Card( - Modifier - .fillMaxWidth() - .widthIn(max = 600.dp) - .padding(8.dp) - ) { - VoicePermissionSwitch( - onCancel = { - showVoiceUI = false - } - ) { - voiceChannelId?.let { - VoiceSheet( - it, - onDisconnect = { - showVoiceUI = false - voiceChannelId = null - } - ) - } - } - } - } - } - } - } -} + + + // Set the default platform for the Platform API. + viewModel.setDefaultPlatformOnCreate() + +// SentryAndroid.init(this) { options -> +// options.dsn = BuildConfig.SENTRY_DSN +// options.release = BuildConfig.VERSION_NAME +// } + + @Suppress("DEPRECATION") // We are fixing a bug in the splash screen + window.statusBarColor = Color.Transparent.toArgb() + WindowCompat.setDecorFitsSystemWindows(window, false) + + PeptideAPI.hydrateFromPersistentCache() + + // Handle deep link data if available + handleDeepLinkData(intent) + + setContent { + val windowSizeClass = calculateWindowSizeClass(this) + AppEntrypoint( + windowSizeClass, + viewModel.nextDestination.collectAsState().value, + viewModel.isConnected.collectAsState().value, + viewModel.activeAlert.collectAsState().value, + viewModel.isAlertActive.collectAsState().value, + viewModel.couldNotLogIn.collectAsState().value, + viewModel::logOut, + viewModel::onDismissHealthAlert, + viewModel::onDismissLoginError, + viewModel::checkLoggedInState, + viewModel::updateNextDestination + ) + } + + val content: View = findViewById(android.R.id.content) + content.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + // Check whether the initial data is ready. + return if (viewModel.isReady.value) { + // The content is ready. Start drawing. + content.viewTreeObserver.removeOnPreDrawListener(this) + true + } else { + // The content isn't ready. Suspend. + false + } + } + } + ) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + handleDeepLinkData(intent) + } + + private fun handleDeepLinkData(intent: Intent) { + // Check for deep link data from DeepLinkActivity + val channelId = intent.getStringExtra(DeepLinkActivity.PATH_CHANNEL) + val serverId = intent.getStringExtra(DeepLinkActivity.PATH_SERVER) + val userId = intent.getStringExtra(DeepLinkActivity.PATH_USER) + val messageId = intent.getStringExtra(DeepLinkActivity.PATH_MESSAGE) + val nextDestination = intent.getStringExtra("next_destination") + + if (nextDestination != null) { + Log.d(TAG, "Setting next destination from deep link: $nextDestination") + viewModel.updateNextDestination(nextDestination) + return + } + + // Store deep link data in a static companion object for ChatRouterScreen to access + DeepLinkHandler.channelId = channelId + DeepLinkHandler.serverId = serverId + DeepLinkHandler.userId = userId + DeepLinkHandler.messageId = messageId + DeepLinkHandler.hasDeepLink = channelId != null || serverId != null || userId != null || messageId != null + + // Wait for the app to be ready before navigating to the deep link destination + viewModel.viewModelScope.launch { + viewModel.isReady.collect { isReady -> + if (isReady) { + when { + channelId != null -> { + Log.d(TAG, "Navigating to channel from deep link: $channelId") + // Check if Polar mode is enabled + if (Experiments.usePolar.isEnabled) { + viewModel.updateNextDestination("main/conversation/$channelId") + } else { + // For non-Polar mode, we'll use the DeepLinkHandler to communicate with ChatRouterScreen + viewModel.updateNextDestination("chat") + } + } + serverId != null -> { + Log.d(TAG, "Navigating to server from deep link: $serverId") + // Navigate to chat screen, the ChatRouterScreen will handle the server navigation + viewModel.updateNextDestination("chat") + } + userId != null -> { + Log.d(TAG, "Navigating to user from deep link: $userId") + // Navigate to chat screen, the ChatRouterScreen will handle the user navigation + viewModel.updateNextDestination("chat") + } + messageId != null -> { + Log.d(TAG, "Navigating to message from deep link: $messageId") + // Navigate to chat screen, the ChatRouterScreen will handle the message navigation + viewModel.updateNextDestination("chat") + } + } + // Only collect once + return@collect + } + } + } + } + + override fun onProvideKeyboardShortcuts( + data: MutableList?, + menu: Menu?, + deviceId: Int + ) { + val messaging = KeyboardShortcutGroup( + getString(R.string.keyboard_shortcut_messaging), + listOf( + KeyboardShortcutInfo( + getString(R.string.keyboard_shortcut_messaging_new_line), + KeyEvent.KEYCODE_ENTER, + 0 + ), + KeyboardShortcutInfo( + getString(R.string.keyboard_shortcut_messaging_send_message), + KeyEvent.KEYCODE_ENTER, + KeyEvent.META_CTRL_ON + ) + ) + ) + + data?.add(messaging) + } + + companion object { + private const val TAG = "MainActivity" + init { + NativeLibraries.init() + } + } +} + +val PeptideTweenInt: FiniteAnimationSpec = tween(400, easing = EaseInOutExpo) +val PeptideTweenFloat: FiniteAnimationSpec = tween(400, easing = EaseInOutExpo) +val PeptideTweenDp: FiniteAnimationSpec = tween(400, easing = EaseInOutExpo) + +val NavTweenInt: FiniteAnimationSpec = tween(350, easing = EaseInOutExpo) +val NavTweenFloat: FiniteAnimationSpec = tween(350, easing = EaseInOutExpo) + +// This composable handles the main compose entrypoint of the app, provides the main navigation +// graph, and handles the animation and layout for the voice chat UI. +@Composable +fun AppEntrypoint( + windowSizeClass: WindowSizeClass, + nextDestination: String?, + isConnected: Boolean, + healthNotice: HealthNotice?, + isHealthAlertActive: Boolean, + couldNotLogIn: Boolean, + onLogout: () -> Unit = {}, + onDismissHealthAlert: () -> Unit = {}, + onDismissLoginError: () -> Unit = {}, + onRetryConnection: () -> Unit, + onUpdateNextDestination: (String) -> Unit = {} +) { + var showVoiceUI by rememberSaveable { mutableStateOf(false) } + var voiceChannelId by rememberSaveable { mutableStateOf(null) } + + val chatUIScale by animateFloatAsState( + if (showVoiceUI) 0.8f else 1.0f, + animationSpec = tween( + durationMillis = 300, + easing = EasingTokens.EmphasizedDecelerate + ) + ) + val chatUIOpacity by animateFloatAsState( + if (showVoiceUI) 0.8f else 1.0f, + animationSpec = tween( + durationMillis = 300, + easing = EasingTokens.EmphasizedDecelerate + ) + ) + + BackHandler(showVoiceUI) { + showVoiceUI = false + } + + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(showVoiceUI) { + if (showVoiceUI) keyboardController?.hide() + } + + val navController = rememberNavController() + + PeptideTheme( + requestedTheme = LoadedSettings.theme, + colourOverrides = SyncedSettings.android.colourOverrides, + ) { + Box( + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceContainerLowest) + ) { + Surface( + modifier = Modifier + .fillMaxSize() + .scale(chatUIScale) + .alpha(chatUIOpacity), + color = MaterialTheme.colorScheme.background + ) { + if (isHealthAlertActive) { + healthNotice?.let { + HealthAlert(notice = healthNotice, onDismiss = onDismissHealthAlert) + } + } + + if (couldNotLogIn) { + AlertDialog( + onDismissRequest = { + // no-op + }, + title = { + Text(stringResource(R.string.could_not_log_in_heading)) + }, + text = { + Text(stringResource(R.string.could_not_log_in_body)) + }, + confirmButton = { + PepTextButton( + onClick = { + onDismissLoginError() + onRetryConnection() + } + ) { + Text(stringResource(R.string.could_not_log_in_cta_try_again)) + } + }, + dismissButton = { + PepTextButton( + onClick = { + onDismissLoginError() + onLogout() + } + ) { + Text(stringResource(R.string.could_not_log_in_cta_logout)) + } + } + ) + } + + NavHost( + navController = navController, + startDestination = "default", + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = NavTweenInt, + initialOffset = { it / 3 } + ) + fadeIn(animationSpec = NavTweenFloat) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = NavTweenInt, + targetOffset = { it / 3 } + ) + fadeOut(animationSpec = NavTweenFloat) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = NavTweenInt, + initialOffset = { it / 3 } + ) + fadeIn(animationSpec = NavTweenFloat) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = NavTweenInt, + targetOffset = { it / 2 } + ) + fadeOut(animationSpec = NavTweenFloat) + } + ) { + composable("default") { + DefaultDestinationScreen( + navController, + nextDestination, + isConnected, + onRetryConnection + ) + } + + composable("choose-platform") { ChoosePlatformScreen(navController) } + composable("login/greeting") { LoginGreetingScreen(navController) } + composable("login/login") { LoginScreen(navController) } + composable("login/mfa/{mfaTicket}/{allowedAuthTypes}") { backStackEntry -> + val mfaTicket = backStackEntry.arguments?.getString("mfaTicket") ?: "" + val allowedAuthTypes = + backStackEntry.arguments?.getString("allowedAuthTypes") ?: "" + + MfaScreen(navController, allowedAuthTypes, mfaTicket) + } + + composable("register/greeting") { RegisterGreetingScreen(navController) } + composable("register/details") { RegisterDetailsScreen(navController) } + composable("register/verify/{email}") { backStackEntry -> + val email = backStackEntry.arguments?.getString("email") ?: "" + + RegisterVerifyScreen(navController, email) + } + composable("register/onboarding") { + OnboardingScreen( + navController, + onOnboardingComplete = { + onUpdateNextDestination("chat") + navController.popBackStack( + navController.graph.startDestinationRoute!!, + inclusive = true + ) + navController.navigate("default") + } + ) + } + + composable("login2/init") { InitScreen(navController, windowSizeClass) } + + // This is only used outside of Polar mode + // Otherwise you may be looking for "main" right below + composable( + "chat", + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Up, + animationSpec = tween( + 400, + easing = EasingTokens.EmphasizedDecelerate + ), + initialOffset = { it / 3 } + ) + fadeIn(animationSpec = PeptideTweenFloat) + } + ) { + ChatRouterScreen( + navController, + windowSizeClass, + onNullifiedUser = { + onRetryConnection() + navController.popBackStack( + navController.graph.startDestinationRoute!!, + inclusive = true + ) + navController.navigate("default") + }, + onEnterVoiceUI = { channelId -> + showVoiceUI = true + voiceChannelId = channelId + }, + ) + } + + // This is only the main screen in Polar mode + // Otherwise you may be looking for "chat" right above + composable( + "main", + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Up, + animationSpec = tween( + 400, + easing = EasingTokens.EmphasizedDecelerate + ), + initialOffset = { it / 3 } + ) + fadeIn(animationSpec = PeptideTweenFloat) + scaleIn( + animationSpec = tween( + 400, + easing = EasingTokens.EmphasizedDecelerate + ), + initialScale = 0.8f, + transformOrigin = TransformOrigin.Center + ) + } + ) { + MainScreen(navController) + } + composable( + "main/conversation/{channelId}", + enterTransition = { + slideIntoContainer( + AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = tween( + 600, + easing = EasingTokens.EmphasizedDecelerate + ), + initialOffset = { it } + ) + fadeIn(animationSpec = PeptideTweenFloat) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = tween( + 600, + easing = EasingTokens.EmphasizedDecelerate + ), + targetOffset = { it } + ) + fadeOut(animationSpec = PeptideTweenFloat) + } + ) { backStackEntry -> + val channelId = backStackEntry.arguments?.getString("channelId") ?: "" + ChannelScreen( + channelId = channelId, + useDrawer = false, + useBackButton = true, + backToChannelsScreen = { + navController.navigate("main") + }, + backButtonAction = { + navController.popBackStack() + }, + useChatUI = true + ) + } + + composable("create/group") { CreateGroupScreen(navController) } + + composable("discover") { DiscoverScreen(navController) } + + composable("settings") { SettingsScreen(navController) } + composable("settings/profile") { ProfileSettingsScreen(navController) } + composable("settings/sessions") { SessionSettingsScreen(navController) } + composable("settings/appearance") { AppearanceSettingsScreen(navController) } + composable("settings/chat") { ChatSettingsScreen(navController) } + composable("settings/debug") { DebugSettingsScreen(navController) } + composable("settings/experiments") { ExperimentsSettingsScreen(navController) } + composable("settings/changelogs") { ChangelogsSettingsScreen(navController) } + composable("settings/language") { LanguagePickerSettingsScreen(navController) } + + composable("settings/channel/{channelId}") { backStackEntry -> + val channelId = backStackEntry.arguments?.getString("channelId") ?: "" + ChannelSettingsHome(navController, channelId) + } + composable("settings/channel/{channelId}/overview") { backStackEntry -> + val channelId = backStackEntry.arguments?.getString("channelId") ?: "" + ChannelSettingsOverview(navController, channelId) + } + composable("settings/channel/{channelId}/permissions") { backStackEntry -> + val channelId = backStackEntry.arguments?.getString("channelId") ?: "" + ChannelSettingsPermissions(navController, channelId) + } + + composable("about") { AboutScreen(navController) } + composable("about/oss") { AttributionScreen(navController) } + + composable("labs") { LabsRootScreen(navController) } + } + } + + if (showVoiceUI) { // if tapped outside the voice UI, close it + Box( + Modifier + .fillMaxSize() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) { + showVoiceUI = false + } + ) + } + + AnimatedVisibility( + visible = showVoiceUI, + modifier = Modifier.align(Alignment.BottomCenter), + enter = slideInVertically( + initialOffsetY = { it -> it }, + animationSpec = tween( + durationMillis = 300, + easing = EasingTokens.EmphasizedDecelerate + ) + ), + exit = slideOutVertically( + targetOffsetY = { it -> it }, + animationSpec = tween( + durationMillis = 300, + easing = EasingTokens.EmphasizedDecelerate + ) + ) + ) { + // We need a box as applying the padding elsewhere leads to either + // janky animation or layout + Box(Modifier.safeDrawingPadding()) { + Card( + Modifier + .fillMaxWidth() + .widthIn(max = 600.dp) + .padding(8.dp) + ) { + VoicePermissionSwitch( + onCancel = { + showVoiceUI = false + } + ) { + voiceChannelId?.let { + VoiceSheet( + it, + onDisconnect = { + showVoiceUI = false + voiceChannelId = null + } + ) + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/chat/peptide/api/AppAPI.kt b/app/src/main/java/chat/peptide/api/AppAPI.kt index 0bfd0910..9d56b9d3 100644 --- a/app/src/main/java/chat/peptide/api/AppAPI.kt +++ b/app/src/main/java/chat/peptide/api/AppAPI.kt @@ -35,7 +35,6 @@ import io.ktor.client.plugins.logging.Logging import io.ktor.client.plugins.websocket.WebSockets import io.ktor.client.request.header import io.ktor.serialization.kotlinx.json.json -import io.sentry.Sentry import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -320,7 +319,7 @@ object PeptideAPI { } RealtimeSocket.updateDisconnectionState(DisconnectionState.Disconnected) } catch (e: Exception) { - Sentry.captureMessage("Error in socket error handling: $e") +// Sentry.captureMessage("Error in socket error handling: $e") } } } @@ -400,7 +399,7 @@ object PeptideAPI { if (!openForLocalHydration) { Log.w("PeptideAPI", "Hydration is closed, but was called") // Stale data is worst case, let's track it even in prod - Sentry.captureMessage("Local hydration called twice or after real data was fetched") +// Sentry.captureMessage("Local hydration called twice or after real data was fetched") return } diff --git a/app/src/main/java/chat/peptide/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/peptide/screens/chat/ChatRouterScreen.kt index 2bc3229e..afac9d47 100644 --- a/app/src/main/java/chat/peptide/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/peptide/screens/chat/ChatRouterScreen.kt @@ -101,7 +101,6 @@ import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.messaging.FirebaseMessaging import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import io.sentry.Sentry import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import javax.inject.Inject @@ -221,7 +220,7 @@ class ChatRouterViewModel @Inject constructor( OnCompleteListener { task -> if (!task.isSuccessful) { Log.w("FCM", "Fetching FCM registration token failed", task.exception) - task.exception?.let { Sentry.captureException(it) } +// task.exception?.let { Sentry.captureException(it) } return@OnCompleteListener } @@ -691,6 +690,7 @@ fun ChatRouterScreen( AlertDialog( onDismissRequest = { showChannelUnavailableAlert = false + viewModel.setSaveDestination(ChatRouterDestination.Discover) }, icon = { Icon( @@ -716,6 +716,7 @@ fun ChatRouterScreen( confirmButton = { PepTextButton(onClick = { showChannelUnavailableAlert = false + viewModel.setSaveDestination(ChatRouterDestination.Discover) }) { Text(text = stringResource(id = R.string.ok)) } diff --git a/app/src/main/java/chat/peptide/screens/chat/views/OverviewScreen.kt b/app/src/main/java/chat/peptide/screens/chat/views/OverviewScreen.kt index 9599fa8c..6fbfbc63 100644 --- a/app/src/main/java/chat/peptide/screens/chat/views/OverviewScreen.kt +++ b/app/src/main/java/chat/peptide/screens/chat/views/OverviewScreen.kt @@ -62,7 +62,6 @@ import chat.peptide.composables.skeletons.UserOverviewSkeleton import chat.peptide.internals.extensions.zero import chat.peptide.screens.chat.LocalIsConnected import chat.peptide.sheets.UserCardSheet -import io.sentry.Sentry @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -89,7 +88,7 @@ fun OverviewScreen( } } catch (e: Exception) { Log.e("OverviewScreen", "Failed to fetch self", e) - Sentry.captureException(e) +// Sentry.captureException(e) isLoading = false } }