Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions desktop/src/features/messages/ui/TimelineMessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -440,8 +440,11 @@ function VirtualizedTimelineRows({
(version: number) => version + 1,
0,
);
const { cancel: cancelBottomSettle, settle: settleAtBottom } =
useVirtualizedBottomSettle(hostRef, listRef, itemsLengthRef);
const {
cancel: cancelBottomSettle,
hasBottomIntent,
settle: settleAtBottom,
} = useVirtualizedBottomSettle(hostRef, listRef, itemsLengthRef);
const { arm: armUpwardMomentum } = useUpwardPaginationWheel(
hostRef,
cancelBottomSettle,
Expand Down Expand Up @@ -549,14 +552,23 @@ function VirtualizedTimelineRows({
// emit `onScroll` without any user input. Cancelling here strands the
// channel above its newest message. The settle hook's wheel, pointer,
// touch, and key listeners are the authoritative user-interaction gate.
onAtBottomStateChange?.(distanceFromBottom <= 32);
const atBottom = distanceFromBottom <= 32;
// Geometry remeasurement can emit an intermediate offset while the
// bottom-settle loop still owns the scroller. Reporting that transient as
// reader navigation freezes live arrivals until the user sends or jumps
// to the new-message pill. Only a real input transfer (which cancels
// bottom intent) may report a non-bottom state.
if (atBottom || !hasBottomIntent()) {
onAtBottomStateChange?.(atBottom);
}
if (offset <= 200) {
// Layout scrolls near the top must not poison the reader's next input.
armUpwardMomentum(onStartReached?.() ?? false);
}
},
[
armUpwardMomentum,
hasBottomIntent,
onAtBottomStateChange,
onStartReached,
onVirtualizerRangeChanged,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ async function mountHarness() {

test("bottom intent follows arbitrarily late virtual geometry changes", async () => {
const { content, refs, root, scroller, writes } = await mountHarness();
assert.equal(refs.api.current.hasBottomIntent(), false);
refs.api.current.settle();
assert.equal(refs.api.current.hasBottomIntent(), true);
assert.deepEqual(writes, [{ index: 4, options: { align: "end" } }]);

const geometryObserver = resizeObservers.find((observer) =>
Expand Down Expand Up @@ -195,6 +197,7 @@ for (const eventType of ["pointerdown", "touchmove", "wheel", "keydown"]) {
: undefined,
type: eventType,
});
assert.equal(refs.api.current.hasBottomIntent(), false);

resizeObservers
.find((observer) => observer.targets?.includes(content))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export function useVirtualizedBottomSettle(
pinToBottom();
}, [cancelFrame, pinToBottom]);

const hasBottomIntent = React.useCallback(() => bottomIntentRef.current, []);

React.useEffect(() => cancel, [cancel]);
return { cancel, settle };
return { cancel, hasBottomIntent, settle };
}