diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-01 23:34:49 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-01 23:34:49 +0200 |
| commit | 32855a95e11032302f8d24036f6f2dd44b829368 (patch) | |
| tree | f0c00979232c1fd4c5bad0ddd726fbbde15eff1f /CLAUDE.md | |
| parent | 799d87999c8324564dce5159191532e008dd93d2 (diff) | |
| download | meshbay-32855a95e11032302f8d24036f6f2dd44b829368.tar.gz | |
fix(hub): let the reader scroll up in the chat again
The chat could not be read back: any wheel gesture was undone in the frame
it happened in, and the "jump to latest" button never appeared.
None of the pins in ChatPanel are at fault -- every one of them is guarded
by "only if the reader is at the bottom". The reader never got to stop
being at the bottom.
fit() set the panel's height, read documentElement.scrollHeight back and
subtracted the overflow, so the document alternately did and did not
overflow the window. The page scrollbar appeared and vanished with it and
visualViewport fired resize at every pass -- the event fit() is bound to.
It therefore re-entered itself for the life of the panel: measured at 240
firings in two seconds on a page nobody was touching, against 2 for a bare
document. Each pass ran fitAndPin, which re-pinned the list to the bottom
before the scroll event that would have recorded the gesture was delivered
a frame later, so atBottomRef never went false.
- fit() learns the space below the panel once and remembers it on the
element instead of re-deriving it by writing and measuring back. At the
steady state it writes nothing, so it produces no resize. A real window
resize or an orientation change forgets the learnt value and measures
again (the page under the panel may have reflowed); visualViewport
deliberately does not, since a phone fires it constantly.
- The scroll-to-bottom is now scoped to *arrival*, which is all it was ever
for: opening the group, or coming back to the Chat tab, including the
thumbnails and link-preview cards that keep growing the list for a second
afterwards. It ends when the reader takes hold of the scroll, and the
ResizeObserver disconnects there.
- That release is recorded from the gesture (wheel/touchmove/pointerdown/
keydown), not from the scroll event, which arrives too late to protect
anything.
Unchanged: landing on the newest message, following new messages while
already at the bottom, the "load older" anchor and the unread marker.
tests/harness/chat_scroll_probe.py mounts the real ChatPanel in a browser
and reads a conversation back; test_chat_scroll_up.py asserts against it.
With the fix reverted, five of its six tests fail and the sixth -- landing
on the newest message -- still passes, which is the property that must not
have been traded away. A structural test cannot see any of this, which is
why it is measured.
test_layout_responsive.py pinned the listener's name and follows the
rename.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8oRqEHhnKUr1NfmTVdcyL
Diffstat (limited to 'CLAUDE.md')
| -rw-r--r-- | CLAUDE.md | 17 |
1 files changed, 17 insertions, 0 deletions
@@ -436,6 +436,22 @@ anything that assumes one key per person. `fit()` lifted out of `app.js` — the sizing code is never reimplemented in a test, or the test outlives the code it was written for +- **A handler bound to the event its own writes produce.** The chat panel's + `fit()` set a height, read `documentElement.scrollHeight` back and subtracted + the overflow — so the document alternately did and did not overflow the + window, the page scrollbar appeared and vanished with it, and `visualViewport` + fired `resize` at every pass. `fit()` listens to that event: it re-entered + itself ~120 times a second for the life of the panel (measured: 240 firings in + 2 s on an idle page, against 2 for a bare document). Each pass re-pinned the + list to the bottom, which undid every attempt to scroll up **inside the frame + it happened in** — before the `scroll` event that would have recorded it was + delivered, so `atBottomRef` never went false, no jump button appeared, and the + older messages were unreachable. Every pin in that file reads as correctly + guarded; the reader simply never got to stop being at the bottom. Mutate-then- + measure is a loop wherever layout can raise the event you are handling: learn + the correction once and write nothing in the steady state. And a scroll + position that must survive a gesture has to be released **by the gesture** + (`wheel`/`touchmove`/`pointerdown`), never by the `scroll` event alone - **A refusal that never rejects.** Denying Chromium's `fullscreen` permission does not make `requestFullscreen()` throw — the promise never settles. The deny-everything handler was written from a true sentence ("nothing here needs @@ -764,6 +780,7 @@ SFR residential Fedora 44 → meshbay.org OVH VPS: | Seeking (node) | `webrtc_server.py` | `start` on `stream_req`; `-ss` **before** `-i` (index seek, not decode-and-discard), clamped away from the end, echoed in `stream_init` | | Resume position | `static/video-player.js` | `readResumePosition` / `writeResumePosition` — localStorage, per file, per browser. No protocol, and nothing new learns what you watch | | Layout, measured | `tests/harness/layout_probe.py` | Renders `style.css` in Chrome at any width and returns bounding boxes. Use it for layout, not `test_layout_responsive.py`, which only pins CSS values | +| Chat scrolling, measured | `tests/harness/chat_scroll_probe.py` | Mounts the real `ChatPanel` in Chrome and reads a conversation back. Answers "can the reader scroll up" and "does the panel resize itself"; `test_chat_scroll_bottom.py` only pins the source's shape | | Session renewal (browser) | `static/hub-client.js` | `refreshAccessToken` / `ensureFreshToken` — one writer (`setAuth`), one in-flight renewal, rotated refresh token stored. `hubFetch` renews on 401 and replays. Moved out of app.js in the 2026-08-23 split | | Token lifetimes (hub) | `meshbay_hub.config` | `[jwt] access_token_ttl` 4 h, `refresh_token_ttl` 30 days. **Production sets both in `~/.config/meshbay/hub.toml`** — changing the code default alone does nothing there | |