2025-05-01 02:28:24 -07:00
|
|
|
import useSWR from 'swr';
|
2025-09-07 00:04:51 +01:00
|
|
|
import { useRef, useEffect, useCallback, useState } from 'react';
|
2025-05-01 02:28:24 -07:00
|
|
|
|
|
|
|
|
type ScrollFlag = ScrollBehavior | false;
|
|
|
|
|
|
|
|
|
|
export function useScrollToBottom() {
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const endRef = useRef<HTMLDivElement>(null);
|
2025-09-07 00:04:51 +01:00
|
|
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
2025-05-01 02:28:24 -07:00
|
|
|
|
|
|
|
|
const { data: scrollBehavior = false, mutate: setScrollBehavior } =
|
|
|
|
|
useSWR<ScrollFlag>('messages:should-scroll', null, { fallbackData: false });
|
|
|
|
|
|
2025-09-07 00:04:51 +01:00
|
|
|
const handleScroll = useCallback(() => {
|
|
|
|
|
if (!containerRef.current) return;
|
|
|
|
|
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
2025-09-09 15:44:07 -04:00
|
|
|
|
2025-09-07 00:04:51 +01:00
|
|
|
// Check if we are within 100px of the bottom (like v0 does)
|
|
|
|
|
setIsAtBottom(scrollTop + clientHeight >= scrollHeight - 100);
|
2025-09-16 11:41:52 +01:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!containerRef.current) return;
|
|
|
|
|
|
|
|
|
|
const container = containerRef.current;
|
|
|
|
|
|
|
|
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
handleScroll();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const mutationObserver = new MutationObserver(() => {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
handleScroll();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
resizeObserver.observe(container);
|
|
|
|
|
mutationObserver.observe(container, {
|
|
|
|
|
childList: true,
|
|
|
|
|
subtree: true,
|
|
|
|
|
attributes: true,
|
|
|
|
|
attributeFilter: ['style', 'class', 'data-state']
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
handleScroll();
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
resizeObserver.disconnect();
|
|
|
|
|
mutationObserver.disconnect();
|
|
|
|
|
};
|
|
|
|
|
}, [handleScroll]);
|
2025-09-07 00:04:51 +01:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const container = containerRef.current;
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
container.addEventListener('scroll', handleScroll);
|
|
|
|
|
handleScroll(); // Check initial state
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
container.removeEventListener('scroll', handleScroll);
|
|
|
|
|
};
|
|
|
|
|
}, [handleScroll]);
|
|
|
|
|
|
2025-05-01 02:28:24 -07:00
|
|
|
useEffect(() => {
|
2025-09-07 00:04:51 +01:00
|
|
|
if (scrollBehavior && containerRef.current) {
|
|
|
|
|
const container = containerRef.current;
|
|
|
|
|
const scrollOptions: ScrollToOptions = {
|
|
|
|
|
top: container.scrollHeight,
|
2025-09-09 15:44:07 -04:00
|
|
|
behavior: scrollBehavior,
|
2025-09-07 00:04:51 +01:00
|
|
|
};
|
|
|
|
|
container.scrollTo(scrollOptions);
|
2025-05-01 02:28:24 -07:00
|
|
|
setScrollBehavior(false);
|
|
|
|
|
}
|
2025-09-07 00:04:51 +01:00
|
|
|
}, [scrollBehavior, setScrollBehavior]);
|
2025-05-01 02:28:24 -07:00
|
|
|
|
|
|
|
|
const scrollToBottom = useCallback(
|
|
|
|
|
(scrollBehavior: ScrollBehavior = 'smooth') => {
|
|
|
|
|
setScrollBehavior(scrollBehavior);
|
|
|
|
|
},
|
|
|
|
|
[setScrollBehavior],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
function onViewportEnter() {
|
|
|
|
|
setIsAtBottom(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onViewportLeave() {
|
|
|
|
|
setIsAtBottom(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
containerRef,
|
|
|
|
|
endRef,
|
|
|
|
|
isAtBottom,
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
onViewportEnter,
|
|
|
|
|
onViewportLeave,
|
|
|
|
|
};
|
|
|
|
|
}
|