repository combine layer
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user