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.Close
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -43,11 +45,18 @@ fun HistoryScreen(
|
||||
onClose: () -> 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 fiatSatsPerUnit by vm.fiatSatsPerUnit.collectAsState()
|
||||
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(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -59,91 +68,139 @@ fun HistoryScreen(
|
||||
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 ->
|
||||
Box(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
when (val s = state) {
|
||||
|
||||
is HistoryState.Loading -> {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
// ── 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)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is HistoryState.Error -> {
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = s.message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Button(onClick = { vm.refresh() }) { Text("Retry") }
|
||||
// ── Content ───────────────────────────────────────────────────────
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
when (val s = state) {
|
||||
|
||||
is HistoryState.Loading -> {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
}
|
||||
|
||||
is HistoryState.Success -> {
|
||||
val shouldLoadMore by remember {
|
||||
derivedStateOf {
|
||||
val lastVisible = listState.layoutInfo.visibleItemsInfo.lastOrNull()
|
||||
val totalItems = listState.layoutInfo.totalItemsCount
|
||||
lastVisible != null && lastVisible.index >= totalItems - 3
|
||||
is HistoryState.Error -> {
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = s.message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Button(onClick = { vm.refresh() }) { Text("Retry") }
|
||||
}
|
||||
}
|
||||
LaunchedEffect(shouldLoadMore) {
|
||||
if (shouldLoadMore && s.canLoadMore) vm.loadMore()
|
||||
}
|
||||
if (s.payments.isEmpty()) {
|
||||
Text(
|
||||
text = "No payments yet.",
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
} else PullToRefreshBox(
|
||||
isRefreshing = s.isRefreshing,
|
||||
onRefresh = { vm.refresh() }
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(vertical = 8.dp)
|
||||
) {
|
||||
items(
|
||||
items = s.payments,
|
||||
key = { it.paymentHash }
|
||||
) { payment ->
|
||||
PaymentRow(
|
||||
payment = payment,
|
||||
fiatCurrency = fiatCurrency,
|
||||
fiatSatsPerUnit = fiatSatsPerUnit,
|
||||
onClick = { onPaymentClick(payment) }
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
thickness = 0.5.dp
|
||||
)
|
||||
}
|
||||
|
||||
if (s.canLoadMore) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
is HistoryState.Success -> {
|
||||
val shouldLoadMore by remember {
|
||||
derivedStateOf {
|
||||
val lastVisible = listState.layoutInfo.visibleItemsInfo.lastOrNull()
|
||||
val totalItems = listState.layoutInfo.totalItemsCount
|
||||
lastVisible != null && lastVisible.index >= totalItems - 3
|
||||
}
|
||||
}
|
||||
LaunchedEffect(shouldLoadMore) {
|
||||
if (shouldLoadMore && s.canLoadMore) vm.loadMore()
|
||||
}
|
||||
if (s.payments.isEmpty()) {
|
||||
Text(
|
||||
text = "No payments yet.",
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
} else PullToRefreshBox(
|
||||
isRefreshing = s.isRefreshing,
|
||||
onRefresh = { vm.refresh() }
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(vertical = 8.dp)
|
||||
) {
|
||||
items(
|
||||
items = s.payments,
|
||||
key = { it.paymentHash }
|
||||
) { payment ->
|
||||
PaymentRow(
|
||||
payment = payment,
|
||||
fiatCurrency = fiatCurrency,
|
||||
fiatSatsPerUnit = fiatSatsPerUnit,
|
||||
onClick = { onPaymentClick(payment) }
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
thickness = 0.5.dp
|
||||
)
|
||||
}
|
||||
|
||||
if (s.canLoadMore) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +210,70 @@ 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 ─────────────────────────────────────────────────────
|
||||
@@ -181,13 +302,11 @@ private fun PaymentRow(
|
||||
) {
|
||||
val semantic = semanticColors()
|
||||
|
||||
// ── Pending detection ────────────────────────────────────────────────────
|
||||
val isPending = isPendingStatus(payment.status)
|
||||
val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
|
||||
val icon = if (payment.isOutgoing) Icons.Default.ArrowUpward
|
||||
val isPending = isPendingStatus(payment.status)
|
||||
val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
|
||||
val icon = if (payment.isOutgoing) Icons.Default.ArrowUpward
|
||||
else Icons.Default.ArrowDownward
|
||||
|
||||
// Fiat secondary line — only shown when rate is available
|
||||
val fiatLine: String? = remember(payment.amountSat, fiatSatsPerUnit, fiatCurrency) {
|
||||
if (fiatSatsPerUnit != null && fiatCurrency != null && payment.amountSat > 0)
|
||||
formatFiatForSats(payment.amountSat, fiatSatsPerUnit, fiatCurrency)
|
||||
@@ -201,7 +320,6 @@ private fun PaymentRow(
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Direction icon — tinted to match amount color
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = if (payment.isOutgoing) "Sent" else "Received",
|
||||
@@ -210,7 +328,6 @@ private fun PaymentRow(
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
|
||||
// Left: memo + timestamp + optional pending badge
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = payment.memo?.takeIf { it.isNotBlank() } ?: "No memo",
|
||||
@@ -224,7 +341,6 @@ private fun PaymentRow(
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
// ── Pending badge ────────────────────────────────────────────────
|
||||
if (isPending) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Surface(
|
||||
@@ -346,7 +462,6 @@ private fun PaymentDetailContent(
|
||||
fiatSatsPerUnit : Double?, // ← new
|
||||
modifier : Modifier = Modifier
|
||||
) {
|
||||
val isPending = isPendingStatus(payment.status)
|
||||
val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -12,10 +12,23 @@ import com.bitcointxoko.gudariwallet.service.WalletNotificationService
|
||||
import com.bitcointxoko.gudariwallet.util.WalletConstants
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// ── Filter model ──────────────────────────────────────────────────────────────
|
||||
|
||||
data class PaymentFilter(
|
||||
val direction: DirectionFilter = DirectionFilter.ALL
|
||||
)
|
||||
|
||||
enum class DirectionFilter { ALL, OUTGOING, INCOMING }
|
||||
|
||||
// ── ViewModel ─────────────────────────────────────────────────────────────────
|
||||
|
||||
class HistoryViewModel(
|
||||
private val repo : WalletRepository,
|
||||
private val aliasRepo : NodeAliasRepository,
|
||||
@@ -27,12 +40,31 @@ class HistoryViewModel(
|
||||
private val _state = MutableStateFlow<HistoryState>(HistoryState.Loading)
|
||||
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 loadJob: Job? = null
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
// Serve first page from DB immediately — no TTL, always valid
|
||||
val cached = paymentCache.loadCachedPage(
|
||||
offset = 0,
|
||||
limit = WalletConstants.PAYMENTS_PAGE_SIZE
|
||||
@@ -50,10 +82,6 @@ class HistoryViewModel(
|
||||
}
|
||||
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 {
|
||||
WalletNotificationService.paymentEvents.collect { event ->
|
||||
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) {
|
||||
var syncOffset = 0
|
||||
var totalNew = 0
|
||||
@@ -84,17 +102,13 @@ class HistoryViewModel(
|
||||
Log.d(TAG, "PAYMENTS [SYNC START] scanning for new payments (cap=$maxPayments)")
|
||||
|
||||
while (true) {
|
||||
// ── Checkpoint guard ──────────────────────────────────────────────────
|
||||
if (totalNew >= maxPayments) {
|
||||
Log.d(TAG, "PAYMENTS [SYNC CAP ] reached $maxPayments payment limit — stopping early")
|
||||
break
|
||||
}
|
||||
|
||||
val page = runCatching {
|
||||
repo.getPayments(
|
||||
offset = syncOffset,
|
||||
limit = WalletConstants.PAYMENTS_PAGE_SIZE
|
||||
)
|
||||
repo.getPayments(offset = syncOffset, limit = WalletConstants.PAYMENTS_PAGE_SIZE)
|
||||
}.getOrElse { e ->
|
||||
val current = _state.value
|
||||
if (current is HistoryState.Success) {
|
||||
@@ -110,14 +124,9 @@ class HistoryViewModel(
|
||||
pagesChecked++
|
||||
|
||||
val overlapIndex = page.indexOfFirst { paymentCache.containsPayment(it.checkingId) }
|
||||
|
||||
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 toMerge = if (newPayments.size > allowedNew) newPayments.subList(0, allowedNew)
|
||||
else newPayments
|
||||
val newPayments = if (overlapIndex == -1) page else page.subList(0, overlapIndex)
|
||||
val allowedNew = maxPayments - totalNew
|
||||
val toMerge = if (newPayments.size > allowedNew) newPayments.subList(0, allowedNew) else newPayments
|
||||
|
||||
if (toMerge.isNotEmpty()) {
|
||||
paymentCache.mergePayments(toMerge)
|
||||
@@ -137,7 +146,6 @@ class HistoryViewModel(
|
||||
syncOffset += page.size
|
||||
}
|
||||
|
||||
// Reload first page from DB to reflect any newly merged payments
|
||||
val refreshed = paymentCache.loadCachedPage(
|
||||
offset = 0,
|
||||
limit = currentOffset.coerceAtLeast(WalletConstants.PAYMENTS_PAGE_SIZE)
|
||||
@@ -151,8 +159,6 @@ class HistoryViewModel(
|
||||
)
|
||||
}
|
||||
|
||||
// ── Pull-to-refresh ───────────────────────────────────────────────────────
|
||||
|
||||
fun refresh() {
|
||||
loadJob?.cancel()
|
||||
currentOffset = 0
|
||||
@@ -165,15 +171,12 @@ class HistoryViewModel(
|
||||
viewModelScope.launch { syncNewPayments() }
|
||||
}
|
||||
|
||||
// ── Load more (pagination) ────────────────────────────────────────────────
|
||||
|
||||
fun loadMore() {
|
||||
if (loadJob?.isActive == true) return
|
||||
val current = _state.value
|
||||
if (current !is HistoryState.Success || !current.canLoadMore) return
|
||||
|
||||
loadJob = viewModelScope.launch {
|
||||
// Try DB first — if we have a full page cached, serve it instantly
|
||||
val fromDb = paymentCache.loadCachedPage(
|
||||
offset = currentOffset,
|
||||
limit = WalletConstants.PAYMENTS_PAGE_SIZE
|
||||
@@ -190,7 +193,6 @@ class HistoryViewModel(
|
||||
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")
|
||||
runCatching {
|
||||
repo.getPayments(offset = currentOffset, limit = WalletConstants.PAYMENTS_PAGE_SIZE)
|
||||
@@ -216,14 +218,11 @@ class HistoryViewModel(
|
||||
val detailState: StateFlow<DetailState> = _detailState
|
||||
|
||||
fun loadDetail(checkingId: String, record: PaymentRecord? = null) {
|
||||
// Tier 1: in-memory cache
|
||||
paymentCache.getCachedDetail(checkingId)?.let {
|
||||
Log.d(TAG, "PAYMENTS [DETAIL HIT] $checkingId — instant from cache")
|
||||
_detailState.value = DetailState.Success(it)
|
||||
return
|
||||
}
|
||||
|
||||
// Show partial UI immediately
|
||||
if (record != null) {
|
||||
Log.d(TAG, "PAYMENTS [DETAIL PAR] $checkingId — showing partial from PaymentRecord")
|
||||
_detailState.value = DetailState.Partial(record)
|
||||
@@ -231,9 +230,7 @@ class HistoryViewModel(
|
||||
Log.d(TAG, "PAYMENTS [DETAIL LOD] $checkingId — no record, showing spinner")
|
||||
_detailState.value = DetailState.Loading
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
// Tier 2: Room DB
|
||||
val fromDb = paymentCache.getDetailFromDb(checkingId)
|
||||
if (fromDb != null) {
|
||||
paymentCache.saveDetail(checkingId, fromDb)
|
||||
@@ -241,8 +238,6 @@ class HistoryViewModel(
|
||||
Log.d(TAG, "PAYMENTS [DETAIL DB ] $checkingId — served from Room, skipping network")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Tier 3: network
|
||||
runCatching {
|
||||
repo.getPaymentDetail(checkingId)
|
||||
}.onSuccess { detail ->
|
||||
@@ -283,7 +278,6 @@ class HistoryViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Factory ───────────────────────────────────────────────────────────────────
|
||||
|
||||
class HistoryViewModelFactory(
|
||||
|
||||
Reference in New Issue
Block a user