wire up historyviewmodel
This commit is contained in:
@@ -8,10 +8,13 @@ import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
|||||||
import com.bitcointxoko.gudariwallet.data.ContactRepository
|
import com.bitcointxoko.gudariwallet.data.ContactRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
|
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
||||||
|
import com.bitcointxoko.gudariwallet.data.PaymentFeedRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||||
import com.bitcointxoko.gudariwallet.data.db.ContactEntity
|
import com.bitcointxoko.gudariwallet.data.db.ContactEntity
|
||||||
import com.bitcointxoko.gudariwallet.data.db.ContactWithAddresses
|
import com.bitcointxoko.gudariwallet.data.db.ContactWithAddresses
|
||||||
import com.bitcointxoko.gudariwallet.data.db.PaymentAddressType
|
import com.bitcointxoko.gudariwallet.data.db.PaymentAddressType
|
||||||
|
import com.bitcointxoko.gudariwallet.data.model.PaymentFeedItem
|
||||||
|
import com.bitcointxoko.gudariwallet.data.model.toPaymentFeedItem
|
||||||
import com.bitcointxoko.gudariwallet.service.WalletNotificationService
|
import com.bitcointxoko.gudariwallet.service.WalletNotificationService
|
||||||
import com.bitcointxoko.gudariwallet.ui.DetailState
|
import com.bitcointxoko.gudariwallet.ui.DetailState
|
||||||
import com.bitcointxoko.gudariwallet.ui.HistoryState
|
import com.bitcointxoko.gudariwallet.ui.HistoryState
|
||||||
@@ -35,20 +38,27 @@ class HistoryViewModel(
|
|||||||
private val aliasRepo : NodeAliasRepository,
|
private val aliasRepo : NodeAliasRepository,
|
||||||
private val paymentCache : PaymentCacheRepository,
|
private val paymentCache : PaymentCacheRepository,
|
||||||
private val contactRepo : ContactRepository,
|
private val contactRepo : ContactRepository,
|
||||||
|
private val feedRepository : PaymentFeedRepository,
|
||||||
val fiatCurrency : StateFlow<String?>,
|
val fiatCurrency : StateFlow<String?>,
|
||||||
val fiatSatsPerUnit : StateFlow<Double?>
|
val fiatSatsPerUnit : StateFlow<Double?>
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
// ── Sync manager (owns _state, currentOffset, loadJob, sync loop) ─────────
|
|
||||||
|
// ── Sync manager ──────────────────────────────────────────────────────────
|
||||||
|
// Owns the websocket/sync loop, pagination, and optimistic inserts.
|
||||||
|
// Still operates on PaymentRecord internally — untouched by this refactor.
|
||||||
private val syncManager: PaymentSyncManager = PaymentSyncManager(
|
private val syncManager: PaymentSyncManager = PaymentSyncManager(
|
||||||
repo = repo,
|
repo = repo,
|
||||||
paymentCache = paymentCache,
|
paymentCache = paymentCache,
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
onNewPayments = { payments -> enricher.enrich(payments) }
|
onNewPayments = { payments -> enricher.enrich(payments) }
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Optimistic insert (called after a successful send) ─────────────────────
|
// ── Optimistic insert (called after a successful send) ─────────────────────
|
||||||
fun primePayment(record: PaymentRecord) {
|
fun primePayment(record: PaymentRecord) { syncManager.primePayment(record) }
|
||||||
syncManager.primePayment(record)
|
|
||||||
}
|
// ── Enricher ──────────────────────────────────────────────────────────────
|
||||||
|
// Resolves pubkey/alias post-send and writes back into NodeAliasCache,
|
||||||
|
// which feeds into the feedRepository.observeFeedItems() combine.
|
||||||
private val enricher: PaymentEnricher = PaymentEnricher(
|
private val enricher: PaymentEnricher = PaymentEnricher(
|
||||||
repo = repo,
|
repo = repo,
|
||||||
aliasRepo = aliasRepo,
|
aliasRepo = aliasRepo,
|
||||||
@@ -58,36 +68,40 @@ class HistoryViewModel(
|
|||||||
syncManager.updateEnrichedPayment(paymentHash, pubkey, alias)
|
syncManager.updateEnrichedPayment(paymentHash, pubkey, alias)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── Sync status ───────────────────────────────────────────────────────────
|
||||||
|
// Exposes Loading / Error / Success shell (isRefreshing, canLoadMore).
|
||||||
|
// Does NOT carry payment data — see feedItems below.
|
||||||
val state: StateFlow<HistoryState> = syncManager.state
|
val state: StateFlow<HistoryState> = syncManager.state
|
||||||
|
|
||||||
fun refresh() = syncManager.refresh()
|
fun refresh() = syncManager.refresh()
|
||||||
fun loadMore() = syncManager.loadMore(
|
fun loadMore() = syncManager.loadMore(
|
||||||
filteredCanLoadMore = { (filteredState.value as? HistoryState.Success)?.canLoadMore == true }
|
filteredCanLoadMore = {
|
||||||
|
(state.value as? HistoryState.Success)?.canLoadMore == true
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Detail delegate ───────────────────────────────────────────────────────
|
// ── Detail delegate ────────────────────────────────────────────────────────
|
||||||
private val detailDelegate = PaymentDetailDelegate(
|
private val detailDelegate = PaymentDetailDelegate(
|
||||||
repo = repo,
|
repo = repo,
|
||||||
paymentCache = paymentCache,
|
paymentCache = paymentCache,
|
||||||
scope = viewModelScope
|
scope = viewModelScope
|
||||||
)
|
)
|
||||||
val detailState: StateFlow<DetailState> = detailDelegate.state
|
val detailState: StateFlow<DetailState> = detailDelegate.state
|
||||||
fun loadDetail(checkingId: String, record: PaymentRecord? = null) =
|
fun loadDetail(paymentHash: String, record: PaymentRecord? = null) =
|
||||||
detailDelegate.load(checkingId, record)
|
detailDelegate.load(paymentHash, record)
|
||||||
fun clearDetail() = detailDelegate.clear()
|
fun clearDetail() = detailDelegate.clear()
|
||||||
|
|
||||||
// ── Filter state ──────────────────────────────────────────────────────────
|
// ── Filter state ───────────────────────────────────────────────────────────
|
||||||
private val _filter = MutableStateFlow(PaymentFilter())
|
private val _filter = MutableStateFlow(PaymentFilter())
|
||||||
val filter: StateFlow<PaymentFilter> = _filter
|
val filter: StateFlow<PaymentFilter> = _filter
|
||||||
|
|
||||||
fun setSearchQuery(q: String) { _filter.update { it.copy(searchQuery = q.trim()) } }
|
fun setSearchQuery(q: String) { _filter.update { it.copy(searchQuery = q.trim()) } }
|
||||||
fun clearSearch() { _filter.update { it.copy(searchQuery = "") } }
|
fun clearSearch() { _filter.update { it.copy(searchQuery = "") } }
|
||||||
|
fun setDirectionFilter(d: DirectionFilter) { _filter.update { it.copy(direction = d) } }
|
||||||
fun setDirectionFilter(direction: DirectionFilter) {
|
fun toggleStatusFilter(s: StatusFilter) {
|
||||||
_filter.update { it.copy(direction = direction) }
|
|
||||||
}
|
|
||||||
fun toggleStatusFilter(status: StatusFilter) {
|
|
||||||
_filter.update { f ->
|
_filter.update { f ->
|
||||||
val updated = if (status in f.statuses) f.statuses - status else f.statuses + status
|
val updated = if (s in f.statuses) f.statuses - s else f.statuses + s
|
||||||
f.copy(statuses = updated)
|
f.copy(statuses = updated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,55 +118,55 @@ class HistoryViewModel(
|
|||||||
_filter.update { it.copy(minCreatedAt = min, maxCreatedAt = max, datePreset = preset) }
|
_filter.update { it.copy(minCreatedAt = min, maxCreatedAt = max, datePreset = preset) }
|
||||||
}
|
}
|
||||||
fun applyDatePreset(preset: DatePreset) {
|
fun applyDatePreset(preset: DatePreset) {
|
||||||
val (min, max) = preset.toEpochSecondRange() // ← delegate to PaymentFilter.kt
|
val (min, max) = preset.toEpochSecondRange()
|
||||||
setDateFilter(min, max, preset)
|
setDateFilter(min, max, preset)
|
||||||
}
|
}
|
||||||
fun clearDateFilter() {
|
fun clearDateFilter() {
|
||||||
_filter.update { it.copy(minCreatedAt = null, maxCreatedAt = null, datePreset = null) }
|
_filter.update { it.copy(minCreatedAt = null, maxCreatedAt = null, datePreset = null) }
|
||||||
}
|
}
|
||||||
|
fun toggleContactFilter(contactId: String) {
|
||||||
fun findPayment(checkingId: String): PaymentRecord? {
|
_filter.update { f ->
|
||||||
val fromFiltered = (filteredState.value as? HistoryState.Success)
|
val updated = if (contactId in f.contactIds)
|
||||||
?.payments?.firstOrNull { it.checkingId == checkingId }
|
f.contactIds - contactId else f.contactIds + contactId
|
||||||
if (fromFiltered != null) return fromFiltered
|
f.copy(contactIds = updated)
|
||||||
return (state.value as? HistoryState.Success)
|
|
||||||
?.payments?.firstOrNull { it.checkingId == checkingId }
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
fun clearContactFilter() { _filter.update { it.copy(contactIds = emptySet()) } }
|
||||||
|
|
||||||
// ── Room-backed query results ──────────────────────────────────────────────
|
// ── Room-backed query results ──────────────────────────────────────────────
|
||||||
private val _roomResults = MutableStateFlow<List<PaymentRecord>?>(null)
|
// Null = use the live feed stream.
|
||||||
|
// Non-null = a search / contact-ID Room query has run and its results
|
||||||
|
// override the live stream until the filter is cleared.
|
||||||
|
private val _roomResults = MutableStateFlow<List<PaymentFeedItem>?>(null)
|
||||||
|
|
||||||
// ── Derived state ─────────────────────────────────────────────────────────
|
// ── Feed items — single source of truth for the UI list ───────────────────
|
||||||
|
// Emits a freshly filtered List<PaymentFeedItem> whenever any upstream
|
||||||
|
// source changes: payments DB, contact links, alias cache, fiat rate,
|
||||||
|
// active filter, or Room query override.
|
||||||
|
val feedItems: StateFlow<List<PaymentFeedItem>> = combine(
|
||||||
|
feedRepository.observeFeedItems(),
|
||||||
|
_filter,
|
||||||
|
_roomResults
|
||||||
|
) { feed, f, roomResults ->
|
||||||
|
val base = roomResults ?: feed
|
||||||
|
base.applyInMemoryFilters(f)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
/** Distinct non-null tags present in the currently loaded list. */
|
// ── Available type filter chips ────────────────────────────────────────────
|
||||||
val availableTypes: StateFlow<List<String>> = syncManager.state
|
val availableTypes: StateFlow<List<String>> = feedItems
|
||||||
.map { s ->
|
.map { items -> items.mapNotNull { it.tag }.distinct().sorted() }
|
||||||
if (s !is HistoryState.Success) emptyList()
|
|
||||||
else s.payments.mapNotNull { it.extra?.tag }.distinct().sorted()
|
|
||||||
}
|
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||||
|
|
||||||
val filteredState = combine(
|
// ── Find a single item by paymentHash ─────────────────────────────────────
|
||||||
syncManager.state,
|
// paymentHash supersedes checkingId as the stable unique identifier.
|
||||||
_filter, _roomResults,
|
// Used by PaymentDetailScreen to retrieve a feed item without a DB round-trip.
|
||||||
contactRepo.observeAllPaymentContactNames()
|
fun findPayment(paymentHash: String): PaymentFeedItem? =
|
||||||
) { s, f, roomResults, contactNames ->
|
feedItems.value.firstOrNull { it.paymentHash == paymentHash }
|
||||||
Timber.d("CONTACT [COMBINE ] contactNames has ${contactNames.size} entries")
|
|
||||||
if (s !is HistoryState.Success) return@combine s
|
|
||||||
val payments = if (roomResults == null) {
|
|
||||||
s.payments.applyInMemoryFilters(f)
|
|
||||||
} else {
|
|
||||||
roomResults.applyInMemoryFilters(f)
|
|
||||||
}
|
|
||||||
s.copy(
|
|
||||||
payments = payments,
|
|
||||||
canLoadMore = roomResults == null && s.canLoadMore
|
|
||||||
)
|
|
||||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, HistoryState.Loading)
|
|
||||||
|
|
||||||
// ── In-memory filter logic ────────────────────────────────────────────────
|
// ── In-memory filter logic ─────────────────────────────────────────────────
|
||||||
private fun List<PaymentRecord>.applyInMemoryFilters(f: PaymentFilter) =
|
// All filter keys map directly to PaymentFeedItem fields — no extra
|
||||||
|
// resolution needed (contact names, alias, fiat are already embedded).
|
||||||
|
private fun List<PaymentFeedItem>.applyInMemoryFilters(f: PaymentFilter): List<PaymentFeedItem> =
|
||||||
distinctBy { it.paymentHash }
|
distinctBy { it.paymentHash }
|
||||||
.let { list ->
|
.let { list ->
|
||||||
when (f.direction) {
|
when (f.direction) {
|
||||||
@@ -161,22 +175,53 @@ class HistoryViewModel(
|
|||||||
DirectionFilter.INCOMING -> list.filter { !it.isOutgoing }
|
DirectionFilter.INCOMING -> list.filter { !it.isOutgoing }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.let { list ->
|
||||||
|
if (f.statuses.isEmpty()) list
|
||||||
|
else list.filter { item ->
|
||||||
|
f.statuses.any { status ->
|
||||||
|
when (status) {
|
||||||
|
StatusFilter.PENDING -> item.isPending
|
||||||
|
StatusFilter.FAILED -> item.isFailed
|
||||||
|
StatusFilter.COMPLETED -> !item.isPending && !item.isFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.let { list ->
|
.let { list ->
|
||||||
if (f.types.isEmpty()) list
|
if (f.types.isEmpty()) list
|
||||||
else list.filter { it.extra?.tag in f.types }
|
else list.filter { it.tag in f.types }
|
||||||
|
}
|
||||||
|
.let { list ->
|
||||||
|
val min = f.minAmountSat; val max = f.maxAmountSat
|
||||||
|
if (min == null && max == null) list
|
||||||
|
else list.filter { item ->
|
||||||
|
(min == null || item.amountSat >= min) &&
|
||||||
|
(max == null || item.amountSat <= max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.let { list ->
|
||||||
|
val min = f.minCreatedAt; val max = f.maxCreatedAt
|
||||||
|
if (min == null && max == null) list
|
||||||
|
else list.filter { item ->
|
||||||
|
val ts = item.createdAt ?: item.time
|
||||||
|
ts != null &&
|
||||||
|
(min == null || ts >= min) &&
|
||||||
|
(max == null || ts <= max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.let { list ->
|
||||||
|
val q = f.searchQuery
|
||||||
|
if (q.isBlank()) list
|
||||||
|
else list.filter { item ->
|
||||||
|
item.memo?.contains(q, ignoreCase = true) == true ||
|
||||||
|
item.contactName?.contains(q, ignoreCase = true) == true ||
|
||||||
|
item.paymentHash.contains(q, ignoreCase = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Contact name map ─────────────────────────────────────────────────────
|
// ── Contact display names (for filter chips) ───────────────────────────────
|
||||||
// Maps checkingId → display name for any payment that has a linked contact.
|
// Used to render contact names in active filter chips on the UI.
|
||||||
// Rebuilt whenever the underlying tx_contact_links or contacts tables change.
|
// Payment row contact names are embedded in PaymentFeedItem.contactName.
|
||||||
val contactNames: StateFlow<Map<String, String>> =
|
|
||||||
contactRepo.observeAllPaymentContactNames()
|
|
||||||
.stateIn(
|
|
||||||
scope = viewModelScope,
|
|
||||||
started = SharingStarted.Eagerly,
|
|
||||||
initialValue = emptyMap()
|
|
||||||
)
|
|
||||||
// Maps contactId → display name (for filter chips)
|
|
||||||
val contactDisplayNames: StateFlow<Map<String, String>> =
|
val contactDisplayNames: StateFlow<Map<String, String>> =
|
||||||
contactRepo.observeAllContacts()
|
contactRepo.observeAllContacts()
|
||||||
.map { contacts ->
|
.map { contacts ->
|
||||||
@@ -193,8 +238,7 @@ class HistoryViewModel(
|
|||||||
initialValue = emptyMap()
|
initialValue = emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Contact assignment ───────────────────────────────────────────────────
|
// ── Per-payment contact queries ────────────────────────────────────────────
|
||||||
|
|
||||||
fun contactsForPayment(paymentHash: String): StateFlow<List<ContactEntity>> =
|
fun contactsForPayment(paymentHash: String): StateFlow<List<ContactEntity>> =
|
||||||
contactRepo.observeContactsForPayment(paymentHash)
|
contactRepo.observeContactsForPayment(paymentHash)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
@@ -211,17 +255,14 @@ class HistoryViewModel(
|
|||||||
initialValue = emptyList()
|
initialValue = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── Contact assignment ─────────────────────────────────────────────────────
|
||||||
fun assignContact(paymentHash: String, contactId: String) {
|
fun assignContact(paymentHash: String, contactId: String) {
|
||||||
Timber.d("CONTACT [ASSIGN ] paymentHash=$paymentHash contactId=$contactId")
|
Timber.tag(TAG).d("CONTACT [ASSIGN ] paymentHash=$paymentHash contactId=$contactId")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch { contactRepo.linkPaymentToContact(paymentHash, contactId) }
|
||||||
contactRepo.linkPaymentToContact(paymentHash, contactId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unassignContact(paymentHash: String, contactId: String) {
|
fun unassignContact(paymentHash: String, contactId: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch { contactRepo.unlinkPaymentFromContact(paymentHash, contactId) }
|
||||||
contactRepo.unlinkPaymentFromContact(paymentHash, contactId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveContactAndAssign(
|
fun saveContactAndAssign(
|
||||||
@@ -230,7 +271,7 @@ class HistoryViewModel(
|
|||||||
addressType : PaymentAddressType?,
|
addressType : PaymentAddressType?,
|
||||||
address : String?
|
address : String?
|
||||||
) {
|
) {
|
||||||
Timber.d("CONTACT [CREATE+ASSIGN] paymentHash=$paymentHash name=$name")
|
Timber.tag(TAG).d("CONTACT [CREATE+ASSIGN] paymentHash=$paymentHash name=$name")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val lnAddress = address?.takeIf { addressType == PaymentAddressType.LIGHTNING_ADDRESS }
|
val lnAddress = address?.takeIf { addressType == PaymentAddressType.LIGHTNING_ADDRESS }
|
||||||
val lnUrl = address?.takeIf { addressType == PaymentAddressType.LNURL }
|
val lnUrl = address?.takeIf { addressType == PaymentAddressType.LNURL }
|
||||||
@@ -239,27 +280,19 @@ class HistoryViewModel(
|
|||||||
lnAddress = lnAddress,
|
lnAddress = lnAddress,
|
||||||
lnUrl = lnUrl
|
lnUrl = lnUrl
|
||||||
)
|
)
|
||||||
Timber.d("CONTACT [CREATE+ASSIGN] created contactId=${contact.id} → linking to paymentHash=$paymentHash")
|
Timber.tag(TAG).d("CONTACT [CREATE+ASSIGN] created contactId=${contact.id} → linking")
|
||||||
contactRepo.linkPaymentToContact(paymentHash, contact.id)
|
contactRepo.linkPaymentToContact(paymentHash, contact.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleContactFilter(contactId: String) {
|
// ── Init ───────────────────────────────────────────────────────────────────
|
||||||
_filter.update { f ->
|
|
||||||
val updated = if (contactId in f.contactIds)
|
|
||||||
f.contactIds - contactId else f.contactIds + contactId
|
|
||||||
f.copy(contactIds = updated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun clearContactFilter() {
|
|
||||||
_filter.update { it.copy(contactIds = emptySet()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ── Init ──────────────────────────────────────────────────────────────────
|
|
||||||
init {
|
init {
|
||||||
syncManager.init()
|
syncManager.init()
|
||||||
|
|
||||||
|
// ── Room query path ────────────────────────────────────────────────────
|
||||||
|
// Activated when the filter requires DB-level text search or contact-ID
|
||||||
|
// lookup. Results are stored in _roomResults and override the live feed
|
||||||
|
// stream in feedItems. Cleared back to null when no Room query is needed.
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
combine(
|
combine(
|
||||||
_filter.map { it.searchQuery }.debounce(300.milliseconds),
|
_filter.map { it.searchQuery }.debounce(300.milliseconds),
|
||||||
@@ -267,82 +300,100 @@ class HistoryViewModel(
|
|||||||
) { _, f -> f }
|
) { _, f -> f }
|
||||||
.collect { f ->
|
.collect { f ->
|
||||||
if (!f.needsRoomQuery()) {
|
if (!f.needsRoomQuery()) {
|
||||||
Timber.d("FILTER [IN-MEMORY] filter=default → using cached state")
|
Timber.tag(TAG).d("FILTER [IN-MEMORY] using live feed")
|
||||||
_roomResults.value = null
|
_roomResults.value = null
|
||||||
} else {
|
return@collect
|
||||||
// ── Room query path ───────────────────────────────────
|
}
|
||||||
val baseResults = paymentCache.queryAll(f.searchQuery, f)
|
|
||||||
.toMutableList()
|
|
||||||
|
|
||||||
// ── Supplemental contact search ───────────────────────
|
// ── Explicit contact-ID filter (highest priority) ──────────
|
||||||
// 1. Text query may match contact names not in payment_records
|
if (f.contactIds.isNotEmpty()) {
|
||||||
|
val contactHashes = contactRepo
|
||||||
|
.findPaymentHashesByContactIds(f.contactIds.toList())
|
||||||
|
_roomResults.value = if (contactHashes.isNotEmpty()) {
|
||||||
|
paymentCache.queryByPaymentHashes(contactHashes, f)
|
||||||
|
.map { it.toFeedItem() }
|
||||||
|
.also {
|
||||||
|
Timber.tag(TAG).d(
|
||||||
|
"FILTER [ROOM+CONTACT] contacts=${f.contactIds} → ${it.size} results"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Text search ────────────────────────────────────────────
|
||||||
|
val baseRecords = paymentCache.queryAll(f.searchQuery, f).toMutableList()
|
||||||
|
|
||||||
|
// Supplement: query may match contact names not stored in payment_records
|
||||||
if (f.searchQuery.isNotBlank()) {
|
if (f.searchQuery.isNotBlank()) {
|
||||||
val contactPaymentHashes = contactRepo
|
val contactHashes = contactRepo
|
||||||
.findPaymentHashesByContactName(f.searchQuery)
|
.findPaymentHashesByContactName(f.searchQuery)
|
||||||
if (contactPaymentHashes.isNotEmpty()) {
|
if (contactHashes.isNotEmpty()) {
|
||||||
val contactPayments = paymentCache
|
val existingIds = baseRecords.map { it.checkingId }.toSet()
|
||||||
.queryByPaymentHashes(contactPaymentHashes, f)
|
baseRecords += paymentCache
|
||||||
// Merge without duplicates
|
.queryByPaymentHashes(contactHashes, f)
|
||||||
val existingIds = baseResults.map { it.checkingId }.toSet()
|
|
||||||
baseResults += contactPayments
|
|
||||||
.filter { it.checkingId !in existingIds }
|
.filter { it.checkingId !in existingIds }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Explicit contact ID filter
|
_roomResults.value = baseRecords
|
||||||
if (f.contactIds.isNotEmpty()) {
|
.map { it.toFeedItem() }
|
||||||
val contactPaymentHashes = contactRepo
|
|
||||||
.findPaymentHashesByContactIds(f.contactIds.toList())
|
|
||||||
if (contactPaymentHashes.isNotEmpty()) {
|
|
||||||
// Fetch ALL payments for these contacts, WITH the full filter applied
|
|
||||||
val contactFiltered = paymentCache.queryByPaymentHashes(
|
|
||||||
paymentHashes = contactPaymentHashes,
|
|
||||||
filter = f
|
|
||||||
)
|
|
||||||
_roomResults.value = contactFiltered
|
|
||||||
.also {
|
.also {
|
||||||
Timber.d("FILTER [ROOM+CONTACT] contacts=${f.contactIds} → ${it.size} results")
|
Timber.tag(TAG).d(
|
||||||
|
"FILTER [ROOM] q=\"${f.searchQuery}\" → ${it.size} results"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return@collect
|
|
||||||
} else {
|
|
||||||
// Selected contacts have no payments — return empty
|
|
||||||
_roomResults.value = emptyList()
|
|
||||||
return@collect
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_roomResults.value = baseResults.also {
|
// ── WebSocket payment event listener ───────────────────────────────────
|
||||||
Timber.d("FILTER [ROOM] q=\"${f.searchQuery}\" → ${it.size} results")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
WalletNotificationService.paymentEvents.collect { event ->
|
WalletNotificationService.paymentEvents.collect { event ->
|
||||||
Timber.d("PAYMENTS [WS EVENT ] incoming event ${event.paymentHash} — refreshing list")
|
Timber.tag(TAG).d("PAYMENTS [WS EVENT] ${event.paymentHash} — refreshing")
|
||||||
syncManager.refresh()
|
syncManager.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Private helper ─────────────────────────────────────────────────────────
|
||||||
|
// Converts a Room query PaymentRecord into a PaymentFeedItem for _roomResults.
|
||||||
|
// Contact links, alias, and fiat are intentionally left empty/null here —
|
||||||
|
// applyInMemoryFilters only needs paymentHash, amountSat, tag, memo, and
|
||||||
|
// timestamps. Full enrichment is provided by feedRepository.observeFeedItems()
|
||||||
|
// for all items that pass through the live feed path.
|
||||||
|
private fun PaymentRecord.toFeedItem(): PaymentFeedItem =
|
||||||
|
toPaymentFeedItem(
|
||||||
|
linkedContacts = emptyList(),
|
||||||
|
resolvedAlias = null,
|
||||||
|
fiatCurrency = fiatCurrency.value,
|
||||||
|
fiatSatsPerUnit = fiatSatsPerUnit.value
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Factory ───────────────────────────────────────────────────────────────────
|
// ── Factory ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class HistoryViewModelFactory(
|
class HistoryViewModelFactory(
|
||||||
private val repo : WalletRepository,
|
private val repo : WalletRepository,
|
||||||
private val aliasRepo : NodeAliasRepository,
|
private val aliasRepo : NodeAliasRepository,
|
||||||
private val paymentCache : PaymentCacheRepository,
|
private val paymentCache : PaymentCacheRepository,
|
||||||
private val contactRepo : ContactRepository,
|
private val contactRepo : ContactRepository,
|
||||||
|
private val feedRepository : PaymentFeedRepository,
|
||||||
private val fiatCurrency : StateFlow<String?>,
|
private val fiatCurrency : StateFlow<String?>,
|
||||||
private val fiatSatsPerUnit: StateFlow<Double?>
|
private val fiatSatsPerUnit : StateFlow<Double?>
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
require(modelClass.isAssignableFrom(HistoryViewModel::class.java)) {
|
||||||
|
"Unknown ViewModel class: ${modelClass.name}"
|
||||||
|
}
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return HistoryViewModel(
|
return HistoryViewModel(
|
||||||
repo = repo,
|
repo = repo,
|
||||||
aliasRepo = aliasRepo,
|
aliasRepo = aliasRepo,
|
||||||
paymentCache = paymentCache,
|
paymentCache = paymentCache,
|
||||||
contactRepo = contactRepo,
|
contactRepo = contactRepo,
|
||||||
|
feedRepository = feedRepository,
|
||||||
fiatCurrency = fiatCurrency,
|
fiatCurrency = fiatCurrency,
|
||||||
fiatSatsPerUnit = fiatSatsPerUnit
|
fiatSatsPerUnit = fiatSatsPerUnit
|
||||||
) as T
|
) as T
|
||||||
|
|||||||
Reference in New Issue
Block a user