user lookup feature

This commit is contained in:
PhilReact 2025-02-28 16:31:08 +02:00
parent f9b1e2784f
commit 8af3977178
11 changed files with 858 additions and 202 deletions

View File

@ -35,6 +35,7 @@ import RefreshIcon from "@mui/icons-material/Refresh";
import Logo2 from "./assets/svgs/Logo2.svg"; import Logo2 from "./assets/svgs/Logo2.svg";
import Copy from "./assets/svgs/Copy.svg"; import Copy from "./assets/svgs/Copy.svg";
import ltcLogo from "./assets/ltc.png"; import ltcLogo from "./assets/ltc.png";
import PersonSearchIcon from '@mui/icons-material/PersonSearch';
import qortLogo from "./assets/qort.png"; import qortLogo from "./assets/qort.png";
import { CopyToClipboard } from "react-copy-to-clipboard"; import { CopyToClipboard } from "react-copy-to-clipboard";
import Download from "./assets/svgs/Download.svg"; import Download from "./assets/svgs/Download.svg";
@ -113,6 +114,7 @@ import {
canSaveSettingToQdnAtom, canSaveSettingToQdnAtom,
enabledDevModeAtom, enabledDevModeAtom,
fullScreenAtom, fullScreenAtom,
groupsPropertiesAtom,
hasSettingsChangedAtom, hasSettingsChangedAtom,
isDisabledEditorEnterAtom, isDisabledEditorEnterAtom,
isUsingImportExportSettingsAtom, isUsingImportExportSettingsAtom,
@ -145,6 +147,8 @@ import { QMailStatus } from "./components/QMailStatus";
import { GlobalActions } from "./components/GlobalActions/GlobalActions"; import { GlobalActions } from "./components/GlobalActions/GlobalActions";
import { useBlockedAddresses } from "./components/Group/useBlockUsers"; import { useBlockedAddresses } from "./components/Group/useBlockUsers";
import { WalletIcon } from "./assets/Icons/WalletIcon"; import { WalletIcon } from "./assets/Icons/WalletIcon";
import { DrawerUserLookup } from "./components/Drawer/DrawerUserLookup";
import { UserLookup } from "./components/UserLookup.tsx/UserLookup";
type extStates = type extStates =
| "not-authenticated" | "not-authenticated"
@ -400,6 +404,7 @@ function App() {
const [openSnack, setOpenSnack] = useState(false); const [openSnack, setOpenSnack] = useState(false);
const [hasLocalNode, setHasLocalNode] = useState(false); const [hasLocalNode, setHasLocalNode] = useState(false);
const [isOpenDrawerProfile, setIsOpenDrawerProfile] = useState(false); const [isOpenDrawerProfile, setIsOpenDrawerProfile] = useState(false);
const [isOpenDrawerLookup, setIsOpenDrawerLookup] = useState(false)
const [apiKey, setApiKey] = useState(""); const [apiKey, setApiKey] = useState("");
const [isOpenSendQort, setIsOpenSendQort] = useState(false); const [isOpenSendQort, setIsOpenSendQort] = useState(false);
const [isOpenSendQortSuccess, setIsOpenSendQortSuccess] = useState(false); const [isOpenSendQortSuccess, setIsOpenSendQortSuccess] = useState(false);
@ -488,7 +493,7 @@ function App() {
const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom); const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom);
const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom) const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom)
const resetAtomMailsAtom = useResetRecoilState(mailsAtom) const resetAtomMailsAtom = useResetRecoilState(mailsAtom)
const resetGroupPropertiesAtom = useResetRecoilState(groupsPropertiesAtom)
const resetAllRecoil = () => { const resetAllRecoil = () => {
resetAtomSortablePinnedAppsAtom(); resetAtomSortablePinnedAppsAtom();
resetAtomCanSaveSettingToQdnAtom(); resetAtomCanSaveSettingToQdnAtom();
@ -498,6 +503,8 @@ function App() {
resetAtomIsUsingImportExportSettingsAtom() resetAtomIsUsingImportExportSettingsAtom()
resetAtomQMailLastEnteredTimestampAtom() resetAtomQMailLastEnteredTimestampAtom()
resetAtomMailsAtom() resetAtomMailsAtom()
resetGroupPropertiesAtom()
}; };
useEffect(() => { useEffect(() => {
if (!isMobile) return; if (!isMobile) return;
@ -1696,6 +1703,38 @@ function App() {
/> />
</Tooltip> </Tooltip>
</ButtonBase> </ButtonBase>
<Spacer height="20px" />
<ButtonBase
onClick={() => {
setIsOpenDrawerLookup(true);
}}
>
<Tooltip
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>USER LOOKUP</span>}
placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<PersonSearchIcon
sx={{
color: "rgba(255, 255, 255, 0.5)",
}}
/>
</Tooltip>
</ButtonBase>
{desktopViewMode !== 'home' && ( {desktopViewMode !== 'home' && (
<> <>
@ -2055,7 +2094,7 @@ function App() {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
zIndex: 6, zIndex: 10000,
}} }}
> >
<Spacer height="22px" /> <Spacer height="22px" />
@ -3110,7 +3149,7 @@ function App() {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
zIndex: 6, zIndex: 10000,
}} }}
> >
<Spacer height="48px" /> <Spacer height="48px" />
@ -3210,6 +3249,9 @@ function App() {
open={isShow} open={isShow}
aria-labelledby="alert-dialog-title" aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
sx={{
zIndex: 10001
}}
> >
<DialogTitle id="alert-dialog-title">{message.paymentFee ? "Payment" : "Publish"}</DialogTitle> <DialogTitle id="alert-dialog-title">{message.paymentFee ? "Payment" : "Publish"}</DialogTitle>
<DialogContent> <DialogContent>
@ -3635,7 +3677,7 @@ function App() {
> >
{renderProfileLeft()} {renderProfileLeft()}
</DrawerComponent> </DrawerComponent>
<UserLookup isOpenDrawerLookup={isOpenDrawerLookup} setIsOpenDrawerLookup={setIsOpenDrawerLookup} />
</GlobalContext.Provider> </GlobalContext.Provider>
{extState === "create-wallet" && walletToBeDownloaded && ( {extState === "create-wallet" && walletToBeDownloaded && (
<ButtonBase onClick={()=> { <ButtonBase onClick={()=> {

View File

@ -169,4 +169,9 @@ export const qMailLastEnteredTimestampAtom = atom({
export const mailsAtom = atom({ export const mailsAtom = atom({
key: 'mailsAtom', key: 'mailsAtom',
default: [], default: [],
});
export const groupsPropertiesAtom = atom({
key: 'groupsPropertiesAtom',
default: {},
}); });

View File

@ -845,7 +845,7 @@ export async function getNameInfoForOthers(address) {
return ""; return "";
} }
} }
async function getAddressInfo(address) { export async function getAddressInfo(address) {
const validApi = await getBaseApi(); const validApi = await getBaseApi();
const response = await fetch(validApi + "/addresses/" + address); const response = await fetch(validApi + "/addresses/" + address);
const data = await response.json(); const data = await response.json();

View File

@ -1,4 +1,4 @@
import React, { useContext, useState } from "react"; import React, { useContext, useMemo, useState } from "react";
import { import {
Avatar, Avatar,
Box, Box,
@ -18,7 +18,7 @@ import {
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { useHandlePrivateApps } from "./useHandlePrivateApps"; import { useHandlePrivateApps } from "./useHandlePrivateApps";
import { useRecoilState, useSetRecoilState } from "recoil"; import { useRecoilState, useSetRecoilState } from "recoil";
import { myGroupsWhereIAmAdminAtom } from "../../atoms/global"; import { groupsPropertiesAtom, myGroupsWhereIAmAdminAtom } from "../../atoms/global";
import { Label } from "../Group/AddGroup"; import { Label } from "../Group/AddGroup";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { import {
@ -43,14 +43,22 @@ export const AppsPrivate = ({myName}) => {
const [logo, setLogo] = useState(null); const [logo, setLogo] = useState(null);
const [qortalUrl, setQortalUrl] = useState(""); const [qortalUrl, setQortalUrl] = useState("");
const [selectedGroup, setSelectedGroup] = useState(0); const [selectedGroup, setSelectedGroup] = useState(0);
const [groupsProperties] = useRecoilState(groupsPropertiesAtom)
const [valueTabPrivateApp, setValueTabPrivateApp] = useState(0); const [valueTabPrivateApp, setValueTabPrivateApp] = useState(0);
const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState( const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState(
myGroupsWhereIAmAdminAtom myGroupsWhereIAmAdminAtom
); );
const myGroupsWhereIAmAdmin = useMemo(()=> {
return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false)
}, [myGroupsWhereIAmAdminFromGlobal, groupsProperties])
const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false); const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false);
const { show, setInfoSnackCustom, setOpenSnackGlobal, memberGroups } = useContext(MyContext); const { show, setInfoSnackCustom, setOpenSnackGlobal, memberGroups } = useContext(MyContext);
const myGroupsPrivate = useMemo(()=> {
return memberGroups?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false)
}, [memberGroups, groupsProperties])
const [privateAppValues, setPrivateAppValues] = useState({ const [privateAppValues, setPrivateAppValues] = useState({
name: "", name: "",
service: "DOCUMENT", service: "DOCUMENT",
@ -95,7 +103,7 @@ export const AppsPrivate = ({myName}) => {
await openApp(privateAppValues, true); await openApp(privateAppValues, true);
} catch (error) { } catch (error) {
console.log('error', error?.message) console.error(error)
} }
}; };
@ -311,7 +319,7 @@ export const AppsPrivate = ({myName}) => {
> >
<MenuItem value={0}>No group selected</MenuItem> <MenuItem value={0}>No group selected</MenuItem>
{memberGroups {myGroupsPrivate
?.filter((item) => !item?.isOpen) ?.filter((item) => !item?.isOpen)
.map((group) => { .map((group) => {
return ( return (

View File

@ -194,7 +194,7 @@ export const GroupAnnouncements = ({
}; };
}); });
} catch (error) { } catch (error) {
console.log("error", error); console.error("error", error);
} }
}; };

View File

@ -0,0 +1,22 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Drawer from '@mui/material/Drawer';
export const DrawerUserLookup = ({open, setOpen, children}) => {
const toggleDrawer = (newOpen: boolean) => () => {
setOpen(newOpen);
};
return (
<div>
<Drawer disableEnforceFocus hideBackdrop={true} open={open} onClose={toggleDrawer(false)}>
<Box sx={{ width: '70vw', height: '100%', maxWidth: '1000px' }} role="presentation">
{children}
</Box>
</Drawer>
</div>
);
}

View File

@ -74,8 +74,8 @@ import { HubsIcon } from "../../assets/Icons/HubsIcon";
import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
import { formatEmailDate } from "./QMailMessages"; import { formatEmailDate } from "./QMailMessages";
import { AdminSpace } from "../Chat/AdminSpace"; import { AdminSpace } from "../Chat/AdminSpace";
import { useSetRecoilState } from "recoil"; import { useRecoilState, useSetRecoilState } from "recoil";
import { addressInfoControllerAtom, selectedGroupIdAtom } from "../../atoms/global"; import { addressInfoControllerAtom, groupsPropertiesAtom, selectedGroupIdAtom } from "../../atoms/global";
import { sortArrayByTimestampAndGroupName } from "../../utils/time"; import { sortArrayByTimestampAndGroupName } from "../../utils/time";
import BlockIcon from '@mui/icons-material/Block'; import BlockIcon from '@mui/icons-material/Block';
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from '@mui/icons-material/Lock';
@ -446,7 +446,7 @@ export const Group = ({
const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false) const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false)
const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false) const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false)
const [groupsProperties, setGroupsProperties] = useState({}) const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom)
const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom); const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom);
const isPrivate = useMemo(()=> { const isPrivate = useMemo(()=> {

View File

@ -1,209 +1,257 @@
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from "react";
import { getBaseApiReact } from '../../App'; import { getBaseApiReact } from "../../App";
import { Box, Tooltip, Typography } from '@mui/material'; import { Box, Tooltip, Typography } from "@mui/material";
import { BarSpinner } from '../../common/Spinners/BarSpinner/BarSpinner'; import { BarSpinner } from "../../common/Spinners/BarSpinner/BarSpinner";
import { formatDate } from "../../utils/time";
function getAverageLtcPerQort(trades) { function getAverageLtcPerQort(trades) {
let totalQort = 0; let totalQort = 0;
let totalLtc = 0; let totalLtc = 0;
trades.forEach((trade) => {
const qort = parseFloat(trade.qortAmount);
const ltc = parseFloat(trade.foreignAmount);
totalQort += qort;
totalLtc += ltc;
});
// Avoid division by zero
if (totalQort === 0) return 0;
// Weighted average price
return parseFloat((totalLtc / totalQort).toFixed(8));
}
function getTwoWeeksAgoTimestamp() { trades.forEach((trade) => {
const now = new Date(); const qort = parseFloat(trade.qortAmount);
now.setDate(now.getDate() - 14); // Subtract 14 days const ltc = parseFloat(trade.foreignAmount);
return now.getTime(); // Get timestamp in milliseconds
}
function formatWithCommasAndDecimals(number) {
return Number(number).toLocaleString(); totalQort += qort;
} totalLtc += ltc;
});
// Avoid division by zero
if (totalQort === 0) return 0;
// Weighted average price
return parseFloat((totalLtc / totalQort).toFixed(8));
}
function getTwoWeeksAgoTimestamp() {
const now = new Date();
now.setDate(now.getDate() - 14); // Subtract 14 days
return now.getTime(); // Get timestamp in milliseconds
}
function formatWithCommasAndDecimals(number) {
return Number(number).toLocaleString();
}
export const QortPrice = () => { export const QortPrice = () => {
const [ltcPerQort, setLtcPerQort] = useState(null) const [ltcPerQort, setLtcPerQort] = useState(null);
const [supply, setSupply] = useState(null) const [supply, setSupply] = useState(null);
const [lastBlock, setLastBlock] = useState(null) const [lastBlock, setLastBlock] = useState(null);
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true);
const getPrice = useCallback(async () => {
try {
setLoading(true)
const response = await fetch(`${getBaseApiReact()}/crosschain/trades?foreignBlockchain=LITECOIN&minimumTimestamp=${getTwoWeeksAgoTimestamp()}&limit=20&reverse=true`);
const data = await response.json();
setLtcPerQort(getAverageLtcPerQort(data));
} catch (error) {
console.error(error);
} finally {
setLoading(false)
}
}, [])
const getLastBlock = useCallback(async () => { const getPrice = useCallback(async () => {
try { try {
setLoading(true) setLoading(true);
const response = await fetch(`${getBaseApiReact()}/blocks/last`);
const data = await response.json();
setLastBlock(data); const response = await fetch(
} catch (error) { `${getBaseApiReact()}/crosschain/trades?foreignBlockchain=LITECOIN&minimumTimestamp=${getTwoWeeksAgoTimestamp()}&limit=20&reverse=true`
console.error(error); );
} finally { const data = await response.json();
setLoading(false)
}
}, [])
const getSupplyInCirculation = useCallback(async () => { setLtcPerQort(getAverageLtcPerQort(data));
try { } catch (error) {
setLoading(true) console.error(error);
} finally {
const response = await fetch(`${getBaseApiReact()}/stats/supply/circulating`); setLoading(false);
const data = await response.text(); }
formatWithCommasAndDecimals(data) }, []);
setSupply(formatWithCommasAndDecimals(data));
} catch (error) { const getLastBlock = useCallback(async () => {
console.error(error); try {
} finally { setLoading(true);
setLoading(false)
const response = await fetch(`${getBaseApiReact()}/blocks/last`);
} const data = await response.json();
}, [])
setLastBlock(data);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}, []);
const getSupplyInCirculation = useCallback(async () => {
try {
setLoading(true);
const response = await fetch(
`${getBaseApiReact()}/stats/supply/circulating`
);
const data = await response.text();
formatWithCommasAndDecimals(data);
setSupply(formatWithCommasAndDecimals(data));
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
getPrice();
getSupplyInCirculation();
getLastBlock();
const interval = setInterval(() => {
getPrice();
getSupplyInCirculation();
getLastBlock();
}, 900000);
return () => clearInterval(interval);
}, [getPrice]);
useEffect(() => {
getPrice();
getSupplyInCirculation()
getLastBlock()
const interval = setInterval(() => {
getPrice();
getSupplyInCirculation()
getLastBlock()
}, 900000);
return () => clearInterval(interval);
}, [getPrice]);
console.log('supply', supply)
return ( return (
<Box <Box
sx={{ sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
flexDirection: 'column',
width: "322px"
}}
>
<Tooltip
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>Based on the latest 20 trades</span>}
placement="bottom"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<Box sx={{
width: "322px",
display: "flex", display: "flex",
flexDirection: "row", gap: "20px",
gap: '10px', flexWrap: "wrap",
flexDirection: "column",
width: "322px",
}}
>
<Tooltip
title={
<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>
Based on the latest 20 trades
</span>
}
placement="bottom"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<Box
sx={{
width: "322px",
display: "flex",
flexDirection: "row",
gap: "10px",
justifyContent: 'space-between' justifyContent: "space-between",
}}> }}
<Typography sx={{ >
fontSize: "1rem", <Typography
fontWeight: 'bold' sx={{
}}>Price</Typography>
{!ltcPerQort ? (
<BarSpinner width="16px" color="white" />
): (
<Typography sx={{
fontSize: "1rem", fontSize: "1rem",
}}>{ltcPerQort} LTC/QORT</Typography> fontWeight: "bold",
}}
>
Price
</Typography>
{!ltcPerQort ? (
<BarSpinner width="16px" color="white" />
) : (
<Typography
sx={{
fontSize: "1rem",
}}
>
{ltcPerQort} LTC/QORT
</Typography>
)} )}
</Box>
</Tooltip>
<Box
sx={{
width: "322px",
display: "flex",
flexDirection: "row",
gap: "10px",
justifyContent: "space-between",
}}
>
<Typography
sx={{
fontSize: "1rem",
fontWeight: "bold",
}}
>
Supply
</Typography>
{!supply ? (
<BarSpinner width="16px" color="white" />
) : (
<Typography
sx={{
fontSize: "1rem",
}}
>
{supply} QORT
</Typography>
)}
</Box>
<Tooltip
title={
<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>
{lastBlock?.timestamp && formatDate(lastBlock?.timestamp)}
</span>
}
placement="bottom"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<Box
sx={{
width: "322px",
display: "flex",
flexDirection: "row",
gap: "10px",
justifyContent: "space-between",
}}
>
<Typography
</Box> sx={{
</Tooltip>
<Box sx={{
width: "322px",
display: "flex",
flexDirection: "row",
gap: '10px',
justifyContent: 'space-between'
}}>
<Typography sx={{
fontSize: "1rem",
fontWeight: 'bold'
}}>Supply</Typography>
{!supply ? (
<BarSpinner width="16px" color="white" />
): (
<Typography sx={{
fontSize: "1rem", fontSize: "1rem",
}}>{supply} QORT</Typography> fontWeight: "bold",
)} }}
>
</Box> Last height
<Box sx={{ </Typography>
width: "322px", {!lastBlock?.height ? (
display: "flex",
flexDirection: "row",
gap: '10px',
justifyContent: 'space-between'
}}>
<Typography sx={{
fontSize: "1rem",
fontWeight: 'bold'
}}>Last height</Typography>
{!lastBlock?.height ? (
<BarSpinner width="16px" color="white" /> <BarSpinner width="16px" color="white" />
): ( ) : (
<Typography sx={{ <Typography
fontSize: "1rem", sx={{
}}>{lastBlock?.height}</Typography> fontSize: "1rem",
}}
>
{lastBlock?.height}
</Typography>
)} )}
</Box>
</Box>
</Tooltip>
</Box> </Box>
) );
} };

View File

@ -0,0 +1,495 @@
import React, { useCallback, useEffect, useState } from "react";
import { DrawerUserLookup } from "../Drawer/DrawerUserLookup";
import {
Avatar,
Box,
Button,
ButtonBase,
Card,
Divider,
TableBody,
TableCell,
TableHead,
TableRow,
TextField,
Tooltip,
Typography,
Table,
CircularProgress,
} from "@mui/material";
import { getAddressInfo, getNameOrAddress } from "../../background";
import { getBaseApiReact } from "../../App";
import { getNameInfo } from "../Group/Group";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import { Spacer } from "../../common/Spacer";
import { formatTimestamp } from "../../utils/time";
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
import SearchIcon from '@mui/icons-material/Search';
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
function formatAddress(str) {
if (str.length <= 12) return str;
const first6 = str.slice(0, 6);
const last6 = str.slice(-6);
return `${first6}....${last6}`;
}
export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
const [nameOrAddress, setNameOrAddress] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const [addressInfo, setAddressInfo] = useState(null);
const [isLoadingUser, setIsLoadingUser] = useState(false);
const [isLoadingPayments, setIsLoadingPayments] = useState(false);
const [payments, setPayments] = useState([]);
const lookupFunc = useCallback(async (messageAddressOrName) => {
try {
setErrorMessage('')
setIsLoadingUser(true)
setPayments([])
setAddressInfo(null)
const inputAddressOrName = messageAddressOrName || nameOrAddress
if (!inputAddressOrName?.trim())
throw new Error("Please insert a name or address");
const owner = await getNameOrAddress(inputAddressOrName);
if (!owner) throw new Error("Name does not exist");
const addressInfoRes = await getAddressInfo(owner);
if (!addressInfoRes?.publicKey) {
throw new Error("Address does not exist on blockchain");
}
const name = await getNameInfo(owner);
const balanceRes = await fetch(
`${getBaseApiReact()}/addresses/balance/${owner}`
);
const balanceData = await balanceRes.json();
setAddressInfo({
...addressInfoRes,
balance: balanceData,
name,
});
setIsLoadingUser(false)
setIsLoadingPayments(true)
const getPayments = await fetch(
`${getBaseApiReact()}/transactions/search?txType=PAYMENT&address=${owner}&confirmationStatus=CONFIRMED&limit=20&reverse=true`
);
const paymentsData = await getPayments.json();
setPayments(paymentsData);
} catch (error) {
setErrorMessage(error?.message)
console.error(error);
} finally {
setIsLoadingUser(false)
setIsLoadingPayments(false)
}
}, [nameOrAddress]);
const openUserLookupDrawerFunc = useCallback((e) => {
setIsOpenDrawerLookup(true)
const message = e.detail?.addressOrName;
if(message){
lookupFunc(message)
}
}, [lookupFunc, setIsOpenDrawerLookup]);
useEffect(() => {
subscribeToEvent("openUserLookupDrawer", openUserLookupDrawerFunc);
return () => {
unsubscribeFromEvent("openUserLookupDrawer", openUserLookupDrawerFunc);
};
}, [openUserLookupDrawerFunc]);
return (
<DrawerUserLookup open={isOpenDrawerLookup} setOpen={setIsOpenDrawerLookup}>
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: "15px",
height: "100vh",
overflow: "hidden",
}}
>
<Box
sx={{
display: "flex",
gap: "5px",
alignItems: "center",
flexShrink: 0,
}}
>
<TextField
value={nameOrAddress}
onChange={(e) => setNameOrAddress(e.target.value)}
size="small"
placeholder="Address or Name"
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter" && nameOrAddress) {
lookupFunc();
}
}}
/>
<ButtonBase onClick={lookupFunc} >
<SearchIcon sx={{
color: 'white',
marginRight: '20px'
}} />
</ButtonBase>
<ButtonBase sx={{
marginLeft: 'auto',
}} onClick={()=> {
setIsOpenDrawerLookup(false)
}}>
<CloseFullscreenIcon sx={{
color: 'white'
}} />
</ButtonBase>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
overflow: "auto",
}}
>
{!isLoadingUser && errorMessage && (
<Box sx={{
display: 'flex',
width: '100%',
justifyContent: 'center',
marginTop: '40px'
}}>
<Typography>{errorMessage}</Typography>
</Box>
)}
{isLoadingUser && (
<Box sx={{
display: 'flex',
width: '100%',
justifyContent: 'center',
marginTop: '40px'
}}>
<CircularProgress sx={{
color: 'white'
}} />
</Box>
)}
{!isLoadingUser && addressInfo && (
<>
<Spacer height="30px" />
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
flexDirection: "row",
width: "100%",
justifyContent: "center",
}}
>
<Card
sx={{
padding: "15px",
minWidth: "320px",
alignItems: "center",
minHeight: "200px",
background: "var(--bg-primary)",
display: "flex",
flexDirection: "column",
}}
>
<Typography
sx={{
textAlign: "center",
}}
>
{addressInfo?.name ?? "Name not registered"}
</Typography>
<Spacer height="20px" />
<Divider>
{addressInfo?.name ? (
<Avatar
sx={{
height: "50px",
width: "50px",
"& img": {
objectFit: "fill",
},
}}
alt={addressInfo?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
addressInfo?.name
}/qortal_avatar?async=true`}
>
<AccountCircleIcon
sx={{
fontSize: "50px",
}}
/>
</Avatar>
) : (
<AccountCircleIcon
sx={{
fontSize: "50px",
}}
/>
)}
</Divider>
<Spacer height="20px" />
<Typography
sx={{
textAlign: "center",
}}
>
Level {addressInfo?.level}
</Typography>
</Card>
<Card
sx={{
padding: "15px",
minWidth: "320px",
minHeight: "200px",
gap: "20px",
display: "flex",
flexDirection: "column",
background: "var(--bg-primary)",
}}
>
<Box
sx={{
display: "flex",
gap: "20px",
justifyContent: "space-between",
width: "100%",
}}
>
<Box
sx={{
display: "flex",
flexShrink: 0,
}}
>
<Typography>Address</Typography>
</Box>
<Tooltip
title={
<span
style={{
color: "white",
fontSize: "14px",
fontWeight: 700,
}}
>
copy address
</span>
}
placement="bottom"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<ButtonBase
onClick={() => {
navigator.clipboard.writeText(addressInfo?.address);
}}
>
<Typography
sx={{
textAlign: "end",
}}
>
{addressInfo?.address}
</Typography>
</ButtonBase>
</Tooltip>
</Box>
<Box
sx={{
display: "flex",
gap: "20px",
justifyContent: "space-between",
width: "100%",
}}
>
<Typography>Balance</Typography>
<Typography>{addressInfo?.balance}</Typography>
</Box>
<Spacer height="20px" />
<Button variant="contained" onClick={()=> {
executeEvent('openPaymentInternal', {
address: addressInfo?.address,
name: addressInfo?.name,
});
}}>Send QORT</Button>
</Card>
</Box>
</>
)}
<Spacer height="40px" />
{isLoadingPayments && (
<Box sx={{
display: 'flex',
width: '100%',
justifyContent: 'center'
}}>
<CircularProgress sx={{
color: 'white'
}} />
</Box>
)}
{!isLoadingPayments && addressInfo && (
<Card
sx={{
padding: "15px",
overflow: "auto",
display: "flex",
flexDirection: "column",
background: "var(--bg-primary)",
}}
>
<Typography>20 most recent payments</Typography>
<Spacer height="20px" />
{!isLoadingPayments && payments?.length === 0 && (
<Box sx={{
display: 'flex',
width: '100%',
justifyContent: 'center'
}}>
<Typography>No payments</Typography>
</Box>
)}
<Table>
<TableHead>
<TableRow>
<TableCell>Sender</TableCell>
<TableCell>Reciver</TableCell>
<TableCell>Amount</TableCell>
<TableCell>Time</TableCell>
</TableRow>
</TableHead>
<TableBody>
{payments.map((payment, index) => (
<TableRow key={payment?.signature}>
<TableCell>
<Tooltip
title={
<span
style={{
color: "white",
fontSize: "14px",
fontWeight: 700,
}}
>
copy address
</span>
}
placement="bottom"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<ButtonBase
onClick={() => {
navigator.clipboard.writeText(
payment?.creatorAddress
);
}}
>
{formatAddress(payment?.creatorAddress)}
</ButtonBase>
</Tooltip>
</TableCell>
<TableCell>
<Tooltip
title={
<span
style={{
color: "white",
fontSize: "14px",
fontWeight: 700,
}}
>
copy address
</span>
}
placement="bottom"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<ButtonBase
onClick={() => {
navigator.clipboard.writeText(payment?.recipient);
}}
>
{formatAddress(payment?.recipient)}
</ButtonBase>
</Tooltip>
</TableCell>
<TableCell>
{payment?.amount}
</TableCell>
<TableCell>{formatTimestamp(payment?.timestamp)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
)}
</Box>
</Box>
</DrawerUserLookup>
);
};

View File

@ -121,6 +121,42 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => {
> >
Copy address Copy address
</Button> </Button>
<Button
variant="text"
onClick={() => {
executeEvent('openPaymentInternal', {
address,
name,
});
handleClose();
}}
sx={{
color: 'white',
justifyContent: 'flex-start'
}}
>
Send QORT
</Button>
<Button
variant="text"
onClick={() => {
executeEvent('openUserLookupDrawer', {
addressOrName: name || address
})
handleClose();
}}
sx={{
color: 'white',
justifyContent: 'flex-start'
}}
>
User lookup
</Button>
<BlockUser handleClose={handleClose} address={address} name={name} /> <BlockUser handleClose={handleClose} address={address} name={name} />
</Box> </Box>
</Popover> </Popover>

View File

@ -12,7 +12,7 @@ export function formatTimestamp(timestamp: number): string {
} else if (elapsedTime < 1440) { } else if (elapsedTime < 1440) {
return `${Math.floor(elapsedTime / 60)}h ago` return `${Math.floor(elapsedTime / 60)}h ago`
} else { } else {
return timestampMoment.format('MMM D') return timestampMoment.format('MMM D, YYYY')
} }
} }
export function formatTimestampForum(timestamp: number): string { export function formatTimestampForum(timestamp: number): string {