diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt index 898372cc7..e9f3f5b6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementScreen.kt @@ -47,6 +47,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold @@ -67,16 +68,28 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.nip05DnsIdentifiers.Nip05State +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip86RelayManagement.Nip86Retriever +import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture +import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton +import com.vitorpamplona.amethyst.ui.theme.Font14SP +import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize +import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.nip05 import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl @@ -197,7 +210,7 @@ fun RelayManagementScreen( ) } } else { - RelayManagementContent(pad, viewModel, supportedMethods, error) + RelayManagementContent(pad, viewModel, supportedMethods, error, accountViewModel) } } } @@ -208,6 +221,7 @@ private fun RelayManagementContent( viewModel: RelayManagementViewModel, supportedMethods: List, error: String?, + accountViewModel: AccountViewModel, ) { val tabs = remember(supportedMethods) { @@ -271,7 +285,7 @@ private fun RelayManagementContent( when (tabs.getOrNull(selectedTab)) { ManagementTab.PUBKEYS -> { - PubkeysTab(viewModel, supportedMethods) + PubkeysTab(viewModel, supportedMethods, accountViewModel) } ManagementTab.EVENTS -> { @@ -322,9 +336,10 @@ private enum class ManagementTab( private fun PubkeysTab( viewModel: RelayManagementViewModel, supportedMethods: List, + accountViewModel: AccountViewModel, ) { - val bannedPubkeys by viewModel.bannedPubkeys.collectAsState() - val allowedPubkeys by viewModel.allowedPubkeys.collectAsState() + val bannedPubkeyUsers by viewModel.bannedPubkeyUsers.collectAsStateWithLifecycle(emptyList()) + val allowedPubkeyUsers by viewModel.allowedPubkeyUsers.collectAsStateWithLifecycle(emptyList()) var showBanDialog by remember { mutableStateOf(false) } var showAllowDialog by remember { mutableStateOf(false) } @@ -341,15 +356,15 @@ private fun PubkeysTab( ) } - if (bannedPubkeys.isEmpty()) { + if (bannedPubkeyUsers.isEmpty()) { item { EmptyListMessage(stringResource(R.string.relay_management_no_banned_pubkeys)) } } else { - items(bannedPubkeys, key = { it.pubkey }) { entry -> - HexEntryCard( - hex = entry.pubkey, - reason = entry.reason, + items(bannedPubkeyUsers, key = { it.user.pubkeyHex }) { entry -> + PubkeyUserCard( + entry = entry, showRemove = supportedMethods.contains(Nip86Method.UNBAN_PUBKEY), - onRemove = { viewModel.unbanPubkey(entry.pubkey) }, + onRemove = { viewModel.unbanPubkey(entry.user.pubkeyHex) }, + accountViewModel = accountViewModel, ) } } @@ -365,15 +380,15 @@ private fun PubkeysTab( ) } - if (allowedPubkeys.isEmpty()) { + if (allowedPubkeyUsers.isEmpty()) { item { EmptyListMessage(stringResource(R.string.relay_management_no_allowed_pubkeys)) } } else { - items(allowedPubkeys, key = { it.pubkey }) { entry -> - HexEntryCard( - hex = entry.pubkey, - reason = entry.reason, + items(allowedPubkeyUsers, key = { it.user.pubkeyHex }) { entry -> + PubkeyUserCard( + entry = entry, showRemove = supportedMethods.contains(Nip86Method.UNALLOW_PUBKEY), - onRemove = { viewModel.unallowPubkey(entry.pubkey) }, + onRemove = { viewModel.unallowPubkey(entry.user.pubkeyHex) }, + accountViewModel = accountViewModel, ) } } @@ -405,6 +420,92 @@ private fun PubkeysTab( } } +@Composable +private fun PubkeyUserCard( + entry: PubkeyUser, + showRemove: Boolean, + onRemove: () -> Unit, + accountViewModel: AccountViewModel, +) { + SlimListItem( + modifier = Modifier.fillMaxWidth(), + leadingContent = { + ClickableUserPicture(entry.user, Size55dp, accountViewModel = accountViewModel, onClick = null) + }, + headlineContent = { + UsernameDisplay(entry.user, accountViewModel = accountViewModel) + }, + supportingContent = { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + PubkeyNip05Row(entry.user, accountViewModel) + } + entry.reason?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + }, + trailingContent = { + if (showRemove) { + IconButton(onClick = onRemove) { + Icon( + Icons.Default.Close, + contentDescription = stringResource(R.string.relay_management_remove), + tint = MaterialTheme.colorScheme.error, + ) + } + } + }, + ) +} + +@Composable +private fun PubkeyNip05Row( + user: User, + accountViewModel: AccountViewModel, +) { + val nip05StateMetadata by user.nip05State().flow.collectAsStateWithLifecycle() + + when (val nip05State = nip05StateMetadata) { + is Nip05State.Exists -> { + if (nip05State.nip05.name != "_") { + Text( + text = remember(nip05State) { AnnotatedString(nip05State.nip05.name) }, + fontSize = Font14SP, + color = MaterialTheme.colorScheme.nip05, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + ObserveAndRenderNIP05VerifiedSymbol(nip05State, 1, NIP05IconSize, accountViewModel) + + Text( + text = nip05State.nip05.domain, + style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.nip05, fontSize = Font14SP), + maxLines = 1, + overflow = TextOverflow.Visible, + ) + } + + else -> { + Text( + text = user.pubkeyDisplayHex(), + fontSize = Font14SP, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + // Events Tab @Composable private fun EventsTab( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementViewModel.kt index 3182d025d..1b1effd97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip86/RelayManagementViewModel.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86 import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip86RelayManagement.Nip86Retriever import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner @@ -32,10 +34,17 @@ import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BannedPubkey import com.vitorpamplona.quartz.nip86RelayManagement.rpc.BlockedIp import com.vitorpamplona.quartz.nip86RelayManagement.rpc.EventNeedingModeration import com.vitorpamplona.quartz.nip86RelayManagement.rpc.Nip86Request +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +class PubkeyUser( + val user: User, + val reason: String?, +) + class RelayManagementViewModel( relayUrl: NormalizedRelayUrl, signer: NostrSigner, @@ -64,6 +73,20 @@ class RelayManagementViewModel( private val _blockedIps = MutableStateFlow>(emptyList()) val blockedIps: StateFlow> = _blockedIps + val bannedPubkeyUsers: Flow> = + _bannedPubkeys.map { list -> + list.mapNotNull { entry -> + LocalCache.checkGetOrCreateUser(entry.pubkey)?.let { PubkeyUser(it, entry.reason) } + } + } + + val allowedPubkeyUsers: Flow> = + _allowedPubkeys.map { list -> + list.mapNotNull { entry -> + LocalCache.checkGetOrCreateUser(entry.pubkey)?.let { PubkeyUser(it, entry.reason) } + } + } + private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow = _isLoading