chat performance improvements

This commit is contained in:
PhilReact 2025-02-26 02:41:40 +02:00
parent cbdb28c5d5
commit 44dd926869
8 changed files with 237 additions and 236 deletions

View File

@ -673,7 +673,7 @@ const handleNotification = async (groups) => {
let mutedGroups = (await getUserSettings({ key: "mutedGroups" })) || []; let mutedGroups = (await getUserSettings({ key: "mutedGroups" })) || [];
if (!isArray(mutedGroups)) mutedGroups = []; if (!isArray(mutedGroups)) mutedGroups = [];
mutedGroups.push('0')
let isFocused; let isFocused;
const data = groups.filter( const data = groups.filter(
(group) => (group) =>
@ -3182,6 +3182,7 @@ export const checkNewMessages = async () => {
try { try {
let mutedGroups = (await getUserSettings({ key: "mutedGroups" })) || []; let mutedGroups = (await getUserSettings({ key: "mutedGroups" })) || [];
if (!isArray(mutedGroups)) mutedGroups = []; if (!isArray(mutedGroups)) mutedGroups = [];
mutedGroups.push('0')
let myName = ""; let myName = "";
const userData = await getUserInfo(); const userData = await getUserInfo();
if (userData?.name) { if (userData?.name) {

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useMemo } from 'react';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import './styles.css'; import './styles.css';
import { executeEvent } from '../../utils/events'; import { executeEvent } from '../../utils/events';
@ -63,30 +63,34 @@ function processText(input) {
return wrapper.innerHTML; return wrapper.innerHTML;
} }
export const MessageDisplay = ({ htmlContent, isReply }) => { const linkify = (text) => {
const linkify = (text) => { if (!text) return ""; // Return an empty string if text is null or undefined
if (!text) return ""; // Return an empty string if text is null or undefined
let textFormatted = text;
const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g;
textFormatted = text.replace(urlPattern, (url) => {
const href = url.startsWith('http') ? url : `https://${url}`;
return `<a href="${DOMPurify.sanitize(href)}" class="auto-link">${DOMPurify.sanitize(url)}</a>`;
});
return processText(textFormatted);
};
const sanitizedContent = DOMPurify.sanitize(linkify(htmlContent), { let textFormatted = text;
ALLOWED_TAGS: [ const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g;
'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'div', 'span', 'img', textFormatted = text.replace(urlPattern, (url) => {
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 's', 'hr' const href = url.startsWith('http') ? url : `https://${url}`;
], return `<a href="${DOMPurify.sanitize(href)}" class="auto-link">${DOMPurify.sanitize(url)}</a>`;
ALLOWED_ATTR: [ });
'href', 'target', 'rel', 'class', 'src', 'alt', 'title', return processText(textFormatted);
'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url' };
],
}).replace(/<span[^>]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, '');;
export const MessageDisplay = ({ htmlContent, isReply }) => {
const sanitizedContent = useMemo(()=> {
return DOMPurify.sanitize(linkify(htmlContent), {
ALLOWED_TAGS: [
'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'div', 'span', 'img',
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 's', 'hr'
],
ALLOWED_ATTR: [
'href', 'target', 'rel', 'class', 'src', 'alt', 'title',
'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url'
],
}).replace(/<span[^>]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, '');
}, [])
const handleClick = async (e) => { const handleClick = async (e) => {
e.preventDefault(); e.preventDefault();

View File

@ -1,5 +1,5 @@
import { Message } from "@chatscope/chat-ui-kit-react"; import { Message } from "@chatscope/chat-ui-kit-react";
import React, { useContext, useEffect, useState } from "react"; import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { MessageDisplay } from "./MessageDisplay"; import { MessageDisplay } from "./MessageDisplay";
import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material"; import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material";
@ -50,7 +50,7 @@ const getBadgeImg = (level)=> {
default: return level0Img default: return level0Img
} }
} }
export const MessageItem = ({ export const MessageItem = React.memo(({
message, message,
onSeen, onSeen,
isLast, isLast,
@ -68,36 +68,78 @@ export const MessageItem = ({
onEdit, onEdit,
isPrivate isPrivate
}) => { }) => {
const { ref, inView } = useInView({
threshold: 0.7, // Fully visible
triggerOnce: false, // Only trigger once when it becomes visible
});
const {getIndividualUserInfo} = useContext(MyContext) const {getIndividualUserInfo} = useContext(MyContext)
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const [selectedReaction, setSelectedReaction] = useState(null); const [selectedReaction, setSelectedReaction] = useState(null);
const userInfo = useRecoilValue(addressInfoKeySelector(message?.sender)); const [userInfo, setUserInfo] = useState(null)
useEffect(() => {
if (inView && isLast && onSeen) {
onSeen(message.id);
}
}, [inView, message.id, isLast]);
useEffect(()=> { useEffect(()=> {
if(message?.sender){ const getInfo = async ()=> {
getIndividualUserInfo(message?.sender) if(!message?.sender) return
try {
const res = await getIndividualUserInfo(message?.sender)
if(!res) return null
setUserInfo(res)
} catch (error) {
//
}
} }
}, [message?.sender])
getInfo()
}, [message?.sender, getIndividualUserInfo])
const htmlText = useMemo(()=> {
if(message?.messageText){
return generateHTML(message?.messageText, [
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])
}
}, [])
const htmlReply = useMemo(()=> {
if(reply?.messageText){
return generateHTML(reply?.messageText, [
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])
}
}, [])
const userAvatarUrl = useMemo(()=> {
return message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
message?.senderName
}/qortal_avatar?async=true` : ''
}, [])
const onSeenFunc = useCallback(()=> {
onSeen(message.id);
}, [message?.id])
return ( return (
<> <MessageWragger lastMessage={lastSignature === message?.signature} isLast={isLast} onSeen={onSeenFunc}>
{message?.divide && ( {message?.divide && (
<div className="unread-divider" id="unread-divider-id"> <div className="unread-divider" id="unread-divider-id">
Unread messages below Unread messages below
</div> </div>
)} )}
<div <div
ref={lastSignature === message?.signature ? ref : null}
style={{ style={{
padding: "10px", padding: "10px",
backgroundColor: "#232428", backgroundColor: "#232428",
@ -132,25 +174,25 @@ useEffect(()=> {
sx={{ sx={{
backgroundColor: "#27282c", backgroundColor: "#27282c",
color: "white", color: "white",
height: '40px',
width: '40px'
}} }}
alt={message?.senderName} alt={message?.senderName}
src={message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${ src={userAvatarUrl}
message?.senderName
}/qortal_avatar?async=true` : ''}
> >
{message?.senderName?.charAt(0)} {message?.senderName?.charAt(0)}
</Avatar> </Avatar>
</WrapperUserAction> </WrapperUserAction>
<Tooltip disableFocusListener title={`level ${userInfo?.level}`}> <Tooltip disableFocusListener title={`level ${userInfo}`}>
<img style={{ <img style={{
visibility: userInfo?.level !== undefined ? 'visible' : 'hidden', visibility: userInfo !== undefined ? 'visible' : 'hidden',
width: '30px', width: '30px',
height: 'auto' height: 'auto'
}} src={getBadgeImg(userInfo?.level)} /> }} src={getBadgeImg(userInfo)} />
</Tooltip> </Tooltip>
</Box> </Box>
)} )}
@ -257,13 +299,7 @@ useEffect(()=> {
}}>Replied to {reply?.senderName || reply?.senderAddress}</Typography> }}>Replied to {reply?.senderName || reply?.senderAddress}</Typography>
{reply?.messageText && ( {reply?.messageText && (
<MessageDisplay <MessageDisplay
htmlContent={generateHTML(reply?.messageText, [ htmlContent={htmlReply}
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])}
/> />
)} )}
{reply?.decryptedData?.type === "notification" ? ( {reply?.decryptedData?.type === "notification" ? (
@ -275,17 +311,11 @@ useEffect(()=> {
</Box> </Box>
</> </>
)} )}
{message?.messageText && (
<MessageDisplay <MessageDisplay
htmlContent={generateHTML(message?.messageText, [ htmlContent={htmlText}
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])}
/> />
)}
{message?.decryptedData?.type === "notification" ? ( {message?.decryptedData?.type === "notification" ? (
<MessageDisplay htmlContent={message.decryptedData?.data?.message} /> <MessageDisplay htmlContent={message.decryptedData?.data?.message} />
) : ( ) : (
@ -457,21 +487,11 @@ useEffect(()=> {
</Box> </Box>
</Box> </Box>
{/* <Message
model={{
direction: 'incoming',
message: message.text,
position: 'single',
sender: message.senderName,
sentTime: message.timestamp
}}
></Message> */}
{/* {!message.unread && <span style={{ color: 'green' }}> Seen</span>} */}
</div> </div>
</> </MessageWragger>
); );
}; });
export const ReplyPreview = ({message, isEdit})=> { export const ReplyPreview = ({message, isEdit})=> {
@ -530,4 +550,36 @@ export const ReplyPreview = ({message, isEdit})=> {
</Box> </Box>
) )
}
const MessageWragger = ({lastMessage, onSeen, isLast, children})=> {
if(lastMessage){
return (
<WatchComponent onSeen={onSeen} isLast={isLast}>{children}</WatchComponent>
)
}
return children
}
const WatchComponent = ({onSeen, isLast, children})=> {
const { ref, inView } = useInView({
threshold: 0.7, // Fully visible
triggerOnce: true, // Only trigger once when it becomes visible
});
useEffect(() => {
if (inView && isLast && onSeen) {
onSeen();
}
}, [inView, isLast, onSeen]);
return <div ref={ref} style={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
{children}
</div>
} }

View File

@ -118,7 +118,7 @@ export const DesktopHeader = ({
fontWeight: 600, fontWeight: 600,
}} }}
> >
{selectedGroup?.groupName} {selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName}
</Typography> </Typography>
</Box> </Box>
<Box <Box
@ -126,76 +126,10 @@ export const DesktopHeader = ({
display: "flex", display: "flex",
gap: "20px", gap: "20px",
alignItems: "center", alignItems: "center",
visibility: selectedGroup?.groupId === '0' ? 'hidden' : 'visibile'
}} }}
> >
{/* <ButtonBase
onClick={() => {
goToHome();
}}
>
<IconWrapper
color="rgba(250, 250, 250, 0.5)"
label="Home"
selected={isHome}
>
<HomeIcon
height={25}
color={isHome ? "white" : "rgba(250, 250, 250, 0.5)"}
/>
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopSideView("groups");
}}
>
<IconWrapper
color="rgba(250, 250, 250, 0.5)"
label="Groups"
selected={isGroups}
>
<HubsIcon
height={25}
color={
hasUnreadGroups
? "var(--danger)"
: isGroups
? "white"
: "rgba(250, 250, 250, 0.5)"
}
/>
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopSideView("directs");
}}
>
<IconWrapper
color="rgba(250, 250, 250, 0.5)"
label="Messaging"
selected={isDirects}
>
<MessagingIcon
height={25}
color={
hasUnreadDirects
? "var(--danger)"
: isDirects
? "white"
: "rgba(250, 250, 250, 0.5)"
}
/>
</IconWrapper>
</ButtonBase> */}
{/* <Box
sx={{
width: "1px",
height: "50px",
background: "white",
borderRadius: "50px",
}}
/> */}
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
goToAnnouncements() goToAnnouncements()

View File

@ -447,12 +447,14 @@ export const Group = ({
const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom); const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom);
const isPrivate = useMemo(()=> { const isPrivate = useMemo(()=> {
if(selectedGroup?.groupId === '0') return false
if(!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null if(!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null
if(groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false if(groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false
if(groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true if(groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true
return null return null
}, [selectedGroup]) }, [selectedGroup])
const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom) const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom)
const toggleSideViewDirects = ()=> { const toggleSideViewDirects = ()=> {
if(isOpenSideViewGroups){ if(isOpenSideViewGroups){
@ -1937,7 +1939,7 @@ export const Group = ({
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={group.groupName} primary={group.groupId === '0' ? 'General' : group.groupName}
secondary={!group?.timestamp ? 'no messages' :`last message: ${formatEmailDate(group?.timestamp)}`} secondary={!group?.timestamp ? 'no messages' :`last message: ${formatEmailDate(group?.timestamp)}`}
primaryTypographyProps={{ primaryTypographyProps={{
style: { style: {

View File

@ -78,7 +78,15 @@ export const WebSocketActive = ({ myAddress, setIsLoadingGroups }) => {
} }
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
const filteredGroups = data.groups?.filter(item => item?.groupId !== 0) || []; const copyGroups = [...(data?.groups || [])]
const findIndex = copyGroups?.findIndex(item => item?.groupId === 0)
if(findIndex !== -1){
copyGroups[findIndex] = {
...(copyGroups[findIndex] || {}),
groupId: "0"
}
}
const filteredGroups = copyGroups
const sortedGroups = filteredGroups.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); const sortedGroups = filteredGroups.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
const sortedDirects = (data?.direct || []).filter(item => const sortedDirects = (data?.direct || []).filter(item =>
item?.name !== 'extension-proxy' && item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH' item?.name !== 'extension-proxy' && item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH'

View File

@ -1,34 +1,32 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useRef } from "react";
import { getBaseApiReact } from "../../App"; import { getBaseApiReact } from "../../App";
import { useRecoilState, useSetRecoilState } from "recoil";
import { addressInfoControllerAtom } from "../../atoms/global";
export const useHandleUserInfo = () => { export const useHandleUserInfo = () => {
const [userInfo, setUserInfo] = useRecoilState(addressInfoControllerAtom); const userInfoRef = useRef({})
const getIndividualUserInfo = useCallback(async (address)=> { const getIndividualUserInfo = useCallback(async (address)=> {
try { try {
if(!address || userInfo[address]) return if(!address) return null
if(userInfoRef.current[address] !== undefined) return userInfoRef.current[address]
const url = `${getBaseApiReact()}/addresses/${address}`; const url = `${getBaseApiReact()}/addresses/${address}`;
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error("network error"); throw new Error("network error");
} }
const data = await response.json(); const data = await response.json();
setUserInfo((prev)=> { userInfoRef.current = {
return { ...userInfoRef.current,
...prev, [address]: data?.level
[address]: data }
} return data?.level
})
} catch (error) { } catch (error) {
//error //error
} }
}, [userInfo]) }, [])
return { return {
getIndividualUserInfo, getIndividualUserInfo,

View File

@ -46,81 +46,83 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => {
</Box> </Box>
{/* Popover */} {/* Popover */}
<Popover {open && (
id={id} <Popover
open={open} id={id}
anchorEl={anchorEl} open={open}
onClose={handleClose} // Close popover on click outside anchorEl={anchorEl}
anchorOrigin={{ onClose={handleClose} // Close popover on click outside
vertical: 'bottom', anchorOrigin={{
horizontal: 'center', vertical: 'bottom',
}} horizontal: 'center',
transformOrigin={{ }}
vertical: 'top', transformOrigin={{
horizontal: 'center', vertical: 'top',
}} horizontal: 'center',
componentsProps={{ }}
paper: { componentsProps={{
onClick: (event) => event.stopPropagation(), // Stop propagation inside popover paper: {
}, onClick: (event) => event.stopPropagation(), // Stop propagation inside popover
}} },
> }}
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}> >
{/* Option 1: Message */} <Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Button {/* Option 1: Message */}
variant="text" <Button
onClick={() => { variant="text"
onClick={() => {
handleClose();
setTimeout(() => { handleClose();
executeEvent('openDirectMessageInternal', { setTimeout(() => {
address, executeEvent('openDirectMessageInternal', {
name, address,
}); name,
}, 200); });
}} }, 200);
sx={{ }}
color: 'white', sx={{
justifyContent: 'flex-start' color: 'white',
}} justifyContent: 'flex-start'
> }}
Message >
</Button> Message
</Button>
{/* Option 2: Send QORT */}
<Button {/* Option 2: Send QORT */}
variant="text" <Button
onClick={() => { variant="text"
executeEvent('openPaymentInternal', { onClick={() => {
address, executeEvent('openPaymentInternal', {
name, address,
}); name,
handleClose(); });
handleClose();
}}
sx={{ }}
color: 'white', sx={{
justifyContent: 'flex-start' color: 'white',
}} justifyContent: 'flex-start'
> }}
Send QORT >
</Button> Send QORT
<Button </Button>
variant="text" <Button
onClick={() => { variant="text"
navigator.clipboard.writeText(address|| ""); onClick={() => {
handleClose(); navigator.clipboard.writeText(address|| "");
handleClose();
}}
sx={{ }}
color: 'white', sx={{
justifyContent: 'flex-start' color: 'white',
}} justifyContent: 'flex-start'
> }}
Copy address >
</Button> Copy address
</Box> </Button>
</Popover> </Box>
</Popover>
)}
</> </>
); );
}; };