feat(relayauth): modernize the per-relay list to match the app's relay rows

Restore the "Per-relay overrides" heading and rebuild each relay card using the
same conventions as the other relay screens: the relay's NIP-11 icon (robohash
fallback), the shortened displayUrl instead of the raw URL, last-used as a
subtitle under the name, and the whole card tappable to open the relay's
NIP-11 info screen (Route.RelayInfo). The Allow/Deny chip and Forget stay on
the trailing edge; the served-people facepile sits on its own row below.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
This commit is contained in:
Claude
2026-07-10 22:40:13 +00:00
parent 7c58ea31fd
commit 3e6cda8407
2 changed files with 55 additions and 20 deletions
@@ -64,16 +64,24 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.relayauth.AuthPurposeKind
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthDecision
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPolicy
import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo
import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.LoadRelayAuthUser
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.DataStoreRelayAuthPermissionStore
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.RelayAuthPermissionLedger
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.PolicyCard
import com.vitorpamplona.amethyst.ui.theme.MediumRelayIconModifier
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -229,6 +237,7 @@ fun RelayAuthSettingsScreen(
.orEmpty(),
lastUsedSecs = lastUsed[url],
accountViewModel = accountViewModel,
nav = nav,
onToggle = {
scope.launch {
// null (allowed by policy) or ALLOW -> block; DENY -> allow.
@@ -260,9 +269,10 @@ fun RelayAuthSettingsScreen(
}
/**
* One relay's card in the merged list: URL, an allow/deny chip, a facepile of the people it serves,
* and when it was last used. [decision] is null when the relay is allowed by policy rather than an
* explicit override; the chip still reads "Allowed" and tapping it records an explicit block.
* One relay's card in the merged list: NIP-11 icon + shortened URL (tap the card to open the relay's
* info screen), when it was last used, an Allow/Deny chip, a Forget button, and a facepile of the
* people it serves. [decision] is null when the relay is allowed by policy rather than an explicit
* override; the chip still reads "Allowed" and tapping it records an explicit block.
*/
@Composable
private fun RelayCard(
@@ -271,30 +281,43 @@ private fun RelayCard(
servedUsers: List<HexKey>,
lastUsedSecs: Long?,
accountViewModel: AccountViewModel,
nav: INav,
onToggle: () -> Unit,
onForget: () -> Unit,
) {
val context = LocalContext.current
val relay = remember(url) { url.normalizeRelayUrlOrNull() }
Surface(
onClick = { nav.nav(Route.RelayInfo(url)) },
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.medium,
modifier = Modifier.fillMaxWidth(),
) {
Column(
modifier = Modifier.padding(start = 12.dp, top = 8.dp, end = 4.dp, bottom = 12.dp),
modifier = Modifier.padding(start = 12.dp, top = 10.dp, end = 4.dp, bottom = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
text = url,
style = MaterialTheme.typography.titleSmall,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
modifier = Modifier.weight(1f),
)
RelayIcon(relay, url, accountViewModel)
Column(Modifier.weight(1f)) {
Text(
text = relay?.displayUrl() ?: url,
style = MaterialTheme.typography.titleSmall,
maxLines = 1,
overflow = TextOverflow.MiddleEllipsis,
)
if (lastUsedSecs != null && lastUsedSecs > 0L) {
Text(
text = stringResource(R.string.relay_auth_last_used, timeAgo(lastUsedSecs, context, prefix = "")),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
DecisionChip(decision = decision, onToggle = onToggle)
IconButton(onClick = onForget) {
Icon(MaterialSymbols.Close, contentDescription = stringResource(R.string.relay_auth_forget))
@@ -303,17 +326,29 @@ private fun RelayCard(
if (servedUsers.isNotEmpty()) {
UserFacepile(servedUsers, accountViewModel)
}
if (lastUsedSecs != null && lastUsedSecs > 0L) {
Text(
text = stringResource(R.string.relay_auth_last_used, timeAgo(lastUsedSecs, context, prefix = "")),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}
/** The relay's NIP-11 icon (robohash fallback), matching the other relay lists in the app. */
@Composable
private fun RelayIcon(
relay: NormalizedRelayUrl?,
url: String,
accountViewModel: AccountViewModel,
) {
val info = if (relay != null) loadRelayInfo(relay).value else null
RobohashFallbackAsyncImage(
robot = info?.id ?: relay?.displayUrl() ?: url,
model = info?.icon,
contentDescription = stringResource(R.string.relay_info, url),
colorFilter = RelayIconFilter,
modifier = MediumRelayIconModifier,
loadProfilePicture = accountViewModel.settings.showProfilePictures(),
loadRobohash = accountViewModel.settings.isNotPerformanceMode(),
)
}
/** Allow/deny pill for a relay. Green when allowed (explicitly or by policy), red when blocked. */
@Composable
private fun DecisionChip(
+1 -1
View File
@@ -855,7 +855,7 @@
<string name="relay_auth_always_allow">Always allow this relay</string>
<string name="relay_auth_always_allow_follows">Always allow my follows\' relays</string>
<string name="relay_auth_block">Block this relay</string>
<string name="relay_auth_per_relay_overrides">Individual relays</string>
<string name="relay_auth_per_relay_overrides">Per-relay overrides</string>
<string name="relay_auth_forget">Forget</string>
<string name="relay_auth_last_used">Last used %1$s ago</string>
<string name="relay_send_failed_title">Couldn\'t deliver your event</string>