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.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<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 {
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",

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="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="password_forgot">Forgot password?</string>
<string name="resend_verification">Resend a verification email</string>