Skip to content

fix: auto-open the keyboard when launching into a chat, without fighting Back - #3205

Open
gsxdsm wants to merge 2 commits into
BlueBubblesApp:masterfrom
gsxdsm:fix/keyboard-auto-open-and-dismiss
Open

fix: auto-open the keyboard when launching into a chat, without fighting Back#3205
gsxdsm wants to merge 2 commits into
BlueBubblesApp:masterfrom
gsxdsm:fix/keyboard-auto-open-and-dismiss

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What

When Auto-open keyboard is enabled, opening a conversation directly — from a notification/URI, a launcher shortcut, or by re-selecting an already-open chat — did not raise the keyboard. A plain requestFocus() isn't enough on Android: during startup the engine restarts its input connection and silently drops the TextInput.show request.

This adds a composer-owned helper, ensureComposerKeyboard, that focuses the message field and retries TextInput.show until the keyboard is actually up. It's invoked from the composer's initState, the app-resume path (StartupTasks), and IntentsService when re-opening an already-open chat.

Retry window: cold vs warm

  • Warm opens use a short (~1s) window. In split-screen / multi-window (common on foldables) the IME doesn't resize the window, so neither viewInsets nor KeyboardVisibilityController ever report the keyboard visible; a long loop there would keep firing TextInput.show and re-raise the keyboard on every Back, making dismissal look broken.
  • Cold (just-launched) opens use a longer (~5s) window, because a shortcut opening straight into a chat outlasts the engine's input-connection churn. A cold open is fullscreen and reports visibility, so the loop still exits the instant the keyboard is up and doesn't fight a dismiss.

Don't re-raise after dismiss

ConversationViewController releases composer focus on a genuine open→closed keyboard transition (scoped to the foreground chat), so the engine doesn't re-raise the keyboard for the still-focused field (flutter/flutter#52599). Android's IME swallows the dismiss Back before any PopScope handler runs, so the keyboard-visibility transition is the only place to catch it.

Stale overlay/sub-route guard

The retry loop bails while an overlay or sub-route is showing. Those flags live on the reused controller and are toggled by pickers / the details route / the share sheet; a skipped cleanup left one stuck true and blocked the keyboard on every later open ("works a few times then stops"). They're now cleared at the single entry point for every explicit keyboard-raise.

Controller lifecycle / render-crash fixes

Rapid navigation surfaced "FocusNode used after disposed" / "deactivated widget" render crashes, rooted in leaked ConversationViewControllers:

  • FocusNodes and text controllers are now owned and disposed by the controller (onClose), not by the composer widget — so a reused controller never hands a freshly-mounted composer a disposed FocusNode.
  • Controllers are disposed when their view is permanently gone (previously close() ran only on an explicit Back), and cvc() never returns a controller whose onClose() already ran.

Files

  • conversation_text_field.dartensureComposerKeyboard / bounded, cold-aware retry loop; stale-flag reset; listeners added/removed (not disposing controller-owned nodes).
  • conversation_view_controller.dart — exposes the hook; unfocus-on-dismiss; owns FocusNode/controller disposal; disposed guard; cancels the keyboard-visibility subscription.
  • conversation_view.dart — dispose the controller when its view is gone (mobile).
  • startup_tasks.dart — drive the keyboard through the composer helper on resume.
  • intents_service.dart — raise the keyboard when re-opening an already-open chat.

Testing

Verified on a Samsung Fold (One UI), in split-screen and fullscreen, on a release/profile build: shortcut launches raise the keyboard reliably, Back dismisses without it popping back, and rapid back-and-forth no longer throws render errors.

Supersedes #3201.

gsxdsm and others added 2 commits August 11, 2026 11:26
…ing Back

When autoOpenKeyboard is on, opening a chat directly (from a URI, a launcher
shortcut, or re-selecting an already-open chat) never raised the keyboard,
because a plain requestFocus() is not enough on Android — the engine's
input-connection restart during startup swallows the show request.

Add a composer-owned helper (ensureComposerKeyboard) that focuses the message
field and briefly retries TextInput.show until the keyboard is up. It is invoked
from the composer's initState, from the resume path, and from IntentsService when
re-opening an already-open chat (which never rebuilds the composer).

The retry loop is deliberately short (~1s). In split-screen / multi-window the
IME does not resize the app window, so neither viewInsets nor
KeyboardVisibilityController ever report the keyboard visible; a long loop would
keep firing TextInput.show and re-raise the keyboard every time the user pressed
Back, making dismissal appear broken. Bounding it to cold-start churn and exiting
early on the visibility signal keeps Back working. As a companion, the controller
releases composer focus on a genuine open->closed keyboard transition so the
engine does not re-raise the keyboard for the still-focused field (flutter#52599),
and the keyboard-visibility subscription is now cancelled on close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er lifecycle

Follow-ups that make the keyboard auto-open reliable and stop the render crashes it
surfaced under rapid navigation:

- Give cold (just-launched) opens a longer keyboard-retry window (~5s vs ~1s), since a
  shortcut opening straight into a chat outlasts the engine's input-connection churn.
  Warm opens keep the short window so split-screen Back-to-dismiss still sticks.
- Clear stale showingOverlays/showingSubRoute before raising the keyboard, so a flag left
  set by a picker/details route can't make the retry loop bail ('works a few times then
  stops').
- FocusNodes/text controllers are owned and disposed by ConversationViewController, not the
  composer widget, so a reused controller never hands a fresh composer a disposed FocusNode.
- Dispose ConversationViewControllers when their view is permanently gone (close() only ran
  on an explicit Back), and never resolve a controller whose onClose() already ran. Fixes
  the leaked controllers behind the 'used after disposed' render crashes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant