feat(scroll): implement instant scroll-to-bottom behavior for chat messages (#438)
This commit is contained in:
committed by
GitHub
Unverified
parent
c0c8701cc3
commit
c0a3903377
45
src/hooks/use-min-loading.ts
Normal file
45
src/hooks/use-min-loading.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* A hook that ensures a loading state remains true for at least a minimum duration (e.g., 1000ms),
|
||||
* preventing flickering for very fast loading times.
|
||||
*
|
||||
* @param isLoading - The actual loading state from the data source
|
||||
* @param minDurationMs - Minimum duration to show loading (default: 1000)
|
||||
*/
|
||||
export function useMinLoading(isLoading: boolean, minDurationMs: number = 500) {
|
||||
const [showLoading, setShowLoading] = useState(isLoading);
|
||||
const startTime = useRef<number>(0);
|
||||
|
||||
// Guarantee that the loading UI activates immediately without any asynchronous delay
|
||||
if (isLoading && !showLoading) {
|
||||
setShowLoading(true);
|
||||
}
|
||||
|
||||
// Record the actual timestamp in an effect to respect React purity rules
|
||||
useEffect(() => {
|
||||
if (isLoading && startTime.current === 0) {
|
||||
startTime.current = Date.now();
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
if (!isLoading && showLoading) {
|
||||
const elapsed = startTime.current > 0 ? Date.now() - startTime.current : 0;
|
||||
const remaining = Math.max(0, minDurationMs - elapsed);
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
setShowLoading(false);
|
||||
startTime.current = 0;
|
||||
}, remaining);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
};
|
||||
}, [isLoading, showLoading, minDurationMs]);
|
||||
|
||||
return isLoading || showLoading;
|
||||
}
|
||||
58
src/hooks/use-stick-to-bottom-instant.ts
Normal file
58
src/hooks/use-stick-to-bottom-instant.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useStickToBottom } from "use-stick-to-bottom";
|
||||
|
||||
/**
|
||||
* A wrapper around useStickToBottom that ensures the initial scroll
|
||||
* to bottom happens instantly without any visible animation.
|
||||
*
|
||||
* @param resetKey - When this key changes, the scroll position will be reset to bottom instantly.
|
||||
* Typically this should be the conversation ID.
|
||||
*/
|
||||
export function useStickToBottomInstant(resetKey?: string) {
|
||||
const lastKeyRef = useRef(resetKey);
|
||||
const hasInitializedRef = useRef(false);
|
||||
|
||||
const result = useStickToBottom({
|
||||
initial: "instant",
|
||||
resize: "smooth",
|
||||
});
|
||||
|
||||
const { scrollRef } = result;
|
||||
|
||||
// Reset initialization when key changes
|
||||
useEffect(() => {
|
||||
if (resetKey !== lastKeyRef.current) {
|
||||
hasInitializedRef.current = false;
|
||||
lastKeyRef.current = resetKey;
|
||||
}
|
||||
}, [resetKey]);
|
||||
|
||||
// Scroll to bottom instantly on mount or when key changes
|
||||
useEffect(() => {
|
||||
if (hasInitializedRef.current) return;
|
||||
|
||||
const scrollElement = scrollRef.current;
|
||||
if (!scrollElement) return;
|
||||
|
||||
// Hide, scroll, reveal pattern to avoid visible animation
|
||||
scrollElement.style.visibility = "hidden";
|
||||
|
||||
// Use double RAF to ensure content is rendered
|
||||
const frame1 = requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// Direct scroll to bottom
|
||||
scrollElement.scrollTop = scrollElement.scrollHeight;
|
||||
|
||||
// Small delay to ensure scroll is applied
|
||||
setTimeout(() => {
|
||||
scrollElement.style.visibility = "";
|
||||
hasInitializedRef.current = true;
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
return () => cancelAnimationFrame(frame1);
|
||||
}, [scrollRef, resetKey]);
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user