refactor: share user-load and reason-string helpers across relay-auth UI

The auth prompt dialog (RelayAuthPromptHost) and the relay-auth settings
screen each carried an identical copy of a pubkey→User loader and the
AuthPurposeKind→reason-string mapping. Extract both into
RelayAuthComposeHelpers (LoadRelayAuthUser, relayAuthReasonRes) and point
both call sites at the shared versions.

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:12 +00:00
parent a8fe2eee92
commit 72f1f72ea7
3 changed files with 70 additions and 55 deletions
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.service.relayClient.authCommand.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.relayauth.AuthPurposeKind
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
/**
* Loads [pubkey] from the local cache, get-or-creating (and subscribing) if absent, then hands the
* [User] (or null while it loads) to [content]. Shared by the auth prompt dialog and the relay-auth
* settings screen, which both render a person by pubkey while their metadata streams in.
*/
@Composable
internal fun LoadRelayAuthUser(
pubkey: HexKey,
accountViewModel: AccountViewModel,
content: @Composable (User?) -> Unit,
) {
var user by remember(pubkey) { mutableStateOf(accountViewModel.getUserIfExists(pubkey)) }
if (user == null) {
LaunchedEffect(pubkey) { user = accountViewModel.checkGetOrCreateUser(pubkey) }
}
content(user)
}
/** The string explaining a single [AuthPurposeKind] ("To send DMs to", "To download posts from", …). */
internal fun relayAuthReasonRes(kind: AuthPurposeKind): Int =
when (kind) {
AuthPurposeKind.SEND_DM -> R.string.relay_auth_reason_send_dm
AuthPurposeKind.NOTIFY_INBOX -> R.string.relay_auth_reason_notify_inbox
AuthPurposeKind.READ_OUTBOX -> R.string.relay_auth_reason_read_outbox
AuthPurposeKind.POST_VENUE -> R.string.relay_auth_reason_post_venue
AuthPurposeKind.READ_VENUE -> R.string.relay_auth_reason_read_venue
AuthPurposeKind.MY_OWN_RELAY -> R.string.relay_auth_reason_my_own_relay
AuthPurposeKind.OTHER -> R.string.relay_auth_reason_other
}
@@ -56,7 +56,6 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.Channel
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.channel.observeChannel
@@ -143,7 +142,7 @@ private fun RelayAuthPromptDialog(
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
if (showLabels) {
Text(
text = stringRes(reasonRes(purpose.kind)),
text = stringRes(relayAuthReasonRes(purpose.kind)),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -211,7 +210,7 @@ private fun CounterpartyRow(
pubkey: HexKey,
accountViewModel: AccountViewModel,
) {
LoadUser(pubkey, accountViewModel) { user ->
LoadRelayAuthUser(pubkey, accountViewModel) { user ->
if (user != null) {
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -234,7 +233,7 @@ private fun CounterpartyFacepile(
horizontalArrangement = Arrangement.spacedBy((-8).dp),
) {
pubkeys.take(FACEPILE_MAX).forEach { pubkey ->
LoadUser(pubkey, accountViewModel) { user ->
LoadRelayAuthUser(pubkey, accountViewModel) { user ->
if (user != null) {
ClickableUserPicture(
baseUser = user,
@@ -257,30 +256,6 @@ private fun CounterpartyFacepile(
}
}
@Composable
private fun LoadUser(
pubkey: HexKey,
accountViewModel: AccountViewModel,
content: @Composable (User?) -> Unit,
) {
var user by remember(pubkey) { mutableStateOf(accountViewModel.getUserIfExists(pubkey)) }
if (user == null) {
LaunchedEffect(pubkey) { user = accountViewModel.checkGetOrCreateUser(pubkey) }
}
content(user)
}
private fun reasonRes(kind: AuthPurposeKind): Int =
when (kind) {
AuthPurposeKind.SEND_DM -> R.string.relay_auth_reason_send_dm
AuthPurposeKind.NOTIFY_INBOX -> R.string.relay_auth_reason_notify_inbox
AuthPurposeKind.READ_OUTBOX -> R.string.relay_auth_reason_read_outbox
AuthPurposeKind.POST_VENUE -> R.string.relay_auth_reason_post_venue
AuthPurposeKind.READ_VENUE -> R.string.relay_auth_reason_read_venue
AuthPurposeKind.MY_OWN_RELAY -> R.string.relay_auth_reason_my_own_relay
AuthPurposeKind.OTHER -> R.string.relay_auth_reason_other
}
/** The purpose that best describes what the user was doing (most user-facing first). */
private fun List<AuthPurpose>.primaryNamed(): AuthPurpose? =
listOf(
@@ -63,7 +63,8 @@ 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.User
import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.LoadRelayAuthUser
import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.relayAuthReasonRes
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.DataStoreRelayAuthPermissionStore
import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.RelayAuthPermissionLedger
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -308,7 +309,7 @@ private fun RelayRationaleCard(
}
rationale.forEach { (kind, pubkeys) ->
Text(
text = stringResource(reasonRes(kind)),
text = stringResource(relayAuthReasonRes(kind)),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -334,7 +335,7 @@ private fun RationaleUserRow(
pubkey: HexKey,
accountViewModel: AccountViewModel,
) {
LoadUserForRationale(pubkey, accountViewModel) { user ->
LoadRelayAuthUser(pubkey, accountViewModel) { user ->
if (user != null) {
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -348,30 +349,6 @@ private fun RationaleUserRow(
}
}
@Composable
private fun LoadUserForRationale(
pubkey: HexKey,
accountViewModel: AccountViewModel,
content: @Composable (User?) -> Unit,
) {
var user by remember(pubkey) { mutableStateOf(accountViewModel.getUserIfExists(pubkey)) }
if (user == null) {
LaunchedEffect(pubkey) { user = accountViewModel.checkGetOrCreateUser(pubkey) }
}
content(user)
}
private fun reasonRes(kind: AuthPurposeKind): Int =
when (kind) {
AuthPurposeKind.SEND_DM -> R.string.relay_auth_reason_send_dm
AuthPurposeKind.NOTIFY_INBOX -> R.string.relay_auth_reason_notify_inbox
AuthPurposeKind.READ_OUTBOX -> R.string.relay_auth_reason_read_outbox
AuthPurposeKind.POST_VENUE -> R.string.relay_auth_reason_post_venue
AuthPurposeKind.READ_VENUE -> R.string.relay_auth_reason_read_venue
AuthPurposeKind.MY_OWN_RELAY -> R.string.relay_auth_reason_my_own_relay
AuthPurposeKind.OTHER -> R.string.relay_auth_reason_other
}
@Composable
private fun PerRelayOverrideRow(
url: String,