Improve chat stream scroll escape

This commit is contained in:
Federico Jaramillo Martinez
2026-05-09 00:26:10 +02:00
parent 1360c40e70
commit 0d10f7f829
+52 -5
View File
@@ -40,6 +40,8 @@ export class ChatView extends LitElement {
@state() private loadedScrollPercent = 100; @state() private loadedScrollPercent = 100;
private suppressScrollSave = false; private suppressScrollSave = false;
private saveScrollTimer?: number; private saveScrollTimer?: number;
private lastScrollTop = 0;
private touchStartY: number | undefined;
override disconnectedCallback(): void { override disconnectedCallback(): void {
window.clearTimeout(this.saveScrollTimer); window.clearTimeout(this.saveScrollTimer);
@@ -48,7 +50,7 @@ export class ChatView extends LitElement {
protected override willUpdate(changed: Map<string, unknown>): void { protected override willUpdate(changed: Map<string, unknown>): void {
if (changed.has("sessionId")) this.openGroupKeys = this.readOpenGroupKeys(); if (changed.has("sessionId")) this.openGroupKeys = this.readOpenGroupKeys();
this.pinnedToBottom = this.isNearBottom(); if (changed.has("messages")) this.pinnedToBottom = this.pinnedToBottom && this.isNearBottom();
} }
protected override updated(changed: Map<string, unknown>): void { protected override updated(changed: Map<string, unknown>): void {
@@ -61,7 +63,7 @@ export class ChatView extends LitElement {
return html` return html`
<div class="chat-wrap"> <div class="chat-wrap">
${this.renderHistoryIndicator()} ${this.renderHistoryIndicator()}
<div class="chat" @scroll=${() => { this.onScroll(); }}> <div class="chat" @scroll=${() => { this.onScroll(); }} @wheel=${(event: WheelEvent) => { this.onWheel(event); }} @touchstart=${(event: TouchEvent) => { this.onTouchStart(event); }} @touchmove=${(event: TouchEvent) => { this.onTouchMove(event); }}>
${this.renderHistoryBoundary()} ${this.renderHistoryBoundary()}
${groupChatMessages(this.messages, this.messageStart).map((group) => group.kind === "message" ${groupChatMessages(this.messages, this.messageStart).map((group) => group.kind === "message"
? this.renderMessage(group.message, group.index) ? this.renderMessage(group.message, group.index)
@@ -213,10 +215,33 @@ export class ChatView extends LitElement {
private onScroll() { private onScroll() {
this.updateLoadedScrollPercent(); this.updateLoadedScrollPercent();
if (this.chat && this.chat.scrollTop < 64 && this.hasMore && !this.loadingMore) this.onLoadMore?.(); if (this.chat && this.chat.scrollTop < 64 && this.hasMore && !this.loadingMore) this.onLoadMore?.();
this.pinnedToBottom = this.isNearBottom(); this.updatePinnedToBottomFromScroll();
if (!this.suppressScrollSave) this.scheduleScrollPositionSave(); if (!this.suppressScrollSave) this.scheduleScrollPositionSave();
} }
private onWheel(event: WheelEvent) {
if (event.deltaY < 0 && this.canScrollUp()) this.pinnedToBottom = false;
}
private onTouchStart(event: TouchEvent) {
this.touchStartY = event.touches[0]?.clientY;
}
private onTouchMove(event: TouchEvent) {
const y = event.touches[0]?.clientY;
if (this.touchStartY !== undefined && y !== undefined && y > this.touchStartY && this.canScrollUp()) this.pinnedToBottom = false;
}
private updatePinnedToBottomFromScroll() {
const chat = this.chat;
if (!chat) return;
const scrollingUp = chat.scrollTop < this.lastScrollTop;
if (this.isAtBottom()) this.pinnedToBottom = true;
else if (scrollingUp) this.pinnedToBottom = false;
else this.pinnedToBottom = this.isNearBottom();
this.lastScrollTop = chat.scrollTop;
}
private updateLoadedScrollPercent(): void { private updateLoadedScrollPercent(): void {
const chat = this.chat; const chat = this.chat;
if (!chat) return; if (!chat) return;
@@ -228,7 +253,22 @@ export class ChatView extends LitElement {
private isNearBottom(): boolean { private isNearBottom(): boolean {
const chat = this.chat; const chat = this.chat;
if (!chat) return true; if (!chat) return true;
return chat.scrollHeight - chat.scrollTop - chat.clientHeight < 48; return this.distanceFromBottom(chat) < 48;
}
private isAtBottom(): boolean {
const chat = this.chat;
if (!chat) return true;
return this.distanceFromBottom(chat) < 2;
}
private canScrollUp(): boolean {
const chat = this.chat;
return chat !== undefined && chat.scrollTop > 0;
}
private distanceFromBottom(chat: HTMLDivElement): number {
return chat.scrollHeight - chat.scrollTop - chat.clientHeight;
} }
private scrollToBottom() { private scrollToBottom() {
@@ -237,6 +277,7 @@ export class ChatView extends LitElement {
if (!chat) return; if (!chat) return;
this.withSuppressedScrollSave(() => { this.withSuppressedScrollSave(() => {
chat.scrollTop = chat.scrollHeight; chat.scrollTop = chat.scrollHeight;
this.lastScrollTop = chat.scrollTop;
}); });
}); });
} }
@@ -247,7 +288,10 @@ export class ChatView extends LitElement {
const stored = this.readStoredScrollPosition(); const stored = this.readStoredScrollPosition();
if (!chat || !stored) { if (!chat || !stored) {
this.withSuppressedScrollSave(() => { this.withSuppressedScrollSave(() => {
if (chat) chat.scrollTop = chat.scrollHeight; if (chat) {
chat.scrollTop = chat.scrollHeight;
this.lastScrollTop = chat.scrollTop;
}
}); });
return; return;
} }
@@ -256,6 +300,7 @@ export class ChatView extends LitElement {
if (!article) { if (!article) {
this.withSuppressedScrollSave(() => { this.withSuppressedScrollSave(() => {
chat.scrollTop = chat.scrollHeight; chat.scrollTop = chat.scrollHeight;
this.lastScrollTop = chat.scrollTop;
}); });
return; return;
} }
@@ -263,6 +308,7 @@ export class ChatView extends LitElement {
const chatTop = chat.getBoundingClientRect().top; const chatTop = chat.getBoundingClientRect().top;
const currentOffset = article.getBoundingClientRect().top - chatTop; const currentOffset = article.getBoundingClientRect().top - chatTop;
chat.scrollTop += currentOffset - stored.offset; chat.scrollTop += currentOffset - stored.offset;
this.lastScrollTop = chat.scrollTop;
}); });
}); });
} }
@@ -278,6 +324,7 @@ export class ChatView extends LitElement {
if (!chat || !anchor) return; if (!chat || !anchor) return;
this.withSuppressedScrollSave(() => { this.withSuppressedScrollSave(() => {
chat.scrollTop = anchor.scrollTop + (chat.scrollHeight - anchor.scrollHeight); chat.scrollTop = anchor.scrollTop + (chat.scrollHeight - anchor.scrollHeight);
this.lastScrollTop = chat.scrollTop;
}); });
this.updateLoadedScrollPercent(); this.updateLoadedScrollPercent();
} }