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 requestToVanish(
relay: String,
relays: List<NormalizedRelayUrl>,
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(
@@ -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,
@@ -961,10 +961,10 @@ class AccountViewModel(
fun delete(note: Note) = launchSigner { account.delete(note) }
fun requestToVanish(
relay: String,
relays: List<NormalizedRelayUrl>,
reason: String,
createdAt: Long,
) = launchSigner { account.requestToVanish(relay, reason, createdAt) }
) = launchSigner { account.requestToVanish(relays, reason, createdAt) }
fun requestToVanishFromEverywhere(
reason: String,
@@ -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,
@@ -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<String?>(null) }
var selectedRelayUrls by remember { mutableStateOf(emptyList<NormalizedRelayUrl>()) }
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<NormalizedRelayUrl>,
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(),
)
}
}
@@ -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(
+1 -1
View File
@@ -1978,7 +1978,7 @@
<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="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_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>
@@ -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<RequestToVanishEvent>.() -> Unit = {},
@@ -59,7 +60,7 @@ class RequestToVanishEvent(
}
fun build(
relays: List<String>,
relays: List<NormalizedRelayUrl>,
reason: String = "",
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<RequestToVanishEvent>.() -> Unit = {},
@@ -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)
}
@@ -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<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) })