atagen

malice

public
Star0
Watch0

Commit d1818305ffd6

tarball

atagen <boss@atagen.co>2026-10-26 10:44 UTC

1 file changed+19-2
web: only unstick the timeline on scrolls the user made

When content shrank the browser clamped scrollTop, and that clamp's scroll event landed after newer rows had rendered, so it read as an upward user scroll with a gap and dropped stickToBottom while the room was still filling. Upward scrolls now unstick only within 400ms of wheel, touch, key or pointer input or while the scrollbar is held.
Modified file: web/src/App.svelte+19 additions−2 deletions
@@ -745,10 +745,24 @@
745745 void tick().then(() => timelineViewport?.restore());
746746 }
747747
748+ const USER_SCROLL_MS = 400;
749+ let userScrollAt = -Infinity;
750+ let scrollbarHeld = false;
751+
748752 function noteUserScroll(): void {
753+ userScrollAt = performance.now();
749754 timelineViewport?.releaseScroll();
750755 }
751756
757+ function onTimelinePointer(e: PointerEvent): void {
758+ if (e.target === e.currentTarget) scrollbarHeld = true;
759+ noteUserScroll();
760+ }
761+
762+ function releaseScrollbar(): void {
763+ scrollbarHeld = false;
764+ }
765+
752766 function onTimelineKey(e: KeyboardEvent): void {
753767 if (['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' '].includes(e.key)) noteUserScroll();
754768 }
@@ -764,7 +778,8 @@
764778 if (timelineViewport?.consumeScroll()) return;
765779 const gap = timelineEl.scrollHeight - top - timelineEl.clientHeight;
766780 const wasAtBottom = stickToBottom;
767- stickToBottom = (gap < 2 || (wasAtBottom && !scrolledUp)) && (timelineRows?.atTail() ?? true);
781+ const byUser = scrollbarHeld || performance.now() - userScrollAt < USER_SCROLL_MS;
782+ stickToBottom = (gap < 2 || (wasAtBottom && !(byUser && scrolledUp))) && (timelineRows?.atTail() ?? true);
768783 if (stickToBottom && gap >= 2) timelineViewport?.scrollToBottom(false);
769784 farFromLatest = gap > 240 || !(timelineRows?.atTail() ?? true);
770785 if (stickToBottom && !wasAtBottom) queueReadReceipt();
@@ -1963,6 +1978,8 @@
19631978 bind:innerWidth={viewportWidth}
19641979 on:keydown={onKeydown}
19651980 on:pointerdown={closeRoomMenuOutside}
1981+ on:pointerup={releaseScrollbar}
1982+ on:pointercancel={releaseScrollbar}
19661983 />
19671984
19681985 {#if !booted}
@@ -2659,7 +2676,7 @@
26592676 on:wheel={noteUserScroll}
26602677 on:touchmove={noteUserScroll}
26612678 on:keydown={onTimelineKey}
2662- on:pointerdown={noteUserScroll}
2679+ on:pointerdown={onTimelinePointer}
26632680 aria-label={`Messages in ${selectedRoom.name}`}
26642681 >
26652682 <div class="timeline-inner" use:pinTimeline>