refactor: combine filter and search states
This commit is contained in:
@@ -54,7 +54,6 @@ fun HistoryScreen(
|
||||
) {
|
||||
val state by vm.filteredState.collectAsState()
|
||||
val filter by vm.filter.collectAsState()
|
||||
val searchQuery by vm.searchQuery.collectAsState()
|
||||
val fiatCurrency by vm.fiatCurrency.collectAsState()
|
||||
val fiatSatsPerUnit by vm.fiatSatsPerUnit.collectAsState()
|
||||
val listState = rememberLazyListState()
|
||||
@@ -87,7 +86,7 @@ fun HistoryScreen(
|
||||
actions = {
|
||||
// Search button
|
||||
BadgedBox(
|
||||
badge = { if (searchQuery.isNotBlank()) Badge() }
|
||||
badge = { if (filter.searchQuery.isNotBlank()) Badge() }
|
||||
) {
|
||||
IconButton(onClick = {
|
||||
searchVisible = !searchVisible
|
||||
@@ -143,7 +142,7 @@ fun HistoryScreen(
|
||||
}
|
||||
// Text field — takes all available space
|
||||
TextField(
|
||||
value = searchQuery,
|
||||
value = filter.searchQuery,
|
||||
onValueChange = { vm.setSearchQuery(it) },
|
||||
placeholder = { Text("Search transactions") },
|
||||
singleLine = true,
|
||||
@@ -173,12 +172,12 @@ fun HistoryScreen(
|
||||
// Clear button — only enabled when query is non-empty
|
||||
IconButton(
|
||||
onClick = { vm.clearSearch() },
|
||||
enabled = searchQuery.isNotEmpty()
|
||||
enabled = filter.searchQuery.isNotEmpty()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Clear search",
|
||||
tint = if (searchQuery.isNotEmpty())
|
||||
tint = if (filter.searchQuery.isNotEmpty())
|
||||
LocalContentColor.current
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||
@@ -188,7 +187,7 @@ fun HistoryScreen(
|
||||
HorizontalDivider()
|
||||
}
|
||||
// ── Active filter chips ───────────────────────────────────────────
|
||||
val searchActive = searchQuery.isNotBlank()
|
||||
val searchActive = filter.searchQuery.isNotBlank()
|
||||
|
||||
if (isFiltered || searchActive) {
|
||||
FlowRow(
|
||||
@@ -312,7 +311,7 @@ fun HistoryScreen(
|
||||
vm.clearSearch()
|
||||
searchVisible = false
|
||||
},
|
||||
label = { Text("search: …${searchQuery.takeLast(8)}") },
|
||||
label = { Text("search: …${filter.searchQuery.takeLast(8)}") },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
|
||||
@@ -25,16 +25,26 @@ import kotlinx.coroutines.launch
|
||||
// ── Filter model ──────────────────────────────────────────────────────────────
|
||||
|
||||
data class PaymentFilter(
|
||||
val direction: DirectionFilter = DirectionFilter.ALL,
|
||||
val statuses: Set<StatusFilter> = emptySet(),
|
||||
val types: Set<String> = emptySet(),
|
||||
val searchQuery : String = "",
|
||||
val direction : DirectionFilter = DirectionFilter.ALL,
|
||||
val statuses : Set<StatusFilter> = emptySet(),
|
||||
val types : Set<String> = emptySet(),
|
||||
val minAmountSat : Long? = null,
|
||||
val maxAmountSat : Long? = null,
|
||||
val minCreatedAt : Long? = null, // epoch seconds, inclusive
|
||||
val maxCreatedAt : Long? = null, // epoch seconds, inclusive
|
||||
val datePreset : DatePreset? = null // which preset chip is active
|
||||
val minCreatedAt : Long? = null,
|
||||
val maxCreatedAt : Long? = null,
|
||||
val datePreset : DatePreset? = null
|
||||
)
|
||||
|
||||
val PaymentFilter.isActive: Boolean
|
||||
get() = searchQuery.isNotBlank()
|
||||
|| direction != DirectionFilter.ALL
|
||||
|| statuses.isNotEmpty()
|
||||
|| types.isNotEmpty()
|
||||
|| minAmountSat != null
|
||||
|| maxAmountSat != null
|
||||
|| minCreatedAt != null
|
||||
|| maxCreatedAt != null
|
||||
|
||||
enum class DirectionFilter { ALL, OUTGOING, INCOMING }
|
||||
enum class StatusFilter { COMPLETED, PENDING, FAILED }
|
||||
@@ -54,19 +64,14 @@ class HistoryViewModel(
|
||||
private val _state = MutableStateFlow<HistoryState>(HistoryState.Loading)
|
||||
val state: StateFlow<HistoryState> = _state
|
||||
|
||||
// ── Search state ───────────────────────────────────────────────────────
|
||||
|
||||
private val _searchQuery = MutableStateFlow("")
|
||||
val searchQuery: StateFlow<String> = _searchQuery
|
||||
|
||||
fun setSearchQuery(q: String) { _searchQuery.update { q.trim() } }
|
||||
fun clearSearch() { _searchQuery.value = "" }
|
||||
|
||||
// ── Filter state ──────────────────────────────────────────────────────────
|
||||
// ── Filter state (search is just another filter field) ────────────────────
|
||||
|
||||
private val _filter = MutableStateFlow(PaymentFilter())
|
||||
val filter: StateFlow<PaymentFilter> = _filter
|
||||
|
||||
fun setSearchQuery(q: String) { _filter.update { it.copy(searchQuery = q.trim()) } }
|
||||
fun clearSearch() { _filter.update { it.copy(searchQuery = "") } }
|
||||
|
||||
fun setDirectionFilter(direction: DirectionFilter) {
|
||||
_filter.update { it.copy(direction = direction) }
|
||||
}
|
||||
@@ -126,46 +131,37 @@ class HistoryViewModel(
|
||||
|
||||
// ── Room-backed query results ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Holds the result of the last Room query, or null when no query/filter is
|
||||
* active (i.e. we're on the fast in-memory path).
|
||||
*/
|
||||
private val _roomResults = MutableStateFlow<List<PaymentRecord>?>(null)
|
||||
|
||||
/**
|
||||
* Returns true when at least one SQL-capable condition is active.
|
||||
* Direction and type are excluded — they're always handled in-memory.
|
||||
*/
|
||||
private fun PaymentFilter.needsRoomQuery(): Boolean =
|
||||
statuses.isNotEmpty() ||
|
||||
searchQuery.isNotBlank() ||
|
||||
statuses.isNotEmpty() ||
|
||||
minAmountSat != null || maxAmountSat != null ||
|
||||
minCreatedAt != null || maxCreatedAt != null
|
||||
|
||||
init {
|
||||
// Watch search query (debounced) + filter for SQL-capable conditions.
|
||||
// Whenever either changes and a Room query is needed, fire queryAll().
|
||||
// When neither is active, clear _roomResults to fall back to in-memory.
|
||||
// Debounce only the search query field; other filter changes are instant.
|
||||
// We do this by splitting the flow: debounce a search-only signal, then
|
||||
// re-combine with the rest of the filter so non-search changes are fast.
|
||||
viewModelScope.launch {
|
||||
combine(
|
||||
_searchQuery.debounce(300),
|
||||
_filter.map { it.searchQuery }.debounce(300),
|
||||
_filter
|
||||
) { q, f -> q to f }
|
||||
.collect { (q, f) ->
|
||||
if (q.isBlank() && !f.needsRoomQuery()) {
|
||||
Log.d(TAG, "SEARCH/FILTER [IN-MEMORY] q=blank filter=default → using cached _state")
|
||||
_roomResults.value = null // back to in-memory path
|
||||
) { _, f -> f }
|
||||
.collect { f ->
|
||||
if (!f.needsRoomQuery()) {
|
||||
Log.d(TAG, "FILTER [IN-MEMORY] filter=default → using cached _state")
|
||||
_roomResults.value = null
|
||||
} else {
|
||||
Log.d(TAG, "SEARCH/FILTER [ROOM ] q=\"$q\" statuses=${f.statuses} " +
|
||||
"dir=${f.direction} types=${f.types} " +
|
||||
Log.d(TAG, "FILTER [ROOM ] q=\"${f.searchQuery}\" " +
|
||||
"statuses=${f.statuses} dir=${f.direction} types=${f.types} " +
|
||||
"amt=${f.minAmountSat}–${f.maxAmountSat} " +
|
||||
"date=${f.minCreatedAt}–${f.maxCreatedAt}")
|
||||
val results = paymentCache.queryAll(q, f)
|
||||
_roomResults.value = results
|
||||
_roomResults.value = paymentCache.queryAll(f.searchQuery, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Existing init block below — unchanged
|
||||
viewModelScope.launch {
|
||||
val cached = paymentCache.loadCachedPage(
|
||||
offset = 0,
|
||||
@@ -197,27 +193,15 @@ class HistoryViewModel(
|
||||
}
|
||||
|
||||
// ── filteredState ─────────────────────────────────────────────────────────
|
||||
/**
|
||||
* The list shown in the UI.
|
||||
*
|
||||
* Two paths:
|
||||
* A) _roomResults == null → in-memory path (no search, no SQL-capable filter active)
|
||||
* Fast, zero DB overhead, identical to the old behaviour.
|
||||
*
|
||||
* B) _roomResults != null → Room path (search or SQL-capable filter active)
|
||||
* Base list comes from Room (full table, already filtered by SQL).
|
||||
* Direction + type filters are then applied in-memory on top.
|
||||
* canLoadMore is forced false — Room already returned everything.
|
||||
*/
|
||||
val filteredState: StateFlow<HistoryState> = combine(
|
||||
_state, _filter, _searchQuery, _roomResults
|
||||
) { s, f, _, roomResults ->
|
||||
_state, _filter, _roomResults
|
||||
) { s, f, roomResults ->
|
||||
|
||||
if (s !is HistoryState.Success) return@combine s
|
||||
|
||||
// ── Path A: pure in-memory (search blank, no SQL-capable filter) ──────
|
||||
// ── Path A: pure in-memory ────────────────────────────────────────────
|
||||
if (roomResults == null) {
|
||||
Log.d(TAG, "SEARCH/FILTER [IN-MEMORY] filteredState: base=${s.payments.size} payments")
|
||||
Log.d(TAG, "FILTER [IN-MEMORY] filteredState: base=${s.payments.size} payments")
|
||||
val filtered = s.payments
|
||||
.distinctBy { it.paymentHash }
|
||||
.let { list ->
|
||||
@@ -231,12 +215,12 @@ class HistoryViewModel(
|
||||
if (f.types.isEmpty()) list
|
||||
else list.filter { it.extra?.tag in f.types }
|
||||
}
|
||||
Log.d(TAG, "SEARCH/FILTER [IN-MEMORY] filteredState: after filters → ${filtered.size} shown")
|
||||
Log.d(TAG, "FILTER [IN-MEMORY] filteredState: after filters → ${filtered.size} shown")
|
||||
return@combine s.copy(payments = filtered)
|
||||
}
|
||||
|
||||
// ── Path B: Room results — apply direction + type in-memory ───────────
|
||||
Log.d(TAG, "SEARCH/FILTER [ROOM ] filteredState: base=${roomResults.size} from Room")
|
||||
Log.d(TAG, "FILTER [ROOM ] filteredState: base=${roomResults.size} from Room")
|
||||
val filtered = roomResults
|
||||
.distinctBy { it.paymentHash }
|
||||
.let { list ->
|
||||
@@ -251,8 +235,7 @@ class HistoryViewModel(
|
||||
else list.filter { it.extra?.tag in f.types }
|
||||
}
|
||||
|
||||
// canLoadMore = false: Room returned the full matching set already
|
||||
Log.d(TAG, "SEARCH/FILTER [ROOM ] filteredState: after dir+type → ${filtered.size} shown")
|
||||
Log.d(TAG, "FILTER [ROOM ] filteredState: after dir+type → ${filtered.size} shown")
|
||||
s.copy(payments = filtered, canLoadMore = false)
|
||||
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, HistoryState.Loading)
|
||||
|
||||
Reference in New Issue
Block a user