fix: scroll behavior, browser back navigation, and weather widget sizing (#1336)

This commit is contained in:
josh 2025-11-30 03:48:27 +00:00 committed by GitHub
parent e90a6ee209
commit b3a155d2a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 157 additions and 121 deletions

View file

@ -1,102 +1,119 @@
import { useCallback, useEffect, useRef, useState } from "react";
import useSWR from "swr";
type ScrollFlag = ScrollBehavior | false;
export function useScrollToBottom() {
const containerRef = useRef<HTMLDivElement>(null);
const endRef = useRef<HTMLDivElement>(null);
const [isAtBottom, setIsAtBottom] = useState(true);
const isAtBottomRef = useRef(true);
const isUserScrollingRef = useRef(false);
const { data: scrollBehavior = false, mutate: setScrollBehavior } =
useSWR<ScrollFlag>("messages:should-scroll", null, { fallbackData: false });
// Keep ref in sync with state
useEffect(() => {
isAtBottomRef.current = isAtBottom;
}, [isAtBottom]);
const handleScroll = useCallback(() => {
const checkIfAtBottom = useCallback(() => {
if (!containerRef.current) {
return;
return true;
}
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
// Check if we are within 100px of the bottom (like v0 does)
setIsAtBottom(scrollTop + clientHeight >= scrollHeight - 100);
return scrollTop + clientHeight >= scrollHeight - 100;
}, []);
useEffect(() => {
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
if (!containerRef.current) {
return;
}
const container = containerRef.current;
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => {
handleScroll();
});
containerRef.current.scrollTo({
top: containerRef.current.scrollHeight,
behavior,
});
}, []);
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]);
// Handle user scroll events
useEffect(() => {
const container = containerRef.current;
if (!container) {
return;
}
container.addEventListener("scroll", handleScroll);
handleScroll(); // Check initial state
let scrollTimeout: ReturnType<typeof setTimeout>;
const handleScroll = () => {
// Mark as user scrolling
isUserScrollingRef.current = true;
clearTimeout(scrollTimeout);
// Update isAtBottom state
const atBottom = checkIfAtBottom();
setIsAtBottom(atBottom);
isAtBottomRef.current = atBottom;
// Reset user scrolling flag after scroll ends
scrollTimeout = setTimeout(() => {
isUserScrollingRef.current = false;
}, 150);
};
container.addEventListener("scroll", handleScroll, { passive: true });
return () => {
container.removeEventListener("scroll", handleScroll);
clearTimeout(scrollTimeout);
};
}, [handleScroll]);
}, [checkIfAtBottom]);
// Auto-scroll when content changes
useEffect(() => {
if (scrollBehavior && containerRef.current) {
const container = containerRef.current;
const scrollOptions: ScrollToOptions = {
top: container.scrollHeight,
behavior: scrollBehavior,
};
container.scrollTo(scrollOptions);
setScrollBehavior(false);
const container = containerRef.current;
if (!container) {
return;
}
}, [scrollBehavior, setScrollBehavior]);
const scrollToBottom = useCallback(
(currentScrollBehavior: ScrollBehavior = "smooth") => {
setScrollBehavior(currentScrollBehavior);
},
[setScrollBehavior]
);
const scrollIfNeeded = () => {
// Only auto-scroll if user was at bottom and isn't actively scrolling
if (isAtBottomRef.current && !isUserScrollingRef.current) {
requestAnimationFrame(() => {
container.scrollTo({
top: container.scrollHeight,
behavior: "instant",
});
setIsAtBottom(true);
isAtBottomRef.current = true;
});
}
};
// Watch for DOM changes
const mutationObserver = new MutationObserver(scrollIfNeeded);
mutationObserver.observe(container, {
childList: true,
subtree: true,
characterData: true,
});
// Watch for size changes
const resizeObserver = new ResizeObserver(scrollIfNeeded);
resizeObserver.observe(container);
// Also observe children for size changes
for (const child of container.children) {
resizeObserver.observe(child);
}
return () => {
mutationObserver.disconnect();
resizeObserver.disconnect();
};
}, []);
function onViewportEnter() {
setIsAtBottom(true);
isAtBottomRef.current = true;
}
function onViewportLeave() {
setIsAtBottom(false);
isAtBottomRef.current = false;
}
return {