Merge pull request #500 from greenart7c3/claude/relay-trusted-score-toggle-vt8j12

Add user-configurable trust score toggle in settings
This commit is contained in:
greenart7c3
2026-07-13 15:53:12 -03:00
committed by GitHub
11 changed files with 93 additions and 12 deletions
@@ -77,6 +77,7 @@ private enum class SettingsKeys(val key: String) {
RATE_LIMIT_MAX_PER_WINDOW("rate_limit_max_per_window"),
RATE_LIMIT_WINDOW_SECONDS("rate_limit_window_seconds"),
PROFILE_FETCH_INTERVAL("profile_fetch_interval"),
TRUST_SCORE_ENABLED("trust_score_enabled"),
}
@Immutable
@@ -156,6 +157,7 @@ object LocalPreferences {
putBoolean(SettingsKeys.RATE_LIMIT_ENABLED.key, settings.rateLimitEnabled)
putInt(SettingsKeys.RATE_LIMIT_MAX_PER_WINDOW.key, settings.rateLimitMaxPerWindow)
putInt(SettingsKeys.RATE_LIMIT_WINDOW_SECONDS.key, settings.rateLimitWindowSeconds)
putBoolean(SettingsKeys.TRUST_SCORE_ENABLED.key, settings.trustScoreEnabled)
}
}
}
@@ -319,6 +321,7 @@ object LocalPreferences {
} catch (_: IllegalArgumentException) {
ProfileFetchInterval.FIFTEEN_MINUTES
},
trustScoreEnabled = getBoolean(SettingsKeys.TRUST_SCORE_ENABLED.key, true),
)
}
}
@@ -39,6 +39,7 @@ data class AmberSettings(
val rateLimitMaxPerWindow: Int = 5,
val rateLimitWindowSeconds: Int = 30,
val profileFetchInterval: ProfileFetchInterval = ProfileFetchInterval.FIFTEEN_MINUTES,
val trustScoreEnabled: Boolean = true,
) {
val useProxy: Boolean get() = torMode != TorMode.DISABLED
}
@@ -23,12 +23,18 @@ object TrustScoreService {
private val cache = ConcurrentHashMap<String, CachedScore>()
private val mapper = jacksonObjectMapper()
/**
* Whether trust scores should be fetched and displayed.
* False on the offline flavor or when the user disabled trust scores in the settings.
*/
fun isEnabled(): Boolean = !BuildFlavorChecker.isOfflineFlavor() && Amber.instance.settings.trustScoreEnabled
/**
* Gets the trust score for a relay URL.
* Returns cached value if available and not expired, otherwise fetches from API.
*/
suspend fun getScore(relayUrl: String): Int? {
if (BuildFlavorChecker.isOfflineFlavor()) {
if (!isEnabled()) {
return null
}
@@ -50,6 +56,10 @@ object TrustScoreService {
* Returns null if not cached or expired.
*/
fun getCachedScore(relayUrl: String): Int? {
if (!isEnabled()) {
return null
}
val normalizedUrl = normalizeUrl(relayUrl)
val cached = cache[normalizedUrl] ?: return null
@@ -64,7 +74,7 @@ object TrustScoreService {
* Prefetches scores for multiple relay URLs in parallel.
*/
suspend fun prefetchScores(relayUrls: List<String>) {
if (BuildFlavorChecker.isOfflineFlavor()) {
if (!isEnabled()) {
return
}
@@ -83,7 +93,7 @@ object TrustScoreService {
* Fetches the trust score from the API.
*/
private suspend fun fetchScore(relayUrl: String): Int? {
if (BuildFlavorChecker.isOfflineFlavor()) {
if (!isEnabled()) {
return null
}
@@ -33,7 +33,6 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.BuildFlavorChecker
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.service.RelayUrlUtils
@@ -56,7 +55,7 @@ fun AuthWhitelistScreen(
val loadingScores = remember { mutableStateMapOf<String, Boolean>() }
LaunchedEffect(whitelist.toList()) {
if (!BuildFlavorChecker.isOfflineFlavor()) {
if (TrustScoreService.isEnabled()) {
whitelist.forEach { hostname ->
if (!trustScores.containsKey(hostname)) {
loadingScores[hostname] = true
@@ -74,7 +74,7 @@ fun DefaultProfileRelaysScreen(
// Fetch trust scores for all relays
LaunchedEffect(relays2.toList()) {
if (!BuildFlavorChecker.isOfflineFlavor()) {
if (TrustScoreService.isEnabled()) {
relays2.forEach { relay ->
val url = relay.url
if (!trustScores.containsKey(url)) {
@@ -47,7 +47,6 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.BuildFlavorChecker
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.database.ApplicationWithPermissions
import com.greenart7c3.nostrsigner.database.generateBunkerPrivKey
@@ -99,7 +98,7 @@ fun EditConfigurationScreen(
)
}
if (!BuildFlavorChecker.isOfflineFlavor()) {
if (TrustScoreService.isEnabled()) {
relays.forEach { relay ->
val url = relay.url
if (!trustScores.containsKey(url)) {
@@ -1,21 +1,55 @@
package com.greenart7c3.nostrsigner.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.BuildFlavorChecker
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.service.TrustScoreService
import com.greenart7c3.nostrsigner.ui.components.AmberButton
import com.greenart7c3.nostrsigner.ui.navigation.Route
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun RelaysScreen(
modifier: Modifier = Modifier,
navController: NavController,
) {
val scope = rememberCoroutineScope()
var trustScoreEnabled by remember { mutableStateOf(Amber.instance.settings.trustScoreEnabled) }
fun setTrustScoreEnabled(enabled: Boolean) {
trustScoreEnabled = enabled
Amber.instance.settings = Amber.instance.settings.copy(trustScoreEnabled = enabled)
if (!enabled) {
TrustScoreService.clearCache()
}
scope.launch(Dispatchers.IO) {
LocalPreferences.saveSettingsToEncryptedStorage(Amber.instance.settings)
}
}
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp),
@@ -33,5 +67,33 @@ fun RelaysScreen(
navController.navigate(Route.DefaultProfileRelaysScreen.route)
},
)
if (!BuildFlavorChecker.isOfflineFlavor()) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
setTrustScoreEnabled(!trustScoreEnabled)
}
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
Column(modifier = Modifier.weight(1f)) {
Text(text = stringResource(R.string.relay_trust_score))
Text(
text = stringResource(R.string.relay_trust_score_description),
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
)
}
Switch(
checked = trustScoreEnabled,
onCheckedChange = { enabled ->
setTrustScoreEnabled(enabled)
},
)
}
}
}
}
@@ -113,7 +113,7 @@ fun DefaultRelaysScreen(
// Fetch trust scores for all relays
LaunchedEffect(relays2.toList()) {
if (!BuildFlavorChecker.isOfflineFlavor()) {
if (TrustScoreService.isEnabled()) {
relays2.forEach { relay ->
val url = relay.url
if (!trustScores.containsKey(url)) {
@@ -613,7 +613,7 @@ fun ActiveRelaysScreen(
// Fetch trust scores for every relay shown
LaunchedEffect(defaultRelays.toList(), connectionRelays.toList()) {
if (!BuildFlavorChecker.isOfflineFlavor()) {
if (TrustScoreService.isEnabled()) {
(defaultRelays + connectionRelays).forEach { relay ->
val url = relay.url
if (!trustScores.containsKey(url)) {
@@ -53,7 +53,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.greenart7c3.nostrsigner.Amber
import com.greenart7c3.nostrsigner.BuildFlavorChecker
import com.greenart7c3.nostrsigner.LocalPreferences
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.Account
@@ -130,7 +129,7 @@ fun BunkerConnectRequestScreen(
// Fetch trust scores for connection relays
LaunchedEffect(connectionRelays) {
if (!BuildFlavorChecker.isOfflineFlavor() && connectionRelays.isNotEmpty()) {
if (TrustScoreService.isEnabled() && connectionRelays.isNotEmpty()) {
connectionRelays.forEach { relay ->
val url = relay.url
if (!trustScores.containsKey(url)) {
@@ -18,6 +18,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.service.TrustScoreService
enum class TrustLevel(val color: Color) {
EXCELLENT(Color(0xFF4CAF50)), // Green - 80-100
@@ -47,6 +48,7 @@ fun getTrustLevelLabel(level: TrustLevel): String = when (level) {
/**
* A badge displaying the trust score with color-coding.
* Shows the numeric score and optionally a label.
* Renders nothing when trust scores are disabled in the settings or on the offline flavor.
*/
@Composable
fun TrustScoreBadge(
@@ -55,6 +57,10 @@ fun TrustScoreBadge(
isLoading: Boolean = false,
showLabel: Boolean = false,
) {
if (!TrustScoreService.isEnabled()) {
return
}
val trustLevel = getTrustLevel(score)
if (isLoading) {
+2
View File
@@ -667,6 +667,8 @@
<string name="trust_fair">Fair</string>
<string name="trust_poor">Poor</string>
<string name="trust_unknown">Unknown</string>
<string name="relay_trust_score">Relay trust scores</string>
<string name="relay_trust_score_description">Fetch and display relay trust scores from trustedrelays.xyz</string>
<string name="relays_used">Relays used</string>
<string name="do_not_leave_the_app_until_the_key_is_generated">Do not leave the app until the key is generated</string>
<string name="status_detail">Status Detail</string>