182 lines
4.9 KiB
TypeScript
182 lines
4.9 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
|
|
import { Button, Icon, IconName, Spinner } from "@/components/ui";
|
|
import { useChat } from "@/providers/chat-provider";
|
|
|
|
import { ChatMessage, Suggestions } from "..";
|
|
|
|
import styles from "./ChatMessagesWrapper.module.scss";
|
|
|
|
export default function ChatMessagesWrapper() {
|
|
const messagesWrapperRef = useRef<HTMLDivElement>(null);
|
|
|
|
const [isScrolledUp, setIsScrolledUp] = useState(false);
|
|
|
|
const {
|
|
messages: socketMessages,
|
|
isLoadingAdvisorMessage,
|
|
hasMoreOlderMessages,
|
|
isLoadingOlder,
|
|
// unreadMessagesCount,
|
|
loadOlder,
|
|
send,
|
|
} = useChat();
|
|
|
|
const messages = useMemo(() => {
|
|
const msgs = [...socketMessages];
|
|
if (isLoadingAdvisorMessage) {
|
|
msgs.unshift({
|
|
id: "typing",
|
|
type: "text",
|
|
text: "…",
|
|
role: "assistant",
|
|
isRead: false,
|
|
createdDate: new Date().toISOString(),
|
|
});
|
|
}
|
|
return msgs;
|
|
}, [isLoadingAdvisorMessage, socketMessages]);
|
|
|
|
const virtualizer = useVirtualizer({
|
|
enabled: messages.length > 0,
|
|
count: hasMoreOlderMessages ? messages.length + 1 : messages.length,
|
|
getScrollElement: () => messagesWrapperRef.current,
|
|
measureElement: el => el.getBoundingClientRect().height,
|
|
getItemKey: idx => messages[idx]?.id ?? idx,
|
|
estimateSize: _i => 100,
|
|
overscan: 5,
|
|
paddingStart: 36,
|
|
paddingEnd: 36,
|
|
gap: 8,
|
|
});
|
|
|
|
const items = virtualizer.getVirtualItems();
|
|
|
|
const scrollToBottom = useCallback(() => {
|
|
virtualizer.scrollToOffset(0);
|
|
}, [virtualizer]);
|
|
|
|
useEffect(() => {
|
|
const handleScroll = (e: WheelEvent) => {
|
|
e.preventDefault();
|
|
const currentTarget = e.currentTarget as HTMLElement;
|
|
|
|
if (currentTarget) {
|
|
currentTarget.scrollTop -= e.deltaY;
|
|
}
|
|
};
|
|
messagesWrapperRef.current?.addEventListener("wheel", handleScroll, {
|
|
passive: false,
|
|
});
|
|
return () => {
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
messagesWrapperRef.current?.removeEventListener("wheel", handleScroll);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const [lastItem] = [...items].reverse();
|
|
|
|
if (!lastItem) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
lastItem.index >= messages.length - 1 &&
|
|
hasMoreOlderMessages &&
|
|
!isLoadingOlder
|
|
) {
|
|
loadOlder();
|
|
}
|
|
}, [hasMoreOlderMessages, loadOlder, messages.length, isLoadingOlder, items]);
|
|
|
|
useEffect(() => {
|
|
if (!messagesWrapperRef.current || messages.length === 0) return;
|
|
|
|
setIsScrolledUp((virtualizer.scrollOffset || 0) > 600);
|
|
}, [virtualizer.scrollOffset, messages.length, messagesWrapperRef]);
|
|
|
|
return (
|
|
<>
|
|
{isScrolledUp && (
|
|
<Button
|
|
className={styles.scrollToBottomButton}
|
|
onClick={scrollToBottom}
|
|
aria-label="Scroll to bottom"
|
|
style={{
|
|
top: `${messagesWrapperRef.current?.clientHeight}px`,
|
|
}}
|
|
>
|
|
<Icon
|
|
name={IconName.Chevron}
|
|
style={{ transform: "rotate(-90deg)" }}
|
|
/>
|
|
{/* {!!unreadMessagesCount && (
|
|
<Badge className={styles.badge}>
|
|
<Typography weight="semiBold" size="xs" color="black">
|
|
{unreadMessagesCount > 99 ? "99+" : unreadMessagesCount}
|
|
</Typography>
|
|
</Badge>
|
|
)} */}
|
|
</Button>
|
|
)}
|
|
<div ref={messagesWrapperRef} className={styles.messagesWrapper}>
|
|
<Suggestions
|
|
className={styles.suggestions}
|
|
onSuggestionClick={suggestion => {
|
|
send(suggestion);
|
|
}}
|
|
/>
|
|
<div
|
|
style={{
|
|
height: virtualizer.getTotalSize(),
|
|
width: "100%",
|
|
position: "relative",
|
|
}}
|
|
>
|
|
{items.map(virtualRow => {
|
|
const message = messages[virtualRow.index];
|
|
const isLoaderRow = virtualRow.index > messages.length - 1;
|
|
|
|
return (
|
|
<div
|
|
key={virtualRow.key}
|
|
data-index={virtualRow.index}
|
|
ref={virtualizer.measureElement}
|
|
style={{
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
width: "100%",
|
|
paddingInline: "16px",
|
|
transform: `translateY(${virtualRow.start}px) scaleY(-1)`,
|
|
}}
|
|
>
|
|
{!isLoaderRow && (
|
|
<ChatMessage key={message.id} message={message} />
|
|
)}
|
|
{isLoaderRow && (
|
|
<div className={styles.loaderTop}>
|
|
<Spinner size={16} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export const ChatMessagesWrapperLoader = () => {
|
|
return (
|
|
<div className={styles.loaderContainer}>
|
|
<Spinner />
|
|
</div>
|
|
);
|
|
};
|