Skip to content

fix(desktop): keep bottom pins through virtualizer settle#1328

Draft
tlongwell-block wants to merge 1 commit into
mainfrom
mari/scroll-pin-fix
Draft

fix(desktop): keep bottom pins through virtualizer settle#1328
tlongwell-block wants to merge 1 commit into
mainfrom
mari/scroll-pin-fix

Conversation

@tlongwell-block

@tlongwell-block tlongwell-block commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the desktop timeline bottom-pin race where programmatic bottom jumps could be reclassified as a message anchor while the virtualizer was still settling row measurements.

Changes:

  • Treat every imperative bottom jump as a settle window, not just smooth scroll.
  • During the settle window, chase the physical scroll floor and keep the anchor at bottom until the gap is truly near zero.
  • Arm the same settle guard for the force-bottom-on-send append path.
  • Add a deterministic unit test for the extracted settle helper so the core behavior is pinned without relay/Playwright infrastructure.

Validation

  • cd desktop && pnpm typecheck
  • cd desktop && pnpm exec biome check src/features/messages/ui/useAnchoredScroll.ts src/features/messages/ui/useAnchoredScroll.test.mjs
  • cd desktop && pnpm test -- src/features/messages/ui/useAnchoredScroll.test.mjs ✅ (runs desktop unit suite; 1196 passed including new tests)
  • Patched relay-backed targeted run, single-shell relay + fresh DB:
    pnpm exec playwright test --project=integration stream.spec.ts -g "stays pinned after you send a message|keeps scroll position when new messages arrive above the fold" --repeat-each=5 --retries=0 --reporter=list ✅ 10/10

Mutate → red → restore

Mutated settleProgrammaticBottomPin back to the old behavior: no floor-chasing scrollTo, and clear based on loose isAtBottomNow instead of strict physical-bottom check.

Result: the new deterministic unit tests went red:

  • settleProgrammaticBottomPin chases the physical floor before clearing failed because no scroll write happened.
  • settleProgrammaticBottomPin keeps settling when the floor is still out of reach failed because the loose threshold returned true with a 2px physical gap.

Restored the helper and re-ran typecheck/biome/unit suite green.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
tlongwell-block pushed a commit that referenced this pull request Jun 27, 2026
…oexists in two communities

Conformance matrix row `channels_membership` (re-routed from Mari to
Quinn at Eva's call — Mari's #1328 scroll-fix is still landing on main,
and the row's substrate is the same same-UUID-in-two-communities shape I
already used as the setup for `search_fts`). Fills the
`pending_lane("buzz-db", ...)` stub at
`crates/buzz-test-client/tests/conformance_multitenant.rs::mod
channels_membership`.

The row's scope is the **positive arm** of the same `is_member_cached`
scope branch that `row_zero_host_binding`'s `#h` override-attempt row
exercises as the **negative arm**. Sibling-not-replacement, per the
frame Dawn established when cold-reading row_zero (b): row_zero proves
the override-attempt fails closed against `get_channel(A, U) == None`;
this row proves the coexistence positive — when U exists in *both* A
and B (legal under the `(community_id, id)` PK), `get_channel` finds
the right per-community row and each community's posts land in its own
instance.

A bug that resolves `get_channel`/`is_member_cached` against the
claimed community instead of the host-derived one would pass row_zero
(b)'s negative-arm test (rejection still happens for some reason) but
fail this row's positive-arm test (A's post might land in B's channel
or be returned to B's query). So this row catches a class of bugs
row_zero (b) structurally cannot, even though both share the
`is_member_cached` scope branch.

Shape:

  1. One keypair shared across both communities — proves the fence is
     `community_id`, not `pubkey`.
  2. Same channel UUID `U` created in both A and B via REST kind:9007.
  3. Same key posts kind:9 with community-distinct content to U on
     each WS-AUTH'd connection ("A message in shared-UUID channel" /
     "B message in shared-UUID channel"). Distinct content per the
     named setup-equivalence-vacuity lesson in
     `landed-on-head-discipline` — without it, distinct rows would
     collide on Nostr event id (hash includes content; community is
     server-side provenance, not in the hash) and a leak would be
     indistinguishable from the honest path on the wire.
  4. REST `POST /query` with `{kinds:[9], #h:[U]}` against each host.
  5. Each side: count == 1, content == own community's. A leak surfaces
     as count == 2 (both rows returned through shared `#h: U` filter)
     OR content mismatch on count == 1.

Bar (by my own hands against the live `:3100` harness, PR head
`6aa0cec4a`):

  Clean → GREEN.
  Mutate (single fence — single-fence-per-path topology here, unlike
    search_fts's defense-in-depth):
    - crates/buzz-db/src/event.rs:266-270 — `query_events` non-p-tag
      branch `WHERE community_id = ` → `WHERE TRUE` (using the
      `let _ = q.community_id;` pattern Eva established on row_zero,
      one of the three honest sidesteps for the param-count trap I
      flagged in my prior message; the other two are renumbering and
      `( IS NOT NULL)`).
  → RED on `hits_a.len() == 1` with the failure message listing both
    contents:
      ["B message in shared-UUID channel",
       "A message in shared-UUID channel"]
    Distinct content makes the leak observable as B's message
    surfacing inside A's wire response.
  Restore → diff empty → GREEN.

Bar checklist:
- `cargo fmt -p buzz-test-client -- --check`: exit 0.
- `cargo clippy -p buzz-test-client --tests -- -D warnings`: clean.
- `cargo test -p buzz-test-client --tests`: full package non-ignored
  green (4 nip42_host_binding_live tests are #[ignore]'d by design).
- Strict-FF onto PR head: this is +1 on `6aa0cec4a`, verified by
  `git merge-base --is-ancestor` + `git rev-list --count`.
- Trailers preserved as single pair via initial `--trailer` flag
  (not `--amend --trailer`, which doubled them earlier in the session).
- Live two-host harness on `:3100`: my clean (post-restore) binary,
  recipe per RESEARCH/CONFORMANCE_MATRIX_STATUS_2026-06-27.md v3.

Conformance lane: this row is one of fourteen in the file; per Eva's
row-ownership contract, this commit touches only the
`channels_membership` module. No other rows modified.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.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.

1 participant