diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 7cc6c9389..cf75dcb16 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2046,16 +2046,16 @@ class Account( suspend fun saveBlockedRelayList(blockedRelays: List) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays)) suspend fun requestToVanish( - relay: String, + relays: List, reason: String, createdAt: Long, ) { - if (!isWriteable()) return + if (!isWriteable() || relays.isEmpty()) return - val template = RequestToVanishEvent.build(relay, reason, createdAt) + val template = RequestToVanishEvent.build(relays, reason, createdAt) val signedEvent = signer.sign(template) cache.justConsumeMyOwnEvent(signedEvent) - client.send(signedEvent, setOf(NormalizedRelayUrl(relay))) + client.send(signedEvent, relays.toSet()) } suspend fun requestToVanishFromEverywhere( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt index 567cd6f89..48fa7df62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.model.torState +import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorType @@ -35,6 +36,7 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import okhttp3.OkHttpClient +@Stable class TorRelayState( val okHttpClient: DualHttpClientManager, val torSettingsFlow: TorSettingsFlow, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 35429475e..e22d198e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -961,10 +961,10 @@ class AccountViewModel( fun delete(note: Note) = launchSigner { account.delete(note) } fun requestToVanish( - relay: String, + relays: List, reason: String, createdAt: Long, - ) = launchSigner { account.requestToVanish(relay, reason, createdAt) } + ) = launchSigner { account.requestToVanish(relays, reason, createdAt) } fun requestToVanishFromEverywhere( reason: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt index 35441b625..1b49e15bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt @@ -122,9 +122,23 @@ fun RelayUrlEditField( onNewRelay: (NormalizedRelayUrl) -> Unit, accountViewModel: AccountViewModel, nav: INav, +) { + RelayUrlEditField( + onNewRelay = onNewRelay, + nip11CachedRetriever = Amethyst.instance.nip11Cache, + accountViewModel = accountViewModel, + nav = nav, + ) +} + +@Composable +fun RelayUrlEditField( + onNewRelay: (NormalizedRelayUrl) -> Unit, + nip11CachedRetriever: Nip11CachedRetriever, + accountViewModel: AccountViewModel, + nav: INav, ) { val relaySuggestions = remember { RelaySuggestionState() } - val nip11CachedRetriever = remember { Amethyst.instance.nip11Cache } RelayUrlEditField( onNewRelay = onNewRelay, relaySuggestions = relaySuggestions, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt index ff4ea1958..1fea6cfa9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/vanish/RequestToVanishScreen.kt @@ -65,16 +65,25 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview 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.ui.components.TextSpinner +import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever import com.vitorpamplona.amethyst.ui.components.TitleExplainer +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.toImmutableList @@ -90,12 +99,26 @@ import java.util.Locale fun RequestToVanishScreen( accountViewModel: AccountViewModel, nav: INav, +) { + RequestToVanishScreen( + nip11CachedRetriever = Amethyst.instance.nip11Cache, + accountViewModel = accountViewModel, + nav = nav, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RequestToVanishScreen( + nip11CachedRetriever: Nip11CachedRetriever, + accountViewModel: AccountViewModel, + nav: INav, ) { val connectedRelays by accountViewModel.account.client .connectedRelaysFlow() .collectAsStateWithLifecycle() - var selectedRelayUrl by remember { mutableStateOf(null) } + var selectedRelayUrls by remember { mutableStateOf(emptyList()) } var allRelaysSelected by remember { mutableStateOf(false) } var vanishDate by remember { mutableLongStateOf(TimeUtils.now()) } var reason by remember { mutableStateOf("") } @@ -149,43 +172,22 @@ fun RequestToVanishScreen( Spacer(modifier = Modifier.height(20.dp)) - // Relay Selection - Text( - text = stringRes(R.string.vanish_target_relay), - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.SemiBold, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - TextSpinner( - label = stringRes(R.string.vanish_target_relay), - placeholder = - if (allRelaysSelected) { - stringRes(R.string.vanish_all_relays) - } else { - selectedRelayUrl ?: stringRes(R.string.vanish_select_relay) - }, - options = relayOptions, - onSelect = { index -> - selectedRelayUrl = relayOptions[index].explainer - allRelaysSelected = false - }, - modifier = Modifier.fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(12.dp)) - // ALL RELAYS checkbox Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth(), ) { + // Relay Selection + Text( + text = stringRes(R.string.vanish_target_relay), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.weight(1f)) Checkbox( checked = allRelaysSelected, onCheckedChange = { allRelaysSelected = it - if (it) selectedRelayUrl = null }, ) Text( @@ -196,7 +198,33 @@ fun RequestToVanishScreen( ) } - if (allRelaysSelected) { + if (!allRelaysSelected) { + selectedRelayUrls.forEach { + val info = + remember(it) { + relaySetupInfoBuilder(it, false) + } + + BasicRelaySetupInfoDialog( + info, + onDelete = { selectedRelayUrls -= selectedRelayUrls }, + nip11CachedRetriever = nip11CachedRetriever, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + RelayUrlEditField( + onNewRelay = { + selectedRelayUrls += selectedRelayUrls + it + allRelaysSelected = false + }, + nip11CachedRetriever = nip11CachedRetriever, + accountViewModel = accountViewModel, + nav = nav, + ) + } else { Row( modifier = Modifier @@ -279,7 +307,7 @@ fun RequestToVanishScreen( label = { Text(stringRes(R.string.vanish_reason_label)) }, placeholder = { Text(stringRes(R.string.vanish_reason_placeholder)) }, modifier = Modifier.fillMaxWidth(), - minLines = 2, + minLines = 1, maxLines = 4, ) @@ -289,7 +317,7 @@ fun RequestToVanishScreen( Button( onClick = { showConfirmDialog = true }, modifier = Modifier.fillMaxWidth(), - enabled = allRelaysSelected || selectedRelayUrl != null, + enabled = allRelaysSelected || selectedRelayUrls.isNotEmpty(), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error, @@ -354,14 +382,14 @@ fun RequestToVanishScreen( if (showConfirmDialog) { ConfirmVanishDialog( isAllRelays = allRelaysSelected, - relayUrl = selectedRelayUrl, + relayUrls = selectedRelayUrls, onConfirm = { showConfirmDialog = false if (allRelaysSelected) { accountViewModel.requestToVanishFromEverywhere(reason, vanishDate) } else { - selectedRelayUrl?.let { - accountViewModel.requestToVanish(it, reason, vanishDate) + if (selectedRelayUrls.isNotEmpty()) { + accountViewModel.requestToVanish(selectedRelayUrls, reason, vanishDate) } } accountViewModel.toastManager.toast( @@ -378,7 +406,7 @@ fun RequestToVanishScreen( @Composable private fun ConfirmVanishDialog( isAllRelays: Boolean, - relayUrl: String?, + relayUrls: List, onConfirm: () -> Unit, onDismiss: () -> Unit, ) { @@ -404,7 +432,7 @@ private fun ConfirmVanishDialog( if (isAllRelays) { stringRes(R.string.vanish_confirm_all_relays) } else { - stringRes(R.string.vanish_confirm_single_relay, relayUrl ?: "") + stringRes(R.string.vanish_confirm_single_relay, relayUrls.joinToString(", ") { it.displayUrl() }) }, ) }, @@ -431,3 +459,15 @@ private fun formatTimestamp(epochSeconds: Long): String { val sdf = SimpleDateFormat("MMM dd, yyyy hh:mm a", Locale.getDefault()) return sdf.format(Date(epochSeconds * 1000)) } + +@Preview +@Composable +fun RequestToVanishScreenPreview() { + ThemeComparisonColumn { + RequestToVanishScreen( + nip11CachedRetriever = Nip11CachedRetriever { TODO() }, + accountViewModel = mockAccountViewModel(), + nav = EmptyNav(), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt index 8db53f82d..f95abb1d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt @@ -142,17 +142,17 @@ fun AllSettingsScreen( SettingsNavigationRow( title = R.string.request_to_vanish, icon = Icons.Outlined.DeleteForever, - tint = MaterialTheme.colorScheme.error, + tint = tint, onClick = { nav.nav(Route.RequestToVanish) }, ) - HorizontalDivider() - SettingsNavigationRow( - title = R.string.vanish_history, - icon = Icons.Outlined.History, - tint = tint, - onClick = { nav.nav(Route.VanishEvents) }, - ) } + HorizontalDivider() + SettingsNavigationRow( + title = R.string.vanish_history, + icon = Icons.Outlined.History, + tint = tint, + onClick = { nav.nav(Route.VanishEvents) }, + ) HorizontalDivider(thickness = 4.dp) SettingsSectionHeader(R.string.app_settings) SettingsNavigationRow( diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 66b3af7be..84ace8314 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1978,7 +1978,7 @@ Request to Vanish Request relays to permanently delete all your data up to the selected date. This action is based on NIP-62 and is legally binding in some jurisdictions. Select a relay - Target Relay + Target Relays ALL RELAYS This will request ALL relays to delete everything associated with your key up to the selected date. This event will be broadcast as widely as possible. This action cannot be undone. Delete data up to diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/RequestToVanishEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/RequestToVanishEvent.kt index 891804131..449e8c9fc 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/RequestToVanishEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/RequestToVanishEvent.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip62RequestToVanish import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip62RequestToVanish.tags.shouldVanishFrom @@ -48,7 +49,7 @@ class RequestToVanishEvent( const val ALT = "Request to vanish" fun build( - relay: String, + relay: NormalizedRelayUrl, reason: String = "", createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, @@ -59,7 +60,7 @@ class RequestToVanishEvent( } fun build( - relays: List, + relays: List, reason: String = "", createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/RelayTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/RelayTag.kt index 27906eb9b..dd2aee6f4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/RelayTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/RelayTag.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip62RequestToVanish.tags import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.ensure class RelayTag { @@ -44,7 +45,7 @@ class RelayTag { return tag[1] } - fun assemble(relay: String) = arrayOf(TAG_NAME, relay) + fun assemble(relay: NormalizedRelayUrl) = arrayOf(TAG_NAME, relay.url) fun assembleEverywhere() = arrayOf(TAG_NAME, EVERYWHERE) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayBuilderExt.kt index 988aa9019..8b451d1fa 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayBuilderExt.kt @@ -21,10 +21,11 @@ package com.vitorpamplona.quartz.nip62RequestToVanish.tags import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent fun TagArrayBuilder.vanishFromEverywhere() = add(RelayTag.assembleEverywhere()) -fun TagArrayBuilder.vanishFrom(relay: String) = add(RelayTag.assemble(relay)) +fun TagArrayBuilder.vanishFrom(relay: NormalizedRelayUrl) = add(RelayTag.assemble(relay)) -fun TagArrayBuilder.vanishFrom(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) +fun TagArrayBuilder.vanishFrom(relays: List) = addAll(relays.map { RelayTag.assemble(it) })