Unify payment item #1
@@ -8,10 +8,13 @@ import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
||||
import com.bitcointxoko.gudariwallet.data.ContactRepository
|
||||
import com.bitcointxoko.gudariwallet.data.NodeAliasRepository
|
||||
import com.bitcointxoko.gudariwallet.data.PaymentCacheRepository
|
||||
import com.bitcointxoko.gudariwallet.data.PaymentFeedRepository
|
||||
import com.bitcointxoko.gudariwallet.data.WalletRepository
|
||||
import com.bitcointxoko.gudariwallet.data.db.ContactEntity
|
||||
import com.bitcointxoko.gudariwallet.data.db.ContactWithAddresses
|
||||
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.ui.DetailState
|
||||
import com.bitcointxoko.gudariwallet.ui.HistoryState
|
||||
@@ -31,24 +34,31 @@ private const val TAG = "HistoryViewModel"
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
class HistoryViewModel(
|
||||
private val repo : WalletRepository,
|
||||
private val aliasRepo : NodeAliasRepository,
|
||||
private val paymentCache : PaymentCacheRepository,
|
||||
private val contactRepo : ContactRepository,
|
||||
val fiatCurrency : StateFlow<String?>,
|
||||
val fiatSatsPerUnit : StateFlow<Double?>
|
||||
private val repo : WalletRepository,
|
||||
private val aliasRepo : NodeAliasRepository,
|
||||
private val paymentCache : PaymentCacheRepository,
|
||||
private val contactRepo : ContactRepository,
|
||||
private val feedRepository : PaymentFeedRepository,
|
||||
val fiatCurrency : StateFlow<String?>,
|
||||
val fiatSatsPerUnit : StateFlow<Double?>
|
||||
) : 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(
|
||||
repo = repo,
|
||||
paymentCache = paymentCache,
|
||||
scope = viewModelScope,
|
||||
repo = repo,
|
||||
paymentCache = paymentCache,
|
||||
scope = viewModelScope,
|
||||
onNewPayments = { payments -> enricher.enrich(payments) }
|
||||
)
|
||||
|
||||
// ── Optimistic insert (called after a successful send) ─────────────────────
|
||||
fun primePayment(record: PaymentRecord) {
|
||||
syncManager.primePayment(record)
|
||||
}
|
||||
fun primePayment(record: PaymentRecord) { 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(
|
||||
repo = repo,
|
||||
aliasRepo = aliasRepo,
|
||||
@@ -58,36 +68,40 @@ class HistoryViewModel(
|
||||
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
|
||||
|
||||
fun refresh() = syncManager.refresh()
|
||||
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(
|
||||
repo = repo,
|
||||
paymentCache = paymentCache,
|
||||
scope = viewModelScope
|
||||
)
|
||||
val detailState: StateFlow<DetailState> = detailDelegate.state
|
||||
fun loadDetail(checkingId: String, record: PaymentRecord? = null) =
|
||||
detailDelegate.load(checkingId, record)
|
||||
fun loadDetail(paymentHash: String, record: PaymentRecord? = null) =
|
||||
detailDelegate.load(paymentHash, record)
|
||||
fun clearDetail() = detailDelegate.clear()
|
||||
|
||||
// ── Filter state ──────────────────────────────────────────────────────────
|
||||
// ── Filter state ───────────────────────────────────────────────────────────
|
||||
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) }
|
||||
}
|
||||
fun toggleStatusFilter(status: StatusFilter) {
|
||||
fun setSearchQuery(q: String) { _filter.update { it.copy(searchQuery = q.trim()) } }
|
||||
fun clearSearch() { _filter.update { it.copy(searchQuery = "") } }
|
||||
fun setDirectionFilter(d: DirectionFilter) { _filter.update { it.copy(direction = d) } }
|
||||
fun toggleStatusFilter(s: StatusFilter) {
|
||||
_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)
|
||||
}
|
||||
}
|
||||
@@ -104,55 +118,55 @@ class HistoryViewModel(
|
||||
_filter.update { it.copy(minCreatedAt = min, maxCreatedAt = max, datePreset = preset) }
|
||||
}
|
||||
fun applyDatePreset(preset: DatePreset) {
|
||||
val (min, max) = preset.toEpochSecondRange() // ← delegate to PaymentFilter.kt
|
||||
val (min, max) = preset.toEpochSecondRange()
|
||||
setDateFilter(min, max, preset)
|
||||
}
|
||||
fun clearDateFilter() {
|
||||
_filter.update { it.copy(minCreatedAt = null, maxCreatedAt = null, datePreset = null) }
|
||||
}
|
||||
|
||||
fun findPayment(checkingId: String): PaymentRecord? {
|
||||
val fromFiltered = (filteredState.value as? HistoryState.Success)
|
||||
?.payments?.firstOrNull { it.checkingId == checkingId }
|
||||
if (fromFiltered != null) return fromFiltered
|
||||
return (state.value as? HistoryState.Success)
|
||||
?.payments?.firstOrNull { it.checkingId == checkingId }
|
||||
fun toggleContactFilter(contactId: String) {
|
||||
_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()) } }
|
||||
|
||||
// ── 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. */
|
||||
val availableTypes: StateFlow<List<String>> = syncManager.state
|
||||
.map { s ->
|
||||
if (s !is HistoryState.Success) emptyList()
|
||||
else s.payments.mapNotNull { it.extra?.tag }.distinct().sorted()
|
||||
}
|
||||
// ── Available type filter chips ────────────────────────────────────────────
|
||||
val availableTypes: StateFlow<List<String>> = feedItems
|
||||
.map { items -> items.mapNotNull { it.tag }.distinct().sorted() }
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val filteredState = combine(
|
||||
syncManager.state,
|
||||
_filter, _roomResults,
|
||||
contactRepo.observeAllPaymentContactNames()
|
||||
) { s, f, roomResults, contactNames ->
|
||||
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)
|
||||
// ── Find a single item by paymentHash ─────────────────────────────────────
|
||||
// paymentHash supersedes checkingId as the stable unique identifier.
|
||||
// Used by PaymentDetailScreen to retrieve a feed item without a DB round-trip.
|
||||
fun findPayment(paymentHash: String): PaymentFeedItem? =
|
||||
feedItems.value.firstOrNull { it.paymentHash == paymentHash }
|
||||
|
||||
// ── In-memory filter logic ────────────────────────────────────────────────
|
||||
private fun List<PaymentRecord>.applyInMemoryFilters(f: PaymentFilter) =
|
||||
// ── In-memory filter logic ─────────────────────────────────────────────────
|
||||
// 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 }
|
||||
.let { list ->
|
||||
when (f.direction) {
|
||||
@@ -161,22 +175,53 @@ class HistoryViewModel(
|
||||
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 ->
|
||||
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 ─────────────────────────────────────────────────────
|
||||
// Maps checkingId → display name for any payment that has a linked contact.
|
||||
// Rebuilt whenever the underlying tx_contact_links or contacts tables change.
|
||||
val contactNames: StateFlow<Map<String, String>> =
|
||||
contactRepo.observeAllPaymentContactNames()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = emptyMap()
|
||||
)
|
||||
// Maps contactId → display name (for filter chips)
|
||||
// ── Contact display names (for filter chips) ───────────────────────────────
|
||||
// Used to render contact names in active filter chips on the UI.
|
||||
// Payment row contact names are embedded in PaymentFeedItem.contactName.
|
||||
val contactDisplayNames: StateFlow<Map<String, String>> =
|
||||
contactRepo.observeAllContacts()
|
||||
.map { contacts ->
|
||||
@@ -188,40 +233,36 @@ class HistoryViewModel(
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyMap()
|
||||
)
|
||||
|
||||
// ── Contact assignment ───────────────────────────────────────────────────
|
||||
|
||||
// ── Per-payment contact queries ────────────────────────────────────────────
|
||||
fun contactsForPayment(paymentHash: String): StateFlow<List<ContactEntity>> =
|
||||
contactRepo.observeContactsForPayment(paymentHash)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyList()
|
||||
)
|
||||
|
||||
fun allContacts(): StateFlow<List<ContactWithAddresses>> =
|
||||
contactRepo.observeAllContacts()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = emptyList()
|
||||
)
|
||||
|
||||
// ── Contact assignment ─────────────────────────────────────────────────────
|
||||
fun assignContact(paymentHash: String, contactId: String) {
|
||||
Timber.d("CONTACT [ASSIGN ] paymentHash=$paymentHash contactId=$contactId")
|
||||
viewModelScope.launch {
|
||||
contactRepo.linkPaymentToContact(paymentHash, contactId)
|
||||
}
|
||||
Timber.tag(TAG).d("CONTACT [ASSIGN ] paymentHash=$paymentHash contactId=$contactId")
|
||||
viewModelScope.launch { contactRepo.linkPaymentToContact(paymentHash, contactId) }
|
||||
}
|
||||
|
||||
fun unassignContact(paymentHash: String, contactId: String) {
|
||||
viewModelScope.launch {
|
||||
contactRepo.unlinkPaymentFromContact(paymentHash, contactId)
|
||||
}
|
||||
viewModelScope.launch { contactRepo.unlinkPaymentFromContact(paymentHash, contactId) }
|
||||
}
|
||||
|
||||
fun saveContactAndAssign(
|
||||
@@ -230,36 +271,28 @@ class HistoryViewModel(
|
||||
addressType : PaymentAddressType?,
|
||||
address : String?
|
||||
) {
|
||||
Timber.d("CONTACT [CREATE+ASSIGN] paymentHash=$paymentHash name=$name")
|
||||
Timber.tag(TAG).d("CONTACT [CREATE+ASSIGN] paymentHash=$paymentHash name=$name")
|
||||
viewModelScope.launch {
|
||||
val lnAddress = address?.takeIf { addressType == PaymentAddressType.LIGHTNING_ADDRESS }
|
||||
val lnUrl = address?.takeIf { addressType == PaymentAddressType.LNURL }
|
||||
val contact = contactRepo.createContact(
|
||||
val contact = contactRepo.createContact(
|
||||
localAlias = name,
|
||||
lnAddress = lnAddress,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleContactFilter(contactId: String) {
|
||||
_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()
|
||||
|
||||
// ── 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 {
|
||||
combine(
|
||||
_filter.map { it.searchQuery }.debounce(300.milliseconds),
|
||||
@@ -267,82 +300,100 @@ class HistoryViewModel(
|
||||
) { _, f -> f }
|
||||
.collect { f ->
|
||||
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
|
||||
} else {
|
||||
// ── Room query path ───────────────────────────────────
|
||||
val baseResults = paymentCache.queryAll(f.searchQuery, f)
|
||||
.toMutableList()
|
||||
return@collect
|
||||
}
|
||||
|
||||
// ── Supplemental contact search ───────────────────────
|
||||
// 1. Text query may match contact names not in payment_records
|
||||
if (f.searchQuery.isNotBlank()) {
|
||||
val contactPaymentHashes = contactRepo
|
||||
.findPaymentHashesByContactName(f.searchQuery)
|
||||
if (contactPaymentHashes.isNotEmpty()) {
|
||||
val contactPayments = paymentCache
|
||||
.queryByPaymentHashes(contactPaymentHashes, f)
|
||||
// Merge without duplicates
|
||||
val existingIds = baseResults.map { it.checkingId }.toSet()
|
||||
baseResults += contactPayments
|
||||
.filter { it.checkingId !in existingIds }
|
||||
}
|
||||
// ── Explicit contact-ID filter (highest priority) ──────────
|
||||
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
|
||||
}
|
||||
|
||||
// 2. Explicit contact ID filter
|
||||
if (f.contactIds.isNotEmpty()) {
|
||||
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 {
|
||||
Timber.d("FILTER [ROOM+CONTACT] contacts=${f.contactIds} → ${it.size} results")
|
||||
}
|
||||
return@collect
|
||||
} else {
|
||||
// Selected contacts have no payments — return empty
|
||||
_roomResults.value = emptyList()
|
||||
return@collect
|
||||
}
|
||||
}
|
||||
// ── Text search ────────────────────────────────────────────
|
||||
val baseRecords = paymentCache.queryAll(f.searchQuery, f).toMutableList()
|
||||
|
||||
_roomResults.value = baseResults.also {
|
||||
Timber.d("FILTER [ROOM] q=\"${f.searchQuery}\" → ${it.size} results")
|
||||
// Supplement: query may match contact names not stored in payment_records
|
||||
if (f.searchQuery.isNotBlank()) {
|
||||
val contactHashes = contactRepo
|
||||
.findPaymentHashesByContactName(f.searchQuery)
|
||||
if (contactHashes.isNotEmpty()) {
|
||||
val existingIds = baseRecords.map { it.checkingId }.toSet()
|
||||
baseRecords += paymentCache
|
||||
.queryByPaymentHashes(contactHashes, f)
|
||||
.filter { it.checkingId !in existingIds }
|
||||
}
|
||||
}
|
||||
|
||||
_roomResults.value = baseRecords
|
||||
.map { it.toFeedItem() }
|
||||
.also {
|
||||
Timber.tag(TAG).d(
|
||||
"FILTER [ROOM] q=\"${f.searchQuery}\" → ${it.size} results"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── WebSocket payment event listener ───────────────────────────────────
|
||||
viewModelScope.launch {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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(
|
||||
private val repo : WalletRepository,
|
||||
private val aliasRepo : NodeAliasRepository,
|
||||
private val paymentCache : PaymentCacheRepository,
|
||||
private val repo : WalletRepository,
|
||||
private val aliasRepo : NodeAliasRepository,
|
||||
private val paymentCache : PaymentCacheRepository,
|
||||
private val contactRepo : ContactRepository,
|
||||
private val fiatCurrency : StateFlow<String?>,
|
||||
private val fiatSatsPerUnit: StateFlow<Double?>
|
||||
private val feedRepository : PaymentFeedRepository,
|
||||
private val fiatCurrency : StateFlow<String?>,
|
||||
private val fiatSatsPerUnit : StateFlow<Double?>
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
require(modelClass.isAssignableFrom(HistoryViewModel::class.java)) {
|
||||
"Unknown ViewModel class: ${modelClass.name}"
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return HistoryViewModel(
|
||||
repo = repo,
|
||||
aliasRepo = aliasRepo,
|
||||
paymentCache = paymentCache,
|
||||
contactRepo = contactRepo,
|
||||
feedRepository = feedRepository,
|
||||
fiatCurrency = fiatCurrency,
|
||||
fiatSatsPerUnit = fiatSatsPerUnit
|
||||
) as T
|
||||
|
||||
Reference in New Issue
Block a user