From 9ee2959deeeb10db8d6f73433a65c1c8ffdecf47 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 27 Jul 2026 16:10:04 -0400 Subject: [PATCH 1/4] fix(chat): reopen the keyboard when message input is requested MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #1075 replaced the userState (Reading/Typing) model with the always-visible input and, in doing so, turned OnStartMessageInput into a no-op and dropped the focusRequester.requestFocus() call. The event was still dispatched on return from amount entry, but nothing acted on it, so the keyboard no longer reopened after sending cash from within a chat. Restore it with a transient State.messageInputRequested signal: OnStartMessageInput sets it, and the bottom bar — co-located with the ChatInput so the focusRequester is attached — focuses the field, shows the keyboard, and dispatches OnMessageInputConsumed to clear it. State (not a one-shot event) is used because eventFlow is replay-0, so a request raised at open would be missed before the bar subscribes. --- .../app/messenger/internal/ChatViewModel.kt | 11 ++++++++++- .../internal/screens/components/ChatBottomBar.kt | 13 +++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) 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 a82686b24..34ee35124 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 @@ -132,6 +132,13 @@ internal class ChatViewModel @Inject constructor( val limits: Limits? = null, val isAnonymous: Boolean = false, val cashSymbol: String = "$", + // Transient "focus the message input" request. Set by OnStartMessageInput (dispatched when + // returning from amount entry after a send, and on a post-tip chat open) and cleared by + // OnMessageInputConsumed once the bottom bar has focused the field and shown the keyboard. + // Kept as state (not a one-shot event) because eventFlow is replay-0: a request raised at + // open would be missed by the bottom bar before it subscribes, whereas state is durable + // until the input is actually composed and can consume it. + val messageInputRequested: Boolean = false, ) sealed interface Event { @@ -144,6 +151,7 @@ internal class ChatViewModel @Inject constructor( data object OnSendCash: Event data object OnStartMessageInput: Event data object OnStopMessageInput: Event + data object OnMessageInputConsumed: Event data class TypistsUpdated(val typists: Set) : Event data object ResolveCompleted : Event data object ResolveFailed : Event @@ -845,8 +853,9 @@ internal class ChatViewModel @Inject constructor( is Event.RefreshContact -> { state -> state } is Event.ChatFound -> { state -> state.copy(chatId = event.chatId) } Event.OnSendCash -> { state -> state } - Event.OnStartMessageInput -> { state -> state } + Event.OnStartMessageInput -> { state -> state.copy(messageInputRequested = true) } Event.OnStopMessageInput -> { state -> state } + Event.OnMessageInputConsumed -> { state -> state.copy(messageInputRequested = false) } is Event.TypistsUpdated -> { state -> state.copy(typists = event.typists) } Event.ResolveCompleted -> { state -> state.copy(resolveState = ResolveState.Resolved) 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 f45f23a2b..8be4df7b2 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 @@ -173,6 +173,19 @@ internal fun UserControlBottomBar( keyboard.restartInput() }, ) + + // Restores the pre-#1075 behavior: when OnStartMessageInput raises + // state.messageInputRequested (returning from amount entry after a send, or a + // post-tip open), focus the input and show the keyboard. Co-located with + // ChatInput so focusRequester is guaranteed attached; consumes the request so + // it fires once and a later manual dismiss doesn't re-open it. + LaunchedEffect(state.messageInputRequested) { + if (state.messageInputRequested) { + focusRequester.requestFocus() + keyboard.show() + dispatch(ChatViewModel.Event.OnMessageInputConsumed) + } + } } } } From dc14589a018714b9a53872df04411b2e196fcc99 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 27 Jul 2026 16:10:05 -0400 Subject: [PATCH 2/4] feat(tips): open post-tip chats with the keyboard up After completing a tip from the tip card, hand off into the chat with the message input focused, matching the keyboard-open state you land on when returning from amount entry. A new openKeyboard flag on the Chat route (default false) is set only by the post-tip navigation; ChatFlowScreen dispatches OnStartMessageInput once on open when it is set. Normal opens (tips list, deeplinks, contact DMs) stay keyboard-closed. --- .../kotlin/com/flipcash/app/core/AppRoute.kt | 7 ++++++- .../flipcash/app/messenger/ChatFlowScreen.kt | 20 ++++++++++++++++--- .../internal/bills/decor/TipCardDecorator.kt | 2 +- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt index a6592fdd6..d35383c64 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt @@ -293,7 +293,12 @@ sealed interface AppRoute : NavKey, Parcelable { @Parcelize sealed interface Messaging : AppRoute { @Serializable - data class Chat(val identifier: ChatIdentifier) : Messaging, FlowRoute { + data class Chat( + val identifier: ChatIdentifier, + // Open straight into composing a reply with the keyboard up. Only the post-tip + // hand-off (see TipCardDecorator) sets this; normal opens default to keyboard-closed. + val openKeyboard: Boolean = false, + ) : Messaging, FlowRoute { override val initialStack: List get() = listOf(ChatStep.Conversation) } diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt index 1a2ef0874..9c658fd1c 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/ChatFlowScreen.kt @@ -4,6 +4,9 @@ import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey @@ -38,16 +41,17 @@ fun ChatFlowScreen( initialStack = route.rememberInitialStack(), resultStateRegistry = resultStateRegistry, onExit = { _, _ -> navigator.pop() }, - entryProvider = chatEntryProvider(route.identifier), + entryProvider = chatEntryProvider(route.identifier, route.openKeyboard), ) } @Composable private fun chatEntryProvider( identifier: ChatIdentifier, + openKeyboard: Boolean, ): (NavKey) -> NavEntry = entryProvider { annotatedEntry { - FlowConversationScreen(identifier) + FlowConversationScreen(identifier, openKeyboard) } annotatedEntry { FlowAmountEntryScreen() @@ -55,7 +59,7 @@ private fun chatEntryProvider( } @Composable -private fun FlowConversationScreen(identifier: ChatIdentifier) { +private fun FlowConversationScreen(identifier: ChatIdentifier, openKeyboard: Boolean) { val viewModel = flowSharedViewModel() val navigator = LocalCodeNavigator.current // The sheet-owning (root) navigator — the one whose back stack holds this chat's Main.Sheet and @@ -68,6 +72,16 @@ private fun FlowConversationScreen(identifier: ChatIdentifier) { viewModel.dispatchEvent(ChatViewModel.Event.OnChatOpened(identifier)) } + var hasOpened by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(openKeyboard) { + if (openKeyboard) { + if (!hasOpened) { + viewModel.dispatchEvent(ChatViewModel.Event.OnStartMessageInput) + hasOpened = true + } + } + } + LaunchedEffect(viewModel) { viewModel.eventFlow .filterIsInstance() diff --git a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/decor/TipCardDecorator.kt b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/decor/TipCardDecorator.kt index 8ced74fc6..6a8808057 100644 --- a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/decor/TipCardDecorator.kt +++ b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/decor/TipCardDecorator.kt @@ -82,7 +82,7 @@ internal data class TipCardDecorator(private val tipCard: Scannable.TipCard) : S navigator.navigateAll( listOf( AppRoute.Sheets.Tips(), - AppRoute.Messaging.Chat(event.identifier), + AppRoute.Messaging.Chat(event.identifier, openKeyboard = true), ) ) context.onDismiss() From 47d9d6e5c5cf265af233c1be96d5e0b1388beda2 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 27 Jul 2026 16:10:06 -0400 Subject: [PATCH 3/4] feat(tips): use the minimized send button only in tip chats Tip chats always show the compact dark symbol-only send button. The normal send flow keeps the expanded "Send $" presentation and only collapses to the symbol once the user starts typing. --- .../messenger/internal/screens/components/SendCashButton.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/SendCashButton.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/SendCashButton.kt index 095c30755..6a13cd757 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/SendCashButton.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/components/SendCashButton.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import com.flipcash.app.messenger.internal.ChatParticipant import com.flipcash.app.messenger.internal.ChatViewModel import com.flipcash.features.messenger.R import com.getcode.theme.CodeTheme @@ -44,7 +45,10 @@ internal fun RowScope.SendCashButton( hazeMaterial: HazeBlurStyle, onClick: () -> Unit, ) { - val isTyping = state.chatInputState.text.isNotEmpty() + // Tip chats always use the minimized (dark, symbol-only) button. The normal send flow keeps the + // expanded "Send $" presentation and only collapses to the symbol once the user starts typing. + val isTipChat = state.participant is ChatParticipant.TipUser + val isTyping = isTipChat || state.chatInputState.text.isNotEmpty() val canType = state.typingConstraints.enabled // Colors ease slowly and independently of the width/label so the fill change reads as one From 4376ad8981afa3c2f890a3229387f71ac9d3f4f9 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 27 Jul 2026 16:10:06 -0400 Subject: [PATCH 4/4] docs: clarify code/cash is the sole main branch origin/HEAD was unset, so tooling defaulted the base branch to a non-existent `main`. Note explicitly that code/cash is the only main branch and that PRs target it. --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 35f2bb97b..1ba5e191f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -100,5 +100,5 @@ The feature plugin automatically includes `:libs:logging`, `:ui:core`, `:ui:comp ## Git Conventions - Conventional commits: `feat:`, `fix:`, `chore:`, with optional scope in parens (e.g., `feat(oc):`, `fix(tokens):`) -- Main branch: `code/cash` +- Main branch: `code/cash` (on the `origin` remote). There is **no** `main` branch — open PRs against `origin/code/cash` and branch new work from it. Ignore any tooling that reports the default/base branch as `main`. - CI runs on all PRs (tests via Fastlane) \ No newline at end of file