feat: health alerts

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-10-08 19:32:37 +02:00
parent 48e20d5464
commit a302c2d13c
5 changed files with 145 additions and 0 deletions

View File

@ -43,10 +43,13 @@ import chat.revolt.api.HitRateLimitException
import chat.revolt.api.RevoltAPI import chat.revolt.api.RevoltAPI
import chat.revolt.api.RevoltHttp import chat.revolt.api.RevoltHttp
import chat.revolt.api.api import chat.revolt.api.api
import chat.revolt.api.routes.microservices.health.healthCheck
import chat.revolt.api.routes.onboard.needsOnboarding import chat.revolt.api.routes.onboard.needsOnboarding
import chat.revolt.api.schemas.HealthNotice
import chat.revolt.api.settings.Experiments import chat.revolt.api.settings.Experiments
import chat.revolt.api.settings.LoadedSettings import chat.revolt.api.settings.LoadedSettings
import chat.revolt.api.settings.SyncedSettings import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.generic.HealthAlert
import chat.revolt.ndk.NativeLibraries import chat.revolt.ndk.NativeLibraries
import chat.revolt.persistence.KVStorage import chat.revolt.persistence.KVStorage
import chat.revolt.screens.DefaultDestinationScreen import chat.revolt.screens.DefaultDestinationScreen
@ -132,6 +135,8 @@ class MainActivityViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
Log.d("MainActivity", "Hydrating Experiments from KV") Log.d("MainActivity", "Hydrating Experiments from KV")
Experiments.hydrateWithKv() Experiments.hydrateWithKv()
Log.d("MainActivity", "Performing health check")
doHealthCheck()
} }
} }
@ -219,6 +224,30 @@ class MainActivityViewModel @Inject constructor(
} }
} }
val activeAlert = MutableStateFlow<HealthNotice?>(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 { init {
Log.d("MainActivity", "Starting up") Log.d("MainActivity", "Starting up")
doPreStartupTasks() doPreStartupTasks()
@ -273,6 +302,9 @@ class MainActivity : FragmentActivity() {
windowSizeClass, windowSizeClass,
viewModel.nextDestination.collectAsState().value, viewModel.nextDestination.collectAsState().value,
viewModel.isConnected.collectAsState().value, viewModel.isConnected.collectAsState().value,
viewModel.activeAlert.collectAsState().value,
viewModel.isAlertActive.collectAsState().value,
viewModel::onDismissHealthAlert,
viewModel::checkLoggedInState, viewModel::checkLoggedInState,
viewModel::updateNextDestination viewModel::updateNextDestination
) )
@ -299,6 +331,9 @@ fun AppEntrypoint(
windowSizeClass: WindowSizeClass, windowSizeClass: WindowSizeClass,
nextDestination: String?, nextDestination: String?,
isConnected: Boolean, isConnected: Boolean,
healthNotice: HealthNotice?,
isHealthAlertActive: Boolean,
onDismissHealthAlert: () -> Unit = {},
onRetryConnection: () -> Unit, onRetryConnection: () -> Unit,
onUpdateNextDestination: (String) -> Unit = {} onUpdateNextDestination: (String) -> Unit = {}
) { ) {
@ -313,6 +348,12 @@ fun AppEntrypoint(
.fillMaxSize(), .fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
) { ) {
if (isHealthAlertActive) {
healthNotice?.let {
HealthAlert(notice = healthNotice, onDismiss = onDismissHealthAlert)
}
}
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = "default", startDestination = "default",

View File

@ -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)
}

View File

@ -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<Action>? = null
)
@Serializable
data class Action(
val text: String? = null,
val type: String? = null,
val href: String? = null
)

View File

@ -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))
}
}
)
}

View File

@ -25,6 +25,11 @@
<string name="resend">Resend</string> <string name="resend">Resend</string>
<string name="send_email">Send email</string> <string name="send_email">Send email</string>
<string name="service_health_alert">Currently ongoing</string>
<string name="service_health_alert_body_default">We are currently experiencing issues with our services. Please check our status page for more information.</string>
<string name="service_health_alert_actions_default">Learn more</string>
<string name="service_health_alert_actions_dismiss">Dismiss</string>
<string name="login_heading">Let\'s log you in</string> <string name="login_heading">Let\'s log you in</string>
<string name="password_forgot">Forgot password?</string> <string name="password_forgot">Forgot password?</string>
<string name="resend_verification">Resend a verification email</string> <string name="resend_verification">Resend a verification email</string>