@@ -2,16 +2,16 @@ package com.flipcash.app.messenger.internal
22
33import androidx.compose.foundation.text.input.TextFieldState
44import androidx.compose.foundation.text.input.clearText
5+ import androidx.compose.runtime.snapshotFlow
56import androidx.lifecycle.viewModelScope
67import androidx.paging.PagingData
78import androidx.paging.cachedIn
89import androidx.paging.flatMap
910import androidx.paging.insertSeparators
1011import com.flipcash.app.contacts.ContactCoordinator
11- import com.flipcash.app.contacts.device.DeviceContact
12- import androidx.compose.runtime.snapshotFlow
1312import com.flipcash.app.core.AppRoute
1413import com.flipcash.app.core.chat.ChatIdentifier
14+ import com.flipcash.app.core.contacts.DeviceContact
1515import com.flipcash.app.core.extensions.onResult
1616import com.flipcash.app.core.ui.ConfirmationStyle
1717import com.flipcash.app.featureflags.FeatureFlag
@@ -26,7 +26,6 @@ import com.flipcash.services.models.buildDmPaymentMetadata
2626import com.flipcash.services.models.chat.ChatId
2727import com.flipcash.services.models.chat.DeliveryStatus
2828import com.flipcash.services.models.chat.MessageContent
29- import com.flipcash.services.models.chat.MessagePointer
3029import com.flipcash.services.models.chat.TypingState
3130import com.flipcash.services.user.UserManager
3231import com.flipcash.shared.amountentry.AmountEntryDelegate
@@ -210,56 +209,48 @@ internal class ChatViewModel @Inject constructor(
210209 }
211210 }.cachedIn(viewModelScope)
212211
213- private val maxAmountFlow = combine(
214- transactionController.limits,
215- tokenCoordinator.observeSelectedTokenMint()
216- .flatMapLatest { mint -> tokenCoordinator.balanceForToken(mint) },
217- exchange.observePreferredRate(),
218- ) { limits, balance, rate ->
219- val balanceInLocal = balance.convertingTo(rate)
220- val sendLimit = limits?.sendLimitFor(rate.currency) ? : SendLimit .Zero
221- Fiat (min(sendLimit.nextTransaction, balanceInLocal.toDouble()), rate.currency)
222- }.stateIn(viewModelScope, SharingStarted .WhileSubscribed (5000 ), null )
223-
224- val amountDelegate = AmountEntryDelegate (
225- exchange = exchange,
226- scope = viewModelScope,
227- style = AmountEntryStyle (
228- actionLabel = resources.getString(R .string.action_swipeToSend),
229- actionStyle = ConfirmationStyle .Slide ,
230- infoHint = { resources.getString(R .string.subtitle_sendHint, it) },
231- overMaxHint = { resources.getString(R .string.subtitle_sendHintLimitExceeded, it) },
232- ),
233- loadingState = stateFlow.map { it.sendProgress }
234- .stateIn(viewModelScope, SharingStarted .WhileSubscribed (5000 ), LoadingSuccessState ()),
235- maxAmount = maxAmountFlow,
236- )
237-
238- init {
239- // Token observation
240- tokenCoordinator.observeSelectedTokenMint()
241- .flatMapLatest { mint ->
242- tokenCoordinator.tokenBalances.map { tokens ->
243- tokens.find { it.token.address == mint }
244- }
245- }
246- .filterNotNull()
247- .onEach { tokenWithBalance ->
248- dispatchEvent(Event .TokenUpdated (tokenWithBalance.token))
249- }.launchIn(viewModelScope)
212+ private val maxAmountFlow by lazy {
213+ combine(
214+ transactionController.limits,
215+ tokenCoordinator.observeSelectedTokenMint()
216+ .flatMapLatest { mint -> tokenCoordinator.balanceForToken(mint) },
217+ exchange.observePreferredRate(),
218+ ) { limits, balance, rate ->
219+ val balanceInLocal = balance.convertingTo(rate)
220+ val sendLimit = limits?.sendLimitFor(rate.currency) ? : SendLimit .Zero
221+ Fiat (min(sendLimit.nextTransaction, balanceInLocal.toDouble()), rate.currency)
222+ }.stateIn(viewModelScope, SharingStarted .WhileSubscribed (5000 ), null )
223+ }
250224
251- exchange.observePreferredRate()
252- .onEach { rate ->
253- val currency = exchange.getCurrency(rate.currency.name)
254- if (currency != null ) {
255- amountDelegate.onCurrencyChanged(currency)
256- }
257- }.launchIn(viewModelScope)
225+ val amountDelegate by lazy {
226+ AmountEntryDelegate (
227+ exchange = exchange,
228+ scope = viewModelScope,
229+ style = AmountEntryStyle (
230+ actionLabel = resources.getString(R .string.action_swipeToSend),
231+ actionStyle = ConfirmationStyle .Slide ,
232+ infoHint = { resources.getString(R .string.subtitle_sendHint, it) },
233+ overMaxHint = { resources.getString(R .string.subtitle_sendHintLimitExceeded, it) },
234+ ),
235+ loadingState = stateFlow.map { it.sendProgress }
236+ .stateIn(viewModelScope, SharingStarted .WhileSubscribed (5000 ), LoadingSuccessState ()),
237+ maxAmount = maxAmountFlow,
238+ )
239+ }
258240
259- transactionController.limits
260- .onEach { dispatchEvent(Event .LimitsChanged (it)) }
261- .launchIn(viewModelScope)
241+ init {
242+ // Essential — needed immediately for chat display
243+ initChatHandlers()
244+
245+ viewModelScope.launch {
246+ // Yield to let the first frame render before setting up remaining collectors
247+ initTokenAndExchangeObservers()
248+ initTypingHandlers()
249+ initSendHandlers()
250+ }
251+ }
262252
253+ private fun initChatHandlers () {
263254 // Unified chat open handler — resolves chatId and contact from the identifier
264255 eventFlow
265256 .filterIsInstance<Event .OnChatOpened >()
@@ -269,9 +260,7 @@ internal class ChatViewModel @Inject constructor(
269260 // 1. Resolve chatId
270261 val chatId = when (identifier) {
271262 is ChatIdentifier .ByContact -> identifier.chatId
272- ? : chatCoordinator.getChatId(
273- DeviceContact .unknownContact(identifier.e164)
274- ).getOrNull()
263+ ? : chatCoordinator.getChatId(identifier.contact).getOrNull()
275264 is ChatIdentifier .ByChatId -> identifier.chatId
276265 }
277266
@@ -285,13 +274,7 @@ internal class ChatViewModel @Inject constructor(
285274 // 2. Resolve contact
286275 when (identifier) {
287276 is ChatIdentifier .ByContact -> {
288- val contact = contactCoordinator.lookupContact(identifier.e164).getOrElse {
289- DeviceContact .unknownContact(
290- e164 = identifier.e164,
291- displayName = identifier.displayName.takeIf { it.isNotBlank() },
292- )
293- }
294- dispatchEvent(Event .OnContactFound (contact))
277+ dispatchEvent(Event .OnContactFound (identifier.contact))
295278 }
296279 is ChatIdentifier .ByChatId -> {
297280 val contact = contactCoordinator.lookupContactByDmChatId(
@@ -328,11 +311,39 @@ internal class ChatViewModel @Inject constructor(
328311 chatCoordinator.advanceReadPointer(chatId, event.messageId)
329312 }
330313 .launchIn(viewModelScope)
314+ }
315+
316+ private fun initTokenAndExchangeObservers () {
317+ // Token observation
318+ tokenCoordinator.observeSelectedTokenMint()
319+ .flatMapLatest { mint ->
320+ tokenCoordinator.tokenBalances.map { tokens ->
321+ tokens.find { it.token.address == mint }
322+ }
323+ }
324+ .filterNotNull()
325+ .onEach { tokenWithBalance ->
326+ dispatchEvent(Event .TokenUpdated (tokenWithBalance.token))
327+ }.launchIn(viewModelScope)
328+
329+ exchange.observePreferredRate()
330+ .onEach { rate ->
331+ val currency = exchange.getCurrency(rate.currency.name)
332+ if (currency != null ) {
333+ amountDelegate.onCurrencyChanged(currency)
334+ }
335+ }.launchIn(viewModelScope)
336+
337+ transactionController.limits
338+ .onEach { dispatchEvent(Event .LimitsChanged (it)) }
339+ .launchIn(viewModelScope)
340+ }
331341
342+ @OptIn(ExperimentalCoroutinesApi ::class )
343+ private fun initTypingHandlers () {
332344 // Dispatch typing notifications based on text changes.
333345 // transformLatest auto-cancels the previous block on each new emission,
334346 // replacing manual Job tracking for idle timeout and heartbeats.
335- @OptIn(ExperimentalCoroutinesApi ::class )
336347 snapshotFlow { stateFlow.value.chatInputState.text.toString() }
337348 .drop(1 )
338349 .distinctUntilChanged()
@@ -410,7 +421,9 @@ internal class ChatViewModel @Inject constructor(
410421 }
411422 .onEach { dispatchEvent(Event .TypingEnabled (it)) }
412423 .launchIn(viewModelScope)
424+ }
413425
426+ private fun initSendHandlers () {
414427 // Send text message
415428 eventFlow.filterIsInstance<Event .SendMessage >()
416429 .map { stateFlow.value.chatInputState }
@@ -610,7 +623,12 @@ internal class ChatViewModel @Inject constructor(
610623 companion object {
611624 val updateStateForEvent: (Event ) -> ((State ) -> State ) = { event ->
612625 when (event) {
613- is Event .OnChatOpened -> { state -> state }
626+ is Event .OnChatOpened -> { state ->
627+ when (val id = event.identifier) {
628+ is ChatIdentifier .ByContact -> state.copy(chattingWith = id.contact)
629+ is ChatIdentifier .ByChatId -> state
630+ }
631+ }
614632 is Event .OnContactFound -> { state ->
615633 state.copy(
616634 chattingWith = event.contact
0 commit comments