| index 8254510..01e00a7 100644 |
| --- a/web/static/chat.js |
| +++ b/web/static/chat.js |
| @@ -895,6 +895,35 @@ function renderApp() { |
| feedForBlur.addEventListener("pointerdown", () => { |
| if (document.activeElement === text) text.blur(); |
| }); |
| + // Keep the feed pinned to the bottom when its viewport resizes. The stack |
| + // below it — autogrowing textarea, :emoji/@mention typeahead, reply/edit |
| + // banners, the keyboard inset — sits in normal flow, so any of it growing |
| + // shrinks the feed *without firing a scroll event*, leaving the chat wedged |
| + // not-quite-at-bottom while you type. `stickBottom` is refreshed on scroll, |
| + // so at resize time it reflects where the user was before the change; the |
| + // re-pin fires its own scroll event, which keeps the flag true. |
| + if (feedForBlur) { |
| + let stickBottom = true; // feeds open pinned to the latest message |
| + feedForBlur.addEventListener( |
| + "scroll", |
| + () => { |
| + stickBottom = feedForBlur.scrollHeight - feedForBlur.scrollTop - feedForBlur.clientHeight < 80; |
| + }, |
| + { passive: true }, |
| + ); |
| + new ResizeObserver(() => { |
| + if (!stickBottom) return; |
| + feedForBlur.scrollTop = feedForBlur.scrollHeight; |
| + // WebKit restores the pre-resize scroll position *after* observers run, |
| + // undoing the line above (Chrome doesn't) — re-assert on the next frame, |
| + // once its restoration has happened. The interim scroll event leaves |
| + // stickBottom true (the gap is far below the 80px threshold), so this |
| + // never fights a user who scrolled away. |
| + requestAnimationFrame(() => { |
| + if (stickBottom) feedForBlur.scrollTop = feedForBlur.scrollHeight; |
| + }); |
| + }).observe(feedForBlur); |
| + } |
| // A tap on the composer's own blank padding must never close the keyboard. |
| // The .send-btn::after hit extension is clipped by the field's overflow:hidden, |
| // so the gutter just right of Send falls through to here — treat a near-miss |