feat: add Vanish History screen with relay compliance testing
Add a new screen that fetches and displays all existing NIP-62 Request to Vanish events from connected relays. Each entry shows the target relays and event date. Per-relay "Test" buttons query the relay for events older than the vanish date to check NIP-62 compliance - if events are found, the relay is flagged as non-compliant. https://claude.ai/code/session_019Xrprdfq6pVN8beYrYUSr4
This commit is contained in:
@@ -113,6 +113,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.eventsync.EventSyncScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.vanish.RequestToVanishScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.vanish.VanishEventsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AllSettingsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen
|
||||
@@ -217,6 +218,7 @@ fun AppNavigation(
|
||||
composableFromEndArgs<Route.EditRelays> { AllRelayListScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.EventSync> { EventSyncScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.RequestToVanish> { RequestToVanishScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.VanishEvents> { VanishEventsScreen(accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.EditMediaServers> { AllMediaServersScreen(accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.UpdateReactionType> { UpdateReactionTypeScreen(accountViewModel, nav) }
|
||||
|
||||
|
||||
@@ -140,6 +140,8 @@ sealed class Route {
|
||||
|
||||
@Serializable object RequestToVanish : Route()
|
||||
|
||||
@Serializable object VanishEvents : Route()
|
||||
|
||||
@Serializable object EditMediaServers : Route()
|
||||
|
||||
@Serializable object UpdateReactionType : Route()
|
||||
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
* 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.ui.screen.loggedIn.relays.vanish
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.CheckCircle
|
||||
import androidx.compose.material.icons.outlined.Error
|
||||
import androidx.compose.material.icons.outlined.PublicOff
|
||||
import androidx.compose.material.icons.outlined.Science
|
||||
import androidx.compose.material.icons.outlined.Warning
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun VanishEventsScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val viewModel: VanishEventsViewModel = viewModel()
|
||||
viewModel.account = accountViewModel.account
|
||||
|
||||
val vanishEvents by viewModel.vanishEvents.collectAsStateWithLifecycle()
|
||||
val isLoading by viewModel.isLoading.collectAsStateWithLifecycle()
|
||||
val complianceResults by viewModel.complianceResults.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.load()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBarWithBackButton(
|
||||
stringRes(id = R.string.vanish_events_title),
|
||||
nav::popBack,
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
)
|
||||
} else if (vanishEvents.isEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.Center).padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.PublicOff,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.vanish_events_empty),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.vanish_events_empty_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = stringRes(R.string.vanish_events_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
items(vanishEvents, key = { it.event.id }) { item ->
|
||||
VanishEventCard(
|
||||
item = item,
|
||||
complianceResults = complianceResults,
|
||||
onTestCompliance = { relayUrl, date ->
|
||||
viewModel.testCompliance(relayUrl, date)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VanishEventCard(
|
||||
item: VanishEventItem,
|
||||
complianceResults: Map<String, ComplianceStatus>,
|
||||
onTestCompliance: (String, Long) -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(R.string.vanish_date_label),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = formatTimestamp(item.event.createdAt),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (item.isAllRelays) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Outlined.Warning,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.vanish_all_relays),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringRes(R.string.vanish_all_relays_compliance_hint),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringRes(R.string.vanish_target_relays_label),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
item.relays.forEach { relayUrl ->
|
||||
RelayComplianceRow(
|
||||
relayUrl = relayUrl,
|
||||
vanishDate = item.event.createdAt,
|
||||
status = complianceResults["$relayUrl:${item.event.createdAt}"] ?: ComplianceStatus.UNTESTED,
|
||||
onTest = { onTestCompliance(relayUrl, item.event.createdAt) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.event.content.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
HorizontalDivider(thickness = DividerThickness)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = item.event.content,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayComplianceRow(
|
||||
relayUrl: String,
|
||||
vanishDate: Long,
|
||||
status: ComplianceStatus,
|
||||
onTest: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text =
|
||||
relayUrl
|
||||
.removePrefix("wss://")
|
||||
.removePrefix("ws://")
|
||||
.removeSuffix("/"),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
when (status) {
|
||||
ComplianceStatus.UNTESTED -> {
|
||||
FilledTonalButton(
|
||||
onClick = onTest,
|
||||
modifier = Modifier.height(32.dp),
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Science,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
stringRes(R.string.vanish_test_button),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ComplianceStatus.TESTING -> {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
|
||||
ComplianceStatus.COMPLIANT -> {
|
||||
Icon(
|
||||
Icons.Outlined.CheckCircle,
|
||||
contentDescription = stringRes(R.string.vanish_compliant),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
stringRes(R.string.vanish_compliant),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
||||
ComplianceStatus.NON_COMPLIANT -> {
|
||||
Icon(
|
||||
Icons.Outlined.Error,
|
||||
contentDescription = stringRes(R.string.vanish_non_compliant),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
stringRes(R.string.vanish_non_compliant),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
|
||||
ComplianceStatus.ERROR -> {
|
||||
Icon(
|
||||
Icons.Outlined.Error,
|
||||
contentDescription = stringRes(R.string.vanish_test_error),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
stringRes(R.string.vanish_test_error),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTimestamp(epochSeconds: Long): String {
|
||||
val sdf = SimpleDateFormat("MMM dd, yyyy hh:mm a", Locale.getDefault())
|
||||
return sdf.format(Date(epochSeconds * 1000))
|
||||
}
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* 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.ui.screen.loggedIn.relays.vanish
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirstEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent
|
||||
import com.vitorpamplona.quartz.nip62RequestToVanish.tags.RelayTag
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
@Stable
|
||||
data class VanishEventItem(
|
||||
val event: RequestToVanishEvent,
|
||||
val relays: List<String>,
|
||||
val isAllRelays: Boolean,
|
||||
val sourceRelay: NormalizedRelayUrl,
|
||||
)
|
||||
|
||||
enum class ComplianceStatus {
|
||||
UNTESTED,
|
||||
TESTING,
|
||||
COMPLIANT,
|
||||
NON_COMPLIANT,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
class VanishEventsViewModel : ViewModel() {
|
||||
lateinit var account: Account
|
||||
|
||||
private val _vanishEvents = MutableStateFlow<List<VanishEventItem>>(emptyList())
|
||||
val vanishEvents = _vanishEvents.asStateFlow()
|
||||
|
||||
private val _isLoading = MutableStateFlow(false)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
private val _complianceResults = MutableStateFlow<Map<String, ComplianceStatus>>(emptyMap())
|
||||
val complianceResults = _complianceResults.asStateFlow()
|
||||
|
||||
fun load() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_isLoading.value = true
|
||||
_vanishEvents.value = emptyList()
|
||||
_complianceResults.value = emptyMap()
|
||||
|
||||
val connectedRelays = account.client.connectedRelaysFlow().value
|
||||
if (connectedRelays.isEmpty()) {
|
||||
_isLoading.value = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
val filter =
|
||||
Filter(
|
||||
kinds = listOf(RequestToVanishEvent.KIND),
|
||||
authors = listOf(account.pubKey),
|
||||
limit = 100,
|
||||
)
|
||||
|
||||
val filtersPerRelay = connectedRelays.associateWith { listOf(filter) }
|
||||
val events = mutableListOf<VanishEventItem>()
|
||||
val seenIds = mutableSetOf<String>()
|
||||
val subId = newSubId()
|
||||
val doneChannel = Channel<Unit>(Channel.CONFLATED)
|
||||
var eoseCount = 0
|
||||
val totalRelays = connectedRelays.size
|
||||
|
||||
val listener =
|
||||
object : IRequestListener {
|
||||
override fun onEvent(
|
||||
event: Event,
|
||||
isLive: Boolean,
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
if (event is RequestToVanishEvent && seenIds.add(event.id)) {
|
||||
val relayTags = event.vanishFromRelays()
|
||||
val isAll = relayTags.contains(RelayTag.EVERYWHERE)
|
||||
events.add(
|
||||
VanishEventItem(
|
||||
event = event,
|
||||
relays = relayTags,
|
||||
isAllRelays = isAll,
|
||||
sourceRelay = relay,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
eoseCount++
|
||||
if (eoseCount >= totalRelays) {
|
||||
doneChannel.trySend(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClosed(
|
||||
message: String,
|
||||
relay: NormalizedRelayUrl,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
eoseCount++
|
||||
if (eoseCount >= totalRelays) {
|
||||
doneChannel.trySend(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCannotConnect(
|
||||
relay: NormalizedRelayUrl,
|
||||
message: String,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
eoseCount++
|
||||
if (eoseCount >= totalRelays) {
|
||||
doneChannel.trySend(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
account.client.openReqSubscription(subId, filtersPerRelay, listener)
|
||||
|
||||
withTimeoutOrNull(15_000) {
|
||||
doneChannel.receive()
|
||||
}
|
||||
} finally {
|
||||
account.client.close(subId)
|
||||
doneChannel.close()
|
||||
}
|
||||
|
||||
_vanishEvents.value = events.sortedByDescending { it.event.createdAt }
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun testCompliance(
|
||||
relayUrl: String,
|
||||
vanishDate: Long,
|
||||
) {
|
||||
val key = "$relayUrl:$vanishDate"
|
||||
_complianceResults.value = _complianceResults.value + (key to ComplianceStatus.TESTING)
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val foundEvent =
|
||||
account.client.downloadFirstEvent(
|
||||
relay = relayUrl,
|
||||
filter =
|
||||
Filter(
|
||||
authors = listOf(account.pubKey),
|
||||
until = vanishDate,
|
||||
limit = 1,
|
||||
),
|
||||
)
|
||||
|
||||
_complianceResults.value =
|
||||
_complianceResults.value +
|
||||
(
|
||||
key to
|
||||
if (foundEvent != null) {
|
||||
ComplianceStatus.NON_COMPLIANT
|
||||
} else {
|
||||
ComplianceStatus.COMPLIANT
|
||||
}
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
_complianceResults.value = _complianceResults.value + (key to ComplianceStatus.ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -31,6 +31,7 @@ import androidx.compose.material.icons.outlined.Bolt
|
||||
import androidx.compose.material.icons.outlined.CloudUpload
|
||||
import androidx.compose.material.icons.outlined.DeleteForever
|
||||
import androidx.compose.material.icons.outlined.FavoriteBorder
|
||||
import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material.icons.outlined.Key
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material.icons.outlined.Security
|
||||
@@ -144,6 +145,13 @@ fun AllSettingsScreen(
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
onClick = { nav.nav(Route.RequestToVanish) },
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -1973,4 +1973,17 @@
|
||||
<string name="vanish_request_sent">Vanish request sent</string>
|
||||
<string name="vanish_select_date">Select date</string>
|
||||
<string name="vanish_select_time">Select time</string>
|
||||
|
||||
<string name="vanish_events_title">Vanish History</string>
|
||||
<string name="vanish_events_refresh">Refresh</string>
|
||||
<string name="vanish_events_description">These are your past Request to Vanish events found on connected relays. Relays tagged in these events should not hold any of your data from before the event date.</string>
|
||||
<string name="vanish_events_empty">No vanish requests found</string>
|
||||
<string name="vanish_events_empty_hint">You haven\'t sent any Request to Vanish events yet.</string>
|
||||
<string name="vanish_target_relays_label">Target Relays</string>
|
||||
<string name="vanish_all_relays_compliance_hint">This request targets all relays. Use the Request to Vanish screen to test specific relays for compliance.</string>
|
||||
<string name="vanish_test_button">Test</string>
|
||||
<string name="vanish_compliant">Compliant</string>
|
||||
<string name="vanish_non_compliant">Non-compliant</string>
|
||||
<string name="vanish_test_error">Error</string>
|
||||
<string name="vanish_history">Vanish History</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user