Allows users to pick relays on the request to vanish screen

This commit is contained in:
Vitor Pamplona
2026-03-27 12:13:19 -04:00
parent 0862be2761
commit d3ddb86dbb
10 changed files with 118 additions and 59 deletions
@@ -2046,16 +2046,16 @@ class Account(
suspend fun saveBlockedRelayList(blockedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays)) suspend fun saveBlockedRelayList(blockedRelays: List<NormalizedRelayUrl>) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays))
suspend fun requestToVanish( suspend fun requestToVanish(
relay: String, relays: List<NormalizedRelayUrl>,
reason: String, reason: String,
createdAt: Long, 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) val signedEvent = signer.sign(template)
cache.justConsumeMyOwnEvent(signedEvent) cache.justConsumeMyOwnEvent(signedEvent)
client.send(signedEvent, setOf(NormalizedRelayUrl(relay))) client.send(signedEvent, relays.toSet())
} }
suspend fun requestToVanishFromEverywhere( suspend fun requestToVanishFromEverywhere(
@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.model.torState package com.vitorpamplona.amethyst.model.torState
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.amethyst.ui.tor.TorType
@@ -35,6 +36,7 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@Stable
class TorRelayState( class TorRelayState(
val okHttpClient: DualHttpClientManager, val okHttpClient: DualHttpClientManager,
val torSettingsFlow: TorSettingsFlow, val torSettingsFlow: TorSettingsFlow,
@@ -961,10 +961,10 @@ class AccountViewModel(
fun delete(note: Note) = launchSigner { account.delete(note) } fun delete(note: Note) = launchSigner { account.delete(note) }
fun requestToVanish( fun requestToVanish(
relay: String, relays: List<NormalizedRelayUrl>,
reason: String, reason: String,
createdAt: Long, createdAt: Long,
) = launchSigner { account.requestToVanish(relay, reason, createdAt) } ) = launchSigner { account.requestToVanish(relays, reason, createdAt) }
fun requestToVanishFromEverywhere( fun requestToVanishFromEverywhere(
reason: String, reason: String,
@@ -122,9 +122,23 @@ fun RelayUrlEditField(
onNewRelay: (NormalizedRelayUrl) -> Unit, onNewRelay: (NormalizedRelayUrl) -> Unit,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, 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 relaySuggestions = remember { RelaySuggestionState() }
val nip11CachedRetriever = remember { Amethyst.instance.nip11Cache }
RelayUrlEditField( RelayUrlEditField(
onNewRelay = onNewRelay, onNewRelay = onNewRelay,
relaySuggestions = relaySuggestions, relaySuggestions = relaySuggestions,
@@ -65,16 +65,25 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.R 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.components.TitleExplainer
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.stringRes
import com.vitorpamplona.amethyst.ui.theme.DividerThickness 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.nip01Core.relay.normalizer.displayUrl
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -90,12 +99,26 @@ import java.util.Locale
fun RequestToVanishScreen( fun RequestToVanishScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, 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 val connectedRelays by accountViewModel.account.client
.connectedRelaysFlow() .connectedRelaysFlow()
.collectAsStateWithLifecycle() .collectAsStateWithLifecycle()
var selectedRelayUrl by remember { mutableStateOf<String?>(null) } var selectedRelayUrls by remember { mutableStateOf(emptyList<NormalizedRelayUrl>()) }
var allRelaysSelected by remember { mutableStateOf(false) } var allRelaysSelected by remember { mutableStateOf(false) }
var vanishDate by remember { mutableLongStateOf(TimeUtils.now()) } var vanishDate by remember { mutableLongStateOf(TimeUtils.now()) }
var reason by remember { mutableStateOf("") } var reason by remember { mutableStateOf("") }
@@ -149,43 +172,22 @@ fun RequestToVanishScreen(
Spacer(modifier = Modifier.height(20.dp)) 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 // ALL RELAYS checkbox
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(), 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( Checkbox(
checked = allRelaysSelected, checked = allRelaysSelected,
onCheckedChange = { onCheckedChange = {
allRelaysSelected = it allRelaysSelected = it
if (it) selectedRelayUrl = null
}, },
) )
Text( 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( Row(
modifier = modifier =
Modifier Modifier
@@ -279,7 +307,7 @@ fun RequestToVanishScreen(
label = { Text(stringRes(R.string.vanish_reason_label)) }, label = { Text(stringRes(R.string.vanish_reason_label)) },
placeholder = { Text(stringRes(R.string.vanish_reason_placeholder)) }, placeholder = { Text(stringRes(R.string.vanish_reason_placeholder)) },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
minLines = 2, minLines = 1,
maxLines = 4, maxLines = 4,
) )
@@ -289,7 +317,7 @@ fun RequestToVanishScreen(
Button( Button(
onClick = { showConfirmDialog = true }, onClick = { showConfirmDialog = true },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = allRelaysSelected || selectedRelayUrl != null, enabled = allRelaysSelected || selectedRelayUrls.isNotEmpty(),
colors = colors =
ButtonDefaults.buttonColors( ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error, containerColor = MaterialTheme.colorScheme.error,
@@ -354,14 +382,14 @@ fun RequestToVanishScreen(
if (showConfirmDialog) { if (showConfirmDialog) {
ConfirmVanishDialog( ConfirmVanishDialog(
isAllRelays = allRelaysSelected, isAllRelays = allRelaysSelected,
relayUrl = selectedRelayUrl, relayUrls = selectedRelayUrls,
onConfirm = { onConfirm = {
showConfirmDialog = false showConfirmDialog = false
if (allRelaysSelected) { if (allRelaysSelected) {
accountViewModel.requestToVanishFromEverywhere(reason, vanishDate) accountViewModel.requestToVanishFromEverywhere(reason, vanishDate)
} else { } else {
selectedRelayUrl?.let { if (selectedRelayUrls.isNotEmpty()) {
accountViewModel.requestToVanish(it, reason, vanishDate) accountViewModel.requestToVanish(selectedRelayUrls, reason, vanishDate)
} }
} }
accountViewModel.toastManager.toast( accountViewModel.toastManager.toast(
@@ -378,7 +406,7 @@ fun RequestToVanishScreen(
@Composable @Composable
private fun ConfirmVanishDialog( private fun ConfirmVanishDialog(
isAllRelays: Boolean, isAllRelays: Boolean,
relayUrl: String?, relayUrls: List<NormalizedRelayUrl>,
onConfirm: () -> Unit, onConfirm: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
@@ -404,7 +432,7 @@ private fun ConfirmVanishDialog(
if (isAllRelays) { if (isAllRelays) {
stringRes(R.string.vanish_confirm_all_relays) stringRes(R.string.vanish_confirm_all_relays)
} else { } 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()) val sdf = SimpleDateFormat("MMM dd, yyyy hh:mm a", Locale.getDefault())
return sdf.format(Date(epochSeconds * 1000)) return sdf.format(Date(epochSeconds * 1000))
} }
@Preview
@Composable
fun RequestToVanishScreenPreview() {
ThemeComparisonColumn {
RequestToVanishScreen(
nip11CachedRetriever = Nip11CachedRetriever { TODO() },
accountViewModel = mockAccountViewModel(),
nav = EmptyNav(),
)
}
}
@@ -142,17 +142,17 @@ fun AllSettingsScreen(
SettingsNavigationRow( SettingsNavigationRow(
title = R.string.request_to_vanish, title = R.string.request_to_vanish,
icon = Icons.Outlined.DeleteForever, icon = Icons.Outlined.DeleteForever,
tint = MaterialTheme.colorScheme.error, tint = tint,
onClick = { nav.nav(Route.RequestToVanish) }, 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) HorizontalDivider(thickness = 4.dp)
SettingsSectionHeader(R.string.app_settings) SettingsSectionHeader(R.string.app_settings)
SettingsNavigationRow( SettingsNavigationRow(
+1 -1
View File
@@ -1978,7 +1978,7 @@
<string name="request_to_vanish">Request to Vanish</string> <string name="request_to_vanish">Request to Vanish</string>
<string name="request_to_vanish_description">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.</string> <string name="request_to_vanish_description">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.</string>
<string name="vanish_select_relay">Select a relay</string> <string name="vanish_select_relay">Select a relay</string>
<string name="vanish_target_relay">Target Relay</string> <string name="vanish_target_relay">Target Relays</string>
<string name="vanish_all_relays">ALL RELAYS</string> <string name="vanish_all_relays">ALL RELAYS</string>
<string name="vanish_all_relays_warning">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.</string> <string name="vanish_all_relays_warning">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.</string>
<string name="vanish_date_label">Delete data up to</string> <string name="vanish_date_label">Delete data up to</string>
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip62RequestToVanish
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder 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.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip62RequestToVanish.tags.shouldVanishFrom import com.vitorpamplona.quartz.nip62RequestToVanish.tags.shouldVanishFrom
@@ -48,7 +49,7 @@ class RequestToVanishEvent(
const val ALT = "Request to vanish" const val ALT = "Request to vanish"
fun build( fun build(
relay: String, relay: NormalizedRelayUrl,
reason: String = "", reason: String = "",
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<RequestToVanishEvent>.() -> Unit = {}, initializer: TagArrayBuilder<RequestToVanishEvent>.() -> Unit = {},
@@ -59,7 +60,7 @@ class RequestToVanishEvent(
} }
fun build( fun build(
relays: List<String>, relays: List<NormalizedRelayUrl>,
reason: String = "", reason: String = "",
createdAt: Long = TimeUtils.now(), createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<RequestToVanishEvent>.() -> Unit = {}, initializer: TagArrayBuilder<RequestToVanishEvent>.() -> Unit = {},
@@ -21,6 +21,7 @@
package com.vitorpamplona.quartz.nip62RequestToVanish.tags package com.vitorpamplona.quartz.nip62RequestToVanish.tags
import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.utils.ensure import com.vitorpamplona.quartz.utils.ensure
class RelayTag { class RelayTag {
@@ -44,7 +45,7 @@ class RelayTag {
return tag[1] 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) fun assembleEverywhere() = arrayOf(TAG_NAME, EVERYWHERE)
} }
@@ -21,10 +21,11 @@
package com.vitorpamplona.quartz.nip62RequestToVanish.tags package com.vitorpamplona.quartz.nip62RequestToVanish.tags
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
fun TagArrayBuilder<RequestToVanishEvent>.vanishFromEverywhere() = add(RelayTag.assembleEverywhere()) fun TagArrayBuilder<RequestToVanishEvent>.vanishFromEverywhere() = add(RelayTag.assembleEverywhere())
fun TagArrayBuilder<RequestToVanishEvent>.vanishFrom(relay: String) = add(RelayTag.assemble(relay)) fun TagArrayBuilder<RequestToVanishEvent>.vanishFrom(relay: NormalizedRelayUrl) = add(RelayTag.assemble(relay))
fun TagArrayBuilder<RequestToVanishEvent>.vanishFrom(relays: List<String>) = addAll(relays.map { RelayTag.assemble(it) }) fun TagArrayBuilder<RequestToVanishEvent>.vanishFrom(relays: List<NormalizedRelayUrl>) = addAll(relays.map { RelayTag.assemble(it) })