Merge pull request #605 from dmnyc/feat/tx-history-sheet

feat(wallet): transaction history as swipe-up bottom sheet
This commit is contained in:
Barry Deen
2026-06-30 17:29:57 -04:00
committed by GitHub
@@ -55,6 +55,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -115,11 +116,13 @@ import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -143,6 +146,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -204,6 +208,7 @@ fun WalletScreen(
// app bar to match iOS — leaves more headroom for the centered logo // app bar to match iOS — leaves more headroom for the centered logo
// + title layout. // + title layout.
val hideAppBar = currentPage is WalletPage.Home || val hideAppBar = currentPage is WalletPage.Home ||
currentPage is WalletPage.Transactions ||
currentPage is WalletPage.ModeSelection || currentPage is WalletPage.ModeSelection ||
currentPage is WalletPage.NwcSetup || currentPage is WalletPage.NwcSetup ||
currentPage is WalletPage.SparkSetup || currentPage is WalletPage.SparkSetup ||
@@ -482,17 +487,36 @@ fun WalletScreen(
) )
} }
is WalletPage.Transactions -> { is WalletPage.Transactions -> {
// Observe profile refresh key so UI recomposes when new profiles arrive // Home stays visible behind the transactions bottom sheet.
val profileKey = viewModel.profileRefreshKey.collectAsState().value val profileKey = viewModel.profileRefreshKey.collectAsState().value
TransactionHistoryContent( WalletHomeContent(
transactions = viewModel.transactions.collectAsState().value, balanceMsats = balanceMsats,
error = viewModel.transactionsError.collectAsState().value, walletMode = viewModel.walletMode.collectAsState().value,
isLoading = viewModel.isLoading.collectAsState().value, balanceUnit = viewModel.balanceUnit.collectAsState().value,
isLoadingMore = viewModel.isLoadingMore.collectAsState().value, showSettingsAlert = viewModel.walletMode.collectAsState().value == WalletMode.SPARK
hasMore = viewModel.hasMoreTransactions.collectAsState().value, && !viewModel.seedBackupAcked.collectAsState().value
onLoadMore = { viewModel.loadMoreTransactions() }, && !viewModel.isDefaultWallet.collectAsState().value,
profileLookup = { viewModel.getProfileData(it) }, seedBackupAcked = viewModel.seedBackupAcked.collectAsState().value,
profileRefreshKey = profileKey, backupMissing = viewModel.backupMissing.collectAsState().value,
isDefaultWallet = viewModel.isDefaultWallet.collectAsState().value,
onSend = { viewModel.navigateTo(WalletPage.SendInput) },
onReceive = { viewModel.navigateTo(WalletPage.ReceiveAmount) },
onTransactions = {},
onRefresh = { viewModel.refreshBalance() },
onSettings = { viewModel.navigateTo(WalletPage.Settings) },
onBackupToRelay = {
viewModel.resetBackupStatus()
viewModel.navigateTo(WalletPage.BackupToRelay)
},
onViewSeed = { viewModel.showMnemonicBackup() },
lightningAddress = viewModel.lightningAddress.collectAsState().value,
onSetupAddress = {
viewModel.resetAddressSetupState()
viewModel.navigateTo(WalletPage.LightningAddressSetup)
},
recentTransactions = viewModel.transactions.collectAsState().value,
profileLookup = remember(profileKey) { { viewModel.getProfileData(it) } },
nwcNodeAlias = viewModel.nwcNodeAlias.collectAsState().value,
pubkey = viewModel.keyRepo.getPubkeyHex(), pubkey = viewModel.keyRepo.getPubkeyHex(),
modifier = Modifier.padding(padding) modifier = Modifier.padding(padding)
) )
@@ -612,6 +636,28 @@ fun WalletScreen(
) )
} }
} }
// Transactions full-screen bottom sheet — swipe down to dismiss
if (currentPage is WalletPage.Transactions) {
val txSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val txProfileKey = viewModel.profileRefreshKey.collectAsState().value
ModalBottomSheet(
onDismissRequest = { viewModel.navigateBack() },
sheetState = txSheetState,
) {
TransactionHistoryContent(
transactions = viewModel.transactions.collectAsState().value,
error = viewModel.transactionsError.collectAsState().value,
isLoading = viewModel.isLoading.collectAsState().value,
isLoadingMore = viewModel.isLoadingMore.collectAsState().value,
hasMore = viewModel.hasMoreTransactions.collectAsState().value,
onLoadMore = { viewModel.loadMoreTransactions() },
profileLookup = { viewModel.getProfileData(it) },
profileRefreshKey = txProfileKey,
pubkey = viewModel.keyRepo.getPubkeyHex(),
)
}
}
} }
} }
} }
@@ -1472,7 +1518,24 @@ private fun WalletHomeContent(
// ── Recent transactions inline footer ────────────────────── // ── Recent transactions inline footer ──────────────────────
if (recentTransactions.isNotEmpty()) { if (recentTransactions.isNotEmpty()) {
Column(modifier = Modifier.fillMaxWidth()) { Column(
modifier = Modifier.fillMaxWidth()
.pointerInput(onTransactions) {
var totalDrag = 0f
detectVerticalDragGestures(
onDragStart = { totalDrag = 0f },
onDragEnd = { totalDrag = 0f },
onDragCancel = { totalDrag = 0f }
) { change, delta ->
totalDrag += delta
if (totalDrag < -40f) {
totalDrag = 0f
change.consume()
onTransactions()
}
}
}
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()