diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt index 466c7eee..97d73df2 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/LocalPreferences.kt @@ -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), ) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt index 8114f7a9..ae856f5b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/models/AmberSettings.kt @@ -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 } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/TrustScoreService.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/TrustScoreService.kt index 83f30949..5681262f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/TrustScoreService.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/TrustScoreService.kt @@ -23,12 +23,18 @@ object TrustScoreService { private val cache = ConcurrentHashMap() 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) { - 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 } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AuthWhitelistScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AuthWhitelistScreen.kt index 14c4219a..642682cf 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/AuthWhitelistScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/AuthWhitelistScreen.kt @@ -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() } LaunchedEffect(whitelist.toList()) { - if (!BuildFlavorChecker.isOfflineFlavor()) { + if (TrustScoreService.isEnabled()) { whitelist.forEach { hostname -> if (!trustScores.containsKey(hostname)) { loadingScores[hostname] = true diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/DefaultProfileRelaysScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/DefaultProfileRelaysScreen.kt index a3481376..47732630 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/DefaultProfileRelaysScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/DefaultProfileRelaysScreen.kt @@ -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)) { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditConfigurationScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditConfigurationScreen.kt index 288eb766..40769b27 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditConfigurationScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditConfigurationScreen.kt @@ -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)) { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/RelaysScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/RelaysScreen.kt index 32d096ab..02fb9ef5 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/RelaysScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/RelaysScreen.kt @@ -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) + }, + ) + } + } } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/EditRelaysDialog.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/EditRelaysDialog.kt index eb9659dd..caa263be 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/EditRelaysDialog.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/actions/EditRelaysDialog.kt @@ -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)) { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerConnectRequestScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerConnectRequestScreen.kt index d302b080..9a9a0213 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerConnectRequestScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerConnectRequestScreen.kt @@ -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)) { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/TrustScoreBadge.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/TrustScoreBadge.kt index 9d2ca46f..d9247d19 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/TrustScoreBadge.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/TrustScoreBadge.kt @@ -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) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 58676376..9f754270 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -667,6 +667,8 @@ Fair Poor Unknown + Relay trust scores + Fetch and display relay trust scores from trustedrelays.xyz Relays used Do not leave the app until the key is generated Status Detail