feat: add filtering by direction
This commit is contained in:
@@ -13,10 +13,12 @@ import androidx.compose.material.icons.filled.ArrowDownward
|
|||||||
import androidx.compose.material.icons.filled.ArrowUpward
|
import androidx.compose.material.icons.filled.ArrowUpward
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.ContentCopy
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
|
import androidx.compose.material.icons.filled.FilterList
|
||||||
import androidx.compose.material.icons.outlined.ContentCopy
|
import androidx.compose.material.icons.outlined.ContentCopy
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -43,11 +45,18 @@ fun HistoryScreen(
|
|||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
onPaymentClick: (PaymentRecord) -> Unit
|
onPaymentClick: (PaymentRecord) -> Unit
|
||||||
) {
|
) {
|
||||||
val state by vm.state.collectAsState()
|
val state by vm.filteredState.collectAsState()
|
||||||
|
val filter by vm.filter.collectAsState()
|
||||||
val fiatCurrency by vm.fiatCurrency.collectAsState()
|
val fiatCurrency by vm.fiatCurrency.collectAsState()
|
||||||
val fiatSatsPerUnit by vm.fiatSatsPerUnit.collectAsState()
|
val fiatSatsPerUnit by vm.fiatSatsPerUnit.collectAsState()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
var filterSheetVisible by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val dismissFilterSheet = { filterSheetVisible = false }
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
|
val isFiltered = filter.direction != DirectionFilter.ALL
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -59,15 +68,62 @@ fun HistoryScreen(
|
|||||||
contentDescription = "Close"
|
contentDescription = "Close"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
// Badge the icon when a filter is active
|
||||||
|
BadgedBox(
|
||||||
|
badge = {
|
||||||
|
if (isFiltered) Badge()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
IconButton(onClick = { filterSheetVisible = true }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.FilterList,
|
||||||
|
contentDescription = "Filter payments"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Box(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
) {
|
) {
|
||||||
|
// ── Active filter chips ───────────────────────────────────────────
|
||||||
|
if (isFiltered) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
InputChip(
|
||||||
|
selected = true,
|
||||||
|
onClick = { vm.setDirectionFilter(DirectionFilter.ALL) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
when (filter.direction) {
|
||||||
|
DirectionFilter.OUTGOING -> "Outgoing"
|
||||||
|
else -> "Incoming"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "Clear direction filter",
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Content ───────────────────────────────────────────────────────
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
when (val s = state) {
|
when (val s = state) {
|
||||||
|
|
||||||
is HistoryState.Loading -> {
|
is HistoryState.Loading -> {
|
||||||
@@ -153,6 +209,71 @@ fun HistoryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Filter bottom sheet ───────────────────────────────────────────────────
|
||||||
|
if (filterSheetVisible) {
|
||||||
|
FilterBottomSheet(
|
||||||
|
currentFilter = filter,
|
||||||
|
sheetState = sheetState,
|
||||||
|
onDismiss = dismissFilterSheet,
|
||||||
|
onDirectionSelected = { direction ->
|
||||||
|
vm.setDirectionFilter(direction)
|
||||||
|
dismissFilterSheet()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Filter bottom sheet ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun FilterBottomSheet(
|
||||||
|
currentFilter : PaymentFilter,
|
||||||
|
sheetState : SheetState,
|
||||||
|
onDismiss : () -> Unit,
|
||||||
|
onDirectionSelected: (DirectionFilter) -> Unit
|
||||||
|
) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = sheetState
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 24.dp, end = 24.dp, bottom = 32.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Filter payments",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(20.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Direction",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Direction options as a segmented-style row of filter chips
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
DirectionFilter.entries.forEach { option ->
|
||||||
|
val label = when (option) {
|
||||||
|
DirectionFilter.ALL -> "All"
|
||||||
|
DirectionFilter.OUTGOING -> "Outgoing"
|
||||||
|
DirectionFilter.INCOMING -> "Incoming"
|
||||||
|
}
|
||||||
|
FilterChip(
|
||||||
|
selected = currentFilter.direction == option,
|
||||||
|
onClick = { onDirectionSelected(option) },
|
||||||
|
label = { Text(label) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Payment color helpers ─────────────────────────────────────────────────────
|
// ── Payment color helpers ─────────────────────────────────────────────────────
|
||||||
@@ -181,13 +302,11 @@ private fun PaymentRow(
|
|||||||
) {
|
) {
|
||||||
val semantic = semanticColors()
|
val semantic = semanticColors()
|
||||||
|
|
||||||
// ── Pending detection ────────────────────────────────────────────────────
|
|
||||||
val isPending = isPendingStatus(payment.status)
|
val isPending = isPendingStatus(payment.status)
|
||||||
val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
|
val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
|
||||||
val icon = if (payment.isOutgoing) Icons.Default.ArrowUpward
|
val icon = if (payment.isOutgoing) Icons.Default.ArrowUpward
|
||||||
else Icons.Default.ArrowDownward
|
else Icons.Default.ArrowDownward
|
||||||
|
|
||||||
// Fiat secondary line — only shown when rate is available
|
|
||||||
val fiatLine: String? = remember(payment.amountSat, fiatSatsPerUnit, fiatCurrency) {
|
val fiatLine: String? = remember(payment.amountSat, fiatSatsPerUnit, fiatCurrency) {
|
||||||
if (fiatSatsPerUnit != null && fiatCurrency != null && payment.amountSat > 0)
|
if (fiatSatsPerUnit != null && fiatCurrency != null && payment.amountSat > 0)
|
||||||
formatFiatForSats(payment.amountSat, fiatSatsPerUnit, fiatCurrency)
|
formatFiatForSats(payment.amountSat, fiatSatsPerUnit, fiatCurrency)
|
||||||
@@ -201,7 +320,6 @@ private fun PaymentRow(
|
|||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Direction icon — tinted to match amount color
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = if (payment.isOutgoing) "Sent" else "Received",
|
contentDescription = if (payment.isOutgoing) "Sent" else "Received",
|
||||||
@@ -210,7 +328,6 @@ private fun PaymentRow(
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.width(12.dp))
|
Spacer(Modifier.width(12.dp))
|
||||||
|
|
||||||
// Left: memo + timestamp + optional pending badge
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = payment.memo?.takeIf { it.isNotBlank() } ?: "No memo",
|
text = payment.memo?.takeIf { it.isNotBlank() } ?: "No memo",
|
||||||
@@ -224,7 +341,6 @@ private fun PaymentRow(
|
|||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
// ── Pending badge ────────────────────────────────────────────────
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
Surface(
|
Surface(
|
||||||
@@ -346,7 +462,6 @@ private fun PaymentDetailContent(
|
|||||||
fiatSatsPerUnit : Double?, // ← new
|
fiatSatsPerUnit : Double?, // ← new
|
||||||
modifier : Modifier = Modifier
|
modifier : Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val isPending = isPendingStatus(payment.status)
|
|
||||||
val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
|
val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|||||||
@@ -12,10 +12,23 @@ import com.bitcointxoko.gudariwallet.service.WalletNotificationService
|
|||||||
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
// ── Filter model ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
data class PaymentFilter(
|
||||||
|
val direction: DirectionFilter = DirectionFilter.ALL
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class DirectionFilter { ALL, OUTGOING, INCOMING }
|
||||||
|
|
||||||
|
// ── ViewModel ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class HistoryViewModel(
|
class HistoryViewModel(
|
||||||
private val repo : WalletRepository,
|
private val repo : WalletRepository,
|
||||||
private val aliasRepo : NodeAliasRepository,
|
private val aliasRepo : NodeAliasRepository,
|
||||||
@@ -27,12 +40,31 @@ class HistoryViewModel(
|
|||||||
private val _state = MutableStateFlow<HistoryState>(HistoryState.Loading)
|
private val _state = MutableStateFlow<HistoryState>(HistoryState.Loading)
|
||||||
val state: StateFlow<HistoryState> = _state
|
val state: StateFlow<HistoryState> = _state
|
||||||
|
|
||||||
|
// ── Filter state ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private val _filter = MutableStateFlow(PaymentFilter())
|
||||||
|
val filter: StateFlow<PaymentFilter> = _filter
|
||||||
|
|
||||||
|
fun setDirectionFilter(direction: DirectionFilter) {
|
||||||
|
_filter.update { it.copy(direction = direction) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The list shown in the UI — raw state with filter applied. */
|
||||||
|
val filteredState: StateFlow<HistoryState> = combine(_state, _filter) { s, f ->
|
||||||
|
if (s !is HistoryState.Success) return@combine s
|
||||||
|
val filtered = when (f.direction) {
|
||||||
|
DirectionFilter.ALL -> s.payments
|
||||||
|
DirectionFilter.OUTGOING -> s.payments.filter { it.isOutgoing }
|
||||||
|
DirectionFilter.INCOMING -> s.payments.filter { !it.isOutgoing }
|
||||||
|
}
|
||||||
|
s.copy(payments = filtered)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.Eagerly, HistoryState.Loading)
|
||||||
|
|
||||||
private var currentOffset = 0
|
private var currentOffset = 0
|
||||||
private var loadJob: Job? = null
|
private var loadJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
// Serve first page from DB immediately — no TTL, always valid
|
|
||||||
val cached = paymentCache.loadCachedPage(
|
val cached = paymentCache.loadCachedPage(
|
||||||
offset = 0,
|
offset = 0,
|
||||||
limit = WalletConstants.PAYMENTS_PAGE_SIZE
|
limit = WalletConstants.PAYMENTS_PAGE_SIZE
|
||||||
@@ -50,10 +82,6 @@ class HistoryViewModel(
|
|||||||
}
|
}
|
||||||
syncNewPayments()
|
syncNewPayments()
|
||||||
}
|
}
|
||||||
// ── Live WebSocket updates ────────────────────────────────────────────────
|
|
||||||
// The payment is already written to the DB by WalletNotificationService
|
|
||||||
// before this event is emitted. syncNewPayments() will find it immediately
|
|
||||||
// via containsPayment() on the first page and stop after merging it.
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
WalletNotificationService.paymentEvents.collect { event ->
|
WalletNotificationService.paymentEvents.collect { event ->
|
||||||
Log.d(TAG, "PAYMENTS [WS EVENT ] incoming event ${event.paymentHash} — refreshing list")
|
Log.d(TAG, "PAYMENTS [WS EVENT ] incoming event ${event.paymentHash} — refreshing list")
|
||||||
@@ -66,16 +94,6 @@ class HistoryViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sync: fetch pages from network until we hit a known checkingId ────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches pages from the server starting at offset 0, merging each into the
|
|
||||||
* DB, until a payment is found that already exists in the DB (meaning we've
|
|
||||||
* caught up), or the server returns a partial page (end of history).
|
|
||||||
*
|
|
||||||
* This fills any gap caused by payments that arrived while the app was
|
|
||||||
* offline, without re-fetching history we already have.
|
|
||||||
*/
|
|
||||||
private suspend fun syncNewPayments(maxPayments: Int = WalletConstants.PAYMENTS_SYNC_CHECKPOINT) {
|
private suspend fun syncNewPayments(maxPayments: Int = WalletConstants.PAYMENTS_SYNC_CHECKPOINT) {
|
||||||
var syncOffset = 0
|
var syncOffset = 0
|
||||||
var totalNew = 0
|
var totalNew = 0
|
||||||
@@ -84,17 +102,13 @@ class HistoryViewModel(
|
|||||||
Log.d(TAG, "PAYMENTS [SYNC START] scanning for new payments (cap=$maxPayments)")
|
Log.d(TAG, "PAYMENTS [SYNC START] scanning for new payments (cap=$maxPayments)")
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
// ── Checkpoint guard ──────────────────────────────────────────────────
|
|
||||||
if (totalNew >= maxPayments) {
|
if (totalNew >= maxPayments) {
|
||||||
Log.d(TAG, "PAYMENTS [SYNC CAP ] reached $maxPayments payment limit — stopping early")
|
Log.d(TAG, "PAYMENTS [SYNC CAP ] reached $maxPayments payment limit — stopping early")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
val page = runCatching {
|
val page = runCatching {
|
||||||
repo.getPayments(
|
repo.getPayments(offset = syncOffset, limit = WalletConstants.PAYMENTS_PAGE_SIZE)
|
||||||
offset = syncOffset,
|
|
||||||
limit = WalletConstants.PAYMENTS_PAGE_SIZE
|
|
||||||
)
|
|
||||||
}.getOrElse { e ->
|
}.getOrElse { e ->
|
||||||
val current = _state.value
|
val current = _state.value
|
||||||
if (current is HistoryState.Success) {
|
if (current is HistoryState.Success) {
|
||||||
@@ -110,14 +124,9 @@ class HistoryViewModel(
|
|||||||
pagesChecked++
|
pagesChecked++
|
||||||
|
|
||||||
val overlapIndex = page.indexOfFirst { paymentCache.containsPayment(it.checkingId) }
|
val overlapIndex = page.indexOfFirst { paymentCache.containsPayment(it.checkingId) }
|
||||||
|
val newPayments = if (overlapIndex == -1) page else page.subList(0, overlapIndex)
|
||||||
val newPayments = if (overlapIndex == -1) page
|
|
||||||
else page.subList(0, overlapIndex)
|
|
||||||
|
|
||||||
// Respect the cap — don't write more than allowed
|
|
||||||
val allowedNew = maxPayments - totalNew
|
val allowedNew = maxPayments - totalNew
|
||||||
val toMerge = if (newPayments.size > allowedNew) newPayments.subList(0, allowedNew)
|
val toMerge = if (newPayments.size > allowedNew) newPayments.subList(0, allowedNew) else newPayments
|
||||||
else newPayments
|
|
||||||
|
|
||||||
if (toMerge.isNotEmpty()) {
|
if (toMerge.isNotEmpty()) {
|
||||||
paymentCache.mergePayments(toMerge)
|
paymentCache.mergePayments(toMerge)
|
||||||
@@ -137,7 +146,6 @@ class HistoryViewModel(
|
|||||||
syncOffset += page.size
|
syncOffset += page.size
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload first page from DB to reflect any newly merged payments
|
|
||||||
val refreshed = paymentCache.loadCachedPage(
|
val refreshed = paymentCache.loadCachedPage(
|
||||||
offset = 0,
|
offset = 0,
|
||||||
limit = currentOffset.coerceAtLeast(WalletConstants.PAYMENTS_PAGE_SIZE)
|
limit = currentOffset.coerceAtLeast(WalletConstants.PAYMENTS_PAGE_SIZE)
|
||||||
@@ -151,8 +159,6 @@ class HistoryViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Pull-to-refresh ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
loadJob?.cancel()
|
loadJob?.cancel()
|
||||||
currentOffset = 0
|
currentOffset = 0
|
||||||
@@ -165,15 +171,12 @@ class HistoryViewModel(
|
|||||||
viewModelScope.launch { syncNewPayments() }
|
viewModelScope.launch { syncNewPayments() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Load more (pagination) ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
fun loadMore() {
|
fun loadMore() {
|
||||||
if (loadJob?.isActive == true) return
|
if (loadJob?.isActive == true) return
|
||||||
val current = _state.value
|
val current = _state.value
|
||||||
if (current !is HistoryState.Success || !current.canLoadMore) return
|
if (current !is HistoryState.Success || !current.canLoadMore) return
|
||||||
|
|
||||||
loadJob = viewModelScope.launch {
|
loadJob = viewModelScope.launch {
|
||||||
// Try DB first — if we have a full page cached, serve it instantly
|
|
||||||
val fromDb = paymentCache.loadCachedPage(
|
val fromDb = paymentCache.loadCachedPage(
|
||||||
offset = currentOffset,
|
offset = currentOffset,
|
||||||
limit = WalletConstants.PAYMENTS_PAGE_SIZE
|
limit = WalletConstants.PAYMENTS_PAGE_SIZE
|
||||||
@@ -190,7 +193,6 @@ class HistoryViewModel(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Partial or empty DB page — fetch from network and merge
|
|
||||||
Log.d(TAG, "PAYMENTS [MORE NET ] offset=$currentOffset — DB has ${fromDb.size}, fetching from network")
|
Log.d(TAG, "PAYMENTS [MORE NET ] offset=$currentOffset — DB has ${fromDb.size}, fetching from network")
|
||||||
runCatching {
|
runCatching {
|
||||||
repo.getPayments(offset = currentOffset, limit = WalletConstants.PAYMENTS_PAGE_SIZE)
|
repo.getPayments(offset = currentOffset, limit = WalletConstants.PAYMENTS_PAGE_SIZE)
|
||||||
@@ -216,14 +218,11 @@ class HistoryViewModel(
|
|||||||
val detailState: StateFlow<DetailState> = _detailState
|
val detailState: StateFlow<DetailState> = _detailState
|
||||||
|
|
||||||
fun loadDetail(checkingId: String, record: PaymentRecord? = null) {
|
fun loadDetail(checkingId: String, record: PaymentRecord? = null) {
|
||||||
// Tier 1: in-memory cache
|
|
||||||
paymentCache.getCachedDetail(checkingId)?.let {
|
paymentCache.getCachedDetail(checkingId)?.let {
|
||||||
Log.d(TAG, "PAYMENTS [DETAIL HIT] $checkingId — instant from cache")
|
Log.d(TAG, "PAYMENTS [DETAIL HIT] $checkingId — instant from cache")
|
||||||
_detailState.value = DetailState.Success(it)
|
_detailState.value = DetailState.Success(it)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show partial UI immediately
|
|
||||||
if (record != null) {
|
if (record != null) {
|
||||||
Log.d(TAG, "PAYMENTS [DETAIL PAR] $checkingId — showing partial from PaymentRecord")
|
Log.d(TAG, "PAYMENTS [DETAIL PAR] $checkingId — showing partial from PaymentRecord")
|
||||||
_detailState.value = DetailState.Partial(record)
|
_detailState.value = DetailState.Partial(record)
|
||||||
@@ -231,9 +230,7 @@ class HistoryViewModel(
|
|||||||
Log.d(TAG, "PAYMENTS [DETAIL LOD] $checkingId — no record, showing spinner")
|
Log.d(TAG, "PAYMENTS [DETAIL LOD] $checkingId — no record, showing spinner")
|
||||||
_detailState.value = DetailState.Loading
|
_detailState.value = DetailState.Loading
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
// Tier 2: Room DB
|
|
||||||
val fromDb = paymentCache.getDetailFromDb(checkingId)
|
val fromDb = paymentCache.getDetailFromDb(checkingId)
|
||||||
if (fromDb != null) {
|
if (fromDb != null) {
|
||||||
paymentCache.saveDetail(checkingId, fromDb)
|
paymentCache.saveDetail(checkingId, fromDb)
|
||||||
@@ -241,8 +238,6 @@ class HistoryViewModel(
|
|||||||
Log.d(TAG, "PAYMENTS [DETAIL DB ] $checkingId — served from Room, skipping network")
|
Log.d(TAG, "PAYMENTS [DETAIL DB ] $checkingId — served from Room, skipping network")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 3: network
|
|
||||||
runCatching {
|
runCatching {
|
||||||
repo.getPaymentDetail(checkingId)
|
repo.getPaymentDetail(checkingId)
|
||||||
}.onSuccess { detail ->
|
}.onSuccess { detail ->
|
||||||
@@ -283,7 +278,6 @@ class HistoryViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Factory ───────────────────────────────────────────────────────────────────
|
// ── Factory ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class HistoryViewModelFactory(
|
class HistoryViewModelFactory(
|
||||||
|
|||||||
Reference in New Issue
Block a user