From 6aadb4e120efecffa0d7d566401fd850ca64e5b4 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 27 Jul 2026 16:28:34 -0400 Subject: [PATCH] feat(tips): open post-tip chats with the keyboard up After completing a tip, the chat now opens with the message field focused and the keyboard raised. Added a sibling .tipConversationWithKeyboard destination (matching the currencyInfoForDeposit pattern) used only by the post-tip navigation; the tip list and push-notification opens keep using .tipConversation and stay keyboard-closed. The openKeyboard flag threads down to the composer, which requests focus once on appear after a short delay so the push transition settles first. Mirrors Android #1165. --- .../Navigation/AppRouter+Destination.swift | 15 ++++++++++--- .../AppRouter+DestinationView.swift | 6 ++++++ .../ChatScreenRepresentable.swift | 6 +++++- .../Conversation/ConversationBottomBar.swift | 21 +++++++++++++++++-- .../Conversation/ConversationScreen.swift | 7 ++++++- Flipcash/Core/Screens/Main/Tips/TipFlow.swift | 4 +++- 6 files changed, 51 insertions(+), 8 deletions(-) diff --git a/Flipcash/Core/Navigation/AppRouter+Destination.swift b/Flipcash/Core/Navigation/AppRouter+Destination.swift index 22819963..8db774b2 100644 --- a/Flipcash/Core/Navigation/AppRouter+Destination.swift +++ b/Flipcash/Core/Navigation/AppRouter+Destination.swift @@ -53,8 +53,14 @@ extension AppRouter { /// The signed-in user's own tipcard, pushed from the Tips list. case tipcard /// A tip DM conversation, pushed onto the `.tips` stack — from the - /// Tips list, a completed tip, or a tip-DM push notification. + /// Tips list or a tip-DM push notification. case tipConversation(ConversationID) + /// Same screen as `tipConversation` but opens with the message field + /// focused and the keyboard up. Modelled as a sibling case rather than + /// an associated-value flag (matching `currencyInfoForDeposit`) so the + /// trace shows "post-tip, keyboard up" distinctly, and so the ordinary + /// tip-list / push-notification opens stay keyboard-closed untouched. + case tipConversationWithKeyboard(ConversationID) // Conversation flow /// A DM conversation, pushed onto the `.send` stack — from the Chats @@ -78,7 +84,8 @@ extension AppRouter { .settingsAppSettings, .settingsBetaFlags, .settingsAccountSelection, .settingsApplicationLogs, .accessKey, .withdraw: return .settings - case .profileName, .profilePhoto, .tipcard, .tipConversation: + case .profileName, .profilePhoto, .tipcard, + .tipConversation, .tipConversationWithKeyboard: return .tips case .dmConversation: return .send @@ -115,6 +122,7 @@ extension AppRouter { case .profilePhoto: "profilePhoto" case .tipcard: "tipcard" case .tipConversation: "tipConversation" + case .tipConversationWithKeyboard: "tipConversationWithKeyboard" case .dmConversation: "dmConversation" } } @@ -134,7 +142,8 @@ extension AppRouter { return conversationID.description case .dmConversation(.contact(let contact)): return contact.contactId - case .tipConversation(let conversationID): + case .tipConversation(let conversationID), + .tipConversationWithKeyboard(let conversationID): return conversationID.description case .discoverCurrencies, .currencyCreationSummary, .currencyCreationWizard, .usdcDepositEducation, .usdcDepositAddress, diff --git a/Flipcash/Core/Navigation/AppRouter+DestinationView.swift b/Flipcash/Core/Navigation/AppRouter+DestinationView.swift index 02bfdc43..29d355e2 100644 --- a/Flipcash/Core/Navigation/AppRouter+DestinationView.swift +++ b/Flipcash/Core/Navigation/AppRouter+DestinationView.swift @@ -137,6 +137,12 @@ struct DestinationView: View { ConversationScreen(context: .existing(conversationID)) .id(conversationID) + case .tipConversationWithKeyboard(let conversationID): + // Post-tip open: same screen, but focus the field so the keyboard + // comes up. `.id` forces fresh view identity per conversation. + ConversationScreen(context: .existing(conversationID), openKeyboard: true) + .id(conversationID) + // MARK: - Conversation flow case .dmConversation(let context): diff --git a/Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift b/Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift index 7b016222..216ca0f9 100644 --- a/Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift +++ b/Flipcash/Core/Screens/Conversation/ChatScreenRepresentable.swift @@ -39,6 +39,9 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable { let onSendCash: () -> Void let conversationController: ConversationController let barModel: ConversationBarModel + /// Focus the composer when the bar first appears (post-tip open). One-shot: + /// the composer requests focus in its `.task`, which runs once on appear. + let focusOnAppear: Bool func makeUIViewController(context: Context) -> ChatScreenViewController { let barHost = UIHostingController(rootView: bar(coordinator: context.coordinator)) @@ -96,7 +99,8 @@ struct ChatScreenRepresentable: UIViewControllerRepresentable { conversationID: conversationID, symbol: symbol, onSendCash: onSendCash, - model: barModel + model: barModel, + focusOnAppear: focusOnAppear ) .environment(conversationController) .modifier(MeasuredBarHeight { coordinator.screen?.setBarHeight($0) }) diff --git a/Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift b/Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift index 7d5702aa..0136bd98 100644 --- a/Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift +++ b/Flipcash/Core/Screens/Conversation/ConversationBottomBar.swift @@ -46,6 +46,8 @@ struct ConversationBottomBar: View { let symbol: String let onSendCash: () -> Void let model: ConversationBarModel + /// Focus the composer on first appear (post-tip open); false everywhere else. + var focusOnAppear: Bool = false var body: some View { let content = HStack(alignment: .bottom, spacing: 10) { @@ -58,8 +60,12 @@ struct ConversationBottomBar: View { ) } if chatExists { - ConversationComposer(conversationID: conversationID, model: model) - .transition(.opacity) + ConversationComposer( + conversationID: conversationID, + model: model, + focusOnAppear: focusOnAppear + ) + .transition(.opacity) } } .padding(.horizontal, 12) @@ -80,6 +86,8 @@ struct ConversationComposer: View { let conversationID: ConversationID? @Bindable var model: ConversationBarModel + /// Raise the keyboard on open (post-tip chats only). One-shot via `.task`. + var focusOnAppear: Bool = false @Environment(ConversationController.self) private var conversationController @FocusState private var isFocused: Bool @@ -121,6 +129,15 @@ struct ConversationComposer: View { return field .glassBackground(cornerRadius: BarMetrics.cornerRadius) + // Post-tip open: raise the keyboard once the composer appears. Requested + // after a short delay so the push transition has settled — focus asked + // mid-transition is dropped and the keyboard never rises. `.task` runs + // once on appear and is cancelled on disappear. + .task { + guard focusOnAppear else { return } + try? await Task.delay(milliseconds: 350) + isFocused = true + } // Focus is the single source of `isComposing` — the button morph and the // screen's interactive-dismiss gate both key off it. Losing focus // (keyboard swiped down) ends composing. diff --git a/Flipcash/Core/Screens/Conversation/ConversationScreen.swift b/Flipcash/Core/Screens/Conversation/ConversationScreen.swift index ce38f0de..82e665a1 100644 --- a/Flipcash/Core/Screens/Conversation/ConversationScreen.swift +++ b/Flipcash/Core/Screens/Conversation/ConversationScreen.swift @@ -44,6 +44,10 @@ struct ConversationScreen: View { let context: ConversationContext + /// Focus the message field on open so the keyboard comes up. Set only by the + /// post-tip navigation; every other entry point opens keyboard-closed. + var openKeyboard: Bool = false + @Environment(ConversationController.self) private var conversationController @Environment(ContactSyncController.self) private var contactSyncController @Environment(AppRouter.self) private var router @@ -167,7 +171,8 @@ struct ConversationScreen: View { symbol: ratesController.balanceCurrency.compactSymbol, onSendCash: sendCash, conversationController: conversationController, - barModel: barModel + barModel: barModel, + focusOnAppear: openKeyboard ) .ignoresSafeArea(.keyboard) // Extend the transcript under the navigation bar so content scrolls beneath it — that's diff --git a/Flipcash/Core/Screens/Main/Tips/TipFlow.swift b/Flipcash/Core/Screens/Main/Tips/TipFlow.swift index 6fb1d65a..a288ac0d 100644 --- a/Flipcash/Core/Screens/Main/Tips/TipFlow.swift +++ b/Flipcash/Core/Screens/Main/Tips/TipFlow.swift @@ -277,7 +277,9 @@ final class TipFlow { let chatID = ConversationID.tipDm(between: session.userID, and: recipient.userID) Task { [router] in try? await Task.delay(milliseconds: 600) - router.navigate(to: .tipConversation(chatID)) + // Open the post-tip chat with the keyboard up — the tip-specific + // variant focuses the composer; ordinary opens stay closed. + router.navigate(to: .tipConversationWithKeyboard(chatID)) } } }