Merge pull request #605 from dmnyc/feat/tx-history-sheet
feat(wallet): transaction history as swipe-up bottom sheet
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user