Skip to content

feat(chat): flash the message a reply-quote jump lands on - #738

Merged
bmc08gt merged 1 commit into
mainfrom
feat/chat-jump-attention-flash
Sep 8, 2026
Merged

feat(chat): flash the message a reply-quote jump lands on#738
bmc08gt merged 1 commit into
mainfrom
feat/chat-jump-attention-flash

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Tapping a reply quote scrolled the transcript to the quoted message and stopped there, with nothing to separate it from the rows around it.

The flash

It lives on BubbleBackgroundView — the chrome every bubble and cash card already shares — as a keyframe on its own layer, above the wash and below the border, so it brightens the ground without washing over the text or softening the hairline edge. 0.15s rise, 0.45s hold, 0.40s fade, defined in ChatMotion next to the rest of the transcript's motion.

The resting opacity stays 0 throughout; the animation carries the raised value. A cell reconfigured or recycled mid-flash therefore can't strand a lit bubble, and apply(fill:radii:identity:) drops a running flash when the row identity changes.

A jump lands exactly when the transcript is re-dequeueing rows around the page that brought the target in, and a recycled cell loses its CAAnimations. So willDisplay re-attaches the flash from its original start time: a row displayed twice joins the flash in progress and still ends when it would have, rather than replaying it.

Two ordering bugs underneath

A deferred jump — one that has to wait for the page carrying the row — wasn't landing at all:

  • The reload branch of update(items:) ran the opening scroll-to-bottom before the pending jump. That scroll queues a re-anchor for the next runloop turn, which pulled the transcript off the message a beat after arriving on it. The jump now runs instead of the opening scroll rather than after it.
  • scrollToBottom's deferred re-anchors were fire-and-forget, so one queued by an earlier update still undid a jump that landed before it ran. Both now capture a positionClaim counter and give way if a jump has bumped it since.

The second is the narrower of the two: it needs a jump inside the same runloop turn as the opening scroll. The first fires on any quote tap that has to page.

Tapping a reply quote scrolled to the quoted message and stopped there, with
nothing to separate it from the rows around it.

The flash is a keyframe on `BubbleBackgroundView`, the chrome every bubble and
cash card already shares, on its own layer above the wash and below the border.
The resting opacity stays 0 so a cell recycled mid-flash can't strand a lit
bubble, and `apply(fill:radii:identity:)` drops a running flash when the row
changes. A jump lands right as the transcript re-dequeues rows around the
arriving page, and a recycled cell loses its `CAAnimation`s, so `willDisplay`
re-attaches from the original start time: a row displayed twice joins the flash
in progress rather than replaying it.

Two ordering bugs stopped a deferred jump, one waiting on the page that carries
the row, from landing at all. The reload branch of `update(items:)` ran the
opening scroll-to-bottom before the pending jump, and that scroll queues a
re-anchor for the next runloop turn which pulled the transcript back off the
message; the jump now runs instead of the opening scroll. Separately,
`scrollToBottom`'s deferred re-anchors were fire-and-forget, so one queued by an
earlier update still undid a jump that landed first. Both now give way when a
`positionClaim` counter has moved.
@bmc08gt bmc08gt self-assigned this Sep 8, 2026
@bmc08gt
bmc08gt merged commit fdd7814 into main Sep 8, 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