From 3df48427fb65d90a125c05e12f30808826687903 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 17 Jun 2026 18:32:24 -0400 Subject: [PATCH] chore: improve initial chat load speeds wrt contact resolving Signed-off-by: Brandon McAnsh --- .../flipcash/app/core/chat/ChatIdentifier.kt | 8 +- .../app/core/contacts/DeviceContact.kt | 32 ++++ .../directsend/internal/ContactListItem.kt | 2 +- .../directsend/internal/SendFlowViewModel.kt | 18 +-- .../internal/screens/ContactListScreen.kt | 10 +- .../app/messenger/internal/ChatViewModel.kt | 142 ++++++++++-------- .../internal/screens/MessengerScreen.kt | 10 +- .../components/ContactInfoContainer.kt | 2 +- .../flipcash/shared/chat/ChatCoordinator.kt | 2 +- .../app/contacts/ContactCoordinator.kt | 5 +- .../contacts/device/DeviceContactReader.kt | 25 +-- .../device/ScopeAwareContactReader.kt | 1 + .../internal/FullAccessContactReader.kt | 2 +- .../device/internal/PickerContactReader.kt | 2 +- .../flipcash/app/contacts/ui/ContactAvatar.kt | 3 +- .../scenes/ModalBottomSheetSceneStrategy.kt | 12 +- 16 files changed, 152 insertions(+), 124 deletions(-) create mode 100644 apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/contacts/DeviceContact.kt diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatIdentifier.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatIdentifier.kt index 8927235e2..362628fca 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatIdentifier.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/chat/ChatIdentifier.kt @@ -1,6 +1,7 @@ package com.flipcash.app.core.chat import android.os.Parcelable +import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.services.models.chat.ChatId import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -18,7 +19,10 @@ sealed interface ChatIdentifier : Parcelable { @Serializable @Parcelize - data class ByContact(val e164: String, val displayName: String, val chatId: ChatId? = null) : ChatIdentifier { - override val key: String get() = e164 +` data class ByContact( + val contact: DeviceContact, + val chatId: ChatId? = null + ) : ChatIdentifier { + override val key: String get() = contact.e164 } } \ No newline at end of file diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/contacts/DeviceContact.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/contacts/DeviceContact.kt new file mode 100644 index 000000000..fa7f12aaf --- /dev/null +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/contacts/DeviceContact.kt @@ -0,0 +1,32 @@ +package com.flipcash.app.core.contacts + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable + +@Serializable +@Parcelize +data class DeviceContact( + val e164: String, + val androidContactId: Long = -1L, + val displayName: String, + val photoUri: String?, + val displayNumber: String = "", +): Parcelable { + var isUnknown: Boolean = false + private set + + companion object { + fun unknownContact( + e164: String = "", + displayName: String? = null, + displayNumber: String? = null, + ) = DeviceContact( + e164 = e164, + androidContactId = -1, + displayName = displayName ?: e164, + displayNumber = displayNumber ?: e164, + photoUri = null, + ).apply { isUnknown = true } + } +} \ No newline at end of file diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListItem.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListItem.kt index 0b3de51f0..619888efa 100644 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListItem.kt +++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/ContactListItem.kt @@ -1,6 +1,6 @@ package com.flipcash.app.directsend.internal -import com.flipcash.app.contacts.device.DeviceContact +import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.services.models.chat.ChatId import kotlin.time.Instant 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 d2754eace..6f9157546 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,34 +1,29 @@ package com.flipcash.app.directsend.internal - import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.viewModelScope import com.flipcash.app.contacts.ContactCoordinator import com.flipcash.app.contacts.ContactCoordinator.ContactState -import com.flipcash.app.contacts.device.DeviceContact import com.flipcash.app.contacts.device.PickedContactData -import com.flipcash.app.core.AppRoute import com.flipcash.app.core.chat.ChatIdentifier +import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.app.core.send.SendStep import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.FeatureFlagController -import com.flipcash.app.payments.PurchaseMethodController -import com.flipcash.app.phone.PhoneUtils import com.flipcash.app.permissions.PickedContact +import com.flipcash.app.phone.PhoneUtils import com.flipcash.app.tokens.TokenCoordinator import com.flipcash.features.directsend.R -import com.flipcash.services.models.chat.ChatId import com.flipcash.services.models.chat.ChatType import com.flipcash.services.models.chat.MessageContent import com.flipcash.services.user.UserManager import com.flipcash.shared.chat.ChatCoordinator import com.flipcash.shared.chat.ChatSummary -import com.getcode.opencode.model.financial.Token -import com.getcode.solana.keys.Mint -import com.getcode.manager.BottomBarAction import com.getcode.manager.BottomBarManager import com.getcode.opencode.model.core.ID +import com.getcode.opencode.model.financial.Token +import com.getcode.solana.keys.Mint import com.getcode.util.resources.ResourceHelper import com.getcode.view.BaseViewModel import com.getcode.view.LoadingSuccessState @@ -175,7 +170,10 @@ internal class SendFlowViewModel @Inject constructor( val (contact, isOnFlipcash) = row if (isOnFlipcash) { val identifier = if (contact.e164.isNotEmpty()) { - ChatIdentifier.ByContact(contact.e164, contact.displayName, row.chatId) + ChatIdentifier.ByContact( + contact = contact, + chatId = row.chatId + ) } else { ChatIdentifier.ByChatId(row.chatId!!) } diff --git a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt index 81ff10309..c34af70ab 100644 --- a/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt +++ b/apps/flipcash/features/direct-send/src/main/kotlin/com/flipcash/app/directsend/internal/screens/ContactListScreen.kt @@ -5,13 +5,13 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -24,8 +24,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.GroupAdd -import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Add import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -41,14 +40,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewWrapper -import androidx.compose.foundation.border -import androidx.compose.material.icons.filled.Add import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.flipcash.app.contacts.device.DeviceContact import com.flipcash.app.contacts.ui.ContactAvatar import com.flipcash.app.core.AppRoute +import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.app.core.send.SendResult import com.flipcash.app.core.send.SendStep import com.flipcash.app.directsend.internal.ContactListItem @@ -65,7 +62,6 @@ import com.getcode.theme.White10 import com.getcode.theme.extraSmall import com.getcode.ui.components.AppBarDefaults import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.components.CircularIconButton import com.getcode.ui.components.SearchInput import com.getcode.ui.core.verticalScrollStateGradient import com.getcode.ui.theme.CodeScaffold diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt index e1b1a910a..da35256f4 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/ChatViewModel.kt @@ -2,16 +2,16 @@ package com.flipcash.app.messenger.internal import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.clearText +import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.flatMap import androidx.paging.insertSeparators import com.flipcash.app.contacts.ContactCoordinator -import com.flipcash.app.contacts.device.DeviceContact -import androidx.compose.runtime.snapshotFlow import com.flipcash.app.core.AppRoute import com.flipcash.app.core.chat.ChatIdentifier +import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.app.core.extensions.onResult import com.flipcash.app.core.ui.ConfirmationStyle import com.flipcash.app.featureflags.FeatureFlag @@ -26,7 +26,6 @@ import com.flipcash.services.models.buildDmPaymentMetadata import com.flipcash.services.models.chat.ChatId import com.flipcash.services.models.chat.DeliveryStatus import com.flipcash.services.models.chat.MessageContent -import com.flipcash.services.models.chat.MessagePointer import com.flipcash.services.models.chat.TypingState import com.flipcash.services.user.UserManager import com.flipcash.shared.amountentry.AmountEntryDelegate @@ -210,56 +209,48 @@ internal class ChatViewModel @Inject constructor( } }.cachedIn(viewModelScope) - private val maxAmountFlow = combine( - transactionController.limits, - tokenCoordinator.observeSelectedTokenMint() - .flatMapLatest { mint -> tokenCoordinator.balanceForToken(mint) }, - exchange.observePreferredRate(), - ) { limits, balance, rate -> - val balanceInLocal = balance.convertingTo(rate) - val sendLimit = limits?.sendLimitFor(rate.currency) ?: SendLimit.Zero - Fiat(min(sendLimit.nextTransaction, balanceInLocal.toDouble()), rate.currency) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) - - val amountDelegate = AmountEntryDelegate( - exchange = exchange, - scope = viewModelScope, - style = AmountEntryStyle( - actionLabel = resources.getString(R.string.action_swipeToSend), - actionStyle = ConfirmationStyle.Slide, - infoHint = { resources.getString(R.string.subtitle_sendHint, it) }, - overMaxHint = { resources.getString(R.string.subtitle_sendHintLimitExceeded, it) }, - ), - loadingState = stateFlow.map { it.sendProgress } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), LoadingSuccessState()), - maxAmount = maxAmountFlow, - ) - - init { - // Token observation - tokenCoordinator.observeSelectedTokenMint() - .flatMapLatest { mint -> - tokenCoordinator.tokenBalances.map { tokens -> - tokens.find { it.token.address == mint } - } - } - .filterNotNull() - .onEach { tokenWithBalance -> - dispatchEvent(Event.TokenUpdated(tokenWithBalance.token)) - }.launchIn(viewModelScope) + private val maxAmountFlow by lazy { + combine( + transactionController.limits, + tokenCoordinator.observeSelectedTokenMint() + .flatMapLatest { mint -> tokenCoordinator.balanceForToken(mint) }, + exchange.observePreferredRate(), + ) { limits, balance, rate -> + val balanceInLocal = balance.convertingTo(rate) + val sendLimit = limits?.sendLimitFor(rate.currency) ?: SendLimit.Zero + Fiat(min(sendLimit.nextTransaction, balanceInLocal.toDouble()), rate.currency) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) + } - exchange.observePreferredRate() - .onEach { rate -> - val currency = exchange.getCurrency(rate.currency.name) - if (currency != null) { - amountDelegate.onCurrencyChanged(currency) - } - }.launchIn(viewModelScope) + val amountDelegate by lazy { + AmountEntryDelegate( + exchange = exchange, + scope = viewModelScope, + style = AmountEntryStyle( + actionLabel = resources.getString(R.string.action_swipeToSend), + actionStyle = ConfirmationStyle.Slide, + infoHint = { resources.getString(R.string.subtitle_sendHint, it) }, + overMaxHint = { resources.getString(R.string.subtitle_sendHintLimitExceeded, it) }, + ), + loadingState = stateFlow.map { it.sendProgress } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), LoadingSuccessState()), + maxAmount = maxAmountFlow, + ) + } - transactionController.limits - .onEach { dispatchEvent(Event.LimitsChanged(it)) } - .launchIn(viewModelScope) + init { + // Essential — needed immediately for chat display + initChatHandlers() + + viewModelScope.launch { + // Yield to let the first frame render before setting up remaining collectors + initTokenAndExchangeObservers() + initTypingHandlers() + initSendHandlers() + } + } + private fun initChatHandlers() { // Unified chat open handler — resolves chatId and contact from the identifier eventFlow .filterIsInstance() @@ -269,9 +260,7 @@ internal class ChatViewModel @Inject constructor( // 1. Resolve chatId val chatId = when (identifier) { is ChatIdentifier.ByContact -> identifier.chatId - ?: chatCoordinator.getChatId( - DeviceContact.unknownContact(identifier.e164) - ).getOrNull() + ?: chatCoordinator.getChatId(identifier.contact).getOrNull() is ChatIdentifier.ByChatId -> identifier.chatId } @@ -285,13 +274,7 @@ internal class ChatViewModel @Inject constructor( // 2. Resolve contact when (identifier) { is ChatIdentifier.ByContact -> { - val contact = contactCoordinator.lookupContact(identifier.e164).getOrElse { - DeviceContact.unknownContact( - e164 = identifier.e164, - displayName = identifier.displayName.takeIf { it.isNotBlank() }, - ) - } - dispatchEvent(Event.OnContactFound(contact)) + dispatchEvent(Event.OnContactFound(identifier.contact)) } is ChatIdentifier.ByChatId -> { val contact = contactCoordinator.lookupContactByDmChatId( @@ -328,11 +311,39 @@ internal class ChatViewModel @Inject constructor( chatCoordinator.advanceReadPointer(chatId, event.messageId) } .launchIn(viewModelScope) + } + + private fun initTokenAndExchangeObservers() { + // Token observation + tokenCoordinator.observeSelectedTokenMint() + .flatMapLatest { mint -> + tokenCoordinator.tokenBalances.map { tokens -> + tokens.find { it.token.address == mint } + } + } + .filterNotNull() + .onEach { tokenWithBalance -> + dispatchEvent(Event.TokenUpdated(tokenWithBalance.token)) + }.launchIn(viewModelScope) + + exchange.observePreferredRate() + .onEach { rate -> + val currency = exchange.getCurrency(rate.currency.name) + if (currency != null) { + amountDelegate.onCurrencyChanged(currency) + } + }.launchIn(viewModelScope) + + transactionController.limits + .onEach { dispatchEvent(Event.LimitsChanged(it)) } + .launchIn(viewModelScope) + } + @OptIn(ExperimentalCoroutinesApi::class) + private fun initTypingHandlers() { // Dispatch typing notifications based on text changes. // transformLatest auto-cancels the previous block on each new emission, // replacing manual Job tracking for idle timeout and heartbeats. - @OptIn(ExperimentalCoroutinesApi::class) snapshotFlow { stateFlow.value.chatInputState.text.toString() } .drop(1) .distinctUntilChanged() @@ -410,7 +421,9 @@ internal class ChatViewModel @Inject constructor( } .onEach { dispatchEvent(Event.TypingEnabled(it)) } .launchIn(viewModelScope) + } + private fun initSendHandlers() { // Send text message eventFlow.filterIsInstance() .map { stateFlow.value.chatInputState } @@ -610,7 +623,12 @@ internal class ChatViewModel @Inject constructor( companion object { val updateStateForEvent: (Event) -> ((State) -> State) = { event -> when (event) { - is Event.OnChatOpened -> { state -> state } + is Event.OnChatOpened -> { state -> + when (val id = event.identifier) { + is ChatIdentifier.ByContact -> state.copy(chattingWith = id.contact) + is ChatIdentifier.ByChatId -> state + } + } is Event.OnContactFound -> { state -> state.copy( chattingWith = event.contact diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt index ea816462e..3389406fa 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/MessengerScreen.kt @@ -50,8 +50,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.compose.collectAsLazyPagingItems -import com.flipcash.app.contacts.device.DeviceContact import com.flipcash.app.contacts.ui.ContactAvatar +import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.app.messenger.internal.ChatViewModel import com.flipcash.app.messenger.internal.screens.components.MessageList import com.flipcash.app.messenger.internal.screens.components.SeparatorConfig @@ -252,14 +252,10 @@ private fun UserControlBottomBar( buttonState = ButtonState.Filled, text = stringResource(R.string.action_sendCash), ) { dispatch(ChatViewModel.Event.OnSendCash) } - AnimatedVisibility( - visible = state.typingConstraints.enabled, - modifier = Modifier.weight(1f), - enter = expandHorizontally(expandFrom = Alignment.Start) + fadeIn(), - exit = shrinkHorizontally(shrinkTowards = Alignment.Start) + fadeOut(), - ) { + if (state.typingConstraints.enabled) { CodeButton( modifier = Modifier + .weight(1f) .hazeEffect(hazeState) { blurEffect { style = material diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ContactInfoContainer.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ContactInfoContainer.kt index f587e0821..ab6a4d850 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ContactInfoContainer.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ContactInfoContainer.kt @@ -22,8 +22,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow -import com.flipcash.app.contacts.device.DeviceContact import com.flipcash.app.contacts.ui.ContactAvatar +import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.features.messenger.R import com.getcode.theme.CodeTheme diff --git a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt index d7b73aa2a..a7ad72126 100644 --- a/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt +++ b/apps/flipcash/shared/chat/src/main/kotlin/com/flipcash/shared/chat/ChatCoordinator.kt @@ -11,7 +11,7 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.map -import com.flipcash.app.contacts.device.DeviceContact +import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.app.persistence.sources.ChatMemberDataSource import com.flipcash.app.persistence.sources.mediator.ChatMessageRemoteMediator import com.flipcash.app.persistence.sources.ChatMessageDataSource diff --git a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ContactCoordinator.kt b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ContactCoordinator.kt index 851c155cc..05fc2866b 100644 --- a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ContactCoordinator.kt +++ b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ContactCoordinator.kt @@ -9,18 +9,17 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences -import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStoreFile import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner -import com.flipcash.app.contacts.device.DeviceContact import com.flipcash.app.contacts.device.PickedContactData import com.flipcash.app.contacts.device.ScopeAwareContactReader import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.FeatureFlagController import com.flipcash.app.phone.PhoneUtils import com.flipcash.app.contacts.sync.ContactChecksum +import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.app.persistence.entities.ContactMappingEntity import com.flipcash.app.persistence.sources.ContactDataSource import com.flipcash.services.controllers.ContactListController @@ -60,6 +59,8 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton +import kotlin.collections.map +import kotlin.collections.mapValues @Singleton diff --git a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/DeviceContactReader.kt b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/DeviceContactReader.kt index e06f69cbf..499d7db0a 100644 --- a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/DeviceContactReader.kt +++ b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/DeviceContactReader.kt @@ -1,34 +1,11 @@ package com.flipcash.app.contacts.device -import com.flipcash.app.phone.PhoneUtils +import com.flipcash.app.core.contacts.DeviceContact interface DeviceContactReader { suspend fun readAll(): Result> } -data class DeviceContact( - val e164: String, - val androidContactId: Long, - val displayName: String, - val photoUri: String?, - val displayNumber: String = "", -) { - val isUnknown = androidContactId == -1L - companion object { - fun unknownContact( - e164: String = "", - displayName: String? = null, - displayNumber: String? = null, - ) = DeviceContact( - e164 = e164, - androidContactId = -1, - displayName = displayName ?: e164, - displayNumber = displayNumber ?: e164, - photoUri = null, - ) - } -} - data class PickedContactData( val phoneNumber: String, val displayName: String, diff --git a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/ScopeAwareContactReader.kt b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/ScopeAwareContactReader.kt index 9ced7b178..2071464c2 100644 --- a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/ScopeAwareContactReader.kt +++ b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/ScopeAwareContactReader.kt @@ -2,6 +2,7 @@ package com.flipcash.app.contacts.device import com.flipcash.app.contacts.device.internal.FullAccessContactReader import com.flipcash.app.contacts.device.internal.PickerContactReader +import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.FeatureFlagController import javax.inject.Inject diff --git a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/internal/FullAccessContactReader.kt b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/internal/FullAccessContactReader.kt index e7c810eab..4ea9f49bd 100644 --- a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/internal/FullAccessContactReader.kt +++ b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/internal/FullAccessContactReader.kt @@ -5,8 +5,8 @@ import android.content.Context import android.content.pm.PackageManager import android.provider.ContactsContract import androidx.core.content.ContextCompat -import com.flipcash.app.contacts.device.DeviceContact import com.flipcash.app.contacts.device.DeviceContactReader +import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.app.phone.PhoneUtils import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject diff --git a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/internal/PickerContactReader.kt b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/internal/PickerContactReader.kt index 402be4a18..cd14fce53 100644 --- a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/internal/PickerContactReader.kt +++ b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/device/internal/PickerContactReader.kt @@ -1,8 +1,8 @@ package com.flipcash.app.contacts.device.internal -import com.flipcash.app.contacts.device.DeviceContact import com.flipcash.app.contacts.device.DeviceContactReader import com.flipcash.app.contacts.device.PickedContactData +import com.flipcash.app.core.contacts.DeviceContact import com.flipcash.app.phone.PhoneUtils import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update diff --git a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ui/ContactAvatar.kt b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ui/ContactAvatar.kt index 43fb6e567..8b331d049 100644 --- a/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ui/ContactAvatar.kt +++ b/apps/flipcash/shared/contacts/src/main/kotlin/com/flipcash/app/contacts/ui/ContactAvatar.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.graphicsLayer @@ -34,7 +33,7 @@ import androidx.core.net.toUri import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.crossfade -import com.flipcash.app.contacts.device.DeviceContact +import com.flipcash.app.core.contacts.DeviceContact import com.getcode.theme.CodeTheme import com.getcode.ui.core.addIf diff --git a/ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt b/ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt index 81ad1b8aa..0a2888638 100644 --- a/ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt +++ b/ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp import androidx.navigation3.runtime.NavEntry @@ -189,13 +190,18 @@ internal class ModalBottomSheetScene constructor( } Box(Modifier.fillMaxSize()) { - // Scrim tracks sheet position - val scrimProgress = sheetState.progress(SheetDetent.Hidden, Expanded) + // Scrim tracks sheet position — drawn in draw phase to + // avoid recomposition on every animation frame. val scrimBaseColor = CodeTheme.colors.scrim Box( modifier = Modifier .fillMaxSize() - .background(scrimBaseColor.copy(alpha = scrimBaseColor.alpha * scrimProgress)) + .drawBehind { + val progress = sheetState + .progress(SheetDetent.Hidden, Expanded) + .coerceIn(0f, 1f) + drawRect(scrimBaseColor.copy(alpha = scrimBaseColor.alpha * progress)) + } .then( if (effectiveProperties.dismissOnClickOutside) { Modifier.noRippleClickable { dismiss(true) }