diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml
index 8dd4cc9a9..92787dc3e 100644
--- a/apps/flipcash/core/src/main/res/values/strings.xml
+++ b/apps/flipcash/core/src/main/res/values/strings.xml
@@ -797,4 +797,8 @@
Are You Sure?
Anyone you sent the link to won\'t be able to collect the cash
+ %1$s of %2$s
+ You sent %1$s
+ You received %1$s
+
\ No newline at end of file
diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt
index 2c705ebbb..d3f3a4a73 100644
--- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt
+++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/SendFlowViewModel.kt
@@ -1,4 +1,5 @@
package com.flipcash.app.directsend.internal
+
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.snapshotFlow
@@ -99,7 +100,8 @@ internal class SendFlowViewModel @Inject constructor(
val phoneNumberSendEnabled = phoneNumberSendFlag ||
userState.flags?.enablePhoneNumberSend == true
val hasContacts = contactState.contacts.isNotEmpty()
- val needsContacts = phoneNumberSendEnabled && !hasContacts && !contactState.hasEverSynced
+ val needsContacts =
+ phoneNumberSendEnabled && !hasContacts && !contactState.hasEverSynced
val steps = buildList {
if (!hasLinkedPhone) add(SendStep.PhoneGate)
@@ -263,7 +265,8 @@ internal class SendFlowViewModel @Inject constructor(
val contact = if (deviceContact != null) {
if (searchString.isNotBlank() &&
!deviceContact.displayName.contains(searchString, ignoreCase = true) &&
- !deviceContact.e164.contains(searchString, ignoreCase = true)) {
+ !deviceContact.e164.contains(searchString, ignoreCase = true)
+ ) {
return@mapNotNull null
}
recentsE164s += deviceContact.e164
@@ -287,7 +290,8 @@ internal class SendFlowViewModel @Inject constructor(
displayNumber = formattedPhone,
)
if (searchString.isNotBlank() &&
- !unknown.displayName.contains(searchString, ignoreCase = true)) {
+ !unknown.displayName.contains(searchString, ignoreCase = true)
+ ) {
return@mapNotNull null
}
if (unknown.e164.isNotEmpty()) {
@@ -346,11 +350,17 @@ internal class SendFlowViewModel @Inject constructor(
is MessageContent.Text -> content.text.takeIf { it.isNotEmpty() }
is MessageContent.Cash -> {
val formatted = content.amount.formatted()
- val name = content.tokenName.ifBlank {
- tokensByMint[content.mint]?.name.orEmpty()
+ val name = content.tokenName.ifBlank { tokensByMint[content.mint]?.name.orEmpty() }
+ val label = if (name.isNotBlank()) {
+ resources.getString(R.string.label_chat_preview_cash_suffix, formatted, name)
+ } else {
+ formatted
+ }
+ if (sentBySelf) {
+ resources.getString(R.string.label_chat_preview_sentCash, label)
+ } else {
+ resources.getString(R.string.label_chat_preview_receivedCash, label)
}
- val label = if (name.isNotBlank()) "$formatted of $name" else formatted
- if (sentBySelf) "You sent $label" else "You received $label"
}
// TODO:
@@ -368,9 +378,11 @@ internal class SendFlowViewModel @Inject constructor(
is Event.StepsUpdated -> { state ->
state.copy(steps = event.steps, isPickerMode = event.isPickerMode)
}
+
is Event.OnStepChanged -> { state ->
state.copy(currentStep = event.step)
}
+
is Event.ContactsGranted -> { state -> state }
is Event.ContactsPicked -> { state -> state }
is Event.ContactSyncStateUpdated -> { state ->
@@ -382,6 +394,7 @@ internal class SendFlowViewModel @Inject constructor(
)
)
}
+
is Event.ContactRemoved -> { state -> state }
is Event.ContactSyncComplete -> { state -> state }
is Event.OnItemsPopulated -> { state -> state.copy(listItems = event.items) }
diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/EventStreamDelegate.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/EventStreamDelegate.kt
index ff03e2331..f50aee5a1 100644
--- a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/EventStreamDelegate.kt
+++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/internal/delegates/EventStreamDelegate.kt
@@ -166,7 +166,13 @@ class EventStreamDelegate @Inject constructor(
val latest = delta.messages.maxByOrNull { it.messageId }
latest?.let { msg ->
metadataDataSource.updateLastMessageId(chatId, msg.messageId)
- metadataDataSource.updateLastActivity(chatId, msg.timestamp.toEpochMilliseconds())
+ // Only advance lastActivity — a partial page from a
+ // delta sync must not regress it to an older timestamp.
+ val existing = metadataDataSource.getLastActivity(chatId)
+ val incoming = msg.timestamp.toEpochMilliseconds()
+ if (existing == null || incoming > existing) {
+ metadataDataSource.updateLastActivity(chatId, incoming)
+ }
}
}
if (delta.latestSequence > afterSequence) {
diff --git a/apps/flipcash/shared/onramp/coinbase/src/test/kotlin/com/flipcash/app/onramp/CoinbaseOnRampControllerTest.kt b/apps/flipcash/shared/onramp/coinbase/src/test/kotlin/com/flipcash/app/onramp/CoinbaseOnRampControllerTest.kt
index 747e0acf7..966e91d3b 100644
--- a/apps/flipcash/shared/onramp/coinbase/src/test/kotlin/com/flipcash/app/onramp/CoinbaseOnRampControllerTest.kt
+++ b/apps/flipcash/shared/onramp/coinbase/src/test/kotlin/com/flipcash/app/onramp/CoinbaseOnRampControllerTest.kt
@@ -3,6 +3,7 @@ package com.flipcash.app.onramp
import com.coinbase.onramp.api.CoinbaseApi
import com.coinbase.onramp.data.OnRampApiConfig
import com.flipcash.app.featureflags.FeatureFlagController
+import com.flipcash.app.userflags.UserFlagsCoordinator
import com.flipcash.services.models.UserProfile
import com.flipcash.services.user.UserManager
import com.getcode.opencode.exchange.Exchange
@@ -47,6 +48,7 @@ class CoinbaseOnRampControllerTest {
private val featureFlags = mockk(relaxed = true)
private val googlePayReadiness = mockk(relaxed = true)
private val webViewChannelDetector = mockk(relaxed = true)
+ private val userFlags = mockk(relaxed = true)
private val onRampApiEndpoint = OnRampApiConfig(
scheme = "https",
@@ -86,6 +88,7 @@ class CoinbaseOnRampControllerTest {
transactionController = mockk(relaxed = true),
googlePayReadiness = googlePayReadiness,
webViewChannelDetector = webViewChannelDetector,
+ userFlags = userFlags,
)
}
diff --git a/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/ChatMetadataDao.kt b/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/ChatMetadataDao.kt
index 38b3ff909..fb1512e30 100644
--- a/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/ChatMetadataDao.kt
+++ b/apps/flipcash/shared/persistence/db/src/main/kotlin/com/flipcash/app/persistence/dao/ChatMetadataDao.kt
@@ -22,6 +22,9 @@ interface ChatMetadataDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsert(entities: List)
+ @Query("SELECT last_activity_epoch_ms FROM chat_metadata WHERE chat_id_hex = :chatIdHex")
+ suspend fun getLastActivity(chatIdHex: String): Long?
+
@Query("UPDATE chat_metadata SET last_activity_epoch_ms = :epochMs WHERE chat_id_hex = :chatIdHex")
suspend fun updateLastActivity(chatIdHex: String, epochMs: Long)
diff --git a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/ChatMetadataDataSource.kt b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/ChatMetadataDataSource.kt
index a8f289abd..5f24110d7 100644
--- a/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/ChatMetadataDataSource.kt
+++ b/apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/ChatMetadataDataSource.kt
@@ -32,6 +32,9 @@ class ChatMetadataDataSource @Inject constructor(
db?.chatMetadataDao()?.upsert(metadatas.map { mapper.toEntity(it) })
}
+ suspend fun getLastActivity(chatId: ChatId): Long? =
+ db?.chatMetadataDao()?.getLastActivity(mapper.chatIdHex(chatId))
+
suspend fun updateLastActivity(chatId: ChatId, epochMs: Long) {
db?.chatMetadataDao()?.updateLastActivity(mapper.chatIdHex(chatId), epochMs)
}