From 9ba408e157d1c02e6a7537a57d428da1e3e9fdb0 Mon Sep 17 00:00:00 2001 From: rasputin Date: Wed, 10 Jun 2026 17:32:50 +0200 Subject: [PATCH] refactor: fix loading flash --- .../gudariwallet/ui/WalletScreen.kt | 2 +- .../gudariwallet/ui/nwc/NwcConnectionRow.kt | 15 +++-- .../gudariwallet/ui/nwc/NwcScreen.kt | 49 +++++++-------- .../gudariwallet/ui/nwc/NwcViewModel.kt | 62 ++++++++++++------- 4 files changed, 74 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt index 8c5516a..4131cf2 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt @@ -490,7 +490,7 @@ fun WalletScreen( budgets = state.budgets, onBack = { navController.popBackStack() }, onRevoke = { - nwcVm.revokeConnection(pubkey) + nwcVm.deleteConnection(pubkey) navController.popBackStack() } ) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcConnectionRow.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcConnectionRow.kt index e7886c8..a12ff70 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcConnectionRow.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcConnectionRow.kt @@ -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 } } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcScreen.kt index 11dc38b..9ac0e0f 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcScreen.kt @@ -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(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") } } ) } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcViewModel.kt index 19f5791..b64b010 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcViewModel.kt @@ -65,6 +65,25 @@ class NwcViewModel( val connectionDetailState: StateFlow = _connectionDetailState.asStateFlow() + // ── Sheet / dialog UI state (survives config change) ──────────────────────── + + private val _showAddSheet = MutableStateFlow(false) + val showAddSheet: StateFlow = _showAddSheet.asStateFlow() + + private val _pendingDeletePubkey = MutableStateFlow(null) + val pendingDeletePubkey: StateFlow = _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) :