fix chat scroll

This commit is contained in:
PhilReact 2024-10-25 06:40:15 +03:00
parent d83bbd40a0
commit 600215fb15
5 changed files with 196 additions and 127 deletions

26
package-lock.json generated
View File

@ -17,6 +17,7 @@
"@mui/lab": "^5.0.0-alpha.173", "@mui/lab": "^5.0.0-alpha.173",
"@mui/material": "^5.16.7", "@mui/material": "^5.16.7",
"@reduxjs/toolkit": "^2.2.7", "@reduxjs/toolkit": "^2.2.7",
"@tanstack/react-virtual": "^3.10.8",
"@testing-library/jest-dom": "^6.4.6", "@testing-library/jest-dom": "^6.4.6",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
"@tiptap/extension-color": "^2.5.9", "@tiptap/extension-color": "^2.5.9",
@ -1899,6 +1900,31 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tanstack/react-virtual": {
"version": "3.10.8",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz",
"integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==",
"dependencies": {
"@tanstack/virtual-core": "3.10.8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.10.8",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz",
"integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testing-library/dom": { "node_modules/@testing-library/dom": {
"version": "10.3.0", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.3.0.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.3.0.tgz",

View File

@ -21,6 +21,7 @@
"@mui/lab": "^5.0.0-alpha.173", "@mui/lab": "^5.0.0-alpha.173",
"@mui/material": "^5.16.7", "@mui/material": "^5.16.7",
"@reduxjs/toolkit": "^2.2.7", "@reduxjs/toolkit": "^2.2.7",
"@tanstack/react-virtual": "^3.10.8",
"@testing-library/jest-dom": "^6.4.6", "@testing-library/jest-dom": "^6.4.6",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
"@tiptap/extension-color": "^2.5.9", "@tiptap/extension-color": "^2.5.9",

View File

@ -149,7 +149,7 @@ const defaultValues: MyContextInterface = {
message: "", message: "",
}, },
}; };
export let isMobile = true; export let isMobile = false;
const isMobileDevice = () => { const isMobileDevice = () => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera; const userAgent = navigator.userAgent || navigator.vendor || window.opera;

View File

@ -1,16 +1,25 @@
import React, { useCallback, useState, useEffect, useRef } from 'react'; import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
import { Virtuoso } from 'react-virtuoso'; import { useVirtualizer } from '@tanstack/react-virtual';
import { MessageItem } from './MessageItem'; import { MessageItem } from './MessageItem';
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'; import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
import { useInView } from 'react-intersection-observer'
export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, handleReaction, chatReferences, tempChatReferences }) => { export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, handleReaction, chatReferences, tempChatReferences }) => {
const virtuosoRef = useRef(); const parentRef = useRef();
const [messages, setMessages] = useState(initialMessages); const [messages, setMessages] = useState(initialMessages);
const [showScrollButton, setShowScrollButton] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false);
const hasLoadedInitialRef = useRef(false); const hasLoadedInitialRef = useRef(false);
const isAtBottomRef = useRef(true); // const isAtBottomRef = useRef(true);
// Update message list with unique signatures and tempMessages // const [ref, inView] = useInView({
// threshold: 0.7
// })
// useEffect(() => {
// if (inView) {
// }
// }, [inView])
// Update message list with unique signatures and tempMessages
useEffect(() => { useEffect(() => {
let uniqueInitialMessagesMap = new Map(); let uniqueInitialMessagesMap = new Map();
@ -32,9 +41,12 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
setTimeout(() => { setTimeout(() => {
const hasUnreadMessages = totalMessages.some((msg) => msg.unread && !msg?.chatReference); const hasUnreadMessages = totalMessages.some((msg) => msg.unread && !msg?.chatReference);
console.log('hasUnreadMessages', hasUnreadMessages)
if (virtuosoRef.current) { if (parentRef.current) {
if (virtuosoRef.current && !isAtBottomRef.current && hasUnreadMessages) { const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed
console.log('atBottom', atBottom, {scrollTop, scrollHeight, clientHeight})
if (!atBottom && hasUnreadMessages) {
setShowScrollButton(hasUnreadMessages); setShowScrollButton(hasUnreadMessages);
} else { } else {
handleMessageSeen(); handleMessageSeen();
@ -47,45 +59,31 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
}, 500); }, 500);
}, [initialMessages, tempMessages]); }, [initialMessages, tempMessages]);
const handleMessageSeen = useCallback(() => { const handleMessageSeen = useCallback(() => {
console.log('hello handle seen')
setMessages((prevMessages) => setMessages((prevMessages) =>
prevMessages.map((msg) => ({ prevMessages.map((msg) => ({
...msg, ...msg,
unread: false, unread: false,
})) }))
); );
setShowScrollButton(false)
}, []); }, []);
const scrollToItem = useCallback((index) => { // const scrollToBottom = (initialMsgs) => {
if (virtuosoRef.current) { // const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1;
virtuosoRef.current.scrollToIndex({ index, behavior: 'smooth' }); // if (parentRef.current) {
} // parentRef.current.scrollToIndex(index);
}, []); // }
// };
const scrollToBottom = (initialMsgs) => { const scrollToBottom = (initialMsgs) => {
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1;
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1 if (rowVirtualizer) {
if (virtuosoRef.current) { rowVirtualizer.scrollToIndex(index, { align: 'end' });
virtuosoRef.current.scrollToIndex({ index});
} }
}; };
const handleScroll = (scrollState) => {
const { scrollTop, scrollHeight, clientHeight } = scrollState;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50;
const hasUnreadMessages = messages.some((msg) => msg.unread);
if (isAtBottom) {
handleMessageSeen();
}
setShowScrollButton(!isAtBottom && hasUnreadMessages);
};
const sentNewMessageGroupFunc = useCallback(() => { const sentNewMessageGroupFunc = useCallback(() => {
scrollToBottom(); scrollToBottom();
}, [messages]); }, [messages]);
@ -97,96 +95,136 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
}; };
}, [sentNewMessageGroupFunc]); }, [sentNewMessageGroupFunc]);
const rowRenderer = (index) => { const lastSignature = useMemo(()=> {
let message = messages[index]; if(!messages || messages?.length === 0) return null
let replyIndex = messages.findIndex((msg)=> msg?.signature === message?.repliedTo) const lastIndex = messages.length - 1
let reply return messages[lastIndex]?.signature
let reactions = null }, [messages])
if(message?.repliedTo && replyIndex !== -1){
reply = messages[replyIndex] console.log('messages', messages)
}
if(message?.message && message?.groupDirectId){ // Initialize the virtualizer
replyIndex = messages.findIndex((msg)=> msg?.signature === message?.message?.repliedTo) const rowVirtualizer = useVirtualizer({
reply count: messages.length,
if(message?.message?.repliedTo && replyIndex !== -1){ getScrollElement: () => parentRef.current,
reply = messages[replyIndex] estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed
} overscan: 10, // Number of items to render outside the visible area to improve smoothness
message = { measureElement:
...(message?.message || {}), typeof window !== 'undefined' &&
isTemp: true, navigator.userAgent.indexOf('Firefox') === -1
unread: false ? element => {
console.log('height', element?.getBoundingClientRect().height)
return element?.getBoundingClientRect().height
} }
} : undefined,
});
if(chatReferences && chatReferences[message?.signature]){
if(chatReferences[message.signature]?.reactions){
reactions = chatReferences[message.signature]?.reactions
}
}
let isUpdating = false
if(tempChatReferences && tempChatReferences?.find((item)=> item?.chatReference === message?.signature)){
isUpdating = true
}
return (
<div style={{ padding: '10px 0', display: 'flex', justifyContent: 'center', width: '100%', minHeight: '50px' , overscrollBehavior: "none"}}>
<MessageItem
isLast={index === messages.length - 1}
message={message}
onSeen={handleMessageSeen}
isTemp={!!message?.isTemp}
myAddress={myAddress}
onReply={onReply}
reply={reply}
replyIndex={replyIndex}
scrollToItem={scrollToItem}
handleReaction={handleReaction}
reactions={reactions}
isUpdating={isUpdating}
/>
</div>
);
};
const handleAtBottomStateChange = (atBottom) => {
isAtBottomRef.current = atBottom;
if(atBottom){
handleMessageSeen();
setShowScrollButton(false)
}
};
return ( return (
<div style={{ position: 'relative', height: '100%', display: 'flex', flexDirection: 'column' }}> <>
<Virtuoso <div ref={parentRef} style={{ height: '100%', overflow: 'auto', position: 'relative', display: 'flex' }}>
ref={virtuosoRef} <div
data={messages} style={{
itemContent={rowRenderer} width: '100%',
atBottomThreshold={50} position: 'relative',
followOutput="smooth" display: 'flex',
atBottomStateChange={handleAtBottomStateChange} // Detect bottom status flexDirection: 'column',
increaseViewportBy={3000} alignItems: 'center', // Center items horizontally
gap: '10px', // Add gap between items
flexGrow: 1
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const index = virtualRow.index;
let message = messages[index];
let replyIndex = messages.findIndex((msg) => msg?.signature === message?.repliedTo);
let reply;
let reactions = null;
if (message?.repliedTo && replyIndex !== -1) {
reply = messages[replyIndex];
}
if (message?.message && message?.groupDirectId) {
replyIndex = messages.findIndex((msg) => msg?.signature === message?.message?.repliedTo);
if (message?.message?.repliedTo && replyIndex !== -1) {
reply = messages[replyIndex];
}
message = {
...(message?.message || {}),
isTemp: true,
unread: false,
};
}
if (chatReferences && chatReferences[message?.signature]) {
if (chatReferences[message.signature]?.reactions) {
reactions = chatReferences[message.signature]?.reactions;
}
}
let isUpdating = false;
if (tempChatReferences && tempChatReferences?.find((item) => item?.chatReference === message?.signature)) {
isUpdating = true;
}
return (
<div
data-index={virtualRow.index} //needed for dynamic row height measurement
ref={node => rowVirtualizer.measureElement(node)} //measure dynamic row height
key={message.signature}
style={{
position: 'absolute',
top: 0,
left: '50%', // Move to the center horizontally
transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering
width: '100%', // Control width (90% of the parent)
padding: '10px 0',
display: 'flex',
justifyContent: 'center',
overscrollBehavior: 'none',
}}
>
<MessageItem
isLast={index === messages.length - 1}
lastSignature={lastSignature}
message={message}
onSeen={handleMessageSeen}
isTemp={!!message?.isTemp}
myAddress={myAddress}
onReply={onReply}
reply={reply}
replyIndex={replyIndex}
scrollToItem={(idx) => rowVirtualizer.scrollToIndex(idx)}
handleReaction={handleReaction}
reactions={reactions}
isUpdating={isUpdating}
/>
</div>
);
})}
</div>
/>
{showScrollButton && (
<button
onClick={()=> scrollToBottom()}
style={{
position: 'absolute',
bottom: 20,
right: 20,
backgroundColor: '#ff5a5f',
color: 'white',
padding: '10px 20px',
borderRadius: '20px',
cursor: 'pointer',
zIndex: 10,
}}
>
Scroll to Unread Messages
</button>
)}
</div> </div>
{showScrollButton && (
<button
onClick={() => scrollToBottom()}
style={{
position: 'absolute',
bottom: 20,
right: 20,
backgroundColor: '#ff5a5f',
color: 'white',
padding: '10px 20px',
borderRadius: '20px',
cursor: 'pointer',
zIndex: 10,
}}
>
Scroll to Unread Messages
</button>
)}
</>
); );
}; };

View File

@ -29,7 +29,8 @@ export const MessageItem = ({
scrollToItem, scrollToItem,
handleReaction, handleReaction,
reactions, reactions,
isUpdating isUpdating,
lastSignature
}) => { }) => {
const { ref, inView } = useInView({ const { ref, inView } = useInView({
threshold: 0.7, // Fully visible threshold: 0.7, // Fully visible
@ -38,13 +39,16 @@ export const MessageItem = ({
useEffect(() => { useEffect(() => {
if (inView && message.unread) { if (inView && message.unread) {
console.log('seenlast')
onSeen(message.id); onSeen(message.id);
} }
}, [inView, message.id, message.unread, onSeen]); }, [inView, message.id, message.unread, onSeen]);
console.log('isLast', lastSignature === message?.signature)
return ( return (
<div <div
ref={isLast ? ref : null} ref={lastSignature === message?.signature ? ref : null}
style={{ style={{
padding: "10px", padding: "10px",
backgroundColor: "#232428", backgroundColor: "#232428",