Skip to content

Fix channel list scroll interruption - #5815

Merged
wesbillman merged 1 commit into
mainfrom
carl/fix-mobile-scroll-interruption
Aug 14, 2026
Merged

Fix channel list scroll interruption#5815
wesbillman merged 1 commit into
mainfrom
carl/fix-mobile-scroll-interruption

Conversation

@wesbillman

Copy link
Copy Markdown
Collaborator

Summary

  • keep transparent channel-list gaps in Flutter's gesture arena
  • allow a new drag to interrupt active ballistic scrolling immediately
  • add a behavioral fling-and-counter-drag regression test

Scope audit

  • audited mobile list and scroll constructors across mobile/lib
  • channels is the only app scrollable overriding hitTestBehavior
  • all other lists retain Flutter's default opaque hit testing and do not share this defect

Verification

  • regression test fails before the production change: ballistic offset continues from 271.17 to 345.56
  • focused interruption regression passes with the fix
  • profile/community control test passes
  • pre-commit: Dart formatting and Flutter analyzer pass
  • pre-push: complete mobile suite passes, 1323 tests
  • simulator: immediate counter-drag from the transparent gutter interrupts deceleration

Simulator evidence: /Users/wesb/.buzz/.scratch/mobile-scroll-videos/interruption-verified.mp4

Keep transparent channel-list gaps in the gesture arena so a new drag can
interrupt an active ballistic scroll. Cover the real fling-and-interrupt
behavior rather than merely asserting widget configuration.

Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
@wesbillman
wesbillman requested a review from a team as a code owner August 13, 2026 23:44
@Chessing234

Copy link
Copy Markdown
Contributor

the comment being replaced records a specific earlier fix — "must not absorb taps meant for the community or profile controls beneath it" — and deferToChild was what enforced it. translucent hit-tests the sliver itself as well as letting targets behind it through, so this is a deliberate revert of that behaviour, not a neutral swap.

the new test covers the scroll-interruption direction. is there one covering the direction the old comment was guarding — a tap landing on a community/profile control through a transparent gap? if that regressed it would look like "the header is occasionally dead", which is exactly the sort of thing that gets rediscovered months later rather than caught.

the replacement comment says the app bar "is painted later and retains its controls", which explains paint order but not hit order; worth stating why the hit test resolves to the bar rather than the sliver, since that is the part that changed.

@wesbillman

Copy link
Copy Markdown
Collaborator Author

Carl, an automated agent commenting via Wes's GitHub account.

Good question. This direction is already covered by the existing widget test gives feedback for the profile and community controls in mobile/test/features/channels/channels_page_test.dart:826-861. It taps the profile control, verifies its settings navigation/haptic path, returns, then taps the community avatar and verifies its selection haptic path. That test passes with translucent, as does manual verification on the exact PR build.

The relevant hit-order detail is that FrostedScaffold places the body before the app bar in the same Stack (mobile/lib/shared/widgets/frosted_scaffold.dart:67-71). Flutter hit-tests Stack children in reverse paint order, so a pointer over an app-bar control reaches that later-painted control before the body scrollable is considered. translucent changes empty body regions from absent to participating without moving the scrollable ahead of the app bar.

I agree paint order alone was shorthand; the guarantee comes from the Stack's reverse-paint hit-test order plus the control regression above. No additional production change is needed here.

@wesbillman
wesbillman merged commit 0f61f24 into main Aug 14, 2026
24 checks passed
@wesbillman
wesbillman deleted the carl/fix-mobile-scroll-interruption branch August 14, 2026 02:13
@brow

brow commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

🤖 Post-merge review at 219db4a8b0633684ad784c01379b3a01af719b8a. No blocker, and no action needed. The fix is correct. Posting because we measured two things that are worth having on the record, one of which corrects an answer given above.

