From 290a6b1f85f3520083e2b234685dd2a227d4bbdc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 00:05:55 +0000 Subject: [PATCH 1/2] fix(nwc): scope Send/Receive/Transactions to the wallet shown in the detail screen The wallet detail screen's Send, Receive and Transactions buttons navigated to parameterless routes. Each destination created a fresh WalletViewModel with no selection, so the action ran against `_defaultWalletId` (the account default) instead of the wallet being viewed. Paying, invoicing, and listing transactions could therefore go to the wrong wallet. Parameterize WalletSend/WalletReceive/WalletTransactions with `walletId`, plumb it through AppNavigation, pass it from WalletDetailScreen, and have each screen call `selectWallet(walletId)` before operating. --- .../amethyst/ui/navigation/AppNavigation.kt | 6 +++--- .../amethyst/ui/navigation/routes/Routes.kt | 15 ++++++++++++--- .../screen/loggedIn/wallet/WalletDetailScreen.kt | 6 +++--- .../screen/loggedIn/wallet/WalletReceiveScreen.kt | 4 +++- .../ui/screen/loggedIn/wallet/WalletSendScreen.kt | 4 +++- .../loggedIn/wallet/WalletTransactionsScreen.kt | 4 +++- 6 files changed, 27 insertions(+), 12 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index df8de1d607..06091a298d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -285,9 +285,9 @@ fun BuildNavigation( composable { ChessLobbyScreen(accountViewModel, nav) } composableFromEnd { WalletScreen(accountViewModel, nav) } - composableFromEnd { WalletSendScreen(accountViewModel, nav) } - composableFromEnd { WalletReceiveScreen(accountViewModel, nav) } - composableFromEnd { WalletTransactionsScreen(accountViewModel, nav) } + composableFromEndArgs { WalletSendScreen(it.walletId, accountViewModel, nav) } + composableFromEndArgs { WalletReceiveScreen(it.walletId, accountViewModel, nav) } + composableFromEndArgs { WalletTransactionsScreen(it.walletId, accountViewModel, nav) } composableFromEnd { OnchainTransactionsScreen(accountViewModel, nav) } composableFromEndArgs { WalletDetailScreen(it.walletId, accountViewModel, nav) } composableFromEnd { AddWalletScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index b1dbad770f..f771a61300 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -146,11 +146,20 @@ sealed class Route { @Serializable object Wallet : Route() - @Serializable object WalletSend : Route() + @Serializable + data class WalletSend( + val walletId: String, + ) : Route() - @Serializable object WalletReceive : Route() + @Serializable + data class WalletReceive( + val walletId: String, + ) : Route() - @Serializable object WalletTransactions : Route() + @Serializable + data class WalletTransactions( + val walletId: String, + ) : Route() @Serializable object OnchainTransactions : Route() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletDetailScreen.kt index 4870b8bed1..307789f4e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletDetailScreen.kt @@ -149,7 +149,7 @@ fun WalletDetailScreen( horizontalArrangement = Arrangement.spacedBy(16.dp), ) { Button( - onClick = { nav.nav(Route.WalletReceive) }, + onClick = { nav.nav(Route.WalletReceive(walletId)) }, modifier = Modifier .weight(1f) @@ -171,7 +171,7 @@ fun WalletDetailScreen( } Button( - onClick = { nav.nav(Route.WalletSend) }, + onClick = { nav.nav(Route.WalletSend(walletId)) }, modifier = Modifier .weight(1f) @@ -192,7 +192,7 @@ fun WalletDetailScreen( // Transactions button OutlinedButton( - onClick = { nav.nav(Route.WalletTransactions) }, + onClick = { nav.nav(Route.WalletTransactions(walletId)) }, modifier = Modifier .fillMaxWidth() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt index fa8fa25252..dec9f3852f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt @@ -74,13 +74,15 @@ import java.text.NumberFormat @OptIn(ExperimentalMaterial3Api::class) @Composable fun WalletReceiveScreen( + walletId: String, accountViewModel: AccountViewModel, nav: INav, ) { val walletViewModel: WalletViewModel = viewModel() - LaunchedEffect(accountViewModel) { + LaunchedEffect(accountViewModel, walletId) { walletViewModel.init(accountViewModel) + walletViewModel.selectWallet(walletId) } DisposableEffect(Unit) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt index 2bd8c3dfa2..bff0932ba1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt @@ -64,13 +64,15 @@ import com.vitorpamplona.amethyst.ui.stringRes @OptIn(ExperimentalMaterial3Api::class) @Composable fun WalletSendScreen( + walletId: String, accountViewModel: AccountViewModel, nav: INav, ) { val walletViewModel: WalletViewModel = viewModel() - LaunchedEffect(accountViewModel) { + LaunchedEffect(accountViewModel, walletId) { walletViewModel.init(accountViewModel) + walletViewModel.selectWallet(walletId) } DisposableEffect(Unit) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt index 8170ebcf58..d662e3c6d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt @@ -73,13 +73,15 @@ import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable fun WalletTransactionsScreen( + walletId: String, accountViewModel: AccountViewModel, nav: INav, ) { val walletViewModel: WalletViewModel = viewModel() - LaunchedEffect(accountViewModel) { + LaunchedEffect(accountViewModel, walletId) { walletViewModel.init(accountViewModel) + walletViewModel.selectWallet(walletId) walletViewModel.fetchTransactions() } From 906ac06c57324ff5a690ac2e530966c5ec3b964c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 00:10:05 +0000 Subject: [PATCH 2/2] feat(wallet): drag-and-drop reorder for NWC wallet cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the up/down chevron IconButtons on each wallet card with a drag handle, matching the pattern used across the relay-settings screens. Reuses RelayDragState / rememberRelayDragState / draggableRelayItem / relayDragHandle from relays/common — same gesture handling, elevation animation, and swap-on-threshold behavior. The handle and item modifier are only attached when there is more than one wallet to reorder. --- .../ui/screen/loggedIn/wallet/WalletScreen.kt | 62 ++++++++----------- amethyst/src/main/res/values/strings.xml | 1 + 2 files changed, 27 insertions(+), 36 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt index 0b36f53251..57d31f3d22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt @@ -73,6 +73,10 @@ import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayDragState +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.draggableRelayItem +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relayDragHandle +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.rememberRelayDragState import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.launch import java.text.NumberFormat @@ -197,6 +201,12 @@ private fun MultiWalletHomeContent( } } + val dragState = + rememberRelayDragState( + onMove = { from, to -> walletViewModel.moveWallet(from, to) }, + itemCount = { walletInfoList.size }, + ) + LazyColumn( state = listState, modifier = @@ -204,6 +214,7 @@ private fun MultiWalletHomeContent( .fillMaxSize() .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), + userScrollEnabled = !dragState.isDragging, ) { item { Spacer(modifier = Modifier.height(8.dp)) @@ -218,7 +229,7 @@ private fun MultiWalletHomeContent( WalletCard( walletInfo = walletInfo, index = index, - totalCount = walletInfoList.size, + dragState = if (walletInfoList.size > 1) dragState else null, onSelect = { walletViewModel.selectWallet(walletInfo.walletId) nav.nav(Route.WalletDetail(walletInfo.walletId)) @@ -229,12 +240,6 @@ private fun MultiWalletHomeContent( onRename = { newName -> walletViewModel.renameWallet(walletInfo.walletId, newName) }, - onMoveUp = { - walletViewModel.moveWallet(index, index - 1) - }, - onMoveDown = { - walletViewModel.moveWallet(index, index + 1) - }, onRemove = { walletViewModel.removeWallet(walletInfo.walletId) }, @@ -264,12 +269,10 @@ private fun MultiWalletHomeContent( private fun WalletCard( walletInfo: WalletInfo, index: Int, - totalCount: Int, + dragState: RelayDragState?, onSelect: () -> Unit, onSetDefault: () -> Unit, onRename: (String) -> Unit, - onMoveUp: () -> Unit, - onMoveDown: () -> Unit, onRemove: () -> Unit, ) { var showRemoveDialog by remember { mutableStateOf(false) } @@ -311,6 +314,7 @@ private fun WalletCard( modifier = Modifier .fillMaxWidth() + .let { if (dragState != null) it.draggableRelayItem(index, dragState) else it } .clickable(onClick = onSelect), shape = RoundedCornerShape(16.dp), border = @@ -363,32 +367,18 @@ private fun WalletCard( } } - // Reorder buttons - if (totalCount > 1) { - Column { - IconButton( - onClick = onMoveUp, - enabled = index > 0, - modifier = Modifier.size(28.dp), - ) { - Icon( - MaterialSymbols.KeyboardArrowUp, - contentDescription = stringRes(R.string.wallet_move_up), - modifier = Modifier.size(20.dp), - ) - } - IconButton( - onClick = onMoveDown, - enabled = index < totalCount - 1, - modifier = Modifier.size(28.dp), - ) { - Icon( - MaterialSymbols.KeyboardArrowDown, - contentDescription = stringRes(R.string.wallet_move_down), - modifier = Modifier.size(20.dp), - ) - } - } + // Drag handle for reordering + if (dragState != null) { + Icon( + MaterialSymbols.DragIndicator, + contentDescription = stringRes(R.string.wallet_reorder), + modifier = + Modifier + .size(24.dp) + .relayDragHandle(index, dragState), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.width(8.dp)) } // Balance diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index e9897200d3..487aa62511 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1870,6 +1870,7 @@ Invalid NWC connection URI Move Up Move Down + Reorder wallet Onchain Transactions No on-chain address available for this account. No chain backend is configured.