(feat): add smooth scrolling
This commit is contained in:
parent
f3cf836fcd
commit
ff41763d76
2 changed files with 56 additions and 17 deletions
|
|
@ -29,8 +29,24 @@ function MessageComponent({
|
||||||
user: User | null;
|
user: User | null;
|
||||||
}) {
|
}) {
|
||||||
const actuallyFailed = message.failed && message.message_state === "awaiting";
|
const actuallyFailed = message.failed && message.message_state === "awaiting";
|
||||||
|
const [hasFadedIn, setHasFadedIn] = useState(false);
|
||||||
|
const opacityClass = !hasFadedIn
|
||||||
|
? "opacity-0"
|
||||||
|
: actuallyFailed || message.message_state === "awaiting"
|
||||||
|
? "opacity-50"
|
||||||
|
: "opacity-100";
|
||||||
|
|
||||||
const [isValidURL, setIsValidURL] = useState(false);
|
const [isValidURL, setIsValidURL] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = window.setTimeout(() => {
|
||||||
|
setHasFadedIn(true);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
if (message.content.split(" ").length > 1) throw new Error();
|
if (message.content.split(" ").length > 1) throw new Error();
|
||||||
|
|
@ -45,7 +61,7 @@ function MessageComponent({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
// pt-3 is to get a gap between messages
|
// pt-3 is to get a gap between messages
|
||||||
className={`${grouped ? "" : "pt-3"} w-full flex justify-start transition-opacity duration-150 ${actuallyFailed || message.message_state === "awaiting" ? "opacity-50" : ""}`}
|
className={`${grouped ? "" : "pt-3"} w-full flex justify-start transition-opacity duration-150 ${opacityClass}`}
|
||||||
>
|
>
|
||||||
<MessageContextMenu
|
<MessageContextMenu
|
||||||
content={message.content}
|
content={message.content}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
import { failedUser } from "@tensamin/shared/data";
|
import { failedUser } from "@tensamin/shared/data";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useUser, type User } from "@tensamin/user/context";
|
import { useUser, type User } from "@tensamin/user/context";
|
||||||
|
|
||||||
import InputComponent from "./components/input";
|
import InputComponent from "./components/input";
|
||||||
|
|
@ -55,6 +54,8 @@ export default function Screen() {
|
||||||
const didInitialScrollRef = React.useRef(false);
|
const didInitialScrollRef = React.useRef(false);
|
||||||
const userScrolledUpRef = React.useRef(false);
|
const userScrolledUpRef = React.useRef(false);
|
||||||
const isAtBottomRef = React.useRef(true);
|
const isAtBottomRef = React.useRef(true);
|
||||||
|
const smoothScrollFrameRef = React.useRef<number | null>(null);
|
||||||
|
const smoothScrollTargetRef = React.useRef(0);
|
||||||
|
|
||||||
const [messageUsers, setMessageUsers] = React.useState<
|
const [messageUsers, setMessageUsers] = React.useState<
|
||||||
Record<string, User | undefined>
|
Record<string, User | undefined>
|
||||||
|
|
@ -356,17 +357,52 @@ export default function Screen() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
smoothScrollTargetRef.current = element.scrollTop;
|
||||||
|
|
||||||
|
const animateScroll = () => {
|
||||||
|
const distance = smoothScrollTargetRef.current - element.scrollTop;
|
||||||
|
if (Math.abs(distance) < 0.5) {
|
||||||
|
element.scrollTop = smoothScrollTargetRef.current;
|
||||||
|
smoothScrollFrameRef.current = null;
|
||||||
|
handleContainerScroll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.scrollTop += distance * 0.35;
|
||||||
|
handleContainerScroll();
|
||||||
|
smoothScrollFrameRef.current = requestAnimationFrame(animateScroll);
|
||||||
|
};
|
||||||
|
|
||||||
const handleWheel = (event: WheelEvent) => {
|
const handleWheel = (event: WheelEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
element.scrollTop -= event.deltaY;
|
|
||||||
handleContainerScroll();
|
if (smoothScrollFrameRef.current === null) {
|
||||||
|
smoothScrollTargetRef.current = element.scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxScrollTop = Math.max(
|
||||||
|
0,
|
||||||
|
element.scrollHeight - element.clientHeight,
|
||||||
|
);
|
||||||
|
smoothScrollTargetRef.current = Math.min(
|
||||||
|
Math.max(0, smoothScrollTargetRef.current - event.deltaY),
|
||||||
|
maxScrollTop,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (smoothScrollFrameRef.current === null) {
|
||||||
|
smoothScrollFrameRef.current = requestAnimationFrame(animateScroll);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
element.addEventListener("wheel", handleWheel, { passive: false });
|
element.addEventListener("wheel", handleWheel, { passive: false });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
element.removeEventListener("wheel", handleWheel);
|
element.removeEventListener("wheel", handleWheel);
|
||||||
|
if (smoothScrollFrameRef.current !== null) {
|
||||||
|
cancelAnimationFrame(smoothScrollFrameRef.current);
|
||||||
|
smoothScrollFrameRef.current = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [handleContainerScroll]);
|
}, [handleContainerScroll]);
|
||||||
|
|
||||||
|
|
@ -391,19 +427,6 @@ export default function Screen() {
|
||||||
}}
|
}}
|
||||||
onScroll={handleContainerScroll}
|
onScroll={handleContainerScroll}
|
||||||
>
|
>
|
||||||
{messagesQuery.isPending && (
|
|
||||||
<div className="flex items-center justify-center pt-10 scale-y-[-1]">
|
|
||||||
<p className="text-foreground/45 flex gap-1 items-center justify-center">
|
|
||||||
<Loader2 size={18} className="animate-spin" />
|
|
||||||
Loading...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{messagesQuery.isFetchingNextPage && (
|
|
||||||
<div className="flex items-center justify-center pt-3 pb-1 scale-y-[-1]">
|
|
||||||
<Loader2 size={16} className="animate-spin text-foreground/45" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
<div
|
||||||
className="relative w-full"
|
className="relative w-full"
|
||||||
style={{ height: `${contentHeight}px` }}
|
style={{ height: `${contentHeight}px` }}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue