refactor: fix loading flash
This commit is contained in:
@@ -490,7 +490,7 @@ fun WalletScreen(
|
|||||||
budgets = state.budgets,
|
budgets = state.budgets,
|
||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
onRevoke = {
|
onRevoke = {
|
||||||
nwcVm.revokeConnection(pubkey)
|
nwcVm.deleteConnection(pubkey)
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.bitcointxoko.gudariwallet.api.NwcGetResponse
|
import com.bitcointxoko.gudariwallet.api.NwcGetResponse
|
||||||
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
|
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -43,7 +44,9 @@ internal fun NwcConnectionRow(
|
|||||||
// threshold, even though confirmValueChange returns false and currentValue
|
// threshold, even though confirmValueChange returns false and currentValue
|
||||||
// stays at Settled. Use a flag so we only fire once per swipe gesture.
|
// stays at Settled. Use a flag so we only fire once per swipe gesture.
|
||||||
var dialogTriggered by remember { mutableStateOf(false) }
|
var dialogTriggered by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// Fires when the swipe crosses the threshold — trigger confirm dialog
|
||||||
LaunchedEffect(dismissState.targetValue) {
|
LaunchedEffect(dismissState.targetValue) {
|
||||||
if (dismissState.targetValue == SwipeToDismissBoxValue.EndToStart
|
if (dismissState.targetValue == SwipeToDismissBoxValue.EndToStart
|
||||||
&& !isDeleting
|
&& !isDeleting
|
||||||
@@ -51,12 +54,14 @@ internal fun NwcConnectionRow(
|
|||||||
) {
|
) {
|
||||||
dialogTriggered = true
|
dialogTriggered = true
|
||||||
onDeleteConfirm()
|
onDeleteConfirm()
|
||||||
// Snap the row back to its resting position.
|
// Reset in its own scope — won't be cancelled when targetValue changes back
|
||||||
dismissState.reset()
|
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
|
dialogTriggered = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
|
||||||
// ── Screen ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NwcScreen(
|
fun NwcScreen(
|
||||||
@@ -27,9 +25,11 @@ fun NwcScreen(
|
|||||||
val isDeleting by viewModel.isDeleting.collectAsStateWithLifecycle()
|
val isDeleting by viewModel.isDeleting.collectAsStateWithLifecycle()
|
||||||
val isCreating by viewModel.isCreating.collectAsStateWithLifecycle()
|
val isCreating by viewModel.isCreating.collectAsStateWithLifecycle()
|
||||||
val pairingUrl by viewModel.pairingUrl.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) }
|
// ── Shared refresh lambda (item #13) ──────────────────────────────────────
|
||||||
var pendingDeletePubkey by remember { mutableStateOf<String?>(null) }
|
val refresh = { viewModel.loadConnections(includeExpired = true) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -43,7 +43,7 @@ fun NwcScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(onClick = { showAddSheet = true }) {
|
FloatingActionButton(onClick = { viewModel.openAddSheet() }) {
|
||||||
Icon(Icons.Default.Add, contentDescription = "Add connection")
|
Icon(Icons.Default.Add, contentDescription = "Add connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ fun NwcScreen(
|
|||||||
|
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
onRefresh = { viewModel.loadConnections(includeExpired = true) },
|
onRefresh = refresh, // ← was loadConnections(includeExpired = true)
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
@@ -68,7 +68,7 @@ fun NwcScreen(
|
|||||||
is NwcUiState.Error -> {
|
is NwcUiState.Error -> {
|
||||||
NwcErrorContent(
|
NwcErrorContent(
|
||||||
message = state.message,
|
message = state.message,
|
||||||
onRetry = { viewModel.loadConnections(includeExpired = true) },
|
onRetry = refresh, // ← was loadConnections(includeExpired = true)
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ fun NwcScreen(
|
|||||||
is NwcUiState.Success -> {
|
is NwcUiState.Success -> {
|
||||||
if (state.connections.isEmpty()) {
|
if (state.connections.isEmpty()) {
|
||||||
NwcEmptyContent(
|
NwcEmptyContent(
|
||||||
onAddNew = { showAddSheet = true },
|
onAddNew = { viewModel.openAddSheet() },
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -91,7 +91,7 @@ fun NwcScreen(
|
|||||||
NwcConnectionRow(
|
NwcConnectionRow(
|
||||||
connection,
|
connection,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
onDeleteConfirm = { pendingDeletePubkey = connection.data.pubkey },
|
onDeleteConfirm = { viewModel.requestDelete(connection.data.pubkey) },
|
||||||
onConnectionClick = { onConnectionClick(connection.data.pubkey) }
|
onConnectionClick = { onConnectionClick(connection.data.pubkey) }
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
@@ -117,9 +117,9 @@ fun NwcScreen(
|
|||||||
expiresAt = expiresAt,
|
expiresAt = expiresAt,
|
||||||
budgets = budgets
|
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 ─────────────────────────────────────────────
|
// ── Delete confirmation dialog ─────────────────────────────────────────────
|
||||||
pendingDeletePubkey?.let { pubkey ->
|
pendingDeletePubkey?.let {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { pendingDeletePubkey = null },
|
onDismissRequest = { viewModel.cancelDelete() },
|
||||||
title = { Text("Remove connection?") },
|
title = { Text("Remove connection?") },
|
||||||
text = { Text("This will permanently revoke this Nostr Wallet Connect key. Any app using it will lose access.") },
|
text = { Text("This will permanently revoke this Nostr Wallet Connect key. Any app using it will lose access.") },
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = { viewModel.confirmDelete() } // ← was deleteConnection(pubkey) + cancelDelete()
|
||||||
viewModel.deleteConnection(pubkey)
|
|
||||||
pendingDeletePubkey = null
|
|
||||||
}
|
|
||||||
) { Text("Remove", color = MaterialTheme.colorScheme.error) }
|
) { Text("Remove", color = MaterialTheme.colorScheme.error) }
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { pendingDeletePubkey = null }) { Text("Cancel") }
|
TextButton(onClick = { viewModel.cancelDelete() }) { Text("Cancel") }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,25 @@ class NwcViewModel(
|
|||||||
val connectionDetailState: StateFlow<ConnectionDetailState> =
|
val connectionDetailState: StateFlow<ConnectionDetailState> =
|
||||||
_connectionDetailState.asStateFlow()
|
_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 {
|
init {
|
||||||
loadConnections()
|
loadConnections()
|
||||||
}
|
}
|
||||||
@@ -134,7 +153,7 @@ class NwcViewModel(
|
|||||||
}
|
}
|
||||||
.onSuccess { url ->
|
.onSuccess { url ->
|
||||||
_pairingUrl.value = url
|
_pairingUrl.value = url
|
||||||
loadConnections()
|
silentRefresh()
|
||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
Timber.e(e, "[$TAG] createConnection failed")
|
Timber.e(e, "[$TAG] createConnection failed")
|
||||||
@@ -152,8 +171,13 @@ class NwcViewModel(
|
|||||||
_pairingUrl.value = null
|
_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) {
|
fun deleteConnection(pubkey: String) {
|
||||||
Timber.d("[$TAG] deleteConnection pubkey=${pubkey.take(16)}…")
|
Timber.d("[$TAG] deleteConnection pubkey=${pubkey.take(16)}…")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -168,7 +192,7 @@ class NwcViewModel(
|
|||||||
current.connections.filter { it.data.pubkey != pubkey }
|
current.connections.filter { it.data.pubkey != pubkey }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
loadConnections()
|
silentRefresh()
|
||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
Timber.e(e, "[$TAG] failed to delete connection pubkey=${pubkey.take(16)}…")
|
Timber.e(e, "[$TAG] failed to delete connection pubkey=${pubkey.take(16)}…")
|
||||||
@@ -216,34 +240,28 @@ class NwcViewModel(
|
|||||||
_connectionDetailState.value = ConnectionDetailState.Loading
|
_connectionDetailState.value = ConnectionDetailState.Loading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Silent refresh (no Loading flash) ───────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the key via the repo, then refreshes the list so the revoked
|
* Syncs the connection list from the server without transitioning to
|
||||||
* entry disappears from NwcScreen when the user pops back.
|
* [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) {
|
private fun silentRefresh(includeExpired: Boolean = false) {
|
||||||
Timber.d("[$TAG] revokeConnection pubkey=${pubkey.take(16)}…")
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
runCatching { repo.deleteNwcKey(pubkey) }
|
runCatching { repo.getNwcKeys(includeExpired) }
|
||||||
.onSuccess {
|
.onSuccess { keys ->
|
||||||
Timber.i("[$TAG] revoked connection pubkey=${pubkey.take(16)}… ✓")
|
Timber.d("[$TAG] silentRefresh: ${keys.size} connection(s)")
|
||||||
// Optimistic local removal
|
_uiState.value = NwcUiState.Success(keys)
|
||||||
val current = _uiState.value
|
|
||||||
if (current is NwcUiState.Success) {
|
|
||||||
_uiState.value = NwcUiState.Success(
|
|
||||||
current.connections.filter { it.data.pubkey != pubkey }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
loadConnections()
|
|
||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
Timber.e(e, "[$TAG] failed to revoke connection")
|
Timber.w(e, "[$TAG] silentRefresh failed (non-fatal)")
|
||||||
_uiState.value = NwcUiState.Error(
|
|
||||||
e.message ?: "Failed to revoke connection"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Factory ───────────────────────────────────────────────────────────────
|
// ── Factory ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class Factory(private val repo: WalletRepository) :
|
class Factory(private val repo: WalletRepository) :
|
||||||
|
|||||||
Reference in New Issue
Block a user