mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-04-24 20:07:51 +00:00
chat performance improvements
This commit is contained in:
parent
cbdb28c5d5
commit
44dd926869
@ -673,7 +673,7 @@ const handleNotification = async (groups) => {
|
||||
|
||||
let mutedGroups = (await getUserSettings({ key: "mutedGroups" })) || [];
|
||||
if (!isArray(mutedGroups)) mutedGroups = [];
|
||||
|
||||
mutedGroups.push('0')
|
||||
let isFocused;
|
||||
const data = groups.filter(
|
||||
(group) =>
|
||||
@ -3182,6 +3182,7 @@ export const checkNewMessages = async () => {
|
||||
try {
|
||||
let mutedGroups = (await getUserSettings({ key: "mutedGroups" })) || [];
|
||||
if (!isArray(mutedGroups)) mutedGroups = [];
|
||||
mutedGroups.push('0')
|
||||
let myName = "";
|
||||
const userData = await getUserInfo();
|
||||
if (userData?.name) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import DOMPurify from 'dompurify';
|
||||
import './styles.css';
|
||||
import { executeEvent } from '../../utils/events';
|
||||
@ -63,30 +63,34 @@ function processText(input) {
|
||||
return wrapper.innerHTML;
|
||||
}
|
||||
|
||||
const linkify = (text) => {
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
export const MessageDisplay = ({ htmlContent, isReply }) => {
|
||||
const linkify = (text) => {
|
||||
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), {
|
||||
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 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) => {
|
||||
e.preventDefault();
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { MessageDisplay } from "./MessageDisplay";
|
||||
import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material";
|
||||
@ -50,7 +50,7 @@ const getBadgeImg = (level)=> {
|
||||
default: return level0Img
|
||||
}
|
||||
}
|
||||
export const MessageItem = ({
|
||||
export const MessageItem = React.memo(({
|
||||
message,
|
||||
onSeen,
|
||||
isLast,
|
||||
@ -68,36 +68,78 @@ export const MessageItem = ({
|
||||
onEdit,
|
||||
isPrivate
|
||||
}) => {
|
||||
const { ref, inView } = useInView({
|
||||
threshold: 0.7, // Fully visible
|
||||
triggerOnce: false, // Only trigger once when it becomes visible
|
||||
});
|
||||
|
||||
const {getIndividualUserInfo} = useContext(MyContext)
|
||||
const [anchorEl, setAnchorEl] = 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(()=> {
|
||||
if(message?.sender){
|
||||
getIndividualUserInfo(message?.sender)
|
||||
const getInfo = async ()=> {
|
||||
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 (
|
||||
<>
|
||||
<MessageWragger lastMessage={lastSignature === message?.signature} isLast={isLast} onSeen={onSeenFunc}>
|
||||
|
||||
{message?.divide && (
|
||||
<div className="unread-divider" id="unread-divider-id">
|
||||
Unread messages below
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={lastSignature === message?.signature ? ref : null}
|
||||
style={{
|
||||
padding: "10px",
|
||||
backgroundColor: "#232428",
|
||||
@ -132,25 +174,25 @@ useEffect(()=> {
|
||||
sx={{
|
||||
backgroundColor: "#27282c",
|
||||
color: "white",
|
||||
height: '40px',
|
||||
width: '40px'
|
||||
}}
|
||||
alt={message?.senderName}
|
||||
src={message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
message?.senderName
|
||||
}/qortal_avatar?async=true` : ''}
|
||||
src={userAvatarUrl}
|
||||
>
|
||||
{message?.senderName?.charAt(0)}
|
||||
</Avatar>
|
||||
|
||||
|
||||
</WrapperUserAction>
|
||||
<Tooltip disableFocusListener title={`level ${userInfo?.level}`}>
|
||||
<Tooltip disableFocusListener title={`level ${userInfo}`}>
|
||||
|
||||
|
||||
<img style={{
|
||||
visibility: userInfo?.level !== undefined ? 'visible' : 'hidden',
|
||||
visibility: userInfo !== undefined ? 'visible' : 'hidden',
|
||||
width: '30px',
|
||||
height: 'auto'
|
||||
}} src={getBadgeImg(userInfo?.level)} />
|
||||
}} src={getBadgeImg(userInfo)} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)}
|
||||
@ -257,13 +299,7 @@ useEffect(()=> {
|
||||
}}>Replied to {reply?.senderName || reply?.senderAddress}</Typography>
|
||||
{reply?.messageText && (
|
||||
<MessageDisplay
|
||||
htmlContent={generateHTML(reply?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention,
|
||||
TextStyle
|
||||
])}
|
||||
htmlContent={htmlReply}
|
||||
/>
|
||||
)}
|
||||
{reply?.decryptedData?.type === "notification" ? (
|
||||
@ -275,17 +311,11 @@ useEffect(()=> {
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{message?.messageText && (
|
||||
|
||||
<MessageDisplay
|
||||
htmlContent={generateHTML(message?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention,
|
||||
TextStyle
|
||||
])}
|
||||
htmlContent={htmlText}
|
||||
/>
|
||||
)}
|
||||
|
||||
{message?.decryptedData?.type === "notification" ? (
|
||||
<MessageDisplay htmlContent={message.decryptedData?.data?.message} />
|
||||
) : (
|
||||
@ -457,21 +487,11 @@ useEffect(()=> {
|
||||
</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>
|
||||
</>
|
||||
</MessageWragger>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
export const ReplyPreview = ({message, isEdit})=> {
|
||||
@ -531,3 +551,35 @@ export const ReplyPreview = ({message, isEdit})=> {
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
}
|
@ -118,7 +118,7 @@ export const DesktopHeader = ({
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{selectedGroup?.groupName}
|
||||
{selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
@ -126,76 +126,10 @@ export const DesktopHeader = ({
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
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
|
||||
onClick={() => {
|
||||
goToAnnouncements()
|
||||
|
@ -447,12 +447,14 @@ export const Group = ({
|
||||
const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom);
|
||||
|
||||
const isPrivate = useMemo(()=> {
|
||||
if(selectedGroup?.groupId === '0') return false
|
||||
if(!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null
|
||||
if(groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false
|
||||
if(groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true
|
||||
return null
|
||||
}, [selectedGroup])
|
||||
|
||||
|
||||
const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom)
|
||||
const toggleSideViewDirects = ()=> {
|
||||
if(isOpenSideViewGroups){
|
||||
@ -1937,7 +1939,7 @@ export const Group = ({
|
||||
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={group.groupName}
|
||||
primary={group.groupId === '0' ? 'General' : group.groupName}
|
||||
secondary={!group?.timestamp ? 'no messages' :`last message: ${formatEmailDate(group?.timestamp)}`}
|
||||
primaryTypographyProps={{
|
||||
style: {
|
||||
|
@ -78,7 +78,15 @@ export const WebSocketActive = ({ myAddress, setIsLoadingGroups }) => {
|
||||
|
||||
}
|
||||
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 sortedDirects = (data?.direct || []).filter(item =>
|
||||
item?.name !== 'extension-proxy' && item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH'
|
||||
|
@ -1,34 +1,32 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useCallback, useRef } from "react";
|
||||
import { getBaseApiReact } from "../../App";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import { addressInfoControllerAtom } from "../../atoms/global";
|
||||
|
||||
|
||||
|
||||
export const useHandleUserInfo = () => {
|
||||
const [userInfo, setUserInfo] = useRecoilState(addressInfoControllerAtom);
|
||||
|
||||
const userInfoRef = useRef({})
|
||||
|
||||
|
||||
const getIndividualUserInfo = useCallback(async (address)=> {
|
||||
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 response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error("network error");
|
||||
}
|
||||
const data = await response.json();
|
||||
setUserInfo((prev)=> {
|
||||
return {
|
||||
...prev,
|
||||
[address]: data
|
||||
}
|
||||
})
|
||||
userInfoRef.current = {
|
||||
...userInfoRef.current,
|
||||
[address]: data?.level
|
||||
}
|
||||
return data?.level
|
||||
} catch (error) {
|
||||
//error
|
||||
}
|
||||
}, [userInfo])
|
||||
}, [])
|
||||
|
||||
return {
|
||||
getIndividualUserInfo,
|
||||
|
@ -46,81 +46,83 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => {
|
||||
</Box>
|
||||
|
||||
{/* Popover */}
|
||||
<Popover
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose} // Close popover on click outside
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
componentsProps={{
|
||||
paper: {
|
||||
onClick: (event) => event.stopPropagation(), // Stop propagation inside popover
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{/* Option 1: Message */}
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
{open && (
|
||||
<Popover
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose} // Close popover on click outside
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
componentsProps={{
|
||||
paper: {
|
||||
onClick: (event) => event.stopPropagation(), // Stop propagation inside popover
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{/* Option 1: Message */}
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
|
||||
handleClose();
|
||||
setTimeout(() => {
|
||||
executeEvent('openDirectMessageInternal', {
|
||||
address,
|
||||
name,
|
||||
});
|
||||
}, 200);
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
>
|
||||
Message
|
||||
</Button>
|
||||
handleClose();
|
||||
setTimeout(() => {
|
||||
executeEvent('openDirectMessageInternal', {
|
||||
address,
|
||||
name,
|
||||
});
|
||||
}, 200);
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
>
|
||||
Message
|
||||
</Button>
|
||||
|
||||
{/* Option 2: Send QORT */}
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
executeEvent('openPaymentInternal', {
|
||||
address,
|
||||
name,
|
||||
});
|
||||
handleClose();
|
||||
{/* Option 2: Send QORT */}
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
executeEvent('openPaymentInternal', {
|
||||
address,
|
||||
name,
|
||||
});
|
||||
handleClose();
|
||||
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
>
|
||||
Send QORT
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(address|| "");
|
||||
handleClose();
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
>
|
||||
Send QORT
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(address|| "");
|
||||
handleClose();
|
||||
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
>
|
||||
Copy address
|
||||
</Button>
|
||||
</Box>
|
||||
</Popover>
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
>
|
||||
Copy address
|
||||
</Button>
|
||||
</Box>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user