Skip to content

fix(mobile): make the composer return key insert a newline#3041

Open
rodroudi wants to merge 1 commit into
block:mainfrom
rodroudi:fix/mobile-composer-return-key
Open

fix(mobile): make the composer return key insert a newline#3041
rodroudi wants to merge 1 commit into
block:mainfrom
rodroudi:fix/mobile-composer-return-key

Conversation

@rodroudi

Copy link
Copy Markdown

Summary

The mobile channel composer sets the software keyboard's return key to a send action. The result is two send controls and zero ways to type a newline: the keyboard's return key sends, the in-app ↑ button sends, and a maxLines: 5 field can never reach line two.

This makes the return key insert a newline and leaves sending to the send button alone.

Before / after

Return key Ways to send Can type a newline
Before send (blue) 2 (keyboard + ↑ button) No
After return 1 (↑ button) Yes

Root cause

mobile/lib/features/channels/compose_bar.dart

textInputAction: TextInputAction.send,   // renders the return key as "send"
...
onSubmitted: (_) => send(),              // and wires it to the send path

TextInputAction.send is a platform hint that tells iOS and Android to relabel the return key. With maxLines: 5 the field is visually multi-line, but the only key that could produce a newline has been repurposed, so the extra lines are unreachable by typing.

This is also inconsistent within our own app — the Pulse note composer already does the right thing:

// mobile/lib/features/pulse/compose_note_page.dart
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.newline,

The change

1. Return inserts a newline.

-textInputAction: TextInputAction.send,
+keyboardType: TextInputType.multiline,
+textInputAction: TextInputAction.newline,
 ...
-onSubmitted: (_) => send(),

keyboardType is set explicitly rather than relying on TextField's maxLines != 1 default, because TextInputAction.newline asserts against a non-multiline keyboard type — being explicit keeps the pairing obvious to the next reader and immune to a later maxLines edit.

2. Keep the send button mounted.

Removing the keyboard send key makes the ↑ button the only way to send, so it has to be present in every composer state. It wasn't. _SendButton lived inside the AnimatedSwitcher that swaps the action row for the formatting toolbar, so tapping Aa removed it from the widget tree entirely.

Today that's survivable because the keyboard send key covers the gap. After change 1 it would be a dead end: formatting toolbar open, nothing to tap, no way to send. So _SendButton is hoisted out of the switcher and the now-redundant Spacer() dropped. Layout is unchanged — the button still sits flush to the trailing edge in both states.

This was verified, not assumed. The regression test was written first and run against unmodified main:

Expected: exactly one matching candidate
  Actual: Found 0 widgets with icon "IconData(U+0E04A)"
          (considering only hit-testable widgets)

Alternatives considered

  • Enter sends, Shift+Enter newlines (desktop parity). Correct on desktop, wrong here: there is no Shift chord on a software keyboard, so this degenerates to the current bug on the phones that are 99% of mobile usage.
  • Keep TextInputAction.send, add a separate newline affordance. Adds UI to work around a one-line platform hint, and still leaves the double-send confusion.
  • Change only textInputAction and leave _SendButton where it is. Smaller diff, but ships a state with no way to send. Rejected.

Known follow-up (deliberately out of scope)

With a hardware keyboard (iPad, paired Bluetooth), Enter now inserts a newline and sending requires tapping ↑. That is correct-by-default and matches iMessage, but desktop Buzz uses Enter = send / Shift+Enter = newline, so there's a case for hardware-keyboard-only Enter-to-send. That needs a Shortcuts/Actions layer plus its own tests and is a separate change — happy to open it as a follow-up if maintainers want the parity.

Risk

Low, and contained to mobile/. One widget, no state management, no persistence, no protocol surface. The behavioral risk is muscle memory: anyone who learned to send with the keyboard key now taps ↑ instead. The reverse habit — return producing a newline — is the platform default everywhere else, so the transition costs one tap to learn.

Related issue

None found. Searched open and closed PRs (textInputAction, return key, composer keyboard) and issues (newline, mobile keyboard) on block/buzz — no duplicates. The nearest neighbor is #2729, an unrelated composer whitespace bug.

Testing

Two widget tests added to mobile/test/features/channels/compose_bar_test.dart, both written before the fix and both failing on main:

  • return key inserts a newline instead of sending — asserts textInputAction == newline and keyboardType == multiline, then fires a submit action and asserts onSend was never called. This is the direct regression guard: reintroducing onSubmitted: (_) => send() fails it.
  • keeps the send button reachable while formatting is open — opens the formatting toolbar, asserts the ↑ button is still hit-testable, taps it, and asserts the message actually sent.

Verified locally against the Hermit-pinned toolchain:

dart format .        → Formatted 239 files (0 changed)
flutter analyze      → No issues found!
flutter test         → All tests passed! (668 passed, 1 pre-existing skip)

Note: I have not attached simulator screenshots — repo guidance forbids agents from running flutter run/flutter build, so the verification here is the widget-test layer plus the on-device reproduction that prompted the fix. Screenshots of the before state are available if useful.

The channel composer set `textInputAction: TextInputAction.send`, which
renders the iOS/Android return key as a send key and wires it to
`onSubmitted: (_) => send()`. That gave the composer two send affordances
— the keyboard's return key and the in-app send button — and no way at
all to type a newline, despite `maxLines: 5`.

Switch the field to `TextInputType.multiline` + `TextInputAction.newline`
and drop `onSubmitted`, matching the Pulse note composer
(`compose_note_page.dart`) and the platform conventions users expect from
iMessage, WhatsApp, and Signal. Sending is now the send button's job
alone.

That makes the send button load-bearing, and it wasn't always reachable:
it lived inside the `AnimatedSwitcher` that swaps the action row for the
formatting toolbar, so tapping the `Aa` button removed it from the tree.
Previously the keyboard send key covered that gap; without it the user
would have had no way to send while formatting was open. Hoist
`_SendButton` out of the switcher so it stays mounted in both states. The
layout is unchanged — it still sits flush to the trailing edge.

Two widget tests cover the change: one asserts the return key is a
newline action and that a submit action sends nothing, the other opens
the formatting toolbar and sends from the button.

Co-authored-by: Fizz <noreply@buzz-relay.org>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Rod Roudi <rod@rodroudi.com>
@rodroudi
rodroudi requested a review from a team as a code owner July 26, 2026 21:46
@rodroudi

Copy link
Copy Markdown
Author

Like this:

image

Not like this (current state):

image

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