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 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/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) + } + } } } } 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 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()