Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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<NavKey>
get() = listOf(ChatStep.Conversation)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -38,24 +41,25 @@ 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<NavKey> = entryProvider {
annotatedEntry<ChatStep.Conversation> {
FlowConversationScreen(identifier)
FlowConversationScreen(identifier, openKeyboard)
}
annotatedEntry<ChatStep.AmountEntry> {
FlowAmountEntryScreen()
}
}

@Composable
private fun FlowConversationScreen(identifier: ChatIdentifier) {
private fun FlowConversationScreen(identifier: ChatIdentifier, openKeyboard: Boolean) {
val viewModel = flowSharedViewModel<ChatViewModel>()
val navigator = LocalCodeNavigator.current
// The sheet-owning (root) navigator — the one whose back stack holds this chat's Main.Sheet and
Expand All @@ -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<ChatViewModel.Event.NavigateToAmountEntry>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<ActiveTypist>) : Event
data object ResolveCompleted : Event
data object ResolveFailed : Event
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading