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) }