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 10a2281ee..a82686b24 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 @@ -81,6 +81,10 @@ import kotlin.time.Duration.Companion.seconds data class TypingConstraints( val enabled: Boolean = false, + // False until the async typing-enabled query (does this chat have a cash message?) has + // answered at least once. The bottom bar keeps its layout invisible until this is true so it + // never renders the default full-width state and then snaps to the resolved pill + input. + val resolved: Boolean = false, val interval: Duration = 3.seconds, val timeout: Duration = 5.seconds, ) @@ -327,6 +331,12 @@ internal class ChatViewModel @Inject constructor( chatCoordinator.setActiveChatId(chatId) viewModelScope.launch { chatCoordinator.loadMessages(chatId) } chatCoordinator.dismissNotifications(chatId) + } else { + // No existing chat means no messages yet, so typing stays disabled. The + // observeMessages-driven typing flow only fires once a chatId exists, so mark + // the typing state resolved here explicitly — otherwise the bottom bar would + // stay hidden forever for a brand-new contact. + dispatchEvent(Event.TypingEnabled(false)) } // 2. Resolve contact @@ -865,7 +875,10 @@ internal class ChatViewModel @Inject constructor( Event.OnSelfTypingStopped -> { state -> state.copy(isSelfTyping = false) } is Event.TypingEnabled -> { state -> state.copy( - typingConstraints = state.typingConstraints.copy(enabled = event.enabled) + typingConstraints = state.typingConstraints.copy( + enabled = event.enabled, + resolved = true, + ) ) } is Event.TokenUpdated -> { state -> state.copy(token = event.token) } diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ChatBottomBar.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ChatBottomBar.kt index b645d721b..f45f23a2b 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ChatBottomBar.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/ChatBottomBar.kt @@ -1,6 +1,9 @@ package com.flipcash.app.messenger.internal.screens.components import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn @@ -24,6 +27,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.platform.testTag @@ -113,8 +117,25 @@ internal fun UserControlBottomBar( .measured { buttonHeight = it.height } .padding(horizontal = CodeTheme.dimens.inset) .padding(vertical = CodeTheme.dimens.grid.x3) - .navigationBarsPadding(), + .navigationBarsPadding() + // typingConstraints.enabled starts false and only resolves a frame or two after + // open, once Room confirms whether the chat has a cash message. Rendering the + // default false layout first showed a full-width "Send $" button that then + // scaled down to the pill + input box. Hold the bar invisible (but measured, so + // the message list keeps correct padding) until resolved, then reveal the final + // layout directly — no visible full-width state, no resize. + .alpha(if (state.typingConstraints.resolved) 1f else 0f), targetState = state.typingConstraints.enabled, + // The layout only ever changes on the initial async resolution, which is hidden by + // the alpha gate above, so snap rather than crossfade. The SendCashButton's own + // color/label springs still animate the typing interaction. + transitionSpec = { + ContentTransform( + targetContentEnter = EnterTransition.None, + initialContentExit = ExitTransition.None, + sizeTransform = null, + ) + }, ) { canType -> Row( modifier = Modifier