mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
feat(nip46): security-icon trust picker + reconnect affordance
Two of the three refinements from the Primal comparison: Trust picker as security icons: unify the connect dialog and the app-detail picker on one open→shield→locked glyph set — LockOpen (Full trust, never asks), Shield (Reasonable), Lock (Paranoid) — replacing the connect dialog's emoji and the detail's heart so both surfaces read the same and the icon carries the guard-level at a glance. Reconnect affordance: when an app's relays show Offline (from the live status added last commit), offer a one-tap Reconnect — on the connected-apps row and in the detail's Relays section — that forces the relay pool to re-dial now, ignoring backoff. Shared Nip46ReconnectPill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FHr2mu5SiHwYNR7evYUuF
This commit is contained in:
+11
-5
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.commons.connectedApps.signers.AppSignerPolicy
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
|
||||
@@ -199,21 +200,21 @@ private fun SignerConnectScreen(
|
||||
) {
|
||||
PolicyOption(
|
||||
selected = selected == AppSignerPolicy.FULL_TRUST,
|
||||
icon = "❤",
|
||||
symbol = MaterialSymbols.LockOpen,
|
||||
label = stringResource(R.string.napplet_policy_full_trust),
|
||||
description = stringResource(R.string.napplet_policy_full_trust_desc),
|
||||
onClick = { selected = AppSignerPolicy.FULL_TRUST },
|
||||
)
|
||||
PolicyOption(
|
||||
selected = selected == AppSignerPolicy.REASONABLE,
|
||||
icon = "👍",
|
||||
symbol = MaterialSymbols.Shield,
|
||||
label = stringResource(R.string.napplet_policy_reasonable),
|
||||
description = stringResource(R.string.napplet_policy_reasonable_desc),
|
||||
onClick = { selected = AppSignerPolicy.REASONABLE },
|
||||
)
|
||||
PolicyOption(
|
||||
selected = selected == AppSignerPolicy.PARANOID,
|
||||
icon = "🕶",
|
||||
symbol = MaterialSymbols.Lock,
|
||||
label = stringResource(R.string.napplet_policy_paranoid),
|
||||
description = stringResource(R.string.napplet_policy_paranoid_desc),
|
||||
onClick = { selected = AppSignerPolicy.PARANOID },
|
||||
@@ -254,7 +255,7 @@ private fun SignerConnectScreen(
|
||||
@Composable
|
||||
private fun PolicyOption(
|
||||
selected: Boolean,
|
||||
icon: String,
|
||||
symbol: MaterialSymbol,
|
||||
label: String,
|
||||
description: String,
|
||||
onClick: () -> Unit,
|
||||
@@ -276,7 +277,12 @@ private fun PolicyOption(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(icon, style = MaterialTheme.typography.headlineSmall)
|
||||
Icon(
|
||||
symbol = symbol,
|
||||
contentDescription = null,
|
||||
tint = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(26.dp),
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(label, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onSurface)
|
||||
Text(description, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
|
||||
+29
-3
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -54,6 +55,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -89,6 +91,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46ActivityCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46AppIcon
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46LiveStatus
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46ReconnectPill
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.Nip46StatusDot
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.nip46AppOnline
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.nip46ClientSubtitle
|
||||
@@ -202,7 +205,12 @@ fun ConnectedAppDetailScreen(
|
||||
.collectAsStateWithLifecycle()
|
||||
val inboxRelays by accountViewModel.account.nip46Signer.inboxRelays
|
||||
.collectAsStateWithLifecycle()
|
||||
Nip46RelaysSection(nip46Info?.relays.orEmpty(), inboxRelays, connectedRelays)
|
||||
Nip46RelaysSection(
|
||||
relays = nip46Info?.relays.orEmpty(),
|
||||
inboxRelays = inboxRelays,
|
||||
connectedRelays = connectedRelays,
|
||||
onReconnect = { accountViewModel.account.client.reconnect(ignoreRetryDelays = true) },
|
||||
)
|
||||
}
|
||||
|
||||
// Signing trust level section
|
||||
@@ -312,8 +320,26 @@ private fun Nip46RelaysSection(
|
||||
relays: Set<String>,
|
||||
inboxRelays: Set<NormalizedRelayUrl>,
|
||||
connectedRelays: Set<NormalizedRelayUrl>,
|
||||
onReconnect: () -> Unit,
|
||||
) {
|
||||
SectionHeader(stringResource(R.string.nip46_signer_app_relays_title))
|
||||
val context = LocalContext.current
|
||||
val anyOffline =
|
||||
remember(relays, inboxRelays, connectedRelays) {
|
||||
if (relays.isEmpty()) {
|
||||
nip46AppOnline(emptySet(), inboxRelays, connectedRelays) == false
|
||||
} else {
|
||||
relays.any { RelayUrlNormalizer.normalizeOrNull(it)?.let { r -> r !in connectedRelays } ?: true }
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.weight(1f)) { SectionHeader(stringResource(R.string.nip46_signer_app_relays_title)) }
|
||||
if (anyOffline) {
|
||||
Nip46ReconnectPill {
|
||||
onReconnect()
|
||||
Toast.makeText(context, R.string.nip46_signer_reconnecting, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
@@ -472,7 +498,7 @@ private fun PolicyPicker(
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
PolicyCard(
|
||||
selected = selected == AppSignerPolicy.FULL_TRUST,
|
||||
symbol = MaterialSymbols.Favorite,
|
||||
symbol = MaterialSymbols.LockOpen,
|
||||
label = stringResource(R.string.napplet_policy_full_trust),
|
||||
description = stringResource(R.string.napplet_policy_full_trust_desc),
|
||||
onClick = { onSelect(AppSignerPolicy.FULL_TRUST) },
|
||||
|
||||
+40
-6
@@ -20,7 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -34,6 +36,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
@@ -53,6 +56,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
@@ -108,6 +112,7 @@ fun Nip46ConnectedAppsScreen(
|
||||
) {
|
||||
val account = accountViewModel.account
|
||||
val signerPubKey = remember { account.signer.pubKey }
|
||||
val context = LocalContext.current
|
||||
|
||||
// Live relay state so each app shows whether the signer currently reaches it. An app that brought
|
||||
// its own relays (nostrconnect) is judged on those; a bunker-flow app rides the inbox relays.
|
||||
@@ -167,6 +172,10 @@ fun Nip46ConnectedAppsScreen(
|
||||
Nip46AppCard(
|
||||
entry = entry,
|
||||
online = online,
|
||||
onReconnect = {
|
||||
account.client.reconnect(ignoreRetryDelays = true)
|
||||
Toast.makeText(context, R.string.nip46_signer_reconnecting, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onClick = { nav.nav(Route.ConnectedAppDetail(entry.coordinate)) },
|
||||
)
|
||||
}
|
||||
@@ -179,6 +188,7 @@ fun Nip46ConnectedAppsScreen(
|
||||
private fun Nip46AppCard(
|
||||
entry: Nip46AppEntry,
|
||||
online: Boolean?,
|
||||
onReconnect: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
// Same identity line the detail screen (Nip46AppHeader) uses: the app's self-declared website
|
||||
@@ -241,12 +251,16 @@ private fun Nip46AppCard(
|
||||
label = { Text(policy.shortLabel(), style = MaterialTheme.typography.labelSmall) },
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
MaterialSymbols.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
if (online == false) {
|
||||
Nip46ReconnectPill(onReconnect)
|
||||
} else {
|
||||
Icon(
|
||||
MaterialSymbols.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,6 +282,26 @@ internal fun Nip46StatusDot(online: Boolean) {
|
||||
Box(Modifier.size(8.dp).clip(CircleShape).background(if (online) LiveGreen else MaterialTheme.colorScheme.outline))
|
||||
}
|
||||
|
||||
/**
|
||||
* A small "Reconnect" pill shown when an app's relays are offline. Forces the whole relay pool to
|
||||
* re-dial now (ignoring backoff), which re-establishes the offline relays. Consumes its own tap so a
|
||||
* pill inside a clickable card doesn't also open the card.
|
||||
*/
|
||||
@Composable
|
||||
internal fun Nip46ReconnectPill(onClick: () -> Unit) {
|
||||
Text(
|
||||
stringResource(R.string.nip46_signer_app_reconnect),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier =
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(999.dp))
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 11.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
|
||||
/** A [Nip46StatusDot] + Connected/Offline label showing whether the signer currently reaches an app's relays. */
|
||||
@Composable
|
||||
internal fun Nip46LiveStatus(online: Boolean) {
|
||||
|
||||
@@ -932,6 +932,8 @@
|
||||
<string name="nip46_signer_app_last_used">used %1$s</string>
|
||||
<string name="nip46_signer_app_online">Connected</string>
|
||||
<string name="nip46_signer_app_offline">Offline</string>
|
||||
<string name="nip46_signer_app_reconnect">Reconnect</string>
|
||||
<string name="nip46_signer_reconnecting">Reconnecting to relays…</string>
|
||||
<string name="nip46_signer_app_relays_title">Relays</string>
|
||||
<string name="nip46_signer_app_relays_inbox">Signs over your inbox relays — this app brought none of its own, so it keeps no extra background connection.</string>
|
||||
<string name="nip46_signer_app_relays_own_hint">Amethyst holds a background connection to each of these while this app stays connected. Forget the app to drop them.</string>
|
||||
|
||||
Reference in New Issue
Block a user