Skip to content

fix(chat): scope the send-button spring to the button - #741

Merged
bmc08gt merged 2 commits into
mainfrom
fix/composer-clear-on-send
Sep 9, 2026
Merged

fix(chat): scope the send-button spring to the button#741
bmc08gt merged 2 commits into
mainfrom
fix/composer-clear-on-send

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

The send-button spring was applied to the whole composer row rather than to the button that needed it. Only the button has a .transition, and a transition takes its animation from the transaction its container supplies — so the modifier belongs on a wrapper around the conditional, not on the row that also holds the text field.

Scoping it there keeps the field out of that transaction. The button still pops in and out: the .animation(_:value:) now sits on the Group wrapping the conditional.

This started as a candidate explanation for the composer sometimes keeping the sent text after a send. Tapping Send flips showsSubmit on the same update that empties draft, so with the row-wide spring the field's text update ran as an animated one against its backing text view, where it can be coalesced away; the binding reads empty either way, and an unchanged binding never pushes again, which would make a missed update permanent rather than late. That mechanism is plausible but unconfirmed, and this change should not be read as fixing the reported behaviour.

ComposerModel is untouched. clear() already emptied the draft synchronously before the send was dispatched, ComposerModelTests already covered it, and nothing writes the draft back: draftDidChange only broadcasts typing state, and there is no draft persistence.

The UI tests are regression cover for the send path, which had none. They send a run of messages, a reply, and a message typed mid-edit, reading the field back after each send. The assertion names which side of the binding is stale — the send button is drawn from the same emptiness the field is, so a button that has gone away while the text remains means the draft emptied and the text view never caught up. They pass against the composer as it stood before this change, so they do not stand as evidence for it.

Sending sometimes left the sent text in the field, so the next message had to
be erased before it could be written.

The send-button spring was on the whole composer row rather than on the button.
Tapping Send flips `showsSubmit` true to false on the same update that sets
`draft` to empty, so the field's text update ran as an animated one against its
backing text view, where it can be coalesced away. The binding reads empty
either way, and an unchanged binding never pushes again, so a missed update is
permanent. Whether it is missed depends on the field's pending-edit state at the
tap, which is why it only happened sometimes.

Scoping the spring to the button keeps the field out of that transaction. The
button still pops in and out: the `.animation(_:value:)` now sits on the `Group`
wrapping the conditional, which is what supplies its transition's transaction.

`ComposerModel` is untouched. `clear()` already emptied the draft synchronously
before the send was dispatched, and its tests already covered that.
@bmc08gt bmc08gt self-assigned this Sep 9, 2026
Three UI tests around the send path: a run of plain sends, a reply, and a send
made while the field is mid-edit. Each reads the field back after the send and
fails if the sent text is still there.

The assertion reports which side of the binding is stale. The send button is
drawn from the same emptiness the field is, so a button that has gone away while
the text remains means the draft emptied and the text view never caught up,
rather than the send never firing.

Sends are dispatched without waiting for the button to settle, and delivery is
not awaited between them: the gap between the last keystroke and the tap is the
window the failure was thought to live in, and waiting anywhere in it settles it.

The tests pass against the pre-fix ConversationBottomBar, so they do not
reproduce the reported failure and do not stand as evidence for the fix in
ec986a5. A control that types without sending does fail, so the assertion reads
the field rather than passing vacuously.
@bmc08gt bmc08gt changed the title fix(chat): empty the composer reliably after a send fix(chat): scope the send-button spring to the button Sep 9, 2026
@bmc08gt
bmc08gt merged commit 4d58e59 into main Sep 9, 2026
1 check passed
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