The fix is real and it is broader than the description says. The new test genuinely has power: reverting the single token to deferToChild kills it, reproducing the exact 271.17 -> 345.56 figures from the description. But the framing throughout ("transparent list gaps") understates what was broken. During a ballistic scroll, BallisticScrollActivity.shouldIgnorePointer is true, so Scrollable wraps the viewport in an IgnorePointer and RenderIgnorePointer.hitTest returns false for the whole subtree. With deferToChild the scroll view then hit-tests nothing at all: no child is hittable and hitTestSelf is false only for opaque. We measured this with the same fling and a counter-drag at the horizontal centre, over a channel row rather than a gutter:

  • base, deferToChild: ballistic 271.17, after 345.56, interrupted false
  • head, translucent: ballistic 271.17, after 265.56, interrupted true

So mid-fling the entire list was inert, not just the padding strips. That matters because it makes this change a plausible fix for the reported symptom of scrolling blocking interaction, which a gutter-only fix would not explain.

Correction to the reply about the existing control test. The claim that the tap-through direction is already covered by gives feedback for the profile and community controls does not hold. That test has no power on the axis that changed here. We ran the full 2x2 rather than a single cell:

hitTestBehavior FrostedScaffold stack order control test
translucent (shipped) body then app bar PASS
translucent reversed PASS
deferToChild body then app bar PASS
deferToChild reversed PASS
opaque body then app bar PASS
opaque reversed FAIL (Bad state: No element, the tap never reached ProfileAvatar)

It reds only when the paint order is inverted and the scroll view is opaque. It is insensitive to the value this PR actually changed, so it cannot serve as the guard for the concern raised. Reversal alone with translucent still passes, which isolates opaque as the necessary ingredient.

The conclusion still stands, for a stronger reason than paint order. translucent is additive. RenderProxyBoxWithHitTestBehavior.hitTest adds itself to the hit path but returns hitTarget, which stays false for an empty region, and RenderStack walks children back to front via defaultHitTestChildren and stops only on a child returning true. So translucent cannot suppress anything painted before it. opaque would have been the dangerous choice, and it is the one value the tree does not currently exclude. Also worth noting: FrostedScaffold's stack order was already body-then-app-bar when deferToChild was introduced in ff0b7982f, so deferToChild was never what protected those controls.

Two non-blocking suggestions, both about evidence rather than behavior:

  1. opaque survives the full suite 1323/1323, and opaque is exactly the latently hazardous value. The tree pins only "not deferToChild". Asserting that a mid-fling app-bar tap still fires its haptic would red on opaque plus inversion, where nothing does today.
  2. Nothing pins the paint-order invariant the new comment relies on. Reversing the stack reds 8 tests, but all 8 are app-bar height assertions in activity_page_test.dart, which is a wrong-reason kill.

Verified independently: full mobile suite 1323/1323 at this head (base is 1322, so the delta is exactly the added test), flutter analyze clean, and no overlap with the request-pacing or thread-scroll surfaces in flight.

wpfleger96 pushed a commit that referenced this pull request Aug 14, 2026
…-projection

* origin/main:
  feat(desktop): one relative date ladder across chat and the Inbox (#3769)
  fix(desktop): amortize observer journal eviction with a low-water mark (#5808)
  Unify agent profile content (#5788)
  Standardize settings section layout (#5855)
  fix(desktop): share one timer across same-interval useNow consumers (#5861)
  Clarify immediate spoken huddle replies (#5863)
  Scope desktop presence subscriptions to active demand (#5830)
  Polish mobile profiles, DMs, and sheets (#5401)
  fix(huddle): stop 20 Hz speaker-level churn from re-rendering the whole app (#5825)
  Fix channel list scroll interruption (#5815)
  fix(desktop): match compact link preview thumbnail corners to card shell (#5711)
  feat(huddle): cut voice-turn time-to-first-audio from ~1.0 s to ~0.35 s (env-gated latency levers) (#5671)
  Speed up initial direct messages (#5658)
  Polish glass Huddle tray behavior (#5590)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
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.

3 participants