refactor: fix loading flash

This commit is contained in:
2026-06-10 17:32:50 +02:00
parent f9ddebac8a
commit 9ba408e157
4 changed files with 74 additions and 54 deletions
@@ -490,7 +490,7 @@ fun WalletScreen(
budgets = state.budgets,
onBack = { navController.popBackStack() },
onRevoke = {
nwcVm.revokeConnection(pubkey)
nwcVm.deleteConnection(pubkey)
navController.popBackStack()
}
)
@@ -15,6 +15,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.bitcointxoko.gudariwallet.api.NwcGetResponse
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
import kotlinx.coroutines.launch
import java.time.Instant
@Composable
@@ -43,7 +44,9 @@ internal fun NwcConnectionRow(
// threshold, even though confirmValueChange returns false and currentValue
// stays at Settled. Use a flag so we only fire once per swipe gesture.
var dialogTriggered by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
// Fires when the swipe crosses the threshold — trigger confirm dialog
LaunchedEffect(dismissState.targetValue) {
if (dismissState.targetValue == SwipeToDismissBoxValue.EndToStart
&& !isDeleting
@@ -51,12 +54,14 @@ internal fun NwcConnectionRow(
) {
dialogTriggered = true
onDeleteConfirm()
// Snap the row back to its resting position.
dismissState.reset()
// Reset in its own scope — won't be cancelled when targetValue changes back
scope.launch { dismissState.reset() }
}
// Reset the flag once the swipe returns to Settled so the
// gesture can be triggered again if the user cancels and re-swipes.
if (dismissState.targetValue == SwipeToDismissBoxValue.Settled) {
}
// Fires when the row has fully snapped back — re-arm for next swipe
LaunchedEffect(dismissState.currentValue) {
if (dismissState.currentValue == SwipeToDismissBoxValue.Settled) {
dialogTriggered = false
}
}
@@ -14,22 +14,22 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
// ── Screen ────────────────────────────────────────────────────────────────────
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NwcScreen(
viewModel : NwcViewModel,
onBack : () -> Unit,
viewModel : NwcViewModel,
onBack : () -> Unit,
onConnectionClick : (pubkey: String) -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val isDeleting by viewModel.isDeleting.collectAsStateWithLifecycle()
val isCreating by viewModel.isCreating.collectAsStateWithLifecycle()
val pairingUrl by viewModel.pairingUrl.collectAsStateWithLifecycle()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val isDeleting by viewModel.isDeleting.collectAsStateWithLifecycle()
val isCreating by viewModel.isCreating.collectAsStateWithLifecycle()
val pairingUrl by viewModel.pairingUrl.collectAsStateWithLifecycle()
val showAddSheet by viewModel.showAddSheet.collectAsStateWithLifecycle()
val pendingDeletePubkey by viewModel.pendingDeletePubkey.collectAsStateWithLifecycle()
var showAddSheet by remember { mutableStateOf(false) }
var pendingDeletePubkey by remember { mutableStateOf<String?>(null) }
// ── Shared refresh lambda (item #13) ──────────────────────────────────────
val refresh = { viewModel.loadConnections(includeExpired = true) }
Scaffold(
topBar = {
@@ -43,7 +43,7 @@ fun NwcScreen(
)
},
floatingActionButton = {
FloatingActionButton(onClick = { showAddSheet = true }) {
FloatingActionButton(onClick = { viewModel.openAddSheet() }) {
Icon(Icons.Default.Add, contentDescription = "Add connection")
}
}
@@ -53,7 +53,7 @@ fun NwcScreen(
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = { viewModel.loadConnections(includeExpired = true) },
onRefresh = refresh, // ← was loadConnections(includeExpired = true)
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
@@ -68,7 +68,7 @@ fun NwcScreen(
is NwcUiState.Error -> {
NwcErrorContent(
message = state.message,
onRetry = { viewModel.loadConnections(includeExpired = true) },
onRetry = refresh, // ← was loadConnections(includeExpired = true)
modifier = Modifier.fillMaxSize()
)
}
@@ -76,13 +76,13 @@ fun NwcScreen(
is NwcUiState.Success -> {
if (state.connections.isEmpty()) {
NwcEmptyContent(
onAddNew = { showAddSheet = true },
onAddNew = { viewModel.openAddSheet() },
modifier = Modifier.fillMaxSize()
)
} else {
LazyColumn(
contentPadding = PaddingValues(vertical = 8.dp),
modifier = Modifier.fillMaxSize()
contentPadding = PaddingValues(vertical = 8.dp),
modifier = Modifier.fillMaxSize()
) {
items(
items = state.connections,
@@ -91,7 +91,7 @@ fun NwcScreen(
NwcConnectionRow(
connection,
isDeleting,
onDeleteConfirm = { pendingDeletePubkey = connection.data.pubkey },
onDeleteConfirm = { viewModel.requestDelete(connection.data.pubkey) },
onConnectionClick = { onConnectionClick(connection.data.pubkey) }
)
HorizontalDivider(
@@ -117,9 +117,9 @@ fun NwcScreen(
expiresAt = expiresAt,
budgets = budgets
)
showAddSheet = false
viewModel.closeAddSheet() // ← was openAddSheet()
},
onDismiss = { showAddSheet = false }
onDismiss = { viewModel.closeAddSheet() } // ← was openAddSheet()
)
}
@@ -132,21 +132,18 @@ fun NwcScreen(
}
// ── Delete confirmation dialog ─────────────────────────────────────────────
pendingDeletePubkey?.let { pubkey ->
pendingDeletePubkey?.let {
AlertDialog(
onDismissRequest = { pendingDeletePubkey = null },
onDismissRequest = { viewModel.cancelDelete() },
title = { Text("Remove connection?") },
text = { Text("This will permanently revoke this Nostr Wallet Connect key. Any app using it will lose access.") },
confirmButton = {
TextButton(
onClick = {
viewModel.deleteConnection(pubkey)
pendingDeletePubkey = null
}
onClick = { viewModel.confirmDelete() } // ← was deleteConnection(pubkey) + cancelDelete()
) { Text("Remove", color = MaterialTheme.colorScheme.error) }
},
dismissButton = {
TextButton(onClick = { pendingDeletePubkey = null }) { Text("Cancel") }
TextButton(onClick = { viewModel.cancelDelete() }) { Text("Cancel") }
}
)
}
@@ -65,6 +65,25 @@ class NwcViewModel(
val connectionDetailState: StateFlow<ConnectionDetailState> =
_connectionDetailState.asStateFlow()
// ── Sheet / dialog UI state (survives config change) ────────────────────────
private val _showAddSheet = MutableStateFlow(false)
val showAddSheet: StateFlow<Boolean> = _showAddSheet.asStateFlow()
private val _pendingDeletePubkey = MutableStateFlow<String?>(null)
val pendingDeletePubkey: StateFlow<String?> = _pendingDeletePubkey.asStateFlow()
fun openAddSheet() { _showAddSheet.value = true }
fun closeAddSheet() { _showAddSheet.value = false }
fun requestDelete(pubkey: String) { _pendingDeletePubkey.value = pubkey }
fun cancelDelete() { _pendingDeletePubkey.value = null }
// confirmDelete() calls the existing deleteConnection() then clears the pending key
fun confirmDelete() {
_pendingDeletePubkey.value?.let { deleteConnection(it) }
_pendingDeletePubkey.value = null
}
init {
loadConnections()
}
@@ -134,7 +153,7 @@ class NwcViewModel(
}
.onSuccess { url ->
_pairingUrl.value = url
loadConnections()
silentRefresh()
}
.onFailure { e ->
Timber.e(e, "[$TAG] createConnection failed")
@@ -152,8 +171,13 @@ class NwcViewModel(
_pairingUrl.value = null
}
// ── Delete ────────────────────────────────────────────────────────────────
// ── Delete / Revoke ──────────────────────────────────────────────────────────
/**
* Deletes an NWC key by pubkey. Used by both the list screen (swipe-to-delete)
* and the detail screen (revoke button). Sets [_isDeleting] for the duration
* so the list screen can disable row interactions while in flight.
*/
fun deleteConnection(pubkey: String) {
Timber.d("[$TAG] deleteConnection pubkey=${pubkey.take(16)}")
viewModelScope.launch {
@@ -168,7 +192,7 @@ class NwcViewModel(
current.connections.filter { it.data.pubkey != pubkey }
)
}
loadConnections()
silentRefresh()
}
.onFailure { e ->
Timber.e(e, "[$TAG] failed to delete connection pubkey=${pubkey.take(16)}")
@@ -216,34 +240,28 @@ class NwcViewModel(
_connectionDetailState.value = ConnectionDetailState.Loading
}
// ── Silent refresh (no Loading flash) ───────────────────────────────────────
/**
* Deletes the key via the repo, then refreshes the list so the revoked
* entry disappears from NwcScreen when the user pops back.
* Syncs the connection list from the server without transitioning to
* [NwcUiState.Loading] first. Used after mutations (create, delete) where
* the UI already shows the correct optimistic state.
* Failures are non-fatal — the optimistic state is already correct.
*/
fun revokeConnection(pubkey: String) {
Timber.d("[$TAG] revokeConnection pubkey=${pubkey.take(16)}")
private fun silentRefresh(includeExpired: Boolean = false) {
viewModelScope.launch {
runCatching { repo.deleteNwcKey(pubkey) }
.onSuccess {
Timber.i("[$TAG] revoked connection pubkey=${pubkey.take(16)}… ✓")
// Optimistic local removal
val current = _uiState.value
if (current is NwcUiState.Success) {
_uiState.value = NwcUiState.Success(
current.connections.filter { it.data.pubkey != pubkey }
)
}
loadConnections()
runCatching { repo.getNwcKeys(includeExpired) }
.onSuccess { keys ->
Timber.d("[$TAG] silentRefresh: ${keys.size} connection(s)")
_uiState.value = NwcUiState.Success(keys)
}
.onFailure { e ->
Timber.e(e, "[$TAG] failed to revoke connection")
_uiState.value = NwcUiState.Error(
e.message ?: "Failed to revoke connection"
)
Timber.w(e, "[$TAG] silentRefresh failed (non-fatal)")
}
}
}
// ── Factory ───────────────────────────────────────────────────────────────
class Factory(private val repo: WalletRepository) :