Skip to content

Commit 0269033

Browse files
committed
refac
1 parent 2991d9f commit 0269033

1 file changed

Lines changed: 119 additions & 6 deletions

File tree

src/lib/components/chat/Messages.svelte

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,119 @@
5757
5858
export let onSelect = (e) => {};
5959
60-
export let messagesCount: number | null = 20;
60+
export let messagesCount: number | null = 10;
6161
let messagesLoading = false;
6262
63+
// Off-screen message unloading. Heights are measured on scroll so spacers
64+
// always match real sizes — no scroll jumps, no feedback loops needed.
65+
const OVERSCAN = 3;
66+
const DEFAULT_HEIGHT = 150;
67+
let visibleStart = 0;
68+
let visibleEnd = 0;
69+
let messageHeights = new Map();
70+
let topSpacerHeight = 0;
71+
let bottomSpacerHeight = 0;
72+
let pendingCull = null;
73+
74+
// Helper: get height for a message (cached or default)
75+
const heightOf = (id) => messageHeights.get(id) ?? DEFAULT_HEIGHT;
76+
77+
/** Measure all currently rendered message elements and cache their heights */
78+
const measureMessageHeights = () => {
79+
const elements = document.getElementById('messages-container')?.querySelectorAll('[role="listitem"]');
80+
if (!elements) return;
81+
82+
messageHeights = new Map([
83+
...messageHeights,
84+
...Array.from(elements)
85+
.map((el, i) => [messages[visibleStart + i]?.id, el.getBoundingClientRect().height])
86+
.filter(([id]) => id != null)
87+
]);
88+
};
89+
90+
/** Compute visible range from current scroll position and apply */
91+
const updateVisibleRange = () => {
92+
const container = document.getElementById('messages-container');
93+
if (!container || messages.length === 0) return;
94+
95+
const st = container.scrollTop;
96+
const ch = container.clientHeight;
97+
98+
// Build prefix sums from measured heights
99+
const prefixSums = messages.reduce(
100+
(acc, m) => [...acc, acc[acc.length - 1] + heightOf(m.id)],
101+
[0]
102+
);
103+
104+
const firstVisible = Math.max(0, prefixSums.findIndex((h) => h > st) - 1);
105+
const lastVisible = prefixSums.findIndex((h) => h > st + ch);
106+
107+
// Only cull messages that have been measured (so spacer height is accurate)
108+
// findIndex returns -1 when all are measured → no limit on culling
109+
const firstUnmeasured = messages.findIndex((m) => !messageHeights.has(m.id));
110+
const cullLimit = firstUnmeasured === -1 ? messages.length : firstUnmeasured;
111+
112+
visibleStart = Math.max(0, Math.min(firstVisible - OVERSCAN, cullLimit));
113+
visibleEnd = Math.min(messages.length, (lastVisible === -1 ? messages.length : lastVisible) + OVERSCAN);
114+
topSpacerHeight = prefixSums[visibleStart] ?? 0;
115+
bottomSpacerHeight = (prefixSums[messages.length] ?? 0) - (prefixSums[visibleEnd] ?? 0);
116+
};
117+
118+
/** Scroll handler: measure every frame, cull via rAF (same throttle as pendingRebuild) */
119+
const handleContainerScroll = () => {
120+
measureMessageHeights();
121+
122+
// Don't cull during progressive loading
123+
if (messagesLoading) return;
124+
125+
if (!pendingCull) {
126+
pendingCull = requestAnimationFrame(() => {
127+
pendingCull = null;
128+
updateVisibleRange();
129+
});
130+
}
131+
};
132+
133+
let scrollListenerAttached = false;
134+
135+
const attachScrollListener = () => {
136+
if (scrollListenerAttached) return;
137+
const container = document.getElementById('messages-container');
138+
if (!container) return;
139+
140+
container.addEventListener('scroll', handleContainerScroll, { passive: true });
141+
scrollListenerAttached = true;
142+
};
143+
144+
onMount(() => {
145+
attachScrollListener();
146+
});
147+
148+
onDestroy(() => {
149+
const container = document.getElementById('messages-container');
150+
if (container && scrollListenerAttached) {
151+
container.removeEventListener('scroll', handleContainerScroll);
152+
}
153+
cancelAnimationFrame(pendingCull);
154+
cancelAnimationFrame(pendingRebuild);
155+
});
156+
63157
const loadMoreMessages = async () => {
64158
// scroll slightly down to disable continuous loading
65159
const element = document.getElementById('messages-container');
66160
element.scrollTop = element.scrollTop + 100;
67161
68162
messagesLoading = true;
69-
messagesCount += 20;
163+
messagesCount += 10;
164+
70165
buildMessages();
71166
167+
// Show all messages during progressive loading (no culling)
168+
visibleStart = 0;
169+
visibleEnd = messages.length;
170+
topSpacerHeight = 0;
171+
bottomSpacerHeight = 0;
172+
72173
await tick();
73174
74175
messagesLoading = false;
@@ -95,6 +196,7 @@
95196
}
96197
97198
messages = _messages.reverse();
199+
visibleEnd = messages.length;
98200
};
99201
100202
// Throttle message list rebuilds to once per animation frame during streaming.
@@ -113,6 +215,8 @@
113215
cancelAnimationFrame(pendingRebuild);
114216
pendingRebuild = null;
115217
buildMessages();
218+
// No explicit culling needed — scrollToBottom will fire a scroll event,
219+
// which triggers handleContainerScroll → rAF → updateVisibleRange
116220
} else if (_messages) {
117221
// Content update (streaming) — throttle to once per frame
118222
if (!pendingRebuild) {
@@ -426,9 +530,7 @@
426530
showMessage({ id: parentMessageId }, false);
427531
};
428532
429-
onDestroy(() => {
430-
cancelAnimationFrame(pendingRebuild);
431-
});
533+
432534
433535
const triggerScroll = () => {
434536
if (autoScroll) {
@@ -465,7 +567,13 @@
465567
</Loader>
466568
{/if}
467569
<ul role="log" aria-live="polite" aria-relevant="additions" aria-atomic="false">
468-
{#each messages as message, messageIdx (message.id)}
570+
<!-- Top spacer: sum of cached heights for messages above visible range -->
571+
{#if topSpacerHeight > 0}
572+
<div style="height: {topSpacerHeight}px" aria-hidden="true" />
573+
{/if}
574+
575+
{#each messages.slice(visibleStart, visibleEnd) as message, i (message.id)}
576+
{@const messageIdx = visibleStart + i}
469577
<Message
470578
{chatId}
471579
bind:history
@@ -494,6 +602,11 @@
494602
{topPadding}
495603
/>
496604
{/each}
605+
606+
<!-- Bottom spacer: sum of cached heights for messages below visible range -->
607+
{#if bottomSpacerHeight > 0}
608+
<div style="height: {bottomSpacerHeight}px" aria-hidden="true" />
609+
{/if}
497610
</ul>
498611
</section>
499612
<div class="pb-18" />

0 commit comments

Comments
 (0)