mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
feat(relayauth): follow-trust default + action-aware prompt copy
User-perspective fixes so the auth prompt stops feeling like "why does
this app keep asking me to log into things":
1. Default policy is now TRUSTED_FOLLOWS (new installs only; a persisted
choice is untouched). Messaging/notifying people you follow just works;
only strangers prompt.
2/3. The prompt is reframed around what the user was doing and its
consequence, not "log in":
- Title is action-aware: "Send your message to Alice?" / "Notify …?" /
"Load posts from …?", resolving the counterparty's display name.
- The message states the real tradeoff (the relay confirms it's you;
its operator sees which account you are) instead of "log in".
- A purpose-specific, error-tinted consequence line — "If you don't,
your message to Alice won't be delivered." — so Block/Dismiss no
longer silently break the exact thing the user was trying to do.
Per-purpose reason labels now show only when a challenge spans multiple
purposes (the title carries the single-purpose case).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
This commit is contained in:
@@ -638,7 +638,7 @@ object LocalPreferences {
|
||||
val defaultRelayAuthPolicy =
|
||||
getString(PrefKeys.DEFAULT_RELAY_AUTH_POLICY, null)
|
||||
?.let { runCatching { RelayAuthPolicy.valueOf(it) }.getOrNull() }
|
||||
?: RelayAuthPolicy.IF_IN_MY_LIST
|
||||
?: RelayAuthPolicy.TRUSTED_FOLLOWS
|
||||
val relayGroupViewMode = RelayGroupViewMode.fromName(getString(PrefKeys.RELAY_GROUP_VIEW_MODE, null))
|
||||
val relayAuthTrustFollowsForReads = getBoolean(PrefKeys.RELAY_AUTH_TRUST_FOLLOWS_FOR_READS, false)
|
||||
val splitNotificationsEnabled = getBoolean(PrefKeys.SPLIT_NOTIFICATIONS_ENABLED, false)
|
||||
|
||||
@@ -273,7 +273,7 @@ class AccountSettings(
|
||||
var callVideoResolution: CallVideoResolution = CallVideoResolution.HD_720,
|
||||
var callMaxBitrateBps: Int = 1_500_000,
|
||||
val callsEnabled: MutableStateFlow<Boolean> = MutableStateFlow(true),
|
||||
val defaultRelayAuthPolicy: MutableStateFlow<RelayAuthPolicy> = MutableStateFlow(RelayAuthPolicy.IF_IN_MY_LIST),
|
||||
val defaultRelayAuthPolicy: MutableStateFlow<RelayAuthPolicy> = MutableStateFlow(RelayAuthPolicy.TRUSTED_FOLLOWS),
|
||||
val relayGroupViewMode: MutableStateFlow<RelayGroupViewMode> = MutableStateFlow(RelayGroupViewMode.DEFAULT),
|
||||
val relayAuthTrustFollowsForReads: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
) : EphemeralChatRepository,
|
||||
|
||||
+80
-6
@@ -53,10 +53,12 @@ import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.relayauth.AuthPurpose
|
||||
import com.vitorpamplona.amethyst.commons.relayauth.AuthPurposeKind
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.RelayAuthPrompt
|
||||
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.UserAuthChoice
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -103,6 +105,12 @@ private fun RelayAuthPromptDialog(
|
||||
accountViewModel: AccountViewModel,
|
||||
onChoice: (UserAuthChoice) -> Unit,
|
||||
) {
|
||||
// The action the user was actually doing drives the title and the "if you don't" consequence,
|
||||
// so the out-of-context prompt reconnects to their intent.
|
||||
val primary = remember(prompt) { prompt.purposes.primaryNamed() }
|
||||
val who = primary?.let { counterpartyLabel(it.counterparties, accountViewModel) }
|
||||
val showLabels = prompt.purposes.size > 1
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { onChoice(UserAuthChoice.DISMISS) },
|
||||
icon = {
|
||||
@@ -112,7 +120,7 @@ private fun RelayAuthPromptDialog(
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
},
|
||||
title = { Text(stringRes(R.string.relay_auth_prompt_title)) },
|
||||
title = { Text(titleFor(primary?.kind, who)) },
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
@@ -123,11 +131,13 @@ private fun RelayAuthPromptDialog(
|
||||
|
||||
prompt.purposes.forEach { purpose ->
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text(
|
||||
text = stringRes(reasonRes(purpose.kind)),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
if (showLabels) {
|
||||
Text(
|
||||
text = stringRes(reasonRes(purpose.kind)),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
val people = purpose.counterparties.toList()
|
||||
if (people.size <= NAMED_ROWS_MAX) {
|
||||
people.forEach { CounterpartyRow(it, accountViewModel) }
|
||||
@@ -136,6 +146,14 @@ private fun RelayAuthPromptDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
consequenceFor(primary?.kind, who)?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
@@ -249,3 +267,59 @@ private fun reasonRes(kind: AuthPurposeKind): Int =
|
||||
AuthPurposeKind.READ_OUTBOX -> R.string.relay_auth_reason_read_outbox
|
||||
AuthPurposeKind.MY_OWN_RELAY -> R.string.relay_auth_reason_my_own_relay
|
||||
}
|
||||
|
||||
/** The purpose whose counterparties best describe what the user was doing (most user-facing first). */
|
||||
private fun List<AuthPurpose>.primaryNamed(): AuthPurpose? =
|
||||
listOf(AuthPurposeKind.SEND_DM, AuthPurposeKind.NOTIFY_INBOX, AuthPurposeKind.READ_OUTBOX)
|
||||
.firstNotNullOfOrNull { kind -> firstOrNull { it.kind == kind && it.counterparties.isNotEmpty() } }
|
||||
|
||||
@Composable
|
||||
private fun titleFor(
|
||||
kind: AuthPurposeKind?,
|
||||
who: String?,
|
||||
): String =
|
||||
when (kind) {
|
||||
AuthPurposeKind.SEND_DM -> stringRes(R.string.relay_auth_title_send_dm, who ?: "")
|
||||
AuthPurposeKind.NOTIFY_INBOX -> stringRes(R.string.relay_auth_title_notify, who ?: "")
|
||||
AuthPurposeKind.READ_OUTBOX -> stringRes(R.string.relay_auth_title_read, who ?: "")
|
||||
else -> stringRes(R.string.relay_auth_prompt_title)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun consequenceFor(
|
||||
kind: AuthPurposeKind?,
|
||||
who: String?,
|
||||
): String? =
|
||||
when (kind) {
|
||||
AuthPurposeKind.SEND_DM -> stringRes(R.string.relay_auth_consequence_send_dm, who ?: "")
|
||||
AuthPurposeKind.NOTIFY_INBOX -> stringRes(R.string.relay_auth_consequence_notify, who ?: "")
|
||||
AuthPurposeKind.READ_OUTBOX -> stringRes(R.string.relay_auth_consequence_read, who ?: "")
|
||||
else -> null
|
||||
}
|
||||
|
||||
/** A short label for a set of counterparties: the first person's name, or "Alice and others". */
|
||||
@Composable
|
||||
private fun counterpartyLabel(
|
||||
pubkeys: Set<HexKey>,
|
||||
accountViewModel: AccountViewModel,
|
||||
): String {
|
||||
val first = pubkeys.firstOrNull() ?: return ""
|
||||
val name = rememberDisplayName(first, accountViewModel)
|
||||
return if (pubkeys.size > 1) stringRes(R.string.relay_auth_name_and_others, name) else name
|
||||
}
|
||||
|
||||
/** The best display name for [pubkey], reactive to metadata arriving from relays. */
|
||||
@Composable
|
||||
private fun rememberDisplayName(
|
||||
pubkey: HexKey,
|
||||
accountViewModel: AccountViewModel,
|
||||
): String {
|
||||
var user by remember(pubkey) { mutableStateOf(accountViewModel.getUserIfExists(pubkey)) }
|
||||
if (user == null) {
|
||||
LaunchedEffect(pubkey) { user = accountViewModel.checkGetOrCreateUser(pubkey) }
|
||||
}
|
||||
val loaded = user ?: return pubkey.take(8)
|
||||
// Reading the observed metadata registers a snapshot read, so the name updates when it arrives.
|
||||
val metadata by observeUserInfo(loaded, accountViewModel)
|
||||
return metadata?.info?.bestName() ?: loaded.toBestDisplayName()
|
||||
}
|
||||
|
||||
@@ -829,8 +829,17 @@
|
||||
<string name="relay_auth_policy_trusted_follows_desc">Also authenticate with relays that serve people you follow, such as sending a message to a friend. You\'ll be asked about anyone else.</string>
|
||||
<string name="relay_auth_trust_reads">Also trust when reading their posts</string>
|
||||
<string name="relay_auth_trust_reads_desc">Log in automatically to download posts from people you follow, not just to message or notify them.</string>
|
||||
<string name="relay_auth_prompt_title">Log in to this relay?</string>
|
||||
<string name="relay_auth_prompt_message">This relay asks you to log in before it will:</string>
|
||||
<string name="relay_auth_prompt_title">Confirm it\'s you to this relay?</string>
|
||||
<string name="relay_auth_prompt_message">This relay wants to confirm it\'s really you first. Its operator will see which account you are.</string>
|
||||
<!-- Action-aware titles: %1$s is the person (or "Alice and others"). -->
|
||||
<string name="relay_auth_title_send_dm">Send your message to %1$s?</string>
|
||||
<string name="relay_auth_title_notify">Notify %1$s?</string>
|
||||
<string name="relay_auth_title_read">Load posts from %1$s?</string>
|
||||
<!-- What happens if the user doesn\'t confirm, tied to what they were doing. %1$s is the person. -->
|
||||
<string name="relay_auth_consequence_send_dm">If you don\'t, your message to %1$s won\'t be delivered.</string>
|
||||
<string name="relay_auth_consequence_notify">If you don\'t, %1$s won\'t be notified about this.</string>
|
||||
<string name="relay_auth_consequence_read">If you don\'t, you won\'t see posts from %1$s here.</string>
|
||||
<string name="relay_auth_name_and_others">%1$s and others</string>
|
||||
<string name="relay_auth_reason_send_dm">Send your private message to:</string>
|
||||
<string name="relay_auth_reason_notify_inbox">Notify:</string>
|
||||
<string name="relay_auth_reason_read_outbox">Download posts from:</string>
|
||||
|
||||
Reference in New Issue
Block a user