repository combine layer

This commit is contained in:
2026-06-14 14:04:01 +02:00
parent c6885f6307
commit 19970659b8
4 changed files with 97 additions and 2 deletions
@@ -1,5 +1,8 @@
package com.bitcointxoko.gudariwallet.data package com.bitcointxoko.gudariwallet.data
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import timber.log.Timber import timber.log.Timber
private const val TAG = "FiatRateCache" private const val TAG = "FiatRateCache"
@@ -13,6 +16,12 @@ private const val TAG = "FiatRateCache"
*/ */
class FiatRateCache(private val fiatDataStore: FiatDataStore) { class FiatRateCache(private val fiatDataStore: FiatDataStore) {
private val _fiatFlow = MutableStateFlow<Pair<String?, Double?>>(Pair(null, null))
/** Emits the latest (currency, satsPerUnit) pair whenever [putRate] is called. */
val fiatFlow: StateFlow<Pair<String?, Double?>> = _fiatFlow.asStateFlow()
// ── Rate ────────────────────────────────────────────────────────────────── // ── Rate ──────────────────────────────────────────────────────────────────
private var cachedRate : Double? = null private var cachedRate : Double? = null
@@ -51,6 +60,7 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) {
cachedRate = rate cachedRate = rate
rateLastFetchedAt = now rateLastFetchedAt = now
cachedRateCurrency = currency cachedRateCurrency = currency
_fiatFlow.value = Pair(currency, rate)
fiatDataStore.persistRate(currency, rate) fiatDataStore.persistRate(currency, rate)
Timber.d("FIAT [RATE FRESH ] 1 $currency = $rate sats — cached + persisted") Timber.d("FIAT [RATE FRESH ] 1 $currency = $rate sats — cached + persisted")
} }
@@ -60,6 +70,7 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) {
cachedRate = null cachedRate = null
rateLastFetchedAt = 0L rateLastFetchedAt = 0L
cachedRateCurrency = "" cachedRateCurrency = ""
_fiatFlow.value = Pair(null, null)
} }
// ── Currency list ───────────────────────────────────────────────────────── // ── Currency list ─────────────────────────────────────────────────────────
@@ -91,6 +102,5 @@ class FiatRateCache(private val fiatDataStore: FiatDataStore) {
Timber.d("FIAT [CURRENCIES] cached ${currencies.size} currencies") Timber.d("FIAT [CURRENCIES] cached ${currencies.size} currencies")
} }
val currentRate: Double? val currentRate: Double? get() = cachedRate.takeIf { cachedRateCurrency.isNotBlank() }
get() = cachedRate.takeIf { cachedRateCurrency.isNotBlank() }
} }
@@ -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<List<PaymentFeedItem>> = 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<String, List<PaymentContactSummary>> =
contactSummaries.groupBy { it.paymentHash }
payments.map { entity ->
val linked: List<ContactSummary> = 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
)
}
}
}
@@ -12,6 +12,13 @@ data class ContactWithAddresses(
val addresses: List<PaymentAddressEntity> val addresses: List<PaymentAddressEntity>
) )
/** Flat projection used by PaymentFeedRepository to build ContactSummary lists. */
data class PaymentContactSummary(
val paymentHash: String,
val contactId: String,
val displayName: String
)
@Dao @Dao
interface ContactDao { interface ContactDao {
@@ -125,6 +132,17 @@ interface ContactDao {
""") """)
fun observePaymentContactNames(): Flow<List<PaymentContactName>> fun observePaymentContactNames(): Flow<List<PaymentContactName>>
// 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<List<PaymentContactSummary>>
// For text search matching contact names — returns paymentHashes of matched payments // For text search matching contact names — returns paymentHashes of matched payments
@Query(""" @Query("""
SELECT tcl.paymentHash FROM tx_contact_links tcl SELECT tcl.paymentHash FROM tx_contact_links tcl
@@ -4,6 +4,8 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface PaymentDao { interface PaymentDao {
@@ -56,6 +58,10 @@ interface PaymentDao {
@Query("SELECT * FROM payment_records ORDER BY time DESC") @Query("SELECT * FROM payment_records ORDER BY time DESC")
suspend fun getAll(): List<PaymentRecordEntity> suspend fun getAll(): List<PaymentRecordEntity>
/** 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<List<PaymentRecordEntity>>
/** A single page, ordered newest-first. */ /** A single page, ordered newest-first. */
@Query("SELECT * FROM payment_records ORDER BY time DESC LIMIT :limit OFFSET :offset") @Query("SELECT * FROM payment_records ORDER BY time DESC LIMIT :limit OFFSET :offset")
suspend fun getPage(limit: Int, offset: Int): List<PaymentRecordEntity> suspend fun getPage(limit: Int, offset: Int): List<PaymentRecordEntity>