`compoennt chatList:
…
const handleContactClick = (chat) => {
if (selectedChat?._id !== chat._id) {
if (selectedChat) {
socket.emit('leave chat', selectedChat._id);
if (unreadMessagesCount[selectedChat._id] > 0) {
resetUnreadCount(selectedChat._id);
}
}
socket.emit('join chat', chat._id);
setSelectedChat(chat);
if (unreadMessagesCount[chat._id] > 0) {
resetUnreadCount(chat._id);
}
}
};
};
….
component Chat:
..
const [loadingOlderMessages, setLoadingOlderMessages] = useState(false);
const [hasMoreMessages, setHasMoreMessages] = useState(false);
const [firstUnreadMessageIndex, setFirstUnreadMessageIndex] = useState(null);
const [lastMessage, setLastMessage] = useState(null);
const [previousHeight, setPreviousHeight] = useState(0);
const [unreadCount, setUnreadCount] = useState(0);
const fetchChatMessages = useCallback(async (chatId, lastMessageId = null) => { let endpoint = ${config.URL_CONNECT}/message/getMessageByChatId/${chatId}; const unreadCount unreadMessagesCount[chatId] || 0; const limit = unreadCount > 20 ? unreadCount : 20;
const queryParams = [];
if (lastMessageId) queryParams.push(`lastMessageId=${lastMessageId}`);
queryParams.push(`limit=${limit}`);
if (queryParams.length > 0) endpoint += `?${queryParams.join('&')}`;
try {
const response = await axios.get(endpoint, {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = response.data;
socket.current.emit('read messsages', chatId, data);
const fetchedMessages = data.messages;
setMessages((prevMessages) => {
const newMessages = lastMessageId ? [...fetchedMessages, ...prevMessages] : fetchedMessages;
return newMessages;
});
setFirstUnreadMessageIndex(() => {
if (unreadCount > 0) {
return unreadCount <= 20 ? fetchedMessages.length - unreadCount : 0;
}
return null;
});
setUnreadCount(unreadCount > 0 ? unreadCount : 0);
setLastMessage(lastMessageId || null);
setHasMoreMessages(fetchedMessages.length === limit);
setLoadingOlderMessages(false);
} catch (error) {
console.error('Error fetching chat messages:', error);
setLoadingOlderMessages(false);
}
}, [setMessages, socket, token, unreadMessagesCount]);
component ChatWindow:
useEffect(() => { if (selectedChat._id) { setPreviousHeight(0); setMessages([]); fetchChatMessages(selectedChat._id); } }, [selectedChat._id, fetchChatMessages, setPreviousHeight, setMessages])
useLayoutEffect(() => {
if (messagesListRef.current) {
if (lastMessage) {
// Adjust scroll to maintain position
const newScrollHeight = messagesListRef.current.scrollHeight;
const newPosition = newScrollHeight - previousHeight;
messagesListRef.current.scrollTo({
top: newPosition,
behavior: 'auto',
});
}
}
}, [lastMessage, messagesListRef, previousHeight])
useLayoutEffect(() => {
if (messagesListRef.current && messages && !lastMessage && unreadMessages === 0) {
messagesListRef.current.scrollTo({
top: messagesListRef.current.scrollHeight,
behavior: 'auto',
});
}
}, [lastMessage, messages, messagesListRef, unreadMessages])
useLayoutEffect(() => {
if (messagesListRef.current) {
if (unreadMessages > 0) {
const firstUnreadMessage = messagesListRef.current.children[firstUnreadMessageIndex];
setShowUnreadTitle(true);
if (firstUnreadMessage) {
firstUnreadMessage.scrollIntoView({ behavior: 'smooth' });
}
}
}
}, [messagesListRef, firstUnreadMessageIndex, unreadMessages]);
const handleScroll = () => {
if (messagesListRef.current) {
const { scrollTop, scrollHeight, clientHeight } = messagesListRef.current;
if (scrollTop === 0 && hasMoreMessages && !loadingOlderMessages) {
const oldestMessage = messages[0];
if (oldestMessage) {
setPreviousHeight(messagesListRef.current.scrollHeight)
setLoadingOlderMessages(true);
fetchChatMessages(selectedChat._id, oldestMessage._id)
}
}
if (firstUnreadMessageIndex !== null) {
const firstUnreadMessage = messagesListRef.current.children[firstUnreadMessageIndex];
if (firstUnreadMessage && scrollTop > firstUnreadMessage.offsetTop) {
setShowUnreadTitle(false);
}
}
const bottom = scrollHeight - scrollTop - clientHeight;
setShowScrollToBottomButton(bottom > 400);
}
};
}}
return (
<div className="chat-window" ref={messagesListRef} onScroll={handleScroll}>
{loadingOlderMessages && (
<div className="loader-container">
<div className="loader-message"></div>
<div className="loader-message"></div>
<div className="loader-message"></div>
</div>
)}
{messages && messages.map((message, index) => {
const showDateHeader = index === 0 || new Date(message.timestamp).toDateString() !== new Date(messages[index - 1]?.timestamp).toDateString();
return (
<React.Fragment key={message._id}>
{showDateHeader && (
<div className={`date-header ${loadingOlderMessages ? 'hidden' : ''}`}>
{formatDate(message.timestamp)}
</div>
)}
{index === firstUnreadMessageIndex && showUnreadTitle && message.senderUsername !== currentUser.username && unreadMessages > 0 && (
<div className="unread-messages-title"> Unread Messages ({unreadMessages})</div>
)}
{message.systemMessage ? (
<div className="system-message">
<span className="bg-system">{message.content}</span>
</div>
) : (
<div
className={`chat-message ${message.senderUsername === currentUser.username
? (message.deletedForEveryone ? 'received-deleted-message' : 'received')
: 'sent'
}`}
onTouchStart={message.senderUsername === currentUser.username && !message.deletedForEveryone ? (e) => handleTouchStart(e, message) : null}
onTouchEnd={message.senderUsername === currentUser.username && !message.deletedForEveryone ? handleTouchEnd : null}
onContextMenu={message.senderUsername === currentUser.username && !message.deletedForEveryone ? (e) => handleContextMenu(e, message) : null}
onClick={message.senderUsername === currentUser.username && message.deletedForEveryone !== true ? () => handleMessageClick(message) : null}>
<div className="message-content">{
message.senderUsername === currentUser.username && message.deletedForEveryone
? "You deleted this message"
: message.deletedForEveryone
? "This message has been deleted"
: <>
{message.fileUrl && renderFileContent(message)}
{(message.content && message.content.length > 100 && !expandedMessages[message._id])
? <div className={`${message?.fileUrl && message.fileUrl !== null ? 'message-content-with-media' : ''}`}>
<Linkify componentDecorator={linkDecorator}>{`${message.content.substring(0, 100)}... `}</Linkify>
<span className="read-more" onClick={(e) => {
e.stopPropagation();
handleExpandMessage(message._id);
}}>
Read More
</span>
</div>
: (message.content.length > 100 && expandedMessages[message._id])
? <div className={`${message?.fileUrl && message.fileUrl !== null ? 'message-content-with-media' : ''}`}>
<Linkify componentDecorator={linkDecorator}>{message.content}</Linkify> {/* Show full message */}
<span className="read-more" onClick={(e) => {
e.stopPropagation();
handleExpandMessage(message._id);
}}>
Read Less
</span>
</div>
: <div className={`${message?.fileUrl && message.fileUrl !== null ? 'message-content-with-media' : ''}`}><Linkify componentDecorator={linkDecorator}>{message.content}</Linkify></div>
}
</>
}</div>
<div className="message-time">
{new Date(message.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
)}
</React.Fragment>
);
})}
{showScrollToBottomButton && (
<button className="scroll-to-bottom-btn" onClick={scrollToBottom}>
<i className="bi bi-arrow-down-circle"></i>
</button>
)}
{showImageModal && (
<Modal className="transparent-modal" show={showImageModal} onHide={() => setShowImageModal(false)} size="lg" centered>
<Modal.Body>
<img src={enlargedImageUrl} onClick={() => setShowImageModal(false)} alt="Enlarged" style={{ width: '100%' }} />
</Modal.Body>
</Modal>
)}
{modalReadBy && (
<ModalReadBy
selectedMessage={selectedMessage}
show={modalReadBy}
setModalReadBy={setModalReadBy}
/>
)}
</div>
);
……
The problem is with the scroll, why does it jump to the bottom when entering the chat with messages that have not been read at all, all of his behavior is unstable, can someone give me a solution to this, I am already losing my mind.
I actually want functionality in scroll that is similar to any normal chat application.`
New contributor
tal is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
1