From a302c2d13cc2b5efcdc636103a20144014ffd6b2 Mon Sep 17 00:00:00 2001 From: Infi Date: Tue, 8 Oct 2024 19:32:37 +0200 Subject: [PATCH] feat: health alerts Signed-off-by: Infi --- .../chat/revolt/activities/MainActivity.kt | 41 ++++++++++++ .../api/routes/microservices/health/Health.kt | 12 ++++ .../java/chat/revolt/api/schemas/Health.kt | 25 ++++++++ .../revolt/components/generic/HealthAlert.kt | 62 +++++++++++++++++++ app/src/main/res/values/strings.xml | 5 ++ 5 files changed, 145 insertions(+) create mode 100644 app/src/main/java/chat/revolt/api/routes/microservices/health/Health.kt create mode 100644 app/src/main/java/chat/revolt/api/schemas/Health.kt create mode 100644 app/src/main/java/chat/revolt/components/generic/HealthAlert.kt diff --git a/app/src/main/java/chat/revolt/activities/MainActivity.kt b/app/src/main/java/chat/revolt/activities/MainActivity.kt index f09c01cf..efc814b4 100644 --- a/app/src/main/java/chat/revolt/activities/MainActivity.kt +++ b/app/src/main/java/chat/revolt/activities/MainActivity.kt @@ -43,10 +43,13 @@ import chat.revolt.api.HitRateLimitException import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltHttp import chat.revolt.api.api +import chat.revolt.api.routes.microservices.health.healthCheck import chat.revolt.api.routes.onboard.needsOnboarding +import chat.revolt.api.schemas.HealthNotice import chat.revolt.api.settings.Experiments import chat.revolt.api.settings.LoadedSettings import chat.revolt.api.settings.SyncedSettings +import chat.revolt.components.generic.HealthAlert import chat.revolt.ndk.NativeLibraries import chat.revolt.persistence.KVStorage import chat.revolt.screens.DefaultDestinationScreen @@ -132,6 +135,8 @@ class MainActivityViewModel @Inject constructor( viewModelScope.launch { Log.d("MainActivity", "Hydrating Experiments from KV") Experiments.hydrateWithKv() + Log.d("MainActivity", "Performing health check") + doHealthCheck() } } @@ -219,6 +224,30 @@ class MainActivityViewModel @Inject constructor( } } + 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) + } + } + init { Log.d("MainActivity", "Starting up") doPreStartupTasks() @@ -273,6 +302,9 @@ class MainActivity : FragmentActivity() { windowSizeClass, viewModel.nextDestination.collectAsState().value, viewModel.isConnected.collectAsState().value, + viewModel.activeAlert.collectAsState().value, + viewModel.isAlertActive.collectAsState().value, + viewModel::onDismissHealthAlert, viewModel::checkLoggedInState, viewModel::updateNextDestination ) @@ -299,6 +331,9 @@ fun AppEntrypoint( windowSizeClass: WindowSizeClass, nextDestination: String?, isConnected: Boolean, + healthNotice: HealthNotice?, + isHealthAlertActive: Boolean, + onDismissHealthAlert: () -> Unit = {}, onRetryConnection: () -> Unit, onUpdateNextDestination: (String) -> Unit = {} ) { @@ -313,6 +348,12 @@ fun AppEntrypoint( .fillMaxSize(), color = MaterialTheme.colorScheme.background ) { + if (isHealthAlertActive) { + healthNotice?.let { + HealthAlert(notice = healthNotice, onDismiss = onDismissHealthAlert) + } + } + NavHost( navController = navController, startDestination = "default", diff --git a/app/src/main/java/chat/revolt/api/routes/microservices/health/Health.kt b/app/src/main/java/chat/revolt/api/routes/microservices/health/Health.kt new file mode 100644 index 00000000..5fb2dab5 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/routes/microservices/health/Health.kt @@ -0,0 +1,12 @@ +package chat.revolt.api.routes.microservices.health + +import chat.revolt.api.RevoltHttp +import chat.revolt.api.RevoltJson +import chat.revolt.api.schemas.HealthNotice +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText + +suspend fun healthCheck(): HealthNotice { + val response = RevoltHttp.get("https://health.revolt.chat/api/health").bodyAsText() + return RevoltJson.decodeFromString(HealthNotice.serializer(), response) +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/Health.kt b/app/src/main/java/chat/revolt/api/schemas/Health.kt new file mode 100644 index 00000000..70980109 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/schemas/Health.kt @@ -0,0 +1,25 @@ +package chat.revolt.api.schemas + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class HealthNotice( + val version: String? = null, + val alert: Alert? = null +) + +@Serializable +data class Alert( + val text: String? = null, + @SerialName("dismissable") + val dismissible: Boolean? = null, + val actions: List? = null +) + +@Serializable +data class Action( + val text: String? = null, + val type: String? = null, + val href: String? = null +) diff --git a/app/src/main/java/chat/revolt/components/generic/HealthAlert.kt b/app/src/main/java/chat/revolt/components/generic/HealthAlert.kt new file mode 100644 index 00000000..cd37a3cf --- /dev/null +++ b/app/src/main/java/chat/revolt/components/generic/HealthAlert.kt @@ -0,0 +1,62 @@ +package chat.revolt.components.generic + +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.core.net.toUri +import chat.revolt.R +import chat.revolt.api.schemas.HealthNotice + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HealthAlert(notice: HealthNotice, onDismiss: () -> Unit) { + val context = LocalContext.current + val backgroundColour = MaterialTheme.colorScheme.background + + AlertDialog( + onDismissRequest = { + // Purposefully empty + }, + title = { + Text(stringResource(R.string.service_health_alert)) + }, + text = { + Text(notice.alert?.text ?: stringResource(R.string.service_health_alert_body_default)) + }, + confirmButton = { + notice.alert?.actions?.firstOrNull()?.let { action -> + when (action.type) { + "external" -> TextButton( + onClick = { + val customTab = CustomTabsIntent.Builder() + .setShowTitle(true) + .setDefaultColorSchemeParams( + CustomTabColorSchemeParams.Builder() + .setToolbarColor(backgroundColour.toArgb()) + .build() + ) + .build() + customTab.launchUrl(context, action.href?.toUri() ?: return@TextButton) + } + ) { + Text( + action.text + ?: stringResource(R.string.service_health_alert_actions_default) + ) + } + } + } + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.service_health_alert_actions_dismiss)) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2e0b8f16..cf073d33 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,11 @@ Resend Send email + Currently ongoing + We are currently experiencing issues with our services. Please check our status page for more information. + Learn more + Dismiss + Let\'s log you in Forgot password? Resend a verification email