From 19970659b8be7ffdefc9fe7bef8e07d4c1dffbdb Mon Sep 17 00:00:00 2001 From: rasputin Date: Sun, 14 Jun 2026 14:04:01 +0200 Subject: [PATCH] repository combine layer --- .../gudariwallet/data/FiatRateCache.kt | 14 ++++- .../data/PaymentFeedRepository.kt | 61 +++++++++++++++++++ .../gudariwallet/data/db/ContactDao.kt | 18 ++++++ .../gudariwallet/data/db/PaymentDao.kt | 6 ++ 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/bitcointxoko/gudariwallet/data/PaymentFeedRepository.kt diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/FiatRateCache.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/FiatRateCache.kt index 83d1c91..359387b 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/FiatRateCache.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/FiatRateCache.kt @@ -1,5 +1,8 @@ package com.bitcointxoko.gudariwallet.data +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import timber.log.Timber private const val TAG = "FiatRateCache" @@ -13,6 +16,12 @@ private const val TAG = "FiatRateCache" */ class FiatRateCache(private val fiatDataStore: FiatDataStore) { + private val _fiatFlow = MutableStateFlow>(Pair(null, null)) + + /** Emits the latest (currency, satsPerUnit) pair whenever [putRate] is called. */ + val fiatFlow: StateFlow> = _fiatFlow.asStateFlow() + + // ── Rate ────────────────────────────────────────────────────────────────── private var cachedRate : Double? = null @@ -51,6 +60,7 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) { cachedRate = rate rateLastFetchedAt = now cachedRateCurrency = currency + _fiatFlow.value = Pair(currency, rate) fiatDataStore.persistRate(currency, rate) Timber.d("FIAT [RATE FRESH ] 1 $currency = $rate sats — cached + persisted") } @@ -60,6 +70,7 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) { cachedRate = null rateLastFetchedAt = 0L cachedRateCurrency = "" + _fiatFlow.value = Pair(null, null) } // ── Currency list ───────────────────────────────────────────────────────── @@ -91,6 +102,5 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) { Timber.d("FIAT [CURRENCIES] cached ${currencies.size} currencies") } - val currentRate: Double? - get() = cachedRate.takeIf { cachedRateCurrency.isNotBlank() } + val currentRate: Double? get() = cachedRate.takeIf { cachedRateCurrency.isNotBlank() } } \ No newline at end of file diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/PaymentFeedRepository.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/PaymentFeedRepository.kt new file mode 100644 index 0000000..10b5a32 --- /dev/null +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/PaymentFeedRepository.kt @@ -0,0 +1,61 @@ +package com.bitcointxoko.gudariwallet.data + +import com.bitcointxoko.gudariwallet.data.db.ContactDao +import com.bitcointxoko.gudariwallet.data.db.PaymentContactSummary +import com.bitcointxoko.gudariwallet.data.db.PaymentDao +import com.bitcointxoko.gudariwallet.data.db.toDomain +import com.bitcointxoko.gudariwallet.data.model.ContactSummary +import com.bitcointxoko.gudariwallet.data.model.PaymentFeedItem +import com.bitcointxoko.gudariwallet.data.model.toPaymentFeedItem +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +/** + * Assembles the unified [PaymentFeedItem] stream by combining all four + * independent data sources: + * 1. payment_records — via [PaymentDao.observeAll] + * 2. tx_contact_links — via [ContactDao.observePaymentContactSummaries] + * 3. node_alias_cache — via [NodeAliasRepository.aliases] + * 4. fiat rate — via [FiatRateCache.fiatFlow] + * + * This repository does not own any of these sources — it only combines them. + * Filtering, searching, and sorting are the responsibility of the ViewModel. + */ +class PaymentFeedRepository( + private val paymentDao: PaymentDao, + private val contactDao: ContactDao, + private val nodeAliasRepository: NodeAliasRepository, + private val fiatRateCache: FiatRateCache +) { + + /** + * Emits a new list of [PaymentFeedItem] whenever any upstream source changes. + * Items are ordered newest-first (delegated to [PaymentDao.observeAll]). + */ + fun observeFeedItems(): Flow> = combine( + paymentDao.observeAll(), + contactDao.observePaymentContactSummaries(), + nodeAliasRepository.aliases, + fiatRateCache.fiatFlow + ) { payments, contactSummaries, aliasMap, (fiatCurrency, fiatSatsPerUnit) -> + + // Group contact summaries by paymentHash for O(1) lookup per payment + val contactsByPayment: Map> = + contactSummaries.groupBy { it.paymentHash } + + payments.map { entity -> + val linked: List = contactsByPayment[entity.paymentHash] + ?.map { ContactSummary(id = it.contactId, displayName = it.displayName) } + ?: emptyList() + + val resolvedAlias: String? = entity.pubkey?.let { aliasMap[it] } + + entity.toDomain().toPaymentFeedItem( + linkedContacts = linked, + resolvedAlias = resolvedAlias, // mapper falls back to entity.alias if null + fiatCurrency = fiatCurrency, + fiatSatsPerUnit = fiatSatsPerUnit + ) + } + } +} diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/ContactDao.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/ContactDao.kt index 5be84a2..ee9bb8e 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/ContactDao.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/ContactDao.kt @@ -12,6 +12,13 @@ data class ContactWithAddresses( val addresses: List ) +/** Flat projection used by PaymentFeedRepository to build ContactSummary lists. */ +data class PaymentContactSummary( + val paymentHash: String, + val contactId: String, + val displayName: String +) + @Dao interface ContactDao { @@ -125,6 +132,17 @@ interface ContactDao { """) fun observePaymentContactNames(): Flow> + // Returns all (paymentHash, contactId, displayName) triples — used by + // PaymentFeedRepository to build full ContactSummary lists per payment. + @Query(""" + SELECT tcl.paymentHash AS paymentHash, + c.id AS contactId, + COALESCE(c.localAlias, c.displayName, c.name) AS displayName + FROM tx_contact_links tcl + INNER JOIN contacts c ON c.id = tcl.contactId +""") + fun observePaymentContactSummaries(): Flow> + // For text search matching contact names — returns paymentHashes of matched payments @Query(""" SELECT tcl.paymentHash FROM tx_contact_links tcl diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentDao.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentDao.kt index 5822f45..8b92d79 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentDao.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/data/db/PaymentDao.kt @@ -4,6 +4,8 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import kotlinx.coroutines.flow.Flow + @Dao interface PaymentDao { @@ -56,6 +58,10 @@ interface PaymentDao { @Query("SELECT * FROM payment_records ORDER BY time DESC") suspend fun getAll(): List + /** Reactive version of [getAll] — emits a new list whenever any row changes. */ + @Query("SELECT * FROM payment_records ORDER BY COALESCE(createdAt, CAST(time AS INTEGER)) DESC") + fun observeAll(): Flow> + /** A single page, ordered newest-first. */ @Query("SELECT * FROM payment_records ORDER BY time DESC LIMIT :limit OFFSET :offset") suspend fun getPage(limit: Int, offset: Int): List