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,8 +14,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
// ── Screen ────────────────────────────────────────────────────────────────────
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NwcScreen(
@@ -27,9 +25,11 @@ fun NwcScreen(
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,7 +76,7 @@ fun NwcScreen(
is NwcUiState.Success -> {
if (state.connections.isEmpty()) {
NwcEmptyContent(
onAddNew = { showAddSheet = true },
onAddNew = { viewModel.openAddSheet() },
modifier = Modifier.fillMaxSize()
)
} else {
@@ -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) :