folks, I am building a chat app. Currently, I am working on a feature in which when the user scrolls a sticky date is shown to the top, based on the date of the messages visible in the viewport area. But I don't know how to get the index of the last item visible in the viewport.
Here is the codesandbox
Here is the code
const MessageComponent = () => {
const { id } = useParams();
const {
data: messageData,
isSuccess,
isError,
isLoading,
isFetching,
hasMoreData,
firstData,
isUninitialized,
} = useGetData(id);
const { loadMore } = useGetScrollData(messageData, id);
const user = User();
const chatId =
messageData[0]?.type == "date"
? messageData[1]?.chat?._id
: messageData[0]?.chat?._id;
const islastItemChatId: boolean = messageData.length > 0 && chatId != id;
const scrollRef = useScrollRef(islastItemChatId, id);
const scrollFunc = (e: any) => {
// let m = e.target.scrollHeight + e.target.scrollTop;
// let i = e.target.scrollHeight - m;
// console.log({ i, e });
if (!scrollRef.current) return;
const containerMiddle =
scrollRef.current.scrollTop +
scrollRef.current.getBoundingClientRect().height / 2;
const infiniteScrollItems = scrollRef.current.children[0].children;
console.log({ containerMiddle, infiniteScrollItems, e: e.target });
};
return (
<>
{messageData.length > 0 && (
<div
id="scrollableDiv"
style={{
height: "80%",
overflow: "auto",
display: "flex",
flexDirection: "column-reverse",
padding: "10px 0px",
}}
ref={scrollRef}
>
<InfiniteScroll
dataLength={messageData.length}
hasMore={hasMoreData}
onScroll={scrollFunc}
loader={
<div className="loading-container">
<div className="lds-ring">
<div></div>
</div>
</div>
}
endMessage={
<div className="message-container date">
<div className={`text-container sender large-margin`}>
<span>You have seen all the messages</span>
</div>
</div>
}
style={{ display: "flex", flexDirection: "column-reverse" }}
next={loadMore}
inverse={true}
scrollableTarget="scrollableDiv"
>
{messageData.map((item, index: number) => {
const isUserChat = item?.sender?._id === user._id;
const className =
item?.type == "date"
? "date"
: isUserChat
? "user-message"
: "sender-message";
const prevItem: IMessageData | null =
index < messageData?.length ? messageData[index - 1] : null;
const nextItem: IMessageData | null =
index < messageData?.length ? messageData[index + 1] : null;
return (
<Message
key={item._id}
item={item}
prevItem={prevItem}
className={className}
isUserChat={isUserChat}
index={index}
nextItem={nextItem}
/>
);
})}
</InfiniteScroll>
</div>
)}
</>
);
};
The recommended way to check the visibility of an element is using the Intersection Observer API.
The lib react-intersection-observer can help you to observe all the elements and map the indexes with some states.
However, for your use case, you can minimize the element to observe by only observing the visibility of where a new date starts and where the last message is for that date and setting the stickiness of the date label accordingly.
I implemented the solution here https://codesandbox.io/s/sticky-date-chat-ykmlx2?file=/src/App.js
I built an UI with this structure
This logic is contained in the MessagesBlock component
Of course, to achieve this, I had to change the data a little. I used the groupby function provided by lodash
And render the blocks in the scroll container
I had to make some style changes and omit some details from your version, but I hope you can add them to my provided solution.