mirror of
https://github.com/Qortal/chrome-extension.git
synced 2025-03-14 11:52:33 +00:00
homepage, registername, userlookup, block, fixes
This commit is contained in:
parent
f0d2080a5b
commit
164a380c28
984
src/App.tsx
984
src/App.tsx
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,7 @@ import { CustomButton, TextItalic, TextP, TextSpan } from "../App-styles";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
@ -11,24 +12,24 @@ import {
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
Input,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Typography,
|
||||
ButtonBase,
|
||||
styled,
|
||||
tooltipClasses,
|
||||
TooltipProps
|
||||
Switch,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import Logo1 from "../assets/svgs/Logo1.svg";
|
||||
import Logo1Dark from "../assets/svgs/Logo1Dark.svg";
|
||||
import Info from "../assets/svgs/Info.svg";
|
||||
import HelpIcon from '@mui/icons-material/Help';
|
||||
import { CustomizedSnackbars } from "../components/Snackbar/Snackbar";
|
||||
import { set } from "lodash";
|
||||
import { cleanUrl, isUsingLocal } from "../background";
|
||||
import HelpIcon from '@mui/icons-material/Help';
|
||||
import { cleanUrl, gateways, isUsingLocal } from "../background";
|
||||
import { GlobalContext } from "../App";
|
||||
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
|
||||
|
||||
const manifestData = {
|
||||
version: "0.5.2",
|
||||
};
|
||||
|
||||
const manifestData = chrome?.runtime?.getManifest();
|
||||
|
||||
export const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
<Tooltip {...props} classes={{ popper: className }} />
|
||||
@ -41,40 +42,47 @@ export const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
fontSize: theme.typography.pxToRem(12),
|
||||
},
|
||||
}));
|
||||
function removeTrailingSlash(url) {
|
||||
return url.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
|
||||
export const NotAuthenticated = ({
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
setExtstate,
|
||||
|
||||
|
||||
apiKey,
|
||||
setApiKey,
|
||||
globalApiKey,
|
||||
handleSetGlobalApikey,
|
||||
currentNode,
|
||||
setCurrentNode,
|
||||
useLocalNode,
|
||||
setUseLocalNode
|
||||
}) => {
|
||||
const [isValidApiKey, setIsValidApiKey] = useState<boolean | null>(null);
|
||||
const [hasLocalNode, setHasLocalNode] = useState<boolean | null>(null);
|
||||
const [useLocalNode, setUseLocalNode] = useState(false);
|
||||
// const [useLocalNode, setUseLocalNode] = useState(false);
|
||||
const [openSnack, setOpenSnack] = React.useState(false);
|
||||
const [infoSnack, setInfoSnack] = React.useState(null);
|
||||
const [show, setShow] = React.useState(false);
|
||||
const [mode, setMode] = React.useState("list");
|
||||
const [customNodes, setCustomNodes] = React.useState(null);
|
||||
const [currentNode, setCurrentNode] = React.useState({
|
||||
url: "http://127.0.0.1:12391",
|
||||
});
|
||||
// const [currentNode, setCurrentNode] = React.useState({
|
||||
// url: "http://127.0.0.1:12391",
|
||||
// });
|
||||
const [importedApiKey, setImportedApiKey] = React.useState(null);
|
||||
//add and edit states
|
||||
const [url, setUrl] = React.useState("http://");
|
||||
const [url, setUrl] = React.useState("https://");
|
||||
const [customApikey, setCustomApiKey] = React.useState("");
|
||||
const [customNodeToSaveIndex, setCustomNodeToSaveIndex] =
|
||||
React.useState(null);
|
||||
const importedApiKeyRef = useRef(null)
|
||||
const currentNodeRef = useRef(null)
|
||||
const hasLocalNodeRef = useRef(null)
|
||||
const { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext);
|
||||
const { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext);
|
||||
|
||||
const importedApiKeyRef = useRef(null);
|
||||
const currentNodeRef = useRef(null);
|
||||
const hasLocalNodeRef = useRef(null);
|
||||
const isLocal = cleanUrl(currentNode?.url) === "127.0.0.1:12391";
|
||||
const handleFileChangeApiKey = (event) => {
|
||||
const file = event.target.files[0]; // Get the selected file
|
||||
@ -84,13 +92,34 @@ export const NotAuthenticated = ({
|
||||
const text = e.target.result; // Get the file content
|
||||
|
||||
setImportedApiKey(text); // Store the file content in the state
|
||||
if(customNodes){
|
||||
setCustomNodes((prev)=> {
|
||||
const copyPrev = [...prev]
|
||||
const findLocalIndex = copyPrev?.findIndex((item)=> item?.url === 'http://127.0.0.1:12391')
|
||||
if(findLocalIndex === -1){
|
||||
copyPrev.unshift({
|
||||
url: "http://127.0.0.1:12391",
|
||||
apikey: text
|
||||
})
|
||||
} else {
|
||||
copyPrev[findLocalIndex] = {
|
||||
url: "http://127.0.0.1:12391",
|
||||
apikey: text
|
||||
}
|
||||
}
|
||||
chrome?.runtime?.sendMessage(
|
||||
{ action: "setCustomNodes", copyPrev }
|
||||
);
|
||||
return copyPrev
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
reader.readAsText(file); // Read the file as text
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const checkIfUserHasLocalNode = useCallback(async () => {
|
||||
try {
|
||||
const url = `http://127.0.0.1:12391/admin/status`;
|
||||
@ -103,50 +132,105 @@ export const NotAuthenticated = ({
|
||||
const data = await response.json();
|
||||
if (data?.height) {
|
||||
setHasLocalNode(true);
|
||||
return true
|
||||
}
|
||||
} catch (error) {}
|
||||
return false
|
||||
|
||||
} catch (error) {
|
||||
return false
|
||||
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkIfUserHasLocalNode();
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{ action: "getCustomNodesFromStorage" },
|
||||
(response) => {
|
||||
if (response) {
|
||||
setCustomNodes(response || []);
|
||||
if(Array.isArray(response)){
|
||||
const findLocal = response?.find((item)=> item?.url === 'http://127.0.0.1:12391')
|
||||
if(findLocal && findLocal?.apikey){
|
||||
setImportedApiKey(findLocal?.apikey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(()=> {
|
||||
importedApiKeyRef.current = importedApiKey
|
||||
}, [importedApiKey])
|
||||
useEffect(()=> {
|
||||
currentNodeRef.current = currentNode
|
||||
}, [currentNode])
|
||||
useEffect(() => {
|
||||
importedApiKeyRef.current = importedApiKey;
|
||||
}, [importedApiKey]);
|
||||
useEffect(() => {
|
||||
currentNodeRef.current = currentNode;
|
||||
}, [currentNode]);
|
||||
|
||||
useEffect(() => {
|
||||
hasLocalNodeRef.current = hasLocalNode;
|
||||
}, [hasLocalNode]);
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
hasLocalNodeRef.current = hasLocalNode
|
||||
}, [hasLocalNode])
|
||||
|
||||
const validateApiKey = useCallback(async (key, fromStartUp) => {
|
||||
try {
|
||||
if(!currentNodeRef.current) return
|
||||
const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391";
|
||||
if(isLocalKey && !hasLocalNodeRef.current && !fromStartUp){
|
||||
throw new Error('Please turn on your local node')
|
||||
|
||||
if(key === "isGateway") return
|
||||
const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391";
|
||||
if (fromStartUp && key?.url && key?.apikey && !isLocalKey && !gateways.some(gateway => key?.url?.includes(gateway))) {
|
||||
setCurrentNode({
|
||||
url: key?.url,
|
||||
apikey: key?.apikey,
|
||||
});
|
||||
|
||||
let isValid = false
|
||||
|
||||
|
||||
const url = `${key?.url}/admin/settings/localAuthBypassEnabled`;
|
||||
const response = await fetch(url);
|
||||
|
||||
// Assuming the response is in plain text and will be 'true' or 'false'
|
||||
const data = await response.text();
|
||||
if(data && data === 'true'){
|
||||
isValid = true
|
||||
} else {
|
||||
const url2 = `${key?.url}/admin/apikey/test?apiKey=${key?.apikey}`;
|
||||
const response2 = await fetch(url2);
|
||||
|
||||
// Assuming the response is in plain text and will be 'true' or 'false'
|
||||
const data2 = await response2.text();
|
||||
if (data2 === "true") {
|
||||
isValid = true
|
||||
}
|
||||
}
|
||||
const isCurrentNodeLocal = cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391";
|
||||
if(isLocalKey && !isCurrentNodeLocal) {
|
||||
setIsValidApiKey(false);
|
||||
setUseLocalNode(false);
|
||||
return
|
||||
|
||||
if (isValid) {
|
||||
setIsValidApiKey(true);
|
||||
setUseLocalNode(true);
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
if (!currentNodeRef.current) return;
|
||||
const stillHasLocal = await checkIfUserHasLocalNode()
|
||||
|
||||
if (isLocalKey && !stillHasLocal && !fromStartUp) {
|
||||
throw new Error("Please turn on your local node");
|
||||
}
|
||||
//check custom nodes
|
||||
// !gateways.some(gateway => apiKey?.url?.includes(gateway))
|
||||
const isCurrentNodeLocal =
|
||||
cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391";
|
||||
if (isLocalKey && !isCurrentNodeLocal) {
|
||||
setIsValidApiKey(false);
|
||||
setUseLocalNode(false);
|
||||
return;
|
||||
}
|
||||
let payload = {};
|
||||
|
||||
if (currentNodeRef.current?.url === "http://127.0.0.1:12391") {
|
||||
@ -154,21 +238,32 @@ export const NotAuthenticated = ({
|
||||
apikey: importedApiKeyRef.current || key?.apikey,
|
||||
url: currentNodeRef.current?.url,
|
||||
};
|
||||
} else if(currentNodeRef.current) {
|
||||
} else if (currentNodeRef.current) {
|
||||
payload = currentNodeRef.current;
|
||||
}
|
||||
const url = `${payload?.url}/admin/apikey/test`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "text/plain",
|
||||
"X-API-KEY": payload?.apikey, // Include the API key here
|
||||
},
|
||||
});
|
||||
let isValid = false
|
||||
|
||||
|
||||
const url = `${payload?.url}/admin/settings/localAuthBypassEnabled`;
|
||||
const response = await fetch(url);
|
||||
|
||||
// Assuming the response is in plain text and will be 'true' or 'false'
|
||||
const data = await response.text();
|
||||
if (data === "true") {
|
||||
if(data && data === 'true'){
|
||||
isValid = true
|
||||
} else {
|
||||
const url2 = `${payload?.url}/admin/apikey/test?apiKey=${payload?.apikey}`;
|
||||
const response2 = await fetch(url2);
|
||||
|
||||
// Assuming the response is in plain text and will be 'true' or 'false'
|
||||
const data2 = await response2.text();
|
||||
if (data2 === "true") {
|
||||
isValid = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isValid) {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{ action: "setApiKey", payload },
|
||||
(response) => {
|
||||
@ -176,29 +271,49 @@ export const NotAuthenticated = ({
|
||||
handleSetGlobalApikey(payload);
|
||||
setIsValidApiKey(true);
|
||||
setUseLocalNode(true);
|
||||
if(!fromStartUp){
|
||||
setApiKey(payload)
|
||||
if (!fromStartUp) {
|
||||
setApiKey(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
} else {
|
||||
setIsValidApiKey(false);
|
||||
setUseLocalNode(false);
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
message: "Select a valid apikey",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
if(!fromStartUp){
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
message: "Select a valid apikey",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
}
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
setIsValidApiKey(false);
|
||||
setUseLocalNode(false);
|
||||
if (fromStartUp) {
|
||||
setCurrentNode({
|
||||
url: "http://127.0.0.1:12391",
|
||||
});
|
||||
chrome?.runtime?.sendMessage(
|
||||
{ action: "setApiKey", payload: "isGateway" },
|
||||
(response) => {
|
||||
if (response) {
|
||||
setApiKey(null);
|
||||
handleSetGlobalApikey(null);
|
||||
}
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
if(!fromStartUp){
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
message: error?.message || "Select a valid apikey",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
}
|
||||
console.error("Error validating API key:", error);
|
||||
}
|
||||
}, []);
|
||||
@ -212,22 +327,22 @@ export const NotAuthenticated = ({
|
||||
const addCustomNode = () => {
|
||||
setMode("add-node");
|
||||
};
|
||||
|
||||
const saveCustomNodes = (myNodes) => {
|
||||
const saveCustomNodes = (myNodes, isFullListOfNodes) => {
|
||||
let nodes = [...(myNodes || [])];
|
||||
if (customNodeToSaveIndex !== null) {
|
||||
if (!isFullListOfNodes && customNodeToSaveIndex !== null) {
|
||||
nodes.splice(customNodeToSaveIndex, 1, {
|
||||
url,
|
||||
url: removeTrailingSlash(url),
|
||||
apikey: customApikey,
|
||||
});
|
||||
} else if (url && customApikey) {
|
||||
} else if (!isFullListOfNodes && url) {
|
||||
nodes.push({
|
||||
url,
|
||||
url: removeTrailingSlash(url),
|
||||
apikey: customApikey,
|
||||
});
|
||||
}
|
||||
|
||||
setCustomNodes(nodes);
|
||||
|
||||
setCustomNodeToSaveIndex(null);
|
||||
if (!nodes) return;
|
||||
chrome?.runtime?.sendMessage(
|
||||
@ -235,14 +350,14 @@ export const NotAuthenticated = ({
|
||||
(response) => {
|
||||
if (response) {
|
||||
setMode("list");
|
||||
setUrl("http://");
|
||||
setUrl("https://");
|
||||
setCustomApiKey("");
|
||||
// add alert
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -254,7 +369,7 @@ export const NotAuthenticated = ({
|
||||
height: "154px",
|
||||
}}
|
||||
>
|
||||
<img src={Logo1Dark} className="base-image" />
|
||||
<img src={Logo1Dark} className="base-image" />
|
||||
</div>
|
||||
<Spacer height="30px" />
|
||||
<TextP
|
||||
@ -264,13 +379,12 @@ export const NotAuthenticated = ({
|
||||
fontSize: '18px'
|
||||
}}
|
||||
>
|
||||
WELCOME TO <TextItalic sx={{
|
||||
fontSize: '18px'
|
||||
}}>YOUR</TextItalic> <br></br>
|
||||
WELCOME TO
|
||||
<TextSpan sx={{
|
||||
fontSize: '18px'
|
||||
}}> QORTAL WALLET</TextSpan>
|
||||
}}> QORTAL</TextSpan>
|
||||
</TextP>
|
||||
|
||||
<Spacer height="30px" />
|
||||
<Box
|
||||
sx={{
|
||||
@ -291,9 +405,13 @@ export const NotAuthenticated = ({
|
||||
}
|
||||
>
|
||||
<CustomButton onClick={()=> setExtstate('wallets')}>
|
||||
Wallets
|
||||
{/* <input {...getInputProps()} /> */}
|
||||
Accounts
|
||||
</CustomButton>
|
||||
</HtmlTooltip>
|
||||
{/* <Tooltip title="Authenticate by importing your Qortal JSON file" arrow>
|
||||
<img src={Info} />
|
||||
</Tooltip> */}
|
||||
</Box>
|
||||
|
||||
<Spacer height="6px" />
|
||||
@ -302,9 +420,10 @@ export const NotAuthenticated = ({
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
|
||||
}}
|
||||
>
|
||||
<HtmlTooltip
|
||||
<HtmlTooltip
|
||||
disableHoverListener={hasSeenGettingStarted === true}
|
||||
placement="right"
|
||||
title={
|
||||
@ -333,21 +452,21 @@ export const NotAuthenticated = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create wallet
|
||||
Create account
|
||||
</CustomButton>
|
||||
</HtmlTooltip>
|
||||
|
||||
|
||||
</Box>
|
||||
<Spacer height="15px" />
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
visibility: !useLocalNode && 'hidden'
|
||||
}}
|
||||
>
|
||||
{"Using node: "} {currentNode?.url}
|
||||
</Typography>
|
||||
<Spacer height="15px" />
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
visibility: !useLocalNode && "hidden",
|
||||
}}
|
||||
>
|
||||
{"Using node: "} {currentNode?.url}
|
||||
</Typography>
|
||||
<>
|
||||
<Spacer height="15px" />
|
||||
<Box
|
||||
@ -375,7 +494,7 @@ export const NotAuthenticated = ({
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
sx={{
|
||||
sx={{
|
||||
"& .MuiFormControlLabel-label": {
|
||||
fontSize: '14px'
|
||||
}
|
||||
@ -398,27 +517,25 @@ export const NotAuthenticated = ({
|
||||
validateApiKey(currentNode);
|
||||
} else {
|
||||
setCurrentNode({
|
||||
url: "http://127.0.0.1:12391",
|
||||
})
|
||||
setUseLocalNode(false)
|
||||
chrome?.runtime?.sendMessage(
|
||||
{ action: "setApiKey", payload:null },
|
||||
(response) => {
|
||||
if (response) {
|
||||
setApiKey(null);
|
||||
handleSetGlobalApikey(null);
|
||||
|
||||
}
|
||||
url: "http://127.0.0.1:12391",
|
||||
});
|
||||
setUseLocalNode(false);
|
||||
chrome?.runtime?.sendMessage(
|
||||
{ action: "setApiKey", payload: null },
|
||||
(response) => {
|
||||
if (response) {
|
||||
setApiKey(null);
|
||||
handleSetGlobalApikey(null);
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}}
|
||||
disabled={false}
|
||||
defaultChecked
|
||||
/>
|
||||
}
|
||||
label={`Use ${isLocal ? 'Local' : 'Custom'} Node`}
|
||||
label={`Use ${isLocal ? "Local" : "Custom"} Node`}
|
||||
/>
|
||||
</Box>
|
||||
{currentNode?.url === "http://127.0.0.1:12391" && (
|
||||
@ -432,31 +549,33 @@ export const NotAuthenticated = ({
|
||||
onChange={handleFileChangeApiKey} // File input handler
|
||||
/>
|
||||
</Button>
|
||||
<Typography sx={{
|
||||
fontSize: '12px',
|
||||
visibility: importedApiKey ? 'visible' : 'hidden'
|
||||
}}>{`api key : ${importedApiKey}`}</Typography>
|
||||
|
||||
|
||||
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
visibility: importedApiKey ? "visible" : "hidden",
|
||||
}}
|
||||
>{`api key : ${importedApiKey}`}</Typography>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setShow(true);
|
||||
}}
|
||||
variant="contained"
|
||||
component="label"
|
||||
>
|
||||
Choose custom node
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setShow(true);
|
||||
}}
|
||||
variant="contained"
|
||||
component="label"
|
||||
>
|
||||
Choose custom node
|
||||
</Button>
|
||||
</>
|
||||
<Typography sx={{
|
||||
color: "white",
|
||||
fontSize: '12px'
|
||||
}}>Build version: {manifestData?.version}</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
color: "white",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
Build version: {manifestData?.version}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
<CustomizedSnackbars
|
||||
@ -483,7 +602,6 @@ export const NotAuthenticated = ({
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
|
||||
{mode === "list" && (
|
||||
<Box
|
||||
sx={{
|
||||
@ -525,16 +643,15 @@ export const NotAuthenticated = ({
|
||||
setMode("list");
|
||||
setShow(false);
|
||||
setUseLocalNode(false);
|
||||
chrome?.runtime?.sendMessage(
|
||||
{ action: "setApiKey", payload:null },
|
||||
(response) => {
|
||||
if (response) {
|
||||
setApiKey(null);
|
||||
handleSetGlobalApikey(null);
|
||||
|
||||
}
|
||||
chrome?.runtime?.sendMessage(
|
||||
{ action: "setApiKey", payload: null },
|
||||
(response) => {
|
||||
if (response) {
|
||||
setApiKey(null);
|
||||
handleSetGlobalApikey(null);
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
@ -579,17 +696,16 @@ export const NotAuthenticated = ({
|
||||
setMode("list");
|
||||
setShow(false);
|
||||
setIsValidApiKey(false);
|
||||
setUseLocalNode(false);
|
||||
chrome?.runtime?.sendMessage(
|
||||
{ action: "setApiKey", payload:null },
|
||||
(response) => {
|
||||
if (response) {
|
||||
setApiKey(null);
|
||||
handleSetGlobalApikey(null);
|
||||
|
||||
setUseLocalNode(false);
|
||||
chrome?.runtime?.sendMessage(
|
||||
{ action: "setApiKey", payload: null },
|
||||
(response) => {
|
||||
if (response) {
|
||||
setApiKey(null);
|
||||
handleSetGlobalApikey(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
@ -613,9 +729,8 @@ export const NotAuthenticated = ({
|
||||
const nodesToSave = [
|
||||
...(customNodes || []),
|
||||
].filter((item) => item?.url !== node?.url);
|
||||
|
||||
|
||||
saveCustomNodes(nodesToSave);
|
||||
saveCustomNodes(nodesToSave, true);
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
@ -652,9 +767,7 @@ export const NotAuthenticated = ({
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{mode === "list" && (
|
||||
@ -690,7 +803,7 @@ export const NotAuthenticated = ({
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!customApikey || !url}
|
||||
disabled={!url}
|
||||
onClick={() => saveCustomNodes(customNodes)}
|
||||
autoFocus
|
||||
>
|
||||
@ -701,7 +814,7 @@ export const NotAuthenticated = ({
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)}
|
||||
<ButtonBase onClick={()=> {
|
||||
<ButtonBase onClick={()=> {
|
||||
showTutorial('create-account', true)
|
||||
}} sx={{
|
||||
position: 'fixed',
|
||||
|
@ -134,11 +134,11 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
|
||||
setPassword('')
|
||||
setSeedError('')
|
||||
} else {
|
||||
setSeedError('Could not create wallet.')
|
||||
setSeedError('Could not create account.')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
setSeedError(error?.message || 'Could not create wallet.')
|
||||
setSeedError(error?.message || 'Could not create account.')
|
||||
} finally {
|
||||
setIsLoadingEncryptSeed(false)
|
||||
}
|
||||
@ -176,19 +176,19 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
|
||||
{(wallets?.length === 0 ||
|
||||
!wallets) ? (
|
||||
<>
|
||||
<Typography>No wallets saved</Typography>
|
||||
<Typography>No accounts saved</Typography>
|
||||
<Spacer height="75px" />
|
||||
</>
|
||||
): (
|
||||
<>
|
||||
<Typography>Your saved wallets</Typography>
|
||||
<Typography>Your saved accounts</Typography>
|
||||
<Spacer height="30px" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{rawWallet && (
|
||||
<Box>
|
||||
<Typography>Selected Wallet:</Typography>
|
||||
<Typography>Selected Account:</Typography>
|
||||
{rawWallet?.name && <Typography>{rawWallet.name}</Typography>}
|
||||
{rawWallet?.address0 && (
|
||||
<Typography>{rawWallet?.address0}</Typography>
|
||||
@ -267,7 +267,7 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
|
||||
padding: '10px'
|
||||
}} {...getRootProps()}>
|
||||
<input {...getInputProps()} />
|
||||
Add wallets
|
||||
Add account
|
||||
</CustomButton>
|
||||
</HtmlTooltip>
|
||||
</Box>
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
export const WalletIcon= ({ color, height, width }) => {
|
||||
return (
|
||||
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width={width || 30} height={width || 30} viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.0118 22.0891C18.0124 22.8671 16.6997 23.3391 15.2618 23.3391C13.8241 23.3391 12.5113 22.8671 11.5118 22.0891" stroke={color} stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M3.20108 17.356C2.7598 14.4844 2.53917 13.0486 3.08205 11.7758C3.62493 10.503 4.82938 9.63215 7.23827 7.89044L9.03808 6.58911C12.0347 4.42245 13.5331 3.33911 15.2618 3.33911C16.9907 3.33911 18.4889 4.42245 21.4856 6.58911L23.2854 7.89044C25.6943 9.63215 26.8988 10.503 27.4417 11.7758C27.9846 13.0486 27.7639 14.4844 27.3226 17.356L26.9463 19.8046C26.3208 23.8752 26.0079 25.9106 24.5481 27.1249C23.0882 28.3391 20.9539 28.3391 16.6853 28.3391H13.8383C9.56977 28.3391 7.43548 28.3391 5.97559 27.1249C4.5157 25.9106 4.20293 23.8752 3.57738 19.8046L3.20108 17.356Z" stroke={color} stroke-width="2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
BIN
src/assets/Icons/q-trade-logo.webp
Normal file
BIN
src/assets/Icons/q-trade-logo.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
@ -156,4 +156,9 @@ export const qMailLastEnteredTimestampAtom = atom({
|
||||
export const mailsAtom = atom({
|
||||
key: 'mailsAtom',
|
||||
default: [],
|
||||
});
|
||||
|
||||
export const groupsPropertiesAtom = atom({
|
||||
key: 'groupsPropertiesAtom',
|
||||
default: {},
|
||||
});
|
@ -1,82 +1,142 @@
|
||||
import { getKeyPair, getLastRef, processTransactionVersion2 } from "./background";
|
||||
import {
|
||||
createEndpoint,
|
||||
getKeyPair,
|
||||
getLastRef,
|
||||
processTransactionVersion2,
|
||||
} from "./background";
|
||||
import Base58 from "./deps/Base58";
|
||||
import { createTransaction } from "./transactions/transactions";
|
||||
|
||||
|
||||
export async function createRewardShareCase(data) {
|
||||
const {recipientPublicKey} = data;
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = JSON.parse(resKeyPair);
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
const keyPair = {
|
||||
privateKey: uint8PrivateKey,
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
let lastRef = await getLastRef();
|
||||
|
||||
const tx = await createTransaction(38, keyPair, {
|
||||
recipientPublicKey,
|
||||
percentageShare: 0,
|
||||
lastReference: lastRef,
|
||||
});
|
||||
const { recipientPublicKey } = data;
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = JSON.parse(resKeyPair);
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
const keyPair = {
|
||||
privateKey: uint8PrivateKey,
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
let lastRef = await getLastRef();
|
||||
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
return res
|
||||
|
||||
}
|
||||
const tx = await createTransaction(38, keyPair, {
|
||||
recipientPublicKey,
|
||||
percentageShare: 0,
|
||||
lastReference: lastRef,
|
||||
});
|
||||
|
||||
export async function removeRewardShareCase(data) {
|
||||
|
||||
const {rewardShareKeyPairPublicKey, recipient, percentageShare} = data;
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = JSON.parse(resKeyPair);
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
const keyPair = {
|
||||
privateKey: uint8PrivateKey,
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
let lastRef = await getLastRef();
|
||||
|
||||
const tx = await createTransaction(381, keyPair, {
|
||||
rewardShareKeyPairPublicKey,
|
||||
recipient,
|
||||
percentageShare,
|
||||
lastReference: lastRef,
|
||||
});
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
return res
|
||||
|
||||
}
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function removeRewardShareCase(data) {
|
||||
const { rewardShareKeyPairPublicKey, recipient, percentageShare } = data;
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = JSON.parse(resKeyPair);
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
const keyPair = {
|
||||
privateKey: uint8PrivateKey,
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
let lastRef = await getLastRef();
|
||||
|
||||
export async function getRewardSharePrivateKeyCase(data) {
|
||||
const {recipientPublicKey} = data
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = JSON.parse(resKeyPair);
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
const keyPair = {
|
||||
privateKey: uint8PrivateKey,
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
let lastRef = await getLastRef();
|
||||
|
||||
const tx = await createTransaction(38, keyPair, {
|
||||
recipientPublicKey,
|
||||
percentageShare: 0,
|
||||
lastReference: lastRef,
|
||||
});
|
||||
const tx = await createTransaction(381, keyPair, {
|
||||
rewardShareKeyPairPublicKey,
|
||||
recipient,
|
||||
percentageShare,
|
||||
lastReference: lastRef,
|
||||
});
|
||||
|
||||
return tx?._base58RewardShareSeed
|
||||
}
|
||||
const signedBytes = Base58.encode(tx.signedBytes);
|
||||
|
||||
const res = await processTransactionVersion2(signedBytes);
|
||||
if (!res?.signature)
|
||||
throw new Error("Transaction was not able to be processed");
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function getRewardSharePrivateKeyCase(data) {
|
||||
const { recipientPublicKey } = data;
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = JSON.parse(resKeyPair);
|
||||
const uint8PrivateKey = Base58.decode(parsedData.privateKey);
|
||||
const uint8PublicKey = Base58.decode(parsedData.publicKey);
|
||||
const keyPair = {
|
||||
privateKey: uint8PrivateKey,
|
||||
publicKey: uint8PublicKey,
|
||||
};
|
||||
let lastRef = await getLastRef();
|
||||
|
||||
const tx = await createTransaction(38, keyPair, {
|
||||
recipientPublicKey,
|
||||
percentageShare: 0,
|
||||
lastReference: lastRef,
|
||||
});
|
||||
|
||||
return tx?._base58RewardShareSeed;
|
||||
}
|
||||
|
||||
export async function listActionsCase(data) {
|
||||
|
||||
const { type, listName = "", items = [] } = data;
|
||||
let responseData;
|
||||
|
||||
if (type === "get") {
|
||||
const url = await createEndpoint(`/lists/${listName}`);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error("Failed to fetch");
|
||||
|
||||
responseData = await response.json();
|
||||
} else if (type === "remove") {
|
||||
const url = await createEndpoint(`/lists/${listName}`);
|
||||
const body = {
|
||||
items: items,
|
||||
};
|
||||
const bodyToString = JSON.stringify(body);
|
||||
const response = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: bodyToString,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to remove from list");
|
||||
let res;
|
||||
try {
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
responseData = res;
|
||||
} else if (type === "add") {
|
||||
const url = await createEndpoint(`/lists/${listName}`);
|
||||
const body = {
|
||||
items: items,
|
||||
};
|
||||
const bodyToString = JSON.stringify(body);
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: bodyToString,
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Failed to add to list");
|
||||
let res;
|
||||
try {
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
responseData = res;
|
||||
}
|
||||
|
||||
return responseData;
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ import { Sha256 } from "asmcrypto.js";
|
||||
import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest";
|
||||
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes";
|
||||
import TradeBotRespondRequest from './transactions/TradeBotRespondRequest';
|
||||
import { createRewardShareCase, getRewardSharePrivateKeyCase, removeRewardShareCase } from './background-cases';
|
||||
import { createRewardShareCase, getRewardSharePrivateKeyCase, listActionsCase, removeRewardShareCase } from './background-cases';
|
||||
|
||||
|
||||
|
||||
@ -551,7 +551,8 @@ const handleNotification = async (groups) => {
|
||||
|
||||
let mutedGroups = await getUserSettings({key: 'mutedGroups'}) || []
|
||||
if(!isArray(mutedGroups)) mutedGroups = []
|
||||
|
||||
mutedGroups.push('0')
|
||||
|
||||
let isFocused;
|
||||
const data = groups.filter((group) => group?.sender !== address && !mutedGroups.includes(group.groupId) && !isUpdateMsg(group?.data));
|
||||
const dataWithUpdates = groups.filter((group) => group?.sender !== address && !mutedGroups.includes(group.groupId));
|
||||
@ -832,6 +833,7 @@ 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) {
|
||||
@ -997,7 +999,7 @@ export async function getNameInfoForOthers(address) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
async function getAddressInfo(address) {
|
||||
export async function getAddressInfo(address) {
|
||||
const validApi = await getBaseApi();
|
||||
const response = await fetch(validApi + "/addresses/" + address);
|
||||
const data = await response.json();
|
||||
@ -1117,7 +1119,7 @@ export async function getBalanceInfo() {
|
||||
const validApi = await getBaseApi();
|
||||
const response = await fetch(validApi + "/addresses/balance/" + address);
|
||||
|
||||
if (!response?.ok) throw new Error("Cannot fetch balance");
|
||||
if (!response?.ok) throw new Error("0 QORT in your balance");
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
@ -1250,7 +1252,7 @@ export const getLastRef = async () => {
|
||||
const response = await fetch(
|
||||
validApi + "/addresses/lastreference/" + address
|
||||
);
|
||||
if (!response?.ok) throw new Error("Cannot fetch balance");
|
||||
if (!response?.ok) throw new Error("0 QORT in your balance");
|
||||
const data = await response.text();
|
||||
return data;
|
||||
};
|
||||
@ -3670,6 +3672,21 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
|
||||
|
||||
break;
|
||||
|
||||
case "listActions":
|
||||
{
|
||||
const data = request.payload;
|
||||
listActionsCase(data)
|
||||
.then((res) => {
|
||||
sendResponse(res);
|
||||
})
|
||||
.catch((error) => {
|
||||
sendResponse({ error: error.message });
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "oauth": {
|
||||
const { nodeBaseUrl, senderAddress, senderPublicKey, timestamp } =
|
||||
request.payload;
|
||||
|
10
src/common/Spinners/BarSpinner/BarSpinner.tsx
Normal file
10
src/common/Spinners/BarSpinner/BarSpinner.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import './barSpinner.css'
|
||||
export const BarSpinner = ({width = '20px', color}) => {
|
||||
return (
|
||||
<div style={{
|
||||
width,
|
||||
color: color || 'green'
|
||||
}} className="loader-bar"></div>
|
||||
)
|
||||
}
|
19
src/common/Spinners/BarSpinner/barSpinner.css
Normal file
19
src/common/Spinners/BarSpinner/barSpinner.css
Normal file
@ -0,0 +1,19 @@
|
||||
/* HTML: <div class="loader"></div> */
|
||||
.loader-bar {
|
||||
width: 45px;
|
||||
aspect-ratio: .75;
|
||||
--c:no-repeat linear-gradient(currentColor 0 0);
|
||||
background:
|
||||
var(--c) 0% 100%,
|
||||
var(--c) 50% 100%,
|
||||
var(--c) 100% 100%;
|
||||
background-size: 20% 65%;
|
||||
animation: l8 1s infinite linear;
|
||||
}
|
||||
@keyframes l8 {
|
||||
16.67% {background-position: 0% 0% ,50% 100%,100% 100%}
|
||||
33.33% {background-position: 0% 0% ,50% 0% ,100% 100%}
|
||||
50% {background-position: 0% 0% ,50% 0% ,100% 0% }
|
||||
66.67% {background-position: 0% 100%,50% 0% ,100% 0% }
|
||||
83.33% {background-position: 0% 100%,50% 100%,100% 0% }
|
||||
}
|
@ -11,7 +11,7 @@ import { useQortalMessageListener } from "./useQortalMessageListener";
|
||||
|
||||
|
||||
|
||||
export const AppViewer = React.forwardRef(({ app , hide}, iframeRef) => {
|
||||
export const AppViewer = React.forwardRef(({ app , hide, isDevMode}, iframeRef) => {
|
||||
const { rootHeight } = useContext(MyContext);
|
||||
// const iframeRef = useRef(null);
|
||||
const { document, window: frameWindow } = useFrame();
|
||||
@ -30,6 +30,17 @@ export const AppViewer = React.forwardRef(({ app , hide}, iframeRef) => {
|
||||
const refreshAppFunc = (e) => {
|
||||
const {tabId} = e.detail
|
||||
if(tabId === app?.tabId){
|
||||
|
||||
if(isDevMode){
|
||||
|
||||
resetHistory()
|
||||
if(!app?.isPreview || app?.isPrivate){
|
||||
setUrl(app?.url + `?time=${Date.now()}`)
|
||||
}
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
const constructUrl = `${getBaseApiReact()}/render/${app?.service}/${app?.name}${path != null ? path : ''}?theme=dark&identifier=${app?.identifier != null ? app?.identifier : ''}&time=${new Date().getMilliseconds()}`
|
||||
setUrl(constructUrl)
|
||||
}
|
||||
|
@ -394,7 +394,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
}}>
|
||||
|
||||
<Spacer height="30px" />
|
||||
<AppsHomeDesktop availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
<AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@ -423,6 +423,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
isSelected={tab?.tabId === selectedTab?.tabId}
|
||||
app={tab}
|
||||
ref={iframeRefs.current[tab.tabId]}
|
||||
isDevMode={tab?.service ? false : true}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -438,7 +439,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
|
||||
}}>
|
||||
|
||||
<Spacer height="30px" />
|
||||
<AppsHomeDesktop availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
<AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
@ -16,12 +16,14 @@ import { SortablePinnedApps } from "./SortablePinnedApps";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { extractComponents } from "../Chat/MessageDisplay";
|
||||
import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
|
||||
import { AppsPrivate } from "./AppsPrivate";
|
||||
|
||||
export const AppsHomeDesktop = ({
|
||||
setMode,
|
||||
myApp,
|
||||
myWebsite,
|
||||
availableQapps,
|
||||
myName
|
||||
}) => {
|
||||
const [qortalUrl, setQortalUrl] = useState('')
|
||||
|
||||
@ -136,7 +138,7 @@ export const AppsHomeDesktop = ({
|
||||
<AppCircleLabel>Library</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
|
||||
<AppsPrivate myName={myName} />
|
||||
<SortablePinnedApps
|
||||
isDesktop={true}
|
||||
availableQapps={availableQapps}
|
||||
|
@ -8,7 +8,6 @@ import NavBack from "../../assets/svgs/NavBack.svg";
|
||||
import NavAdd from "../../assets/svgs/NavAdd.svg";
|
||||
import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg";
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
|
||||
import {
|
||||
ButtonBase,
|
||||
ListItemIcon,
|
||||
@ -119,7 +118,6 @@ export const AppsNavBarDesktop = ({disableBack}) => {
|
||||
|
||||
const setTabsToNav = (e) => {
|
||||
const { tabs, selectedTab, isNewTabWindow } = e.detail?.data;
|
||||
|
||||
setTabs([...tabs]);
|
||||
setSelectedTab(!selectedTab ? null : { ...selectedTab });
|
||||
setIsNewTabWindow(isNewTabWindow);
|
||||
@ -135,10 +133,20 @@ export const AppsNavBarDesktop = ({disableBack}) => {
|
||||
|
||||
|
||||
|
||||
const isSelectedAppPinned = !!sortablePinnedApps?.find(
|
||||
(item) =>
|
||||
item?.name === selectedTab?.name && item?.service === selectedTab?.service
|
||||
);
|
||||
const isSelectedAppPinned = useMemo(()=> {
|
||||
if(selectedTab?.isPrivate){
|
||||
return !!sortablePinnedApps?.find(
|
||||
(item) =>
|
||||
item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name && item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service && item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier
|
||||
);
|
||||
} else {
|
||||
return !!sortablePinnedApps?.find(
|
||||
(item) =>
|
||||
item?.name === selectedTab?.name && item?.service === selectedTab?.service
|
||||
);
|
||||
}
|
||||
}, [selectedTab,sortablePinnedApps])
|
||||
|
||||
return (
|
||||
<AppsNavBarParent
|
||||
sx={{
|
||||
@ -283,22 +291,49 @@ export const AppsNavBarDesktop = ({disableBack}) => {
|
||||
|
||||
if (isSelectedAppPinned) {
|
||||
// Remove the selected app if it is pinned
|
||||
updatedApps = prev.filter(
|
||||
(item) =>
|
||||
!(
|
||||
item?.name === selectedTab?.name &&
|
||||
item?.service === selectedTab?.service
|
||||
)
|
||||
);
|
||||
if(selectedTab?.isPrivate){
|
||||
updatedApps = prev.filter(
|
||||
(item) =>
|
||||
!(
|
||||
item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name &&
|
||||
item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service &&
|
||||
item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier
|
||||
)
|
||||
);
|
||||
} else {
|
||||
updatedApps = prev.filter(
|
||||
(item) =>
|
||||
!(
|
||||
item?.name === selectedTab?.name &&
|
||||
item?.service === selectedTab?.service
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Add the selected app if it is not pinned
|
||||
updatedApps = [
|
||||
if(selectedTab?.isPrivate){
|
||||
updatedApps = [
|
||||
...prev,
|
||||
{
|
||||
name: selectedTab?.name,
|
||||
service: selectedTab?.service,
|
||||
isPreview: true,
|
||||
isPrivate: true,
|
||||
privateAppProperties: {
|
||||
...(selectedTab?.privateAppProperties || {})
|
||||
}
|
||||
|
||||
},
|
||||
];
|
||||
} else {
|
||||
updatedApps = [
|
||||
...prev,
|
||||
{
|
||||
name: selectedTab?.name,
|
||||
service: selectedTab?.service,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
saveToLocalStorage(
|
||||
@ -322,7 +357,7 @@ export const AppsNavBarDesktop = ({disableBack}) => {
|
||||
<PushPinIcon
|
||||
height={20}
|
||||
sx={{
|
||||
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)",
|
||||
color: isSelectedAppPinned ? "var(--danger)" : "rgba(250, 250, 250, 0.5)",
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
@ -331,7 +366,7 @@ export const AppsNavBarDesktop = ({disableBack}) => {
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)",
|
||||
color: isSelectedAppPinned ? "var(--danger)" : "rgba(250, 250, 250, 0.5)",
|
||||
},
|
||||
}}
|
||||
primary={`${isSelectedAppPinned ? "Unpin app" : "Pin app"}`}
|
||||
@ -339,9 +374,15 @@ export const AppsNavBarDesktop = ({disableBack}) => {
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
executeEvent("refreshApp", {
|
||||
tabId: selectedTab?.tabId,
|
||||
});
|
||||
if (selectedTab?.refreshFunc) {
|
||||
selectedTab.refreshFunc(selectedTab?.tabId);
|
||||
|
||||
} else {
|
||||
executeEvent("refreshApp", {
|
||||
tabId: selectedTab?.tabId,
|
||||
});
|
||||
}
|
||||
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
@ -369,38 +410,40 @@ export const AppsNavBarDesktop = ({disableBack}) => {
|
||||
primary="Refresh"
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
executeEvent("copyLink", {
|
||||
tabId: selectedTab?.tabId,
|
||||
});
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: "24px !important",
|
||||
marginRight: "5px",
|
||||
{!selectedTab?.isPrivate && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
executeEvent("copyLink", {
|
||||
tabId: selectedTab?.tabId,
|
||||
});
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ContentCopyIcon
|
||||
height={20}
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
color: "rgba(250, 250, 250, 0.5)",
|
||||
minWidth: "24px !important",
|
||||
marginRight: "5px",
|
||||
}}
|
||||
>
|
||||
<ContentCopyIcon
|
||||
height={20}
|
||||
sx={{
|
||||
color: "rgba(250, 250, 250, 0.5)",
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: "rgba(250, 250, 250, 0.5)",
|
||||
},
|
||||
}}
|
||||
primary="Copy link"
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: "rgba(250, 250, 250, 0.5)",
|
||||
},
|
||||
}}
|
||||
primary="Copy link"
|
||||
/>
|
||||
</MenuItem>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</AppsNavBarParent>
|
||||
);
|
||||
|
564
src/components/Apps/AppsPrivate.tsx
Normal file
564
src/components/Apps/AppsPrivate.tsx
Normal file
@ -0,0 +1,564 @@
|
||||
import React, { useContext, useMemo, useState } from "react";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Input,
|
||||
MenuItem,
|
||||
Select,
|
||||
Tab,
|
||||
Tabs,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { useHandlePrivateApps } from "./useHandlePrivateApps";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import { groupsPropertiesAtom, myGroupsWhereIAmAdminAtom } from "../../atoms/global";
|
||||
import { Label } from "../Group/AddGroup";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import {
|
||||
Add,
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppCircleLabel,
|
||||
PublishQAppChoseFile,
|
||||
PublishQAppInfo,
|
||||
} from "./Apps-styles";
|
||||
import ImageUploader from "../../common/ImageUploader";
|
||||
import { isMobile, MyContext } from "../../App";
|
||||
import { fileToBase64 } from "../../utils/fileReading";
|
||||
import { objectToBase64 } from "../../qdn/encryption/group-encryption";
|
||||
import { getFee } from "../../background";
|
||||
|
||||
const maxFileSize = 50 * 1024 * 1024; // 50MB
|
||||
|
||||
export const AppsPrivate = ({myName}) => {
|
||||
const { openApp } = useHandlePrivateApps();
|
||||
const [file, setFile] = useState(null);
|
||||
const [logo, setLogo] = useState(null);
|
||||
const [qortalUrl, setQortalUrl] = useState("");
|
||||
const [selectedGroup, setSelectedGroup] = useState(0);
|
||||
const [groupsProperties] = useRecoilState(groupsPropertiesAtom)
|
||||
const [valueTabPrivateApp, setValueTabPrivateApp] = useState(0);
|
||||
const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState(
|
||||
myGroupsWhereIAmAdminAtom
|
||||
);
|
||||
|
||||
const myGroupsWhereIAmAdmin = useMemo(()=> {
|
||||
return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false)
|
||||
}, [myGroupsWhereIAmAdminFromGlobal, groupsProperties])
|
||||
const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false);
|
||||
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({
|
||||
name: "",
|
||||
service: "DOCUMENT",
|
||||
identifier: "",
|
||||
groupId: 0,
|
||||
});
|
||||
|
||||
const [newPrivateAppValues, setNewPrivateAppValues] = useState({
|
||||
service: "DOCUMENT",
|
||||
identifier: "",
|
||||
name: "",
|
||||
});
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
"application/zip": [".zip"], // Only accept zip files
|
||||
},
|
||||
maxSize: maxFileSize,
|
||||
multiple: false, // Disable multiple file uploads
|
||||
onDrop: (acceptedFiles) => {
|
||||
if (acceptedFiles.length > 0) {
|
||||
setFile(acceptedFiles[0]); // Set the file name
|
||||
}
|
||||
},
|
||||
onDropRejected: (fileRejections) => {
|
||||
fileRejections.forEach(({ file, errors }) => {
|
||||
errors.forEach((error) => {
|
||||
if (error.code === "file-too-large") {
|
||||
console.error(
|
||||
`File ${file.name} is too large. Max size allowed is ${
|
||||
maxFileSize / (1024 * 1024)
|
||||
} MB.`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const addPrivateApp = async () => {
|
||||
try {
|
||||
if (privateAppValues?.groupId === 0) return;
|
||||
|
||||
await openApp(privateAppValues, true);
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
const clearFields = () => {
|
||||
setPrivateAppValues({
|
||||
name: "",
|
||||
service: "DOCUMENT",
|
||||
identifier: "",
|
||||
groupId: 0,
|
||||
});
|
||||
setNewPrivateAppValues({
|
||||
service: "DOCUMENT",
|
||||
identifier: "",
|
||||
name: "",
|
||||
});
|
||||
setFile(null);
|
||||
setValueTabPrivateApp(0);
|
||||
setSelectedGroup(0);
|
||||
setLogo(null);
|
||||
};
|
||||
|
||||
const publishPrivateApp = async () => {
|
||||
try {
|
||||
if (selectedGroup === 0) return;
|
||||
if (!logo) throw new Error("Please select an image for a logo");
|
||||
if (!myName) throw new Error("You need a Qortal name to publish");
|
||||
if (!newPrivateAppValues?.name) throw new Error("Your app needs a name");
|
||||
const base64Logo = await fileToBase64(logo);
|
||||
const base64App = await fileToBase64(file);
|
||||
const objectToSave = {
|
||||
app: base64App,
|
||||
logo: base64Logo,
|
||||
name: newPrivateAppValues.name,
|
||||
};
|
||||
const object64 = await objectToBase64(objectToSave);
|
||||
|
||||
const decryptedData = await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "ENCRYPT_QORTAL_GROUP_DATA",
|
||||
type: "qortalRequest",
|
||||
payload: {
|
||||
base64: object64,
|
||||
groupId: selectedGroup,
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (response.error) {
|
||||
rej(response?.message);
|
||||
return;
|
||||
} else {
|
||||
res(response);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
if (decryptedData?.error) {
|
||||
throw new Error(
|
||||
decryptedData?.error || "Unable to encrypt app. App not published"
|
||||
);
|
||||
}
|
||||
const fee = await getFee("ARBITRARY");
|
||||
|
||||
await show({
|
||||
message: "Would you like to publish this app?",
|
||||
publishFee: fee.fee + " QORT",
|
||||
});
|
||||
await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "publishOnQDN",
|
||||
payload: {
|
||||
data: decryptedData,
|
||||
identifier: newPrivateAppValues?.identifier,
|
||||
service: newPrivateAppValues?.service,
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (!response?.error) {
|
||||
res(response);
|
||||
return;
|
||||
}
|
||||
rej(response.error);
|
||||
}
|
||||
);
|
||||
});
|
||||
openApp(
|
||||
{
|
||||
identifier: newPrivateAppValues?.identifier,
|
||||
service: newPrivateAppValues?.service,
|
||||
name: myName,
|
||||
groupId: selectedGroup,
|
||||
},
|
||||
true
|
||||
);
|
||||
clearFields();
|
||||
} catch (error) {
|
||||
setOpenSnackGlobal(true)
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message: error?.message || "Unable to publish app",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setValueTabPrivateApp(newValue);
|
||||
};
|
||||
|
||||
function a11yProps(index: number) {
|
||||
return {
|
||||
id: `simple-tab-${index}`,
|
||||
"aria-controls": `simple-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setIsOpenPrivateModal(true);
|
||||
}}
|
||||
sx={{
|
||||
width: "80px",
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer
|
||||
sx={{
|
||||
gap: !isMobile ? "10px" : "5px",
|
||||
}}
|
||||
>
|
||||
<AppCircle>
|
||||
<Add>+</Add>
|
||||
</AppCircle>
|
||||
<AppCircleLabel>Private</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
{isOpenPrivateModal && (
|
||||
<Dialog
|
||||
open={isOpenPrivateModal}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (valueTabPrivateApp === 0) {
|
||||
if (
|
||||
!privateAppValues.name ||
|
||||
!privateAppValues.service ||
|
||||
!privateAppValues.identifier ||
|
||||
!privateAppValues?.groupId
|
||||
)
|
||||
return;
|
||||
addPrivateApp();
|
||||
}
|
||||
}
|
||||
}}
|
||||
maxWidth="md"
|
||||
fullWidth={true}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{valueTabPrivateApp === 0
|
||||
? "Access private app"
|
||||
: "Publish private app"}
|
||||
</DialogTitle>
|
||||
|
||||
<Box>
|
||||
<Tabs
|
||||
value={valueTabPrivateApp}
|
||||
onChange={handleChange}
|
||||
aria-label="basic tabs example"
|
||||
variant={isMobile ? "scrollable" : "fullWidth"} // Scrollable on mobile, full width on desktop
|
||||
scrollButtons="auto"
|
||||
allowScrollButtonsMobile
|
||||
sx={{
|
||||
"& .MuiTabs-indicator": {
|
||||
backgroundColor: "white",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
label="Access app"
|
||||
{...a11yProps(0)}
|
||||
sx={{
|
||||
"&.Mui-selected": {
|
||||
color: "white",
|
||||
},
|
||||
fontSize: isMobile ? "0.75rem" : "1rem", // Adjust font size for mobile
|
||||
}}
|
||||
/>
|
||||
<Tab
|
||||
label="Publish app"
|
||||
{...a11yProps(1)}
|
||||
sx={{
|
||||
"&.Mui-selected": {
|
||||
color: "white",
|
||||
},
|
||||
fontSize: isMobile ? "0.75rem" : "1rem", // Adjust font size for mobile
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
{valueTabPrivateApp === 0 && (
|
||||
<>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Label>Select a group</Label>
|
||||
<Label>Only private groups will be shown</Label>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={privateAppValues?.groupId}
|
||||
label="Groups"
|
||||
onChange={(e) => {
|
||||
setPrivateAppValues((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
groupId: e.target.value,
|
||||
};
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MenuItem value={0}>No group selected</MenuItem>
|
||||
|
||||
{myGroupsPrivate
|
||||
?.filter((item) => !item?.isOpen)
|
||||
.map((group) => {
|
||||
return (
|
||||
<MenuItem key={group?.groupId} value={group?.groupId}>
|
||||
{group?.groupName}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Box>
|
||||
<Spacer height="10px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Label>name</Label>
|
||||
<Input
|
||||
placeholder="name"
|
||||
value={privateAppValues?.name}
|
||||
onChange={(e) =>
|
||||
setPrivateAppValues((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
name: e.target.value,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Label>identifier</Label>
|
||||
<Input
|
||||
placeholder="identifier"
|
||||
value={privateAppValues?.identifier}
|
||||
onChange={(e) =>
|
||||
setPrivateAppValues((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
identifier: e.target.value,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setIsOpenPrivateModal(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
!privateAppValues.name ||
|
||||
!privateAppValues.service ||
|
||||
!privateAppValues.identifier ||
|
||||
!privateAppValues?.groupId
|
||||
}
|
||||
variant="contained"
|
||||
onClick={() => addPrivateApp()}
|
||||
autoFocus
|
||||
>
|
||||
Access
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
)}
|
||||
{valueTabPrivateApp === 1 && (
|
||||
<>
|
||||
<DialogContent>
|
||||
<PublishQAppInfo
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
Select .zip file containing static content:{" "}
|
||||
</PublishQAppInfo>
|
||||
<Spacer height="10px" />
|
||||
<PublishQAppInfo
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>{`
|
||||
50mb MB maximum`}</PublishQAppInfo>
|
||||
{file && (
|
||||
<>
|
||||
<Spacer height="5px" />
|
||||
<PublishQAppInfo>{`Selected: (${file?.name})`}</PublishQAppInfo>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Spacer height="18px" />
|
||||
<PublishQAppChoseFile {...getRootProps()}>
|
||||
{" "}
|
||||
<input {...getInputProps()} />
|
||||
{file ? "Change" : "Choose"} File
|
||||
</PublishQAppChoseFile>
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<Label>Select a group</Label>
|
||||
<Label>
|
||||
Only groups where you are an admin will be shown
|
||||
</Label>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={selectedGroup}
|
||||
label="Groups where you are an admin"
|
||||
onChange={(e) => setSelectedGroup(e.target.value)}
|
||||
>
|
||||
<MenuItem value={0}>No group selected</MenuItem>
|
||||
{myGroupsWhereIAmAdmin
|
||||
?.filter((item) => !item?.isOpen)
|
||||
.map((group) => {
|
||||
return (
|
||||
<MenuItem key={group?.groupId} value={group?.groupId}>
|
||||
{group?.groupName}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Box>
|
||||
<Spacer height="20px" />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Label>identifier</Label>
|
||||
<Input
|
||||
placeholder="identifier"
|
||||
value={newPrivateAppValues?.identifier}
|
||||
onChange={(e) =>
|
||||
setNewPrivateAppValues((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
identifier: e.target.value,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Spacer height="10px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "5px",
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<Label>App name</Label>
|
||||
<Input
|
||||
placeholder="App name"
|
||||
value={newPrivateAppValues?.name}
|
||||
onChange={(e) =>
|
||||
setNewPrivateAppValues((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
name: e.target.value,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Spacer height="10px" />
|
||||
<ImageUploader onPick={(file) => setLogo(file)}>
|
||||
<Button variant="contained">Choose logo</Button>
|
||||
</ImageUploader>
|
||||
{logo?.name}
|
||||
<Spacer height="25px" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setIsOpenPrivateModal(false);
|
||||
clearFields();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
!newPrivateAppValues.name ||
|
||||
!newPrivateAppValues.service ||
|
||||
!newPrivateAppValues.identifier ||
|
||||
!selectedGroup
|
||||
}
|
||||
variant="contained"
|
||||
onClick={() => publishPrivateApp()}
|
||||
autoFocus
|
||||
>
|
||||
Publish
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,18 +1,21 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { DndContext, closestCenter } from '@dnd-kit/core';
|
||||
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable';
|
||||
import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Avatar, ButtonBase } from '@mui/material';
|
||||
import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles';
|
||||
import { getBaseApiReact } from '../../App';
|
||||
import { getBaseApiReact, MyContext } from '../../App';
|
||||
import { executeEvent } from '../../utils/events';
|
||||
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { saveToLocalStorage } from './AppsNavBar';
|
||||
import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps';
|
||||
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
import { useHandlePrivateApps } from './useHandlePrivateApps';
|
||||
const SortableItem = ({ id, name, app, isDesktop }) => {
|
||||
const {openApp} = useHandlePrivateApps()
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
@ -28,17 +31,27 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
|
||||
|
||||
return (
|
||||
<ContextMenuPinnedApps app={app} isMine={!!app?.isMine}>
|
||||
<ButtonBase
|
||||
<ButtonBase
|
||||
ref={setNodeRef} {...attributes} {...listeners}
|
||||
sx={{
|
||||
width: "80px",
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
}}
|
||||
onClick={()=> {
|
||||
executeEvent("addTab", {
|
||||
data: app
|
||||
})
|
||||
onClick={async ()=> {
|
||||
if(app?.isPrivate){
|
||||
try {
|
||||
await openApp(app?.privateAppProperties)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
} else {
|
||||
executeEvent("addTab", {
|
||||
data: app
|
||||
})
|
||||
}
|
||||
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer sx={{
|
||||
@ -50,7 +63,15 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
|
||||
<LockIcon
|
||||
sx={{
|
||||
height: "42px",
|
||||
width: "42px",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "42px",
|
||||
width: "42px",
|
||||
@ -59,7 +80,7 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
|
||||
}
|
||||
}}
|
||||
alt={app?.metadata?.title || app?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
src={ app?.privateAppProperties?.logo ? app?.privateAppProperties?.logo :`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
@ -72,10 +93,19 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
)}
|
||||
|
||||
</AppCircle>
|
||||
<AppCircleLabel>
|
||||
{app?.isPrivate ? (
|
||||
<AppCircleLabel>
|
||||
{`${app?.privateAppProperties?.appName || "Private"}`}
|
||||
</AppCircleLabel>
|
||||
) : (
|
||||
<AppCircleLabel>
|
||||
{app?.metadata?.title || app?.name}
|
||||
</AppCircleLabel>
|
||||
)}
|
||||
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
</ContextMenuPinnedApps>
|
||||
|
@ -5,6 +5,7 @@ import { getBaseApiReact } from '../../App';
|
||||
import { Avatar, ButtonBase } from '@mui/material';
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
import { executeEvent } from '../../utils/events';
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
|
||||
const TabComponent = ({isSelected, app}) => {
|
||||
return (
|
||||
@ -34,25 +35,34 @@ const TabComponent = ({isSelected, app}) => {
|
||||
} src={NavCloseTab}/>
|
||||
|
||||
) }
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "28px",
|
||||
width: "28px",
|
||||
}}
|
||||
alt={app?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "28px",
|
||||
height: "auto",
|
||||
}}
|
||||
src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
|
||||
<LockIcon
|
||||
sx={{
|
||||
height: "28px",
|
||||
width: "28px",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "28px",
|
||||
width: "28px",
|
||||
}}
|
||||
alt={app?.name}
|
||||
src={app?.privateAppProperties?.logo ? app?.privateAppProperties?.logo :`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "28px",
|
||||
height: "auto",
|
||||
}}
|
||||
src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
)}
|
||||
</TabParent>
|
||||
</ButtonBase>
|
||||
)
|
||||
|
251
src/components/Apps/useHandlePrivateApps.tsx
Normal file
251
src/components/Apps/useHandlePrivateApps.tsx
Normal file
@ -0,0 +1,251 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
import { getBaseApiReact, MyContext } from "../../App";
|
||||
import { createEndpoint } from "../../background";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import {
|
||||
settingsLocalLastUpdatedAtom,
|
||||
sortablePinnedAppsAtom,
|
||||
} from "../../atoms/global";
|
||||
import { saveToLocalStorage } from "./AppsNavBarDesktop";
|
||||
import { base64ToBlobUrl } from "../../utils/fileReading";
|
||||
import { base64ToUint8Array } from "../../qdn/encryption/group-encryption";
|
||||
import { uint8ArrayToObject } from "../../backgroundFunctions/encryption";
|
||||
|
||||
export const useHandlePrivateApps = () => {
|
||||
const [status, setStatus] = useState("");
|
||||
const {
|
||||
openSnackGlobal,
|
||||
setOpenSnackGlobal,
|
||||
infoSnackCustom,
|
||||
setInfoSnackCustom,
|
||||
} = useContext(MyContext);
|
||||
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(
|
||||
sortablePinnedAppsAtom
|
||||
);
|
||||
const setSettingsLocalLastUpdated = useSetRecoilState(
|
||||
settingsLocalLastUpdatedAtom
|
||||
);
|
||||
const openApp = async (
|
||||
privateAppProperties,
|
||||
addToPinnedApps,
|
||||
setLoadingStatePrivateApp
|
||||
) => {
|
||||
try {
|
||||
|
||||
|
||||
if(setLoadingStatePrivateApp){
|
||||
setLoadingStatePrivateApp(`Downloading and decrypting private app.`);
|
||||
|
||||
}
|
||||
setOpenSnackGlobal(true);
|
||||
|
||||
setInfoSnackCustom({
|
||||
type: "info",
|
||||
message: "Fetching app data",
|
||||
duration: null
|
||||
});
|
||||
const urlData = `${getBaseApiReact()}/arbitrary/${
|
||||
privateAppProperties?.service
|
||||
}/${privateAppProperties?.name}/${
|
||||
privateAppProperties?.identifier
|
||||
}?encoding=base64`;
|
||||
let data;
|
||||
try {
|
||||
const responseData = await fetch(urlData, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if(!responseData?.ok){
|
||||
if(setLoadingStatePrivateApp){
|
||||
setLoadingStatePrivateApp("Error! Unable to download private app.");
|
||||
}
|
||||
|
||||
throw new Error("Unable to fetch app");
|
||||
}
|
||||
|
||||
data = await responseData.text();
|
||||
if (data?.error) {
|
||||
if(setLoadingStatePrivateApp){
|
||||
|
||||
setLoadingStatePrivateApp("Error! Unable to download private app.");
|
||||
}
|
||||
throw new Error("Unable to fetch app");
|
||||
}
|
||||
} catch (error) {
|
||||
if(setLoadingStatePrivateApp){
|
||||
|
||||
setLoadingStatePrivateApp("Error! Unable to download private app.");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
let decryptedData;
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
|
||||
|
||||
decryptedData = await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "DECRYPT_QORTAL_GROUP_DATA",
|
||||
type: "qortalRequest",
|
||||
payload: {
|
||||
base64: data,
|
||||
groupId: privateAppProperties?.groupId,
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (response.error) {
|
||||
rej(response?.message);
|
||||
return;
|
||||
} else {
|
||||
res(response);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
if (decryptedData?.error) {
|
||||
if(setLoadingStatePrivateApp){
|
||||
|
||||
setLoadingStatePrivateApp("Error! Unable to decrypt private app.");
|
||||
}
|
||||
throw new Error(decryptedData?.error);
|
||||
}
|
||||
} catch (error) {
|
||||
if(setLoadingStatePrivateApp){
|
||||
|
||||
setLoadingStatePrivateApp("Error! Unable to decrypt private app.");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
const convertToUint = base64ToUint8Array(decryptedData);
|
||||
const UintToObject = uint8ArrayToObject(convertToUint);
|
||||
|
||||
if (decryptedData) {
|
||||
setInfoSnackCustom({
|
||||
type: "info",
|
||||
message: "Building app",
|
||||
});
|
||||
const endpoint = await createEndpoint(
|
||||
`/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true`
|
||||
);
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
body: UintToObject?.app,
|
||||
});
|
||||
const previewPath = await response.text();
|
||||
const refreshfunc = async (tabId, privateAppProperties) => {
|
||||
const checkIfPreviewLinkStillWorksUrl = await createEndpoint(
|
||||
`/render/hash/HmtnZpcRPwisMfprUXuBp27N2xtv5cDiQjqGZo8tbZS?secret=E39WTiG4qBq3MFcMPeRZabtQuzyfHg9ZuR5SgY7nW1YH`
|
||||
);
|
||||
const res = await fetch(checkIfPreviewLinkStillWorksUrl);
|
||||
if (res.ok) {
|
||||
executeEvent("refreshApp", {
|
||||
tabId: tabId,
|
||||
});
|
||||
} else {
|
||||
const endpoint = await createEndpoint(
|
||||
`/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true`
|
||||
);
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
body: UintToObject?.app,
|
||||
});
|
||||
const previewPath = await response.text();
|
||||
executeEvent("updateAppUrl", {
|
||||
tabId: tabId,
|
||||
url: await createEndpoint(previewPath),
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
executeEvent("refreshApp", {
|
||||
tabId: tabId,
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
const appName = UintToObject?.name;
|
||||
const logo = UintToObject?.logo
|
||||
? `data:image/png;base64,${UintToObject?.logo}`
|
||||
: null;
|
||||
|
||||
const dataBody = {
|
||||
url: await createEndpoint(previewPath),
|
||||
isPreview: true,
|
||||
isPrivate: true,
|
||||
privateAppProperties: { ...privateAppProperties, logo, appName },
|
||||
filePath: "",
|
||||
refreshFunc: (tabId) => {
|
||||
refreshfunc(tabId, privateAppProperties);
|
||||
},
|
||||
};
|
||||
executeEvent("addTab", {
|
||||
data: dataBody,
|
||||
});
|
||||
setInfoSnackCustom({
|
||||
type: "success",
|
||||
message: "Opened",
|
||||
});
|
||||
if(setLoadingStatePrivateApp){
|
||||
|
||||
setLoadingStatePrivateApp(``);
|
||||
}
|
||||
if (addToPinnedApps) {
|
||||
setSortablePinnedApps((prev) => {
|
||||
const updatedApps = [
|
||||
...prev,
|
||||
{
|
||||
isPrivate: true,
|
||||
isPreview: true,
|
||||
privateAppProperties: {
|
||||
...privateAppProperties,
|
||||
logo,
|
||||
appName,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
saveToLocalStorage(
|
||||
"ext_saved_settings",
|
||||
"sortablePinnedApps",
|
||||
updatedApps
|
||||
);
|
||||
return updatedApps;
|
||||
});
|
||||
setSettingsLocalLastUpdated(Date.now());
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if(setLoadingStatePrivateApp){
|
||||
|
||||
setLoadingStatePrivateApp(`Error! ${error?.message || 'Unable to build private app.'}`);
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
setInfoSnackCustom({
|
||||
type: "error",
|
||||
message: error?.message || "Unable to fetch app",
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
return {
|
||||
openApp,
|
||||
status,
|
||||
};
|
||||
};
|
@ -243,7 +243,7 @@ const UIQortalRequests = [
|
||||
'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE',
|
||||
'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER',
|
||||
'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER',
|
||||
'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_PUBLIC_NODE', 'ADMIN_ACTION', 'SIGN_TRANSACTION', 'DECRYPT_QORTAL_GROUP_DATA', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'SHOW_ACTIONS', 'REGISTER_NAME', 'UPDATE_NAME', 'LEAVE_GROUP', 'INVITE_TO_GROUP', 'KICK_FROM_GROUP', 'BAN_FROM_GROUP', 'CANCEL_GROUP_BAN', 'ADD_GROUP_ADMIN','REMOVE_GROUP_ADMIN','DECRYPT_AESGCM', 'CANCEL_GROUP_INVITE', 'CREATE_GROUP'
|
||||
'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_PUBLIC_NODE', 'ADMIN_ACTION', 'SIGN_TRANSACTION', 'DECRYPT_QORTAL_GROUP_DATA', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'SHOW_ACTIONS', 'REGISTER_NAME', 'UPDATE_NAME', 'LEAVE_GROUP', 'INVITE_TO_GROUP', 'KICK_FROM_GROUP', 'BAN_FROM_GROUP', 'CANCEL_GROUP_BAN', 'ADD_GROUP_ADMIN','REMOVE_GROUP_ADMIN','DECRYPT_AESGCM', 'CANCEL_GROUP_INVITE', 'CREATE_GROUP', 'GET_USER_WALLET_TRANSACTIONS'
|
||||
];
|
||||
|
||||
|
||||
|
154
src/components/BuyQortInformation.tsx
Normal file
154
src/components/BuyQortInformation.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Collapse,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Input,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
List,
|
||||
MenuItem,
|
||||
Popover,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Label } from './Group/AddGroup';
|
||||
import { Spacer } from '../common/Spacer';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { getBaseApiReact, MyContext } from '../App';
|
||||
import { getFee } from '../background';
|
||||
import qTradeLogo from "../assets/Icons/q-trade-logo.webp";
|
||||
|
||||
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
|
||||
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../utils/events';
|
||||
import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner';
|
||||
|
||||
|
||||
|
||||
export const BuyQortInformation = ({balance}) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const openBuyQortInfoFunc = useCallback((e) => {
|
||||
setIsOpen(true)
|
||||
|
||||
}, [ setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("openBuyQortInfo", openBuyQortInfoFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("openBuyQortInfo", openBuyQortInfoFunc);
|
||||
};
|
||||
}, [openBuyQortInfoFunc]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{"Get QORT"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
width: "400px",
|
||||
maxWidth: '90vw',
|
||||
height: "400px",
|
||||
maxHeight: '90vh',
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<Typography>Get QORT using Qortal's crosschain trade portal</Typography>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "secondary.main" },
|
||||
transition: "all 0.1s ease-in-out",
|
||||
padding: "5px",
|
||||
borderRadius: "5px",
|
||||
gap: "5px",
|
||||
}}
|
||||
onClick={async () => {
|
||||
executeEvent("addTab", {
|
||||
data: { service: "APP", name: "q-trade" },
|
||||
});
|
||||
executeEvent("open-apps-mode", {});
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
borderRadius: "50%",
|
||||
height: '30px'
|
||||
}}
|
||||
src={qTradeLogo}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
Trade QORT
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
<Spacer height='40px' />
|
||||
<Typography sx={{
|
||||
textDecoration: 'underline'
|
||||
}}>Benefits of having QORT</Typography>
|
||||
<List
|
||||
sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}
|
||||
aria-label="contacts"
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
|
||||
<ListItemIcon>
|
||||
<RadioButtonCheckedIcon sx={{
|
||||
color: 'white'
|
||||
}} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Create transactions on the Qortal Blockchain" />
|
||||
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
|
||||
<ListItemIcon>
|
||||
<RadioButtonCheckedIcon sx={{
|
||||
color: 'white'
|
||||
}} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Having at least 4 QORT in your balance allows you to send chat messages at near instant speed." />
|
||||
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
||||
import { CreateCommonSecret } from './CreateCommonSecret'
|
||||
import { reusableGet } from '../../qdn/publish/pubish'
|
||||
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'
|
||||
@ -10,11 +10,11 @@ import Tiptap from './TipTap'
|
||||
import { CustomButton } from '../../App-styles'
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'
|
||||
import { getBaseApiReact, getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App'
|
||||
import { getBaseApiReact, getBaseApiReactSocket, isMobile, MyContext, pauseAllQueues, resumeAllQueues } from '../../App'
|
||||
import { CustomizedSnackbars } from '../Snackbar/Snackbar'
|
||||
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes'
|
||||
import { useMessageQueue } from '../../MessageQueueContext'
|
||||
import { executeEvent } from '../../utils/events'
|
||||
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'
|
||||
import { Box, ButtonBase, Divider, Typography } from '@mui/material'
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import { ReplyPreview } from './MessageItem'
|
||||
@ -47,6 +47,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
const [isOpenQManager, setIsOpenQManager] = useState(null)
|
||||
const [onEditMessage, setOnEditMessage] = useState(null)
|
||||
const [messageSize, setMessageSize] = useState(0)
|
||||
const {isUserBlocked} = useContext(MyContext)
|
||||
|
||||
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
@ -167,10 +168,28 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
})
|
||||
}
|
||||
|
||||
const middletierFunc = async (data: any, groupId: string) => {
|
||||
const updateChatMessagesWithBlocksFunc = (e) => {
|
||||
if(e.detail){
|
||||
setMessages((prev)=> prev?.filter((item)=> {
|
||||
return !isUserBlocked(item?.sender, item?.senderName)
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("updateChatMessagesWithBlocks", updateChatMessagesWithBlocksFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("updateChatMessagesWithBlocks", updateChatMessagesWithBlocksFunc);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const middletierFunc = async (data: any, groupId: string) => {
|
||||
try {
|
||||
if (hasInitialized.current) {
|
||||
decryptMessages(data, true);
|
||||
const dataRemovedBlock = data?.filter((item)=> !isUserBlocked(item?.sender, item?.senderName))
|
||||
|
||||
decryptMessages(dataRemovedBlock, true);
|
||||
return;
|
||||
}
|
||||
hasInitialized.current = true;
|
||||
@ -182,7 +201,11 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
decryptMessages(responseData, false);
|
||||
const dataRemovedBlock = responseData?.filter((item)=> {
|
||||
return !isUserBlocked(item?.sender, item?.senderName)
|
||||
})
|
||||
|
||||
decryptMessages(dataRemovedBlock, false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
InputBase,
|
||||
MenuItem,
|
||||
Select,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
@ -584,49 +585,89 @@ export const ChatOptions = ({
|
||||
minHeight: "200px",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setMode("search");
|
||||
}}
|
||||
>
|
||||
<SearchIcon />
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setMode("default");
|
||||
setSearchValue("");
|
||||
setSelectedMember(0);
|
||||
openQManager();
|
||||
}}
|
||||
>
|
||||
<InsertLinkIcon
|
||||
sx={{
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
</ButtonBase>
|
||||
<ContextMenuMentions
|
||||
getTimestampMention={getTimestampMention}
|
||||
groupId={selectedGroup}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setMode("mentions");
|
||||
setSearchValue("");
|
||||
setSelectedMember(0);
|
||||
<ButtonBase onClick={() => {
|
||||
setMode("search")
|
||||
}}>
|
||||
<Tooltip
|
||||
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>SEARCH</span>}
|
||||
placement="left"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AlternateEmailIcon
|
||||
sx={{
|
||||
color:
|
||||
mentionList?.length > 0 &&
|
||||
(!lastMentionTimestamp ||
|
||||
lastMentionTimestamp < mentionList[0]?.timestamp)
|
||||
? "var(--unread)"
|
||||
: "white",
|
||||
}}
|
||||
/>
|
||||
</ButtonBase>
|
||||
<SearchIcon />
|
||||
</Tooltip>
|
||||
</ButtonBase>
|
||||
<ButtonBase onClick={() => {
|
||||
setMode("default")
|
||||
setSearchValue('')
|
||||
setSelectedMember(0)
|
||||
openQManager()
|
||||
}}>
|
||||
<Tooltip
|
||||
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>Q-MANAGER</span>}
|
||||
placement="left"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<InsertLinkIcon sx={{ color: 'white' }} />
|
||||
</Tooltip>
|
||||
</ButtonBase>
|
||||
<ContextMenuMentions getTimestampMention={getTimestampMention} groupId={selectedGroup}>
|
||||
<ButtonBase onClick={() => {
|
||||
setMode("mentions")
|
||||
setSearchValue('')
|
||||
setSelectedMember(0)
|
||||
}}>
|
||||
<Tooltip
|
||||
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>MENTIONED</span>}
|
||||
placement="left"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AlternateEmailIcon sx={{
|
||||
color: mentionList?.length > 0 && (!lastMentionTimestamp || lastMentionTimestamp < mentionList[0]?.timestamp) ? 'var(--unread)' : 'white'
|
||||
}} />
|
||||
</Tooltip>
|
||||
</ButtonBase>
|
||||
</ContextMenuMentions>
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -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, '');
|
||||
}, [htmlContent])
|
||||
|
||||
const handleClick = async (e) => {
|
||||
e.preventDefault();
|
||||
@ -94,7 +98,15 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
|
||||
const target = e.target;
|
||||
if (target.tagName === 'A') {
|
||||
const href = target.getAttribute('href');
|
||||
window.electronAPI.openExternal(href);
|
||||
if (chrome && chrome.tabs) {
|
||||
chrome.tabs.create({ url: href }, (tab) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error opening tab:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("Tab opened successfully:", tab);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (target.getAttribute('data-url')) {
|
||||
const url = target.getAttribute('data-url');
|
||||
|
||||
|
@ -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";
|
||||
@ -8,6 +8,7 @@ import { getBaseApi } from "../../background";
|
||||
import { MyContext, getBaseApiReact } from "../../App";
|
||||
import { generateHTML } from "@tiptap/react";
|
||||
import Highlight from "@tiptap/extension-highlight";
|
||||
import Mention from "@tiptap/extension-mention";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
@ -17,7 +18,6 @@ import { Spacer } from "../../common/Spacer";
|
||||
import { ReactionPicker } from "../ReactionPicker";
|
||||
import KeyOffIcon from '@mui/icons-material/KeyOff';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import Mention from "@tiptap/extension-mention";
|
||||
import TextStyle from '@tiptap/extension-text-style';
|
||||
import { addressInfoKeySelector } from "../../atoms/global";
|
||||
import { useRecoilValue } from "recoil";
|
||||
@ -50,8 +50,7 @@ const getBadgeImg = (level)=> {
|
||||
default: return level0Img
|
||||
}
|
||||
}
|
||||
|
||||
export const MessageItem = ({
|
||||
export const MessageItem = React.memo(({
|
||||
message,
|
||||
onSeen,
|
||||
isLast,
|
||||
@ -67,40 +66,80 @@ export const MessageItem = ({
|
||||
isUpdating,
|
||||
lastSignature,
|
||||
onEdit,
|
||||
isPrivate,
|
||||
setMobileViewModeKeepOpen
|
||||
isPrivate
|
||||
}) => {
|
||||
const {getIndividualUserInfo} = useContext(MyContext)
|
||||
const userInfo = useRecoilValue(addressInfoKeySelector(message?.sender));
|
||||
|
||||
const {getIndividualUserInfo} = useContext(MyContext)
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [selectedReaction, setSelectedReaction] = useState(null);
|
||||
const { ref, inView } = useInView({
|
||||
threshold: 0.7, // Fully visible
|
||||
triggerOnce: false, // Only trigger once when it becomes visible
|
||||
});
|
||||
const [userInfo, setUserInfo] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (inView && isLast && onSeen) {
|
||||
onSeen(message.id);
|
||||
}
|
||||
}, [inView, message.id, isLast]);
|
||||
|
||||
useEffect(()=> {
|
||||
if(message?.sender){
|
||||
getIndividualUserInfo(message?.sender)
|
||||
useEffect(()=> {
|
||||
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",
|
||||
@ -135,25 +174,25 @@ export const MessageItem = ({
|
||||
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>
|
||||
)}
|
||||
@ -195,7 +234,7 @@ export const MessageItem = ({
|
||||
gap: '10px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
{message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && (
|
||||
{message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
onEdit(message);
|
||||
@ -260,41 +299,27 @@ export const MessageItem = ({
|
||||
}}>Replied to {reply?.senderName || reply?.senderAddress}</Typography>
|
||||
{reply?.messageText && (
|
||||
<MessageDisplay
|
||||
htmlContent={generateHTML(reply?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention,
|
||||
TextStyle
|
||||
])}
|
||||
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
|
||||
htmlContent={htmlReply}
|
||||
/>
|
||||
)}
|
||||
{reply?.decryptedData?.type === "notification" ? (
|
||||
<MessageDisplay htmlContent={reply.decryptedData?.data?.message} />
|
||||
) : (
|
||||
<MessageDisplay setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} isReply htmlContent={reply.text} />
|
||||
<MessageDisplay isReply htmlContent={reply.text} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{message?.messageText && (
|
||||
|
||||
<MessageDisplay
|
||||
htmlContent={generateHTML(message?.messageText, [
|
||||
StarterKit,
|
||||
Underline,
|
||||
Highlight,
|
||||
Mention,
|
||||
TextStyle
|
||||
])}
|
||||
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
|
||||
htmlContent={htmlText}
|
||||
/>
|
||||
)}
|
||||
|
||||
{message?.decryptedData?.type === "notification" ? (
|
||||
<MessageDisplay htmlContent={message.decryptedData?.data?.message} />
|
||||
) : (
|
||||
<MessageDisplay setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} htmlContent={message.text} />
|
||||
<MessageDisplay htmlContent={message.text} />
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
@ -319,11 +344,13 @@ export const MessageItem = ({
|
||||
background: 'var(--bg-2)',
|
||||
borderRadius: '7px'
|
||||
}} onClick={(event) => {
|
||||
event.stopPropagation(); // Prevent event bubbling
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedReaction(reaction);
|
||||
}}>
|
||||
<div>{reaction}</div> {numberOfReactions > 1 && (
|
||||
event.stopPropagation(); // Prevent event bubbling
|
||||
setAnchorEl(event.currentTarget);
|
||||
setSelectedReaction(reaction);
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '16px'
|
||||
}}>{reaction}</div> {numberOfReactions > 1 && (
|
||||
<Typography sx={{
|
||||
marginLeft: '4px'
|
||||
}}>{' '} {numberOfReactions}</Typography>
|
||||
@ -361,7 +388,7 @@ export const MessageItem = ({
|
||||
</Typography>
|
||||
<List sx={{
|
||||
overflow: 'auto',
|
||||
maxWidth: '80vw',
|
||||
maxWidth: '300px',
|
||||
maxHeight: '300px'
|
||||
}}>
|
||||
{reactions[selectedReaction]?.map((reactionItem) => (
|
||||
@ -404,14 +431,14 @@ export const MessageItem = ({
|
||||
alignItems: 'center',
|
||||
gap: '15px'
|
||||
}}>
|
||||
{message?.isNotEncrypted && isPrivate && (
|
||||
{message?.isNotEncrypted && isPrivate && (
|
||||
<KeyOffIcon sx={{
|
||||
color: 'white',
|
||||
marginLeft: '10px'
|
||||
}} />
|
||||
)}
|
||||
|
||||
{isUpdating ? (
|
||||
{isUpdating ? (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
@ -460,21 +487,11 @@ export const MessageItem = ({
|
||||
</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})=> {
|
||||
@ -501,7 +518,7 @@ export const ReplyPreview = ({message, isEdit})=> {
|
||||
<Box sx={{
|
||||
padding: '5px'
|
||||
}}>
|
||||
{isEdit ? (
|
||||
{isEdit ? (
|
||||
<Typography sx={{
|
||||
fontSize: '12px',
|
||||
fontWeight: 600
|
||||
@ -531,5 +548,38 @@ 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>
|
||||
|
||||
}
|
192
src/components/Chat/useBlockUsers.tsx
Normal file
192
src/components/Chat/useBlockUsers.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { getBaseApiReact } from "../../App";
|
||||
import { truncate } from "lodash";
|
||||
|
||||
|
||||
|
||||
export const useBlockedAddresses = () => {
|
||||
const userBlockedRef = useRef({})
|
||||
const userNamesBlockedRef = useRef({})
|
||||
|
||||
const getAllBlockedUsers = useCallback(()=> {
|
||||
|
||||
return {
|
||||
names: userNamesBlockedRef.current,
|
||||
addresses: userBlockedRef.current
|
||||
}
|
||||
}, [])
|
||||
|
||||
const isUserBlocked = useCallback((address, name)=> {
|
||||
try {
|
||||
if(!address) return false
|
||||
if(userBlockedRef.current[address] || userNamesBlockedRef.current[name]) return true
|
||||
return false
|
||||
|
||||
|
||||
} catch (error) {
|
||||
//error
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(()=> {
|
||||
const fetchBlockedList = async ()=> {
|
||||
try {
|
||||
const response = await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "listActions",
|
||||
payload: {
|
||||
type: 'get',
|
||||
listName: `blockedAddresses`,
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (response.error) {
|
||||
rej(response?.message);
|
||||
return;
|
||||
} else {
|
||||
res(response);
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
const blockedUsers = {}
|
||||
response?.forEach((item)=> {
|
||||
blockedUsers[item] = true
|
||||
})
|
||||
userBlockedRef.current = blockedUsers
|
||||
const response2 = await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "listActions",
|
||||
payload: {
|
||||
type: 'get',
|
||||
listName: `blockedNames`,
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (response.error) {
|
||||
rej(response?.message);
|
||||
return;
|
||||
} else {
|
||||
res(response);
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
|
||||
const blockedUsers2 = {}
|
||||
response2?.forEach((item)=> {
|
||||
blockedUsers2[item] = true
|
||||
})
|
||||
userNamesBlockedRef.current = blockedUsers2
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
fetchBlockedList()
|
||||
}, [])
|
||||
|
||||
const removeBlockFromList = useCallback(async (address, name)=> {
|
||||
await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "listActions",
|
||||
payload: {
|
||||
type: 'remove',
|
||||
items: name ? [name] : [address],
|
||||
listName: name ? 'blockedNames' : 'blockedAddresses'
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (response.error) {
|
||||
rej(response?.message);
|
||||
return;
|
||||
} else {
|
||||
if(!name){
|
||||
const copyObject = {...userBlockedRef.current}
|
||||
delete copyObject[address]
|
||||
userBlockedRef.current = copyObject
|
||||
} else {
|
||||
const copyObject = {...userNamesBlockedRef.current}
|
||||
delete copyObject[name]
|
||||
userNamesBlockedRef.current = copyObject
|
||||
}
|
||||
res(response);
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
|
||||
if(name && userBlockedRef.current[address]){
|
||||
await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "listActions",
|
||||
payload: {
|
||||
type: 'remove',
|
||||
items: !name ? [name] : [address],
|
||||
listName: !name ? 'blockedNames' : 'blockedAddresses'
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (response.error) {
|
||||
rej(response?.message);
|
||||
return;
|
||||
} else {
|
||||
const copyObject = {...userBlockedRef.current}
|
||||
delete copyObject[address]
|
||||
userBlockedRef.current = copyObject
|
||||
res(response);
|
||||
}
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
}, [])
|
||||
|
||||
const addToBlockList = useCallback(async (address, name)=> {
|
||||
await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "listActions",
|
||||
payload: {
|
||||
type: 'add',
|
||||
items: name ? [name] : [address],
|
||||
listName: name ? 'blockedNames' : 'blockedAddresses'
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
console.log('response', response)
|
||||
if (response.error) {
|
||||
rej(response?.message);
|
||||
return;
|
||||
} else {
|
||||
if(name){
|
||||
|
||||
const copyObject = {...userNamesBlockedRef.current}
|
||||
copyObject[name] = true
|
||||
userNamesBlockedRef.current = copyObject
|
||||
}else {
|
||||
const copyObject = {...userBlockedRef.current}
|
||||
copyObject[address] = true
|
||||
userBlockedRef.current = copyObject
|
||||
|
||||
}
|
||||
res(response);
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return {
|
||||
isUserBlocked,
|
||||
addToBlockList,
|
||||
removeBlockFromList,
|
||||
getAllBlockedUsers
|
||||
};
|
||||
};
|
@ -124,11 +124,19 @@ export const ContextMenuPinnedApps = ({ children, app, isMine }) => {
|
||||
<MenuItem onClick={(e) => {
|
||||
handleClose(e);
|
||||
setSortablePinnedApps((prev) => {
|
||||
const updatedApps = prev.filter(
|
||||
(item) => !(item?.name === app?.name && item?.service === app?.service)
|
||||
);
|
||||
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps);
|
||||
return updatedApps;
|
||||
if(app?.isPrivate){
|
||||
const updatedApps = prev.filter(
|
||||
(item) => !(item?.privateAppProperties?.name === app?.privateAppProperties?.name && item?.privateAppProperties?.service === app?.privateAppProperties?.service && item?.privateAppProperties?.identifier === app?.privateAppProperties?.identifier)
|
||||
);
|
||||
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps);
|
||||
return updatedApps;
|
||||
} else {
|
||||
const updatedApps = prev.filter(
|
||||
(item) => !(item?.name === app?.name && item?.service === app?.service)
|
||||
);
|
||||
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps);
|
||||
return updatedApps;
|
||||
}
|
||||
});
|
||||
}}>
|
||||
<ListItemIcon sx={{ minWidth: '32px' }}>
|
||||
|
@ -97,7 +97,7 @@ export const CoreSyncStatus = ({imageSize, position}) => {
|
||||
<h4 className="lineHeight">{message}</h4>
|
||||
<h4 className="lineHeight">Block Height: <span style={{ color: '#03a9f4' }}>{height || ''}</span></h4>
|
||||
<h4 className="lineHeight">Connected Peers: <span style={{ color: '#03a9f4' }}>{numberOfConnections || ''}</span></h4>
|
||||
<h4 className="lineHeight">Using gateway: <span style={{ color: '#03a9f4' }}>{isUsingGateway?.toString()}</span></h4>
|
||||
<h4 className="lineHeight">Using public node: <span style={{ color: '#03a9f4' }}>{isUsingGateway?.toString()}</span></h4>
|
||||
<i></i>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,9 +18,9 @@ import { NotificationIcon2 } from "../../assets/Icons/NotificationIcon2";
|
||||
import { ChatIcon } from "../../assets/Icons/ChatIcon";
|
||||
import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon";
|
||||
import { MembersIcon } from "../../assets/Icons/MembersIcon";
|
||||
import { AdminsIcon } from "../../assets/Icons/AdminsIcon";
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
|
||||
import { AdminsIcon } from "../../assets/Icons/AdminsIcon";
|
||||
|
||||
const IconWrapper = ({ children, label, color, selected, selectColor, customHeight }) => {
|
||||
return (
|
||||
@ -98,7 +98,7 @@ export const DesktopHeader = ({
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '10px'
|
||||
}}>
|
||||
@ -118,7 +118,7 @@ export const DesktopHeader = ({
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{selectedGroup?.groupName}
|
||||
{selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
@ -126,9 +126,10 @@ export const DesktopHeader = ({
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
visibility: selectedGroup?.groupId === '0' ? 'hidden' : 'visibile'
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
goToAnnouncements()
|
||||
@ -139,6 +140,7 @@ export const DesktopHeader = ({
|
||||
label="ANN"
|
||||
selected={isAnnouncement}
|
||||
selectColor="#09b6e8"
|
||||
customHeight="55px"
|
||||
>
|
||||
<NotificationIcon2
|
||||
height={25}
|
||||
|
22
src/components/Drawer/DrawerUserLookup.tsx
Normal file
22
src/components/Drawer/DrawerUserLookup.tsx
Normal 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>
|
||||
);
|
||||
}
|
101
src/components/Explore/Explore.tsx
Normal file
101
src/components/Explore/Explore.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { Box, ButtonBase, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import ChatIcon from "@mui/icons-material/Chat";
|
||||
import qTradeLogo from "../../assets/Icons/q-trade-logo.webp";
|
||||
import AppsIcon from "@mui/icons-material/Apps";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
export const Explore = ({setDesktopViewMode}) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "secondary.main" },
|
||||
transition: "all 0.1s ease-in-out",
|
||||
padding: "5px",
|
||||
borderRadius: "5px",
|
||||
gap: "5px",
|
||||
}}
|
||||
onClick={async () => {
|
||||
executeEvent("addTab", {
|
||||
data: { service: "APP", name: "q-trade" },
|
||||
});
|
||||
executeEvent("open-apps-mode", {});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
borderRadius: "50%",
|
||||
height: '30px'
|
||||
}}
|
||||
src={qTradeLogo}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
Trade QORT
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "secondary.main" },
|
||||
transition: "all 0.1s ease-in-out",
|
||||
padding: "5px",
|
||||
borderRadius: "5px",
|
||||
gap: "5px",
|
||||
}}
|
||||
onClick={()=> {
|
||||
setDesktopViewMode('apps')
|
||||
|
||||
}}
|
||||
>
|
||||
<AppsIcon
|
||||
sx={{
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
See Apps
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
"&:hover": { backgroundColor: "secondary.main" },
|
||||
transition: "all 0.1s ease-in-out",
|
||||
padding: "5px",
|
||||
borderRadius: "5px",
|
||||
gap: "5px",
|
||||
}}
|
||||
onClick={async () => {
|
||||
executeEvent("openGroupMessage", {
|
||||
from: "0" ,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ChatIcon
|
||||
sx={{
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
General Chat
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
);
|
||||
};
|
190
src/components/Group/BlockedUsersModal.tsx
Normal file
190
src/components/Group/BlockedUsersModal.tsx
Normal file
@ -0,0 +1,190 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { MyContext } from "../../App";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
|
||||
export const BlockedUsersModal = ({ close }) => {
|
||||
const [hasChanged, setHasChanged] = useState(false);
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const { getAllBlockedUsers, removeBlockFromList, addToBlockList } = useContext(MyContext);
|
||||
const [blockedUsers, setBlockedUsers] = useState({
|
||||
addresses: {},
|
||||
names: {},
|
||||
});
|
||||
const fetchBlockedUsers = () => {
|
||||
setBlockedUsers(getAllBlockedUsers());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchBlockedUsers();
|
||||
}, []);
|
||||
return (
|
||||
<Dialog
|
||||
open={true}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle>Blocked Users</DialogTitle>
|
||||
<DialogContent sx={{
|
||||
padding: '20px'
|
||||
}}>
|
||||
<Box
|
||||
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
placeholder="Name"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Button variant="contained" onClick={async ()=> {
|
||||
try {
|
||||
if(!value) return
|
||||
await addToBlockList(undefined, value)
|
||||
fetchBlockedUsers()
|
||||
setHasChanged(true)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}}>Block</Button>
|
||||
</Box>
|
||||
|
||||
{Object.entries(blockedUsers?.addresses).length > 0 && (
|
||||
<>
|
||||
<Spacer height="20px" />
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Blocked Users for Chat ( addresses )
|
||||
</DialogContentText>
|
||||
<Spacer height="10px" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
}}>
|
||||
{Object.entries(blockedUsers?.addresses || {})?.map(
|
||||
([key, value]) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
width: '100%',
|
||||
justifyContent: 'space-between'
|
||||
}}
|
||||
>
|
||||
<Typography>{key}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await removeBlockFromList(key, undefined);
|
||||
setHasChanged(true);
|
||||
setValue('')
|
||||
fetchBlockedUsers();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Unblock
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Box>
|
||||
{Object.entries(blockedUsers?.names).length > 0 && (
|
||||
<>
|
||||
<Spacer height="20px" />
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Blocked Users for QDN and Chat (names)
|
||||
</DialogContentText>
|
||||
<Spacer height="10px" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
}}>
|
||||
{Object.entries(blockedUsers?.names || {})?.map(([key, value]) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
width: '100%',
|
||||
justifyContent: 'space-between'
|
||||
}}
|
||||
>
|
||||
<Typography>{key}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await removeBlockFromList(undefined, key);
|
||||
setHasChanged(true);
|
||||
fetchBlockedUsers();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Unblock
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
sx={{
|
||||
backgroundColor: "var(--green)",
|
||||
color: "black",
|
||||
fontWeight: "bold",
|
||||
opacity: 0.7,
|
||||
"&:hover": {
|
||||
backgroundColor: "var(--green)",
|
||||
color: "black",
|
||||
opacity: 1,
|
||||
},
|
||||
}}
|
||||
variant="contained"
|
||||
onClick={()=> {
|
||||
if(hasChanged){
|
||||
executeEvent('updateChatMessagesWithBlocks', true)
|
||||
}
|
||||
close()
|
||||
}}
|
||||
>
|
||||
close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
@ -94,13 +94,16 @@ import { AppsDesktop } from "../Apps/AppsDesktop";
|
||||
import { formatEmailDate } from "./QMailMessages";
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import { addressInfoControllerAtom, selectedGroupIdAtom } from "../../atoms/global";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import { addressInfoControllerAtom, groupsPropertiesAtom, selectedGroupIdAtom } from "../../atoms/global";
|
||||
import { sortArrayByTimestampAndGroupName } from "../../utils/time";
|
||||
import { AdminSpace } from "../Chat/AdminSpace";
|
||||
import { HubsIcon } from "../../assets/Icons/HubsIcon";
|
||||
import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
|
||||
import { DesktopSideBar } from "../DesktopSideBar";
|
||||
import BlockIcon from '@mui/icons-material/Block';
|
||||
import { BlockedUsersModal } from "./BlockedUsersModal";
|
||||
|
||||
|
||||
// let touchStartY = 0;
|
||||
// let disablePullToRefresh = false;
|
||||
@ -480,6 +483,7 @@ export const Group = ({
|
||||
const [mobileViewMode, setMobileViewMode] = useState("home");
|
||||
const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState("");
|
||||
const isFocusedRef = useRef(true);
|
||||
const [isOpenBlockedUserModal, setIsOpenBlockedUserModal] = React.useState(false);
|
||||
const timestampEnterDataRef = useRef({});
|
||||
const selectedGroupRef = useRef(null);
|
||||
const selectedDirectRef = useRef(null);
|
||||
@ -497,9 +501,11 @@ export const Group = ({
|
||||
const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false)
|
||||
const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom)
|
||||
|
||||
const [groupsProperties, setGroupsProperties] = useState({})
|
||||
const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom)
|
||||
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
|
||||
@ -899,7 +905,10 @@ export const Group = ({
|
||||
}
|
||||
if(isPrivate === false){
|
||||
setTriedToFetchSecretKey(true);
|
||||
getAdminsForPublic(selectedGroup)
|
||||
if(selectedGroup?.groupId !== '0'){
|
||||
getAdminsForPublic(selectedGroup)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}, [selectedGroup, isPrivate]);
|
||||
@ -988,7 +997,7 @@ export const Group = ({
|
||||
// Update the component state with the received 'sendqort' state
|
||||
setGroups(sortArrayByTimestampAndGroupName(message.payload));
|
||||
getLatestRegularChat(message.payload)
|
||||
setMemberGroups(message.payload);
|
||||
setMemberGroups(message.payload?.filter((item)=> item?.groupId !== '0'));
|
||||
|
||||
if (selectedGroupRef.current && groupSectionRef.current === "chat") {
|
||||
chrome?.runtime?.sendMessage({
|
||||
@ -1081,7 +1090,7 @@ export const Group = ({
|
||||
!initiatedGetMembers.current &&
|
||||
selectedGroup?.groupId &&
|
||||
secretKey &&
|
||||
admins.includes(myAddress)
|
||||
admins.includes(myAddress) && selectedGroup?.groupId !== '0'
|
||||
) {
|
||||
// getAdmins(selectedGroup?.groupId);
|
||||
getMembers(selectedGroup?.groupId);
|
||||
@ -1432,11 +1441,11 @@ export const Group = ({
|
||||
if (isLoadingOpenSectionFromNotification.current) return;
|
||||
|
||||
const groupId = e.detail?.from;
|
||||
|
||||
const findGroup = groups?.find((group) => +group?.groupId === +groupId);
|
||||
if (findGroup?.groupId === selectedGroup?.groupId) {
|
||||
isLoadingOpenSectionFromNotification.current = false;
|
||||
|
||||
setChatMode("groups");
|
||||
setDesktopViewMode('chat')
|
||||
return;
|
||||
}
|
||||
if (findGroup) {
|
||||
@ -2159,7 +2168,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: {
|
||||
@ -2218,9 +2227,11 @@ export const Group = ({
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
padding: "10px",
|
||||
gap: '10px'
|
||||
}}
|
||||
>
|
||||
{chatMode === "groups" && (
|
||||
{chatMode === "groups" && (
|
||||
<>
|
||||
<CustomButton
|
||||
onClick={() => {
|
||||
setOpenAddGroup(true);
|
||||
@ -2231,8 +2242,24 @@ export const Group = ({
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
Group Mgmt
|
||||
Group Mgmt
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
onClick={() => {
|
||||
setIsOpenBlockedUserModal(true);
|
||||
}}
|
||||
sx={{
|
||||
minWidth: 'unset',
|
||||
padding: '10px'
|
||||
}}
|
||||
>
|
||||
<BlockIcon
|
||||
sx={{
|
||||
color: "white",
|
||||
}}
|
||||
/>
|
||||
</CustomButton>
|
||||
</>
|
||||
)}
|
||||
{chatMode === "directs" && (
|
||||
<CustomButton
|
||||
@ -2742,7 +2769,11 @@ export const Group = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOpenBlockedUserModal && (
|
||||
<BlockedUsersModal close={()=> {
|
||||
setIsOpenBlockedUserModal(false)
|
||||
}} />
|
||||
)}
|
||||
{selectedDirect && !newChat && (
|
||||
<>
|
||||
<Box
|
||||
@ -2815,6 +2846,7 @@ export const Group = ({
|
||||
{!isMobile && (
|
||||
|
||||
<HomeDesktop
|
||||
name={userInfo?.name}
|
||||
refreshHomeDataFunc={refreshHomeDataFunc}
|
||||
myAddress={myAddress}
|
||||
isLoadingGroups={isLoadingGroups}
|
||||
|
@ -10,16 +10,20 @@ import CommentIcon from "@mui/icons-material/Comment";
|
||||
import InfoIcon from "@mui/icons-material/Info";
|
||||
import GroupAddIcon from "@mui/icons-material/GroupAdd";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Box, ButtonBase, Collapse, Typography } from "@mui/material";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { getGroupNames } from "./UserListOfInvites";
|
||||
import { CustomLoader } from "../../common/CustomLoader";
|
||||
import { getBaseApiReact, isMobile } from "../../App";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||
|
||||
export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
|
||||
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState(
|
||||
[]
|
||||
);
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
|
||||
const getJoinRequests = async () => {
|
||||
@ -53,120 +57,129 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
<ButtonBase
|
||||
sx={{
|
||||
width: "322px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexDirection: "row",
|
||||
padding: "0px 20px",
|
||||
gap: '10px',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
onClick={()=> setIsExpanded((prev)=> !prev)}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
Group Invites:
|
||||
Group Invites {groupsWithJoinRequests?.length > 0 && ` (${groupsWithJoinRequests?.length})`}
|
||||
</Typography>
|
||||
<Spacer height="10px" />
|
||||
</Box>
|
||||
{isExpanded ? <ExpandLessIcon sx={{
|
||||
marginLeft: 'auto'
|
||||
}} /> : (
|
||||
<ExpandMoreIcon sx={{
|
||||
marginLeft: 'auto'
|
||||
}}/>
|
||||
)}
|
||||
</ButtonBase>
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<Box
|
||||
sx={{
|
||||
width: "322px",
|
||||
height: isMobile ? "165px" : "250px",
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: "322px",
|
||||
height: isMobile ? "165px" : "250px",
|
||||
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
bgcolor: "background.paper",
|
||||
padding: "20px",
|
||||
borderRadius: "19px",
|
||||
}}
|
||||
>
|
||||
{loading && groupsWithJoinRequests.length === 0 && (
|
||||
<Box
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
bgcolor: "background.paper",
|
||||
padding: "20px",
|
||||
borderRadius: "19px",
|
||||
}}
|
||||
>
|
||||
{loading && groupsWithJoinRequests.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<CustomLoader />
|
||||
</Box>
|
||||
)}
|
||||
{!loading && groupsWithJoinRequests.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "11px",
|
||||
fontWeight: 400,
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
}}
|
||||
>
|
||||
Nothing to display
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<List
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
maxWidth: 360,
|
||||
bgcolor: "background.paper",
|
||||
maxHeight: "300px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
className="scrollable-container"
|
||||
>
|
||||
<CustomLoader />
|
||||
</Box>
|
||||
)}
|
||||
{!loading && groupsWithJoinRequests.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "11px",
|
||||
fontWeight: 400,
|
||||
color: 'rgba(255, 255, 255, 0.2)'
|
||||
}}
|
||||
>
|
||||
Nothing to display
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<List
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 360,
|
||||
bgcolor: "background.paper",
|
||||
maxHeight: "300px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{groupsWithJoinRequests?.map((group) => {
|
||||
return (
|
||||
<ListItem
|
||||
sx={{
|
||||
marginBottom: "20px",
|
||||
}}
|
||||
key={group?.groupId}
|
||||
onClick={() => {
|
||||
setOpenAddGroup(true);
|
||||
setTimeout(() => {
|
||||
executeEvent("openGroupInvitesRequest", {});
|
||||
}, 300);
|
||||
}}
|
||||
disablePadding
|
||||
secondaryAction={
|
||||
<IconButton edge="end" aria-label="comments">
|
||||
<GroupAddIcon
|
||||
{groupsWithJoinRequests?.map((group) => {
|
||||
return (
|
||||
<ListItem
|
||||
sx={{
|
||||
marginBottom: "20px",
|
||||
}}
|
||||
key={group?.groupId}
|
||||
onClick={() => {
|
||||
setOpenAddGroup(true);
|
||||
setTimeout(() => {
|
||||
executeEvent("openGroupInvitesRequest", {});
|
||||
}, 300);
|
||||
}}
|
||||
disablePadding
|
||||
secondaryAction={
|
||||
<IconButton edge="end" aria-label="comments">
|
||||
<GroupAddIcon
|
||||
sx={{
|
||||
color: "white",
|
||||
fontSize: "18px",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemButton disableRipple role={undefined} dense>
|
||||
<ListItemText
|
||||
sx={{
|
||||
color: "white",
|
||||
fontSize: "18px",
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "13px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
}}
|
||||
primary={`${group?.groupName} has invited you`}
|
||||
/>
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemButton disableRipple role={undefined} dense>
|
||||
<ListItemText
|
||||
sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "13px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
}}
|
||||
primary={`${group?.groupName} has invited you`}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -11,16 +11,20 @@ import InfoIcon from "@mui/icons-material/Info";
|
||||
import { RequestQueueWithPromise } from "../../utils/queue/queue";
|
||||
import GroupAddIcon from '@mui/icons-material/GroupAdd';
|
||||
import { executeEvent } from "../../utils/events";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Box, ButtonBase, Collapse, Typography } from "@mui/material";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { CustomLoader } from "../../common/CustomLoader";
|
||||
import { getBaseApi } from "../../background";
|
||||
import { MyContext, getBaseApiReact, isMobile } from "../../App";
|
||||
import { myGroupsWhereIAmAdminAtom } from "../../atoms/global";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2)
|
||||
|
||||
export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection, setMobileViewMode, setDesktopViewMode }) => {
|
||||
const [isExpanded, setIsExpanded] = React.useState(false)
|
||||
|
||||
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([])
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
const {txList, setTxList} = React.useContext(MyContext)
|
||||
@ -34,7 +38,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
|
||||
setLoading(true)
|
||||
|
||||
let groupsAsAdmin = []
|
||||
const getAllGroupsAsAdmin = groups.map(async (group)=> {
|
||||
const getAllGroupsAsAdmin = groups.filter((item)=> item.groupId !== '0').map(async (group)=> {
|
||||
|
||||
const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
|
||||
return fetch(
|
||||
@ -55,7 +59,6 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
|
||||
|
||||
await Promise.all(getAllGroupsAsAdmin)
|
||||
setMyGroupsWhereIAmAdmin(groupsAsAdmin)
|
||||
|
||||
const res = await Promise.all(groupsAsAdmin.map(async (group)=> {
|
||||
|
||||
const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
|
||||
@ -110,26 +113,33 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
|
||||
flexDirection: "column",
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<Box
|
||||
<ButtonBase
|
||||
sx={{
|
||||
width: "322px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexDirection: "row",
|
||||
padding: '0px 20px',
|
||||
|
||||
gap: '10px',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
onClick={()=> setIsExpanded((prev)=> !prev)}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
Join Requests:
|
||||
Join Requests {filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length > 0 && ` (${filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length})`}
|
||||
</Typography>
|
||||
<Spacer height="10px" />
|
||||
</Box>
|
||||
|
||||
{isExpanded ? <ExpandLessIcon sx={{
|
||||
marginLeft: 'auto'
|
||||
}} /> : (
|
||||
<ExpandMoreIcon sx={{
|
||||
marginLeft: 'auto'
|
||||
}}/>
|
||||
)}
|
||||
</ButtonBase>
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<Box
|
||||
sx={{
|
||||
width: "322px",
|
||||
@ -173,7 +183,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<List sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper", maxHeight: '300px', overflow: 'auto' }}>
|
||||
<List className="scrollable-container" sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper", maxHeight: '300px', overflow: 'auto' }}>
|
||||
{filteredJoinRequests?.map((group)=> {
|
||||
if(group?.data?.length === 0) return null
|
||||
return (
|
||||
@ -228,6 +238,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
|
||||
|
||||
</List>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Box, Button, Typography } from "@mui/material";
|
||||
import { Box, Button, Divider, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched";
|
||||
@ -7,10 +7,14 @@ import { GroupJoinRequests } from "./GroupJoinRequests";
|
||||
import { GroupInvites } from "./GroupInvites";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import { ListOfGroupPromotions } from "./ListOfGroupPromotions";
|
||||
|
||||
import { QortPrice } from "../Home/QortPrice";
|
||||
import ExploreIcon from "@mui/icons-material/Explore";
|
||||
import { Explore } from "../Explore/Explore";
|
||||
import { NewUsersCTA } from "../Home/NewUsersCTA";
|
||||
export const HomeDesktop = ({
|
||||
refreshHomeDataFunc,
|
||||
myAddress,
|
||||
name,
|
||||
isLoadingGroups,
|
||||
balance,
|
||||
userInfo,
|
||||
@ -22,140 +26,217 @@ export const HomeDesktop = ({
|
||||
setOpenAddGroup,
|
||||
setMobileViewMode,
|
||||
setDesktopViewMode,
|
||||
desktopViewMode
|
||||
desktopViewMode,
|
||||
}) => {
|
||||
const [checked1, setChecked1] = React.useState(false);
|
||||
const [checked2, setChecked2] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
if (balance && +balance >= 6) {
|
||||
setChecked1(true);
|
||||
}
|
||||
}, [balance]);
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
if (name) setChecked2(true);
|
||||
}, [name]);
|
||||
|
||||
|
||||
const isLoaded = React.useMemo(()=> {
|
||||
if(userInfo !== null) return true
|
||||
return false
|
||||
}, [ userInfo])
|
||||
|
||||
const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> {
|
||||
if(isLoaded && checked1 && checked2) return true
|
||||
return false
|
||||
}, [checked1, isLoaded, checked2])
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: desktopViewMode === 'home' ? 'flex' : 'none',
|
||||
display: desktopViewMode === "home" ? "flex" : "none",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
overflow: "auto",
|
||||
alignItems: "center",
|
||||
|
||||
}}
|
||||
>
|
||||
<Spacer height="20px" />
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
alignItems: "flex-start",
|
||||
maxWidth: '1036px'
|
||||
}}>
|
||||
<Typography
|
||||
<Box
|
||||
sx={{
|
||||
color: "rgba(255, 255, 255, 1)",
|
||||
fontWeight: 400,
|
||||
fontSize: userInfo?.name?.length > 15 ? "16px" : "20px",
|
||||
padding: '10px'
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
alignItems: "flex-start",
|
||||
maxWidth: "1036px",
|
||||
}}
|
||||
>
|
||||
Welcome
|
||||
{userInfo?.name ? (
|
||||
<span
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>{`, ${userInfo?.name}`}</span>
|
||||
) : null}
|
||||
</Typography>
|
||||
<Spacer height="30px" />
|
||||
{!isLoadingGroups && (
|
||||
<Box
|
||||
<Typography
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "15px",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "center",
|
||||
color: "rgba(255, 255, 255, 1)",
|
||||
fontWeight: 400,
|
||||
fontSize: userInfo?.name?.length > 15 ? "16px" : "20px",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
width: '330px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<ThingsToDoInitial
|
||||
balance={balance}
|
||||
myAddress={myAddress}
|
||||
name={userInfo?.name}
|
||||
hasGroups={groups?.length !== 0}
|
||||
userInfo={userInfo}
|
||||
/>
|
||||
</Box>
|
||||
{desktopViewMode === 'home' && (
|
||||
<>
|
||||
<Box sx={{
|
||||
width: '330px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<ListOfThreadPostsWatched />
|
||||
</Box>
|
||||
<Box sx={{
|
||||
width: '330px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<GroupJoinRequests
|
||||
setGroupSection={setGroupSection}
|
||||
setSelectedGroup={setSelectedGroup}
|
||||
getTimestampEnterChat={getTimestampEnterChat}
|
||||
setOpenManageMembers={setOpenManageMembers}
|
||||
myAddress={myAddress}
|
||||
groups={groups}
|
||||
setMobileViewMode={setMobileViewMode}
|
||||
setDesktopViewMode={setDesktopViewMode}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{
|
||||
width: '330px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<GroupInvites
|
||||
setOpenAddGroup={setOpenAddGroup}
|
||||
myAddress={myAddress}
|
||||
groups={groups}
|
||||
setMobileViewMode={setMobileViewMode}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{!isLoadingGroups && (
|
||||
<ListOfGroupPromotions />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Spacer height="26px" />
|
||||
|
||||
{/* <Box
|
||||
Welcome
|
||||
{userInfo?.name ? (
|
||||
<span
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>{`, ${userInfo?.name}`}</span>
|
||||
) : null}
|
||||
</Typography>
|
||||
<Spacer height="30px" />
|
||||
{!isLoadingGroups && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
flexWrap: "wrap",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "330px",
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={refreshHomeDataFunc}
|
||||
sx={{
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
Refresh home data
|
||||
</Button>
|
||||
</Box> */}
|
||||
<ThingsToDoInitial
|
||||
balance={balance}
|
||||
myAddress={myAddress}
|
||||
name={userInfo?.name}
|
||||
userInfo={userInfo}
|
||||
hasGroups={
|
||||
groups?.filter((item) => item?.groupId !== "0").length !== 0
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{desktopViewMode === "home" && (
|
||||
<>
|
||||
|
||||
{hasDoneNameAndBalanceAndIsLoaded && (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
width: "330px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<GroupJoinRequests
|
||||
setGroupSection={setGroupSection}
|
||||
setSelectedGroup={setSelectedGroup}
|
||||
getTimestampEnterChat={getTimestampEnterChat}
|
||||
setOpenManageMembers={setOpenManageMembers}
|
||||
myAddress={myAddress}
|
||||
groups={groups}
|
||||
setMobileViewMode={setMobileViewMode}
|
||||
setDesktopViewMode={setDesktopViewMode}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: "330px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<GroupInvites
|
||||
setOpenAddGroup={setOpenAddGroup}
|
||||
myAddress={myAddress}
|
||||
groups={groups}
|
||||
setMobileViewMode={setMobileViewMode}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<QortPrice />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!isLoadingGroups && (
|
||||
<>
|
||||
<Spacer height="60px" />
|
||||
<Divider
|
||||
color="secondary"
|
||||
sx={{
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<ExploreIcon
|
||||
sx={{
|
||||
color: "white",
|
||||
}}
|
||||
/>{" "}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
Explore
|
||||
</Typography>{" "}
|
||||
</Box>
|
||||
</Divider>
|
||||
{!hasDoneNameAndBalanceAndIsLoaded && (
|
||||
<Spacer height="40px" />
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
flexWrap: "wrap",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{hasDoneNameAndBalanceAndIsLoaded && (
|
||||
<ListOfGroupPromotions />
|
||||
|
||||
)}
|
||||
|
||||
<Explore setDesktopViewMode={setDesktopViewMode} />
|
||||
</Box>
|
||||
|
||||
<NewUsersCTA balance={balance} />
|
||||
</>
|
||||
|
||||
)}
|
||||
</Box>
|
||||
<Spacer height="26px" />
|
||||
|
||||
|
||||
<Spacer height="180px" />
|
||||
</Box>
|
||||
);
|
||||
|
@ -9,6 +9,8 @@ import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Collapse,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
@ -28,8 +30,8 @@ import {
|
||||
import { getNameInfo } from "./Group";
|
||||
import { getBaseApi, getFee } from "../../background";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
|
||||
import LockIcon from "@mui/icons-material/Lock";
|
||||
import NoEncryptionGmailerrorredIcon from "@mui/icons-material/NoEncryptionGmailerrorred";
|
||||
import {
|
||||
MyContext,
|
||||
getArbitraryEndpointReact,
|
||||
@ -40,7 +42,11 @@ import { Spacer } from "../../common/Spacer";
|
||||
import { CustomLoader } from "../../common/CustomLoader";
|
||||
import { RequestQueueWithPromise } from "../../utils/queue/queue";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { myGroupsWhereIAmAdminAtom, promotionTimeIntervalAtom, promotionsAtom } from "../../atoms/global";
|
||||
import {
|
||||
myGroupsWhereIAmAdminAtom,
|
||||
promotionTimeIntervalAtom,
|
||||
promotionsAtom,
|
||||
} from "../../atoms/global";
|
||||
import { Label } from "./AddGroup";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
|
||||
@ -48,7 +54,8 @@ import { getGroupNames } from "./UserListOfInvites";
|
||||
import { WrapperUserAction } from "../WrapperUserAction";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import ErrorBoundary from "../../common/ErrorBoundary";
|
||||
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||
export const requestQueuePromos = new RequestQueueWithPromise(20);
|
||||
|
||||
export function utf8ToBase64(inputString: string): string {
|
||||
@ -65,7 +72,6 @@ export function utf8ToBase64(inputString: string): string {
|
||||
|
||||
const uid = new ShortUniqueId({ length: 8 });
|
||||
|
||||
|
||||
export function getGroupId(str) {
|
||||
const match = str.match(/group-(\d+)-/);
|
||||
return match ? match[1] : null;
|
||||
@ -81,12 +87,12 @@ export const ListOfGroupPromotions = () => {
|
||||
const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState(
|
||||
myGroupsWhereIAmAdminAtom
|
||||
);
|
||||
const [promotions, setPromotions] = useRecoilState(
|
||||
promotionsAtom
|
||||
);
|
||||
const [promotions, setPromotions] = useRecoilState(promotionsAtom);
|
||||
const [promotionTimeInterval, setPromotionTimeInterval] = useRecoilState(
|
||||
promotionTimeIntervalAtom
|
||||
);
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
|
||||
const [openSnack, setOpenSnack] = useState(false);
|
||||
const [infoSnack, setInfoSnack] = useState(null);
|
||||
const [fee, setFee] = useState(null);
|
||||
@ -95,7 +101,6 @@ export const ListOfGroupPromotions = () => {
|
||||
const { show, setTxList } = useContext(MyContext);
|
||||
|
||||
const listRef = useRef();
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: promotions.length,
|
||||
getItemKey: React.useCallback(
|
||||
@ -107,7 +112,6 @@ export const ListOfGroupPromotions = () => {
|
||||
overscan: 10, // Number of items to render outside the visible area to improve smoothness
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
(async () => {
|
||||
@ -118,7 +122,7 @@ export const ListOfGroupPromotions = () => {
|
||||
}, []);
|
||||
const getPromotions = useCallback(async () => {
|
||||
try {
|
||||
setPromotionTimeInterval(Date.now())
|
||||
setPromotionTimeInterval(Date.now());
|
||||
const identifier = `group-promotions-ui24-`;
|
||||
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`;
|
||||
const response = await fetch(url, {
|
||||
@ -169,7 +173,9 @@ export const ListOfGroupPromotions = () => {
|
||||
});
|
||||
|
||||
await Promise.all(getPromos);
|
||||
const groupWithInfo = await getGroupNames(data.sort((a, b) => b.created - a.created));
|
||||
const groupWithInfo = await getGroupNames(
|
||||
data.sort((a, b) => b.created - a.created)
|
||||
);
|
||||
setPromotions(groupWithInfo);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@ -178,22 +184,23 @@ export const ListOfGroupPromotions = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const now = Date.now();
|
||||
|
||||
|
||||
const timeSinceLastFetch = now - promotionTimeInterval;
|
||||
const initialDelay = timeSinceLastFetch >= THIRTY_MINUTES
|
||||
? 0
|
||||
: THIRTY_MINUTES - timeSinceLastFetch;
|
||||
const initialDelay =
|
||||
timeSinceLastFetch >= THIRTY_MINUTES
|
||||
? 0
|
||||
: THIRTY_MINUTES - timeSinceLastFetch;
|
||||
const initialTimeout = setTimeout(() => {
|
||||
getPromotions();
|
||||
|
||||
|
||||
// Start a 30-minute interval
|
||||
const interval = setInterval(() => {
|
||||
getPromotions();
|
||||
}, THIRTY_MINUTES);
|
||||
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, initialDelay);
|
||||
|
||||
|
||||
return () => clearTimeout(initialTimeout);
|
||||
}, [getPromotions, promotionTimeInterval]);
|
||||
|
||||
@ -321,103 +328,144 @@ export const ListOfGroupPromotions = () => {
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
marginTop: "20px",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
marginTop: "25px",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: isMobile ? "320px" : "750px",
|
||||
maxWidth: "90%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: "0px 20px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '20px',
|
||||
width: '100%',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
padding: `0px ${isExpanded ? "24px" : "20px"}`,
|
||||
gap: "10px",
|
||||
justifyContent: "flex-start",
|
||||
alignSelf: isExpanded && "flex-start",
|
||||
}}
|
||||
onClick={() => setIsExpanded((prev) => !prev)}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
Group Promotions
|
||||
Group promotions {promotions.length > 0 && ` (${promotions.length})`}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setIsShowModal(true)}
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
Add Promotion
|
||||
</Button>
|
||||
</Box>
|
||||
<Spacer height="10px" />
|
||||
{isExpanded ? (
|
||||
<ExpandLessIcon
|
||||
sx={{
|
||||
marginLeft: "auto",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ExpandMoreIcon
|
||||
sx={{
|
||||
marginLeft: "auto",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ButtonBase>
|
||||
<Box
|
||||
style={{
|
||||
width: "330px",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: isMobile ? "320px" : "750px",
|
||||
maxWidth: "90%",
|
||||
maxHeight: "700px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
bgcolor: "background.paper",
|
||||
padding: "20px 0px",
|
||||
borderRadius: "19px",
|
||||
}}
|
||||
>
|
||||
{loading && promotions.length === 0 && (
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
width: isMobile ? "320px" : "750px",
|
||||
maxWidth: "90%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
padding: "0px 20px",
|
||||
}}
|
||||
>
|
||||
<CustomLoader />
|
||||
</Box>
|
||||
)}
|
||||
{!loading && promotions.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: "11px",
|
||||
fontWeight: 400,
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
Nothing to display
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
></Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setIsShowModal(true)}
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
Add Promotion
|
||||
</Button>
|
||||
</Box>
|
||||
<Spacer height="10px" />
|
||||
</Box>
|
||||
)}
|
||||
<div
|
||||
<Box
|
||||
sx={{
|
||||
width: isMobile ? "320px" : "750px",
|
||||
maxWidth: "90%",
|
||||
maxHeight: "700px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
bgcolor: "background.paper",
|
||||
padding: "20px 0px",
|
||||
borderRadius: "19px",
|
||||
}}
|
||||
>
|
||||
{loading && promotions.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<CustomLoader />
|
||||
</Box>
|
||||
)}
|
||||
{!loading && promotions.length === 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "11px",
|
||||
fontWeight: 400,
|
||||
color: "rgba(255, 255, 255, 0.2)",
|
||||
}}
|
||||
>
|
||||
Nothing to display
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
height: "600px",
|
||||
position: "relative",
|
||||
@ -455,7 +503,6 @@ export const ListOfGroupPromotions = () => {
|
||||
const index = virtualRow.index;
|
||||
const promotion = promotions[index];
|
||||
return (
|
||||
|
||||
<div
|
||||
data-index={virtualRow.index} //needed for dynamic row height measurement
|
||||
ref={rowVirtualizer.measureElement} //measure dynamic row height
|
||||
@ -474,235 +521,251 @@ export const ListOfGroupPromotions = () => {
|
||||
gap: "5px",
|
||||
}}
|
||||
>
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
padding: "0px 20px",
|
||||
}}
|
||||
>
|
||||
<Popover
|
||||
open={openPopoverIndex === promotion?.groupId}
|
||||
anchorEl={popoverAnchor}
|
||||
onClose={(event, reason) => {
|
||||
if (reason === "backdropClick") {
|
||||
// Prevent closing on backdrop click
|
||||
return;
|
||||
}
|
||||
handlePopoverClose(); // Close only on other events like Esc key press
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
style={{ marginTop: "8px" }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "325px",
|
||||
height: "auto",
|
||||
maxHeight: "400px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Group name: {` ${promotion?.groupName}`}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Number of members: {` ${promotion?.memberCount}`}
|
||||
</Typography>
|
||||
{promotion?.description && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{promotion?.description}
|
||||
</Typography>
|
||||
)}
|
||||
{promotion?.isOpen === false && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
*This is a closed/private group, so you will need to wait
|
||||
until an admin accepts your request
|
||||
</Typography>
|
||||
)}
|
||||
<Spacer height="5px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<LoadingButton
|
||||
loading={isLoadingJoinGroup}
|
||||
loadingPosition="start"
|
||||
variant="contained"
|
||||
onClick={handlePopoverClose}
|
||||
>
|
||||
Close
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
loading={isLoadingJoinGroup}
|
||||
loadingPosition="start"
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
handleJoinGroup(promotion, promotion?.isOpen)
|
||||
}
|
||||
>
|
||||
Join
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Popover>
|
||||
<ErrorBoundary
|
||||
fallback={
|
||||
<Typography>
|
||||
Error loading content: Invalid Data
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
padding: "0px 20px",
|
||||
}}
|
||||
>
|
||||
<Popover
|
||||
open={openPopoverIndex === promotion?.groupId}
|
||||
anchorEl={popoverAnchor}
|
||||
onClose={(event, reason) => {
|
||||
if (reason === "backdropClick") {
|
||||
// Prevent closing on backdrop click
|
||||
return;
|
||||
}
|
||||
handlePopoverClose(); // Close only on other events like Esc key press
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
style={{ marginTop: "8px" }}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "325px",
|
||||
height: "auto",
|
||||
maxHeight: "400px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Group name: {` ${promotion?.groupName}`}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Number of members:{" "}
|
||||
{` ${promotion?.memberCount}`}
|
||||
</Typography>
|
||||
{promotion?.description && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{promotion?.description}
|
||||
</Typography>
|
||||
)}
|
||||
{promotion?.isOpen === false && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
*This is a closed/private group, so you
|
||||
will need to wait until an admin accepts
|
||||
your request
|
||||
</Typography>
|
||||
)}
|
||||
<Spacer height="5px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<LoadingButton
|
||||
loading={isLoadingJoinGroup}
|
||||
loadingPosition="start"
|
||||
variant="contained"
|
||||
onClick={handlePopoverClose}
|
||||
>
|
||||
Close
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
loading={isLoadingJoinGroup}
|
||||
loadingPosition="start"
|
||||
variant="contained"
|
||||
onClick={() =>
|
||||
handleJoinGroup(
|
||||
promotion,
|
||||
promotion?.isOpen
|
||||
)
|
||||
}
|
||||
>
|
||||
Join
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Popover>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: "#27282c",
|
||||
color: "white",
|
||||
}}
|
||||
alt={promotion?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
promotion?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
{promotion?.name?.charAt(0)}
|
||||
</Avatar>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
}}
|
||||
>
|
||||
{promotion?.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
}}
|
||||
>
|
||||
{promotion?.groupName}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Spacer height="20px" />
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '20px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
{promotion?.isOpen === false && (
|
||||
<LockIcon sx={{
|
||||
color: 'var(--green)'
|
||||
}} />
|
||||
)}
|
||||
{promotion?.isOpen === true && (
|
||||
<NoEncryptionGmailerrorredIcon sx={{
|
||||
color: 'var(--danger)'
|
||||
}} />
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "15px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{promotion?.isOpen ? 'Public group' : 'Private group' }
|
||||
</Typography>
|
||||
</Box>
|
||||
<Spacer height="20px" />
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
}}
|
||||
>
|
||||
{promotion?.data}
|
||||
</Typography>
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
// variant="contained"
|
||||
onClick={(event) => handlePopoverOpen(event, promotion?.groupId)}
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: 'white'
|
||||
}}
|
||||
>
|
||||
Join Group: {` ${promotion?.groupName}`}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Spacer height="50px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
backgroundColor: "#27282c",
|
||||
color: "white",
|
||||
}}
|
||||
alt={promotion?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
promotion?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
{promotion?.name?.charAt(0)}
|
||||
</Avatar>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
}}
|
||||
>
|
||||
{promotion?.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
}}
|
||||
>
|
||||
{promotion?.groupName}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{promotion?.isOpen === false && (
|
||||
<LockIcon
|
||||
sx={{
|
||||
color: "var(--green)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{promotion?.isOpen === true && (
|
||||
<NoEncryptionGmailerrorredIcon
|
||||
sx={{
|
||||
color: "var(--danger)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "15px",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{promotion?.isOpen
|
||||
? "Public group"
|
||||
: "Private group"}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Spacer height="20px" />
|
||||
<Typography
|
||||
sx={{
|
||||
fontWight: 600,
|
||||
fontFamily: "Inter",
|
||||
color: "cadetBlue",
|
||||
}}
|
||||
>
|
||||
{promotion?.data}
|
||||
</Typography>
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
// variant="contained"
|
||||
onClick={(event) =>
|
||||
handlePopoverOpen(event, promotion?.groupId)
|
||||
}
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
Join Group: {` ${promotion?.groupName}`}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Spacer height="50px" />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
</Collapse>
|
||||
<Spacer height="20px" />
|
||||
|
||||
{isShowModal && (
|
||||
@ -712,7 +775,7 @@ export const ListOfGroupPromotions = () => {
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{"Promote your group to non-members"}
|
||||
{"Promote your group to non-members"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
@ -738,6 +801,7 @@ export const ListOfGroupPromotions = () => {
|
||||
value={selectedGroup}
|
||||
label="Groups where you are an admin"
|
||||
onChange={(e) => setSelectedGroup(e.target.value)}
|
||||
variant="outlined"
|
||||
>
|
||||
{myGroupsWhereIAmAdmin?.map((group) => {
|
||||
return (
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
import moment from 'moment'
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Box, ButtonBase, Collapse, Typography } from "@mui/material";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { getBaseApiReact, isMobile } from "../../App";
|
||||
import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
|
||||
@ -15,6 +15,9 @@ import { executeEvent } from '../../utils/events';
|
||||
import { CustomLoader } from '../../common/CustomLoader';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import MarkEmailUnreadIcon from '@mui/icons-material/MarkEmailUnread';
|
||||
export const isLessThanOneWeekOld = (timestamp) => {
|
||||
// Current time in milliseconds
|
||||
const now = Date.now();
|
||||
@ -41,8 +44,9 @@ export function formatEmailDate(timestamp: number) {
|
||||
}
|
||||
}
|
||||
export const QMailMessages = ({userName, userAddress}) => {
|
||||
const [mails, setMails] = useRecoilState(mailsAtom)
|
||||
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [mails, setMails] = useRecoilState(mailsAtom)
|
||||
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const getMails = useCallback(async () => {
|
||||
@ -99,7 +103,16 @@ export const QMailMessages = ({userName, userAddress}) => {
|
||||
|
||||
}, [getMails, userName, userAddress]);
|
||||
|
||||
|
||||
const anyUnread = useMemo(()=> {
|
||||
let unread = false
|
||||
|
||||
mails.forEach((mail)=> {
|
||||
if(lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created)){
|
||||
unread = true
|
||||
}
|
||||
})
|
||||
return unread
|
||||
}, [mails, lastEnteredTimestamp])
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -111,26 +124,39 @@ export const QMailMessages = ({userName, userAddress}) => {
|
||||
}}
|
||||
>
|
||||
|
||||
<Box
|
||||
<ButtonBase
|
||||
sx={{
|
||||
width: "322px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexDirection: "row",
|
||||
gap: '10px',
|
||||
padding: "0px 20px",
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
onClick={()=> setIsExpanded((prev)=> !prev)}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
Latest Q-Mails
|
||||
</Typography>
|
||||
<Spacer height="10px" />
|
||||
</Box>
|
||||
|
||||
<MarkEmailUnreadIcon sx={{
|
||||
color: anyUnread ? '--unread' : 'white'
|
||||
}}/>
|
||||
{isExpanded ? <ExpandLessIcon sx={{
|
||||
marginLeft: 'auto'
|
||||
}} /> : (
|
||||
<ExpandMoreIcon sx={{
|
||||
color: anyUnread ? '--unread' : 'white',
|
||||
marginLeft: 'auto'
|
||||
}} />
|
||||
)}
|
||||
</ButtonBase>
|
||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||
<Box
|
||||
className="scrollable-container"
|
||||
sx={{
|
||||
width: "322px",
|
||||
height: isMobile ? "165px" : "250px",
|
||||
@ -247,6 +273,7 @@ export const QMailMessages = ({userName, userAddress}) => {
|
||||
|
||||
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
@ -12,27 +12,17 @@ import { Box, Typography } from "@mui/material";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { isMobile } from "../../App";
|
||||
import { QMailMessages } from "./QMailMessages";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
|
||||
export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInfo }) => {
|
||||
const [checked1, setChecked1] = React.useState(false);
|
||||
const [checked2, setChecked2] = React.useState(false);
|
||||
const [checked3, setChecked3] = React.useState(false);
|
||||
// const [checked3, setChecked3] = React.useState(false);
|
||||
|
||||
// const getAddressInfo = async (address) => {
|
||||
// const response = await fetch(getBaseApiReact() + "/addresses/" + address);
|
||||
// const data = await response.json();
|
||||
// if (data.error && data.error === 124) {
|
||||
// setChecked1(false);
|
||||
// } else if (data.address) {
|
||||
// setChecked1(true);
|
||||
// }
|
||||
// };
|
||||
// React.useEffect(() => {
|
||||
// if (hasGroups) setChecked3(true);
|
||||
// }, [hasGroups]);
|
||||
|
||||
// const checkInfo = async () => {
|
||||
// try {
|
||||
// getAddressInfo(myAddress);
|
||||
// } catch (error) {}
|
||||
// };
|
||||
|
||||
React.useEffect(() => {
|
||||
if (balance && +balance >= 6) {
|
||||
@ -40,9 +30,6 @@ export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInf
|
||||
}
|
||||
}, [balance]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasGroups) setChecked3(true);
|
||||
}, [hasGroups]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (name) setChecked2(true);
|
||||
@ -50,20 +37,21 @@ export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInf
|
||||
|
||||
|
||||
const isLoaded = React.useMemo(()=> {
|
||||
if(userInfo !== null) return true
|
||||
return false
|
||||
}, [userInfo])
|
||||
if(userInfo !== null) return true
|
||||
return false
|
||||
}, [ userInfo])
|
||||
|
||||
const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> {
|
||||
if(isLoaded && checked1 && checked2) return true
|
||||
return false
|
||||
const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> {
|
||||
if(isLoaded && checked1 && checked2) return true
|
||||
return false
|
||||
}, [checked1, isLoaded, checked2])
|
||||
|
||||
if(hasDoneNameAndBalanceAndIsLoaded){
|
||||
return (
|
||||
<QMailMessages userAddress={userInfo?.address} userName={userInfo?.name} />
|
||||
);
|
||||
return (
|
||||
<QMailMessages userAddress={userInfo?.address} userName={userInfo?.name} />
|
||||
);
|
||||
}
|
||||
if(!isLoaded) return null
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -84,12 +72,11 @@ return (
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "13px",
|
||||
fontSize: "1rem",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{!isLoaded ? 'Loading...' : 'Getting Started' }
|
||||
|
||||
{!isLoaded ? 'Loading...' : 'Getting Started' }
|
||||
</Typography>
|
||||
<Spacer height="10px" />
|
||||
</Box>
|
||||
@ -97,7 +84,6 @@ return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "322px",
|
||||
height: isMobile ? "165px" : "250px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
bgcolor: "background.paper",
|
||||
@ -105,149 +91,140 @@ return (
|
||||
borderRadius: "19px",
|
||||
}}
|
||||
>
|
||||
<List sx={{ width: "100%", maxWidth: 360 }}>
|
||||
<ListItem
|
||||
// secondaryAction={
|
||||
// <IconButton edge="end" aria-label="comments">
|
||||
// <InfoIcon
|
||||
// sx={{
|
||||
// color: "white",
|
||||
// }}
|
||||
// />
|
||||
// </IconButton>
|
||||
// }
|
||||
disablePadding
|
||||
sx={{
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
padding: "0px",
|
||||
}}
|
||||
disableRipple
|
||||
role={undefined}
|
||||
dense
|
||||
>
|
||||
<ListItemText
|
||||
sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "13px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
}}
|
||||
primary={`Have at least 6 QORT in your wallet`}
|
||||
/>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
height: "18px",
|
||||
width: "18px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: checked1 ? "rgba(9, 182, 232, 1)" : "transparent",
|
||||
outline: "1px solid rgba(9, 182, 232, 1)",
|
||||
}}
|
||||
/>
|
||||
{/* <Checkbox
|
||||
edge="start"
|
||||
checked={checked1}
|
||||
tabIndex={-1}
|
||||
disableRipple
|
||||
disabled={true}
|
||||
sx={{
|
||||
"&.Mui-checked": {
|
||||
color: "white", // Customize the color when checked
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/> */}
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
sx={{
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
// secondaryAction={
|
||||
// <IconButton edge="end" aria-label="comments">
|
||||
// <InfoIcon
|
||||
// sx={{
|
||||
// color: "white",
|
||||
// }}
|
||||
// />
|
||||
// </IconButton>
|
||||
// }
|
||||
disablePadding
|
||||
>
|
||||
<ListItemButton sx={{
|
||||
padding: "0px",
|
||||
}} disableRipple role={undefined} dense>
|
||||
|
||||
<ListItemText sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "13px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
}} primary={`Register a name`} />
|
||||
<ListItemIcon sx={{
|
||||
justifyContent: "flex-end",
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
height: "18px",
|
||||
width: "18px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: checked2 ? "rgba(9, 182, 232, 1)" : "transparent",
|
||||
outline: "1px solid rgba(9, 182, 232, 1)",
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
// secondaryAction={
|
||||
// <IconButton edge="end" aria-label="comments">
|
||||
// <InfoIcon
|
||||
// sx={{
|
||||
// color: "white",
|
||||
// }}
|
||||
// />
|
||||
// </IconButton>
|
||||
// }
|
||||
disablePadding
|
||||
>
|
||||
<ListItemButton sx={{
|
||||
padding: "0px",
|
||||
}} disableRipple role={undefined} dense>
|
||||
|
||||
<ListItemText sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "13px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
}} primary={`Join a group hub`} />
|
||||
<ListItemIcon sx={{
|
||||
justifyContent: "flex-end",
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
height: "18px",
|
||||
width: "18px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: checked3 ? "rgba(9, 182, 232, 1)" : "transparent",
|
||||
outline: "1px solid rgba(9, 182, 232, 1)",
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
{isLoaded && (
|
||||
<List sx={{ width: "100%", maxWidth: 360 }}>
|
||||
<ListItem
|
||||
|
||||
disablePadding
|
||||
sx={{
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
padding: "0px",
|
||||
}}
|
||||
disableRipple
|
||||
role={undefined}
|
||||
dense
|
||||
onClick={()=> {
|
||||
executeEvent("openBuyQortInfo", {})
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "1rem",
|
||||
fontWeight: 400,
|
||||
},
|
||||
}}
|
||||
primary={`Have at least 6 QORT in your wallet`}
|
||||
/>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
height: "18px",
|
||||
width: "18px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: checked1 ? "rgba(9, 182, 232, 1)" : "transparent",
|
||||
outline: "1px solid rgba(9, 182, 232, 1)",
|
||||
}}
|
||||
/>
|
||||
{/* <Checkbox
|
||||
edge="start"
|
||||
checked={checked1}
|
||||
tabIndex={-1}
|
||||
disableRipple
|
||||
disabled={true}
|
||||
sx={{
|
||||
"&.Mui-checked": {
|
||||
color: "white", // Customize the color when checked
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/> */}
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
sx={{
|
||||
marginBottom: '20px'
|
||||
}}
|
||||
// secondaryAction={
|
||||
// <IconButton edge="end" aria-label="comments">
|
||||
// <InfoIcon
|
||||
// sx={{
|
||||
// color: "white",
|
||||
// }}
|
||||
// />
|
||||
// </IconButton>
|
||||
// }
|
||||
disablePadding
|
||||
>
|
||||
<ListItemButton sx={{
|
||||
padding: "0px",
|
||||
}} disableRipple role={undefined} dense>
|
||||
|
||||
<ListItemText onClick={() => {
|
||||
executeEvent('openRegisterName', {})
|
||||
}} sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "1rem",
|
||||
fontWeight: 400,
|
||||
},
|
||||
}} primary={`Register a name`} />
|
||||
<ListItemIcon sx={{
|
||||
justifyContent: "flex-end",
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
height: "18px",
|
||||
width: "18px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: checked2 ? "rgba(9, 182, 232, 1)" : "transparent",
|
||||
outline: "1px solid rgba(9, 182, 232, 1)",
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
{/* <ListItem
|
||||
disablePadding
|
||||
>
|
||||
<ListItemButton sx={{
|
||||
padding: "0px",
|
||||
}} disableRipple role={undefined} dense>
|
||||
|
||||
<ListItemText sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "13px",
|
||||
fontWeight: 400,
|
||||
},
|
||||
}} primary={`Join a group`} />
|
||||
<ListItemIcon sx={{
|
||||
justifyContent: "flex-end",
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
height: "18px",
|
||||
width: "18px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: checked3 ? "rgba(9, 182, 232, 1)" : "transparent",
|
||||
outline: "1px solid rgba(9, 182, 232, 1)",
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
</ListItem> */}
|
||||
</List>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
@ -80,7 +80,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,
|
||||
|
93
src/components/Home/NewUsersCTA.tsx
Normal file
93
src/components/Home/NewUsersCTA.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { Box, ButtonBase, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
|
||||
export const NewUsersCTA = ({ balance }) => {
|
||||
if (balance === undefined || +balance > 0) return null;
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Spacer height="40px" />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: "320px",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
padding: "15px",
|
||||
outline: "1px solid gray",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
fontSize: "1.2rem",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Are you a new user?
|
||||
</Typography>
|
||||
<Spacer height="20px" />
|
||||
<Typography>
|
||||
Please message us on Telegram or Discord if you need 4 QORT to start
|
||||
chatting without any limitations
|
||||
</Typography>
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (chrome && chrome.tabs) {
|
||||
chrome.tabs.create({ url: "https://link.qortal.dev/telegram-invite" }, (tab) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error opening tab:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("Tab opened successfully:", tab);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}}
|
||||
>
|
||||
Telegram
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
textDecoration: "underline",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (chrome && chrome.tabs) {
|
||||
chrome.tabs.create({ url: "https://link.qortal.dev/discord-invite" }, (tab) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error("Error opening tab:", chrome.runtime.lastError);
|
||||
} else {
|
||||
console.log("Tab opened successfully:", tab);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Discord
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
257
src/components/Home/QortPrice.tsx
Normal file
257
src/components/Home/QortPrice.tsx
Normal file
@ -0,0 +1,257 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { getBaseApiReact } from "../../App";
|
||||
import { Box, Tooltip, Typography } from "@mui/material";
|
||||
import { BarSpinner } from "../../common/Spinners/BarSpinner/BarSpinner";
|
||||
import { formatDate } from "../../utils/time";
|
||||
|
||||
function getAverageLtcPerQort(trades) {
|
||||
let totalQort = 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() {
|
||||
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 = () => {
|
||||
const [ltcPerQort, setLtcPerQort] = useState(null);
|
||||
const [supply, setSupply] = useState(null);
|
||||
const [lastBlock, setLastBlock] = useState(null);
|
||||
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 () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
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]);
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
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",
|
||||
flexDirection: "row",
|
||||
gap: "10px",
|
||||
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
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
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Last height
|
||||
</Typography>
|
||||
{!lastBlock?.height ? (
|
||||
<BarSpinner width="16px" color="white" />
|
||||
) : (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
{lastBlock?.height}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -7,8 +7,9 @@ import ImageUploader from "../common/ImageUploader";
|
||||
import { getFee } from "../background";
|
||||
import { fileToBase64 } from "../utils/fileReading";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
|
||||
export const MainAvatar = ({ myName }) => {
|
||||
export const MainAvatar = ({ myName, balance, setOpenSnack, setInfoSnack }) => {
|
||||
const [hasAvatar, setHasAvatar] = useState(false);
|
||||
const [avatarFile, setAvatarFile] = useState(null);
|
||||
const [tempAvatar, setTempAvatar] = useState(null)
|
||||
@ -52,10 +53,11 @@ const [isLoading, setIsLoading] = useState(false)
|
||||
checkIfAvatarExists();
|
||||
}, [myName]);
|
||||
|
||||
|
||||
const publishAvatar = async ()=> {
|
||||
try {
|
||||
const fee = await getFee('ARBITRARY')
|
||||
|
||||
if(+balance < +fee.fee) throw new Error(`Publishing an Avatar requires ${fee.fee}`)
|
||||
await show({
|
||||
message: "Would you like to publish an avatar?" ,
|
||||
publishFee: fee.fee + ' QORT'
|
||||
@ -63,30 +65,36 @@ const [isLoading, setIsLoading] = useState(false)
|
||||
setIsLoading(true);
|
||||
const avatarBase64 = await fileToBase64(avatarFile)
|
||||
await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "publishOnQDN",
|
||||
payload: {
|
||||
data: avatarBase64,
|
||||
identifier: "qortal_avatar",
|
||||
service: 'THUMBNAIL'
|
||||
},
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "publishOnQDN",
|
||||
payload: {
|
||||
data: avatarBase64,
|
||||
identifier: "qortal_avatar",
|
||||
service: 'THUMBNAIL'
|
||||
},
|
||||
(response) => {
|
||||
|
||||
if (!response?.error) {
|
||||
res(response);
|
||||
return
|
||||
}
|
||||
rej(response.error);
|
||||
},
|
||||
(response) => {
|
||||
|
||||
if (!response?.error) {
|
||||
res(response);
|
||||
return
|
||||
}
|
||||
);
|
||||
});
|
||||
rej(response.error);
|
||||
}
|
||||
);
|
||||
});
|
||||
setAvatarFile(null);
|
||||
setTempAvatar(`data:image/webp;base64,${avatarBase64}`)
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
|
||||
if (error?.message) {
|
||||
setOpenSnack(true)
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
message: error?.message,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@ -115,7 +123,7 @@ const [isLoading, setIsLoading] = useState(false)
|
||||
change avatar
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
<PopoverComp avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} />
|
||||
<PopoverComp myName={myName} avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -143,7 +151,7 @@ const [isLoading, setIsLoading] = useState(false)
|
||||
change avatar
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
<PopoverComp avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} />
|
||||
<PopoverComp myName={myName} avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -161,13 +169,13 @@ const [isLoading, setIsLoading] = useState(false)
|
||||
set avatar
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
<PopoverComp avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} />
|
||||
<PopoverComp myName={myName} avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose, publishAvatar, isLoading}) => {
|
||||
const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose, publishAvatar, isLoading, myName}) => {
|
||||
return (
|
||||
<Popover
|
||||
id={id}
|
||||
@ -196,8 +204,21 @@ const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose
|
||||
</ImageUploader>
|
||||
{avatarFile?.name}
|
||||
<Spacer height="25px" />
|
||||
|
||||
<LoadingButton loading={isLoading} disabled={!avatarFile} onClick={publishAvatar} variant="contained">
|
||||
{!myName && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<ErrorIcon sx={{
|
||||
color: 'white'
|
||||
}} />
|
||||
<Typography>A registered name is required to set an avatar</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Spacer height="25px" />
|
||||
<LoadingButton loading={isLoading} disabled={!avatarFile || !myName} onClick={publishAvatar} variant="contained">
|
||||
Publish avatar
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
|
@ -3,7 +3,7 @@ import QMailLogo from '../assets/QMailLogo.png'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import { mailsAtom, qMailLastEnteredTimestampAtom } from '../atoms/global'
|
||||
import { isLessThanOneWeekOld } from './Group/QMailMessages'
|
||||
import { ButtonBase } from '@mui/material'
|
||||
import { ButtonBase, Tooltip } from '@mui/material'
|
||||
import { executeEvent } from '../utils/events'
|
||||
export const QMailStatus = () => {
|
||||
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom)
|
||||
@ -35,9 +35,28 @@ export const QMailStatus = () => {
|
||||
borderRadius: '50%',
|
||||
outline: '1px solid white'
|
||||
}} />
|
||||
)}<img style={{
|
||||
width: '24px',
|
||||
height: 'auto'
|
||||
}} src={QMailLogo} /></ButtonBase>
|
||||
)}
|
||||
<Tooltip
|
||||
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>Q-MAIL</span>}
|
||||
placement="left"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<img style={{ width: '24px', height: 'auto' }} src={QMailLogo} />
|
||||
</Tooltip>
|
||||
</ButtonBase>
|
||||
)
|
||||
}
|
||||
|
308
src/components/RegisterName.tsx
Normal file
308
src/components/RegisterName.tsx
Normal file
@ -0,0 +1,308 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Collapse,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Input,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
List,
|
||||
MenuItem,
|
||||
Popover,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Label } from './Group/AddGroup';
|
||||
import { Spacer } from '../common/Spacer';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { getBaseApiReact, MyContext } from '../App';
|
||||
import { getFee } from '../background';
|
||||
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
|
||||
import { subscribeToEvent, unsubscribeFromEvent } from '../utils/events';
|
||||
import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
|
||||
enum Availability {
|
||||
NULL = 'null',
|
||||
LOADING = 'loading',
|
||||
AVAILABLE = 'available',
|
||||
NOT_AVAILABLE = 'not-available'
|
||||
}
|
||||
export const RegisterName = ({setOpenSnack, setInfoSnack, userInfo, show, setTxList, balance}) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [registerNameValue, setRegisterNameValue] = useState('')
|
||||
const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false)
|
||||
const [isNameAvailable, setIsNameAvailable] = useState<Availability>(Availability.NULL)
|
||||
const [nameFee, setNameFee] = useState(null)
|
||||
|
||||
const checkIfNameExisits = async (name)=> {
|
||||
if(!name?.trim()){
|
||||
setIsNameAvailable(Availability.NULL)
|
||||
|
||||
return
|
||||
}
|
||||
setIsNameAvailable(Availability.LOADING)
|
||||
try {
|
||||
const res = await fetch(`${getBaseApiReact()}/names/` + name);
|
||||
const data = await res.json()
|
||||
if(data?.message === 'name unknown'){
|
||||
setIsNameAvailable(Availability.AVAILABLE)
|
||||
} else {
|
||||
setIsNameAvailable(Availability.NOT_AVAILABLE)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
// Debounce logic
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
checkIfNameExisits(registerNameValue);
|
||||
}, 500);
|
||||
|
||||
// Cleanup timeout if searchValue changes before the timeout completes
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [registerNameValue]);
|
||||
|
||||
const openRegisterNameFunc = useCallback((e) => {
|
||||
setIsOpen(true)
|
||||
|
||||
}, [ setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("openRegisterName", openRegisterNameFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("openRegisterName", openRegisterNameFunc);
|
||||
};
|
||||
}, [openRegisterNameFunc]);
|
||||
|
||||
useEffect(()=> {
|
||||
const nameRegistrationFee = async ()=> {
|
||||
try {
|
||||
const fee = await getFee("REGISTER_NAME");
|
||||
setNameFee(fee?.fee)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
nameRegistrationFee()
|
||||
}, [])
|
||||
|
||||
const registerName = async () => {
|
||||
try {
|
||||
if (!userInfo?.address) throw new Error("Your address was not found");
|
||||
if(!registerNameValue) throw new Error('Enter a name')
|
||||
|
||||
const fee = await getFee("REGISTER_NAME");
|
||||
await show({
|
||||
message: "Would you like to register this name?",
|
||||
publishFee: fee.fee + " QORT",
|
||||
});
|
||||
setIsLoadingRegisterName(true);
|
||||
new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "registerName",
|
||||
payload: {
|
||||
name: registerNameValue,
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (!response?.error) {
|
||||
res(response);
|
||||
setIsLoadingRegisterName(false);
|
||||
setInfoSnack({
|
||||
type: "success",
|
||||
message:
|
||||
"Successfully registered. It may take a couple of minutes for the changes to propagate",
|
||||
});
|
||||
setIsOpen(false);
|
||||
setRegisterNameValue("");
|
||||
setOpenSnack(true);
|
||||
setTxList((prev) => [
|
||||
{
|
||||
...response,
|
||||
type: "register-name",
|
||||
label: `Registered name: awaiting confirmation. This may take a couple minutes.`,
|
||||
labelDone: `Registered name: success!`,
|
||||
done: false,
|
||||
},
|
||||
...prev.filter((item) => !item.done),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
message: response?.error,
|
||||
});
|
||||
setOpenSnack(true);
|
||||
rej(response.error);
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
if (error?.message) {
|
||||
setOpenSnack(true)
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
message: error?.message,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingRegisterName(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{"Register name"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
width: "400px",
|
||||
maxWidth: '90vw',
|
||||
height: "500px",
|
||||
maxHeight: '90vh',
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<Label>Choose a name</Label>
|
||||
<TextField
|
||||
autoComplete='off'
|
||||
autoFocus
|
||||
onChange={(e) => setRegisterNameValue(e.target.value)}
|
||||
value={registerNameValue}
|
||||
placeholder="Choose a name"
|
||||
/>
|
||||
{(!balance || (nameFee && balance && balance < nameFee))&& (
|
||||
<>
|
||||
<Spacer height="10px" />
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<ErrorIcon sx={{
|
||||
color: 'white'
|
||||
}} />
|
||||
<Typography>Your balance is {balance ?? 0} QORT. A name registration requires a {nameFee} QORT fee</Typography>
|
||||
</Box>
|
||||
<Spacer height="10px" />
|
||||
|
||||
</>
|
||||
)}
|
||||
<Spacer height="5px" />
|
||||
{isNameAvailable === Availability.AVAILABLE && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<CheckIcon sx={{
|
||||
color: 'white'
|
||||
}} />
|
||||
<Typography>{registerNameValue} is available</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{isNameAvailable === Availability.NOT_AVAILABLE && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<ErrorIcon sx={{
|
||||
color: 'white'
|
||||
}} />
|
||||
<Typography>{registerNameValue} is unavailable</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{isNameAvailable === Availability.LOADING && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<BarSpinner width="16px" color="white" />
|
||||
<Typography>Checking if name already existis</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Spacer height="25px" />
|
||||
<Typography sx={{
|
||||
textDecoration: 'underline'
|
||||
}}>Benefits of a name</Typography>
|
||||
<List
|
||||
sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}
|
||||
aria-label="contacts"
|
||||
>
|
||||
<ListItem disablePadding>
|
||||
|
||||
<ListItemIcon>
|
||||
<RadioButtonCheckedIcon sx={{
|
||||
color: 'white'
|
||||
}} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Publish data to Qortal: anything from apps to videos. Fully decentralized!" />
|
||||
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
|
||||
<ListItemIcon>
|
||||
<RadioButtonCheckedIcon sx={{
|
||||
color: 'white'
|
||||
}} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Secure ownership of data published by your name. You can even sell your name, along with your data to a third party." />
|
||||
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
disabled={isLoadingRegisterName}
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setIsOpen(false)
|
||||
setRegisterNameValue('')
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!registerNameValue.trim() ||isLoadingRegisterName || isNameAvailable !== Availability.AVAILABLE || !balance || ((balance && nameFee) && +balance < +nameFee)}
|
||||
variant="contained"
|
||||
onClick={registerName}
|
||||
autoFocus
|
||||
>
|
||||
Register Name
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
@ -22,7 +22,7 @@ export const CustomizedSnackbars = ({open, setOpen, info, setInfo, duration}) =
|
||||
if(!open) return null
|
||||
return (
|
||||
<div>
|
||||
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={duration || 6000} onClose={handleClose}>
|
||||
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={info?.duration === null ? null : (duration || 6000)} onClose={handleClose}>
|
||||
<Alert
|
||||
|
||||
|
||||
|
507
src/components/UserLookup.tsx/UserLookup.tsx
Normal file
507
src/components/UserLookup.tsx/UserLookup.tsx
Normal file
@ -0,0 +1,507 @@
|
||||
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]);
|
||||
|
||||
const onClose = ()=> {
|
||||
setIsOpenDrawerLookup(false)
|
||||
setNameOrAddress('')
|
||||
setErrorMessage('')
|
||||
setPayments([])
|
||||
setIsLoadingUser(false)
|
||||
setIsLoadingPayments(false)
|
||||
setAddressInfo(null)
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
autoFocus
|
||||
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={()=> {
|
||||
onClose()
|
||||
}}>
|
||||
<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>
|
||||
);
|
||||
};
|
@ -1,6 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Popover, Button, Box } from '@mui/material';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Popover, Button, Box, CircularProgress } from '@mui/material';
|
||||
import { executeEvent } from '../utils/events';
|
||||
import { BlockedUsersModal } from './Group/BlockedUsersModal';
|
||||
import { MyContext } from '../App';
|
||||
|
||||
export const WrapperUserAction = ({ children, address, name, disabled }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
@ -46,6 +48,7 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => {
|
||||
</Box>
|
||||
|
||||
{/* Popover */}
|
||||
{open && (
|
||||
<Popover
|
||||
id={id}
|
||||
open={open}
|
||||
@ -119,8 +122,81 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => {
|
||||
>
|
||||
Copy address
|
||||
</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} />
|
||||
|
||||
|
||||
</Box>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const BlockUser = ({address, name, handleClose})=> {
|
||||
const [isAlreadyBlocked, setIsAlreadyBlocked] = useState(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const {isUserBlocked,
|
||||
addToBlockList,
|
||||
removeBlockFromList} = useContext(MyContext)
|
||||
|
||||
useEffect(()=> {
|
||||
if(!address) return
|
||||
setIsAlreadyBlocked(isUserBlocked(address, name))
|
||||
}, [address, setIsAlreadyBlocked, isUserBlocked, name])
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
if(isAlreadyBlocked === true){
|
||||
await removeBlockFromList(address, name)
|
||||
} else if(isAlreadyBlocked === false) {
|
||||
await addToBlockList(address, name)
|
||||
}
|
||||
executeEvent('updateChatMessagesWithBlocks', true)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
handleClose();
|
||||
}
|
||||
|
||||
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
justifyContent: 'flex-start',
|
||||
gap: '10px'
|
||||
}}
|
||||
>
|
||||
{(isAlreadyBlocked === null || isLoading) && (
|
||||
<CircularProgress color="secondary" size={24} />
|
||||
)}
|
||||
{isAlreadyBlocked && (
|
||||
'Unblock name'
|
||||
)}
|
||||
{isAlreadyBlocked === false && (
|
||||
'Block name'
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
18
src/main.tsx
18
src/main.tsx
@ -40,6 +40,24 @@ const theme = createTheme({
|
||||
color: '#b0b0b0', // Lighter text for body2, often used for secondary text
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "white", // ⚪ Default outline color
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiSelect: {
|
||||
styleOverrides: {
|
||||
icon: {
|
||||
color: "white", // ✅ Caret (dropdown arrow) color
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { banFromGroup, gateways, getApiKeyFromStorage } from "./background";
|
||||
import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createBuyOrder, createGroupRequest, createPoll, createSellOrder, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll } from "./qortalRequests/get";
|
||||
import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createBuyOrder, createGroupRequest, createPoll, createSellOrder, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getUserWalletTransactions, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll } from "./qortalRequests/get";
|
||||
|
||||
const listOfAllQortalRequests = [
|
||||
'GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS',
|
||||
@ -756,6 +756,20 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "GET_USER_WALLET_TRANSACTIONS" : {
|
||||
const data = request.payload;
|
||||
|
||||
getUserWalletTransactions(data, isFromExtension, appInfo)
|
||||
.then((res) => {
|
||||
sendResponse(res);
|
||||
})
|
||||
.catch((error) => {
|
||||
sendResponse({ error: error.message });
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
@ -657,7 +657,7 @@ export const decryptData = async (data) => {
|
||||
export const getListItems = async (data, isFromExtension) => {
|
||||
const isGateway = await isRunningGateway()
|
||||
if(isGateway){
|
||||
throw new Error('This action cannot be done through a gateway')
|
||||
throw new Error("This action cannot be done through a public node");
|
||||
}
|
||||
const requiredFields = ["list_name"];
|
||||
const missingFields: string[] = [];
|
||||
@ -711,7 +711,7 @@ export const getListItems = async (data, isFromExtension) => {
|
||||
export const addListItems = async (data, isFromExtension) => {
|
||||
const isGateway = await isRunningGateway()
|
||||
if(isGateway){
|
||||
throw new Error('This action cannot be done through a gateway')
|
||||
throw new Error("This action cannot be done through a public node");
|
||||
}
|
||||
const requiredFields = ["list_name", "items"];
|
||||
const missingFields: string[] = [];
|
||||
@ -766,7 +766,7 @@ export const addListItems = async (data, isFromExtension) => {
|
||||
export const deleteListItems = async (data, isFromExtension) => {
|
||||
const isGateway = await isRunningGateway()
|
||||
if(isGateway){
|
||||
throw new Error('This action cannot be done through a gateway')
|
||||
throw new Error("This action cannot be done through a public node");
|
||||
}
|
||||
const requiredFields = ["list_name"];
|
||||
const missingFields: string[] = [];
|
||||
@ -2280,7 +2280,7 @@ export const getTxActivitySummary = async (data) => {
|
||||
export const updateForeignFee = async (data) => {
|
||||
const isGateway = await isRunningGateway();
|
||||
if (isGateway) {
|
||||
throw new Error("This action cannot be done through a gateway");
|
||||
throw new Error("This action cannot be done through a public node");
|
||||
}
|
||||
const requiredFields = ['coin', 'type', 'value'];
|
||||
const missingFields: string[] = [];
|
||||
@ -2379,7 +2379,7 @@ export const getTxActivitySummary = async (data) => {
|
||||
export const setCurrentForeignServer = async (data) => {
|
||||
const isGateway = await isRunningGateway();
|
||||
if (isGateway) {
|
||||
throw new Error("This action cannot be done through a gateway");
|
||||
throw new Error("This action cannot be done through a public node");
|
||||
}
|
||||
const requiredFields = ['coin'];
|
||||
const missingFields: string[] = [];
|
||||
@ -2440,7 +2440,7 @@ export const getTxActivitySummary = async (data) => {
|
||||
export const addForeignServer = async (data) => {
|
||||
const isGateway = await isRunningGateway();
|
||||
if (isGateway) {
|
||||
throw new Error("This action cannot be done through a gateway");
|
||||
throw new Error("This action cannot be done through a public node");
|
||||
}
|
||||
const requiredFields = ['coin'];
|
||||
const missingFields: string[] = [];
|
||||
@ -2500,7 +2500,7 @@ export const getTxActivitySummary = async (data) => {
|
||||
export const removeForeignServer = async (data) => {
|
||||
const isGateway = await isRunningGateway();
|
||||
if (isGateway) {
|
||||
throw new Error("This action cannot be done through a gateway");
|
||||
throw new Error("This action cannot be done through a public node");
|
||||
}
|
||||
const requiredFields = ['coin'];
|
||||
const missingFields: string[] = [];
|
||||
@ -3053,7 +3053,7 @@ const crosschainAtInfo = await Promise.all(atPromises);
|
||||
}, 0)
|
||||
)}
|
||||
${` ${crosschainAtInfo?.[0]?.foreignBlockchain}`}`,
|
||||
highlightedText: `Is using gateway: ${isGateway}`,
|
||||
highlightedText: `Is using public node: ${isGateway}`,
|
||||
fee: '',
|
||||
foreignFee: `${sellerForeignFee[foreignBlockchain].value} ${sellerForeignFee[foreignBlockchain].ticker}`
|
||||
}, isFromExtension);
|
||||
@ -3224,13 +3224,15 @@ export const createSellOrder = async (data, isFromExtension) => {
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const parsedForeignAmount = Number(data.foreignAmount)?.toFixed(8)
|
||||
|
||||
const receivingAddress = await getUserWalletFunc(data.foreignBlockchain)
|
||||
try {
|
||||
const resPermission = await getUserPermission({
|
||||
text1: "Do you give this application permission to perform a sell order?",
|
||||
text2: `${data.qortAmount}${" "}
|
||||
${`QORT`}`,
|
||||
text3: `FOR ${data.foreignAmount} ${data.foreignBlockchain}`,
|
||||
text3: `FOR ${parsedForeignAmount} ${data.foreignBlockchain}`,
|
||||
fee: '0.02'
|
||||
}, isFromExtension);
|
||||
const { accepted } = resPermission;
|
||||
@ -3247,12 +3249,12 @@ const receivingAddress = await getUserWalletFunc(data.foreignBlockchain)
|
||||
};
|
||||
const response = await tradeBotCreateRequest({
|
||||
creatorPublicKey: userPublicKey,
|
||||
qortAmount: parseFloat(data.qortAmount),
|
||||
fundingQortAmount: parseFloat(data.qortAmount) + 0.001,
|
||||
foreignBlockchain: data.foreignBlockchain,
|
||||
foreignAmount: parseFloat(data.foreignAmount),
|
||||
tradeTimeout: 120,
|
||||
receivingAddress: receivingAddress.address
|
||||
qortAmount: parseFloat(data.qortAmount),
|
||||
fundingQortAmount: parseFloat(data.qortAmount) + 0.01,
|
||||
foreignBlockchain: data.foreignBlockchain,
|
||||
foreignAmount: parseFloat(parsedForeignAmount),
|
||||
tradeTimeout: 120,
|
||||
receivingAddress: receivingAddress.address
|
||||
}, keyPair)
|
||||
|
||||
return response
|
||||
@ -3353,7 +3355,7 @@ export const adminAction = async (data, isFromExtension) => {
|
||||
}
|
||||
const isGateway = await isRunningGateway();
|
||||
if (isGateway) {
|
||||
throw new Error("This action cannot be done through a gateway");
|
||||
throw new Error("This action cannot be done through a public node");
|
||||
}
|
||||
|
||||
let apiEndpoint = "";
|
||||
@ -3769,7 +3771,7 @@ url
|
||||
export const getHostedData = async (data, isFromExtension) => {
|
||||
const isGateway = await isRunningGateway();
|
||||
if (isGateway) {
|
||||
throw new Error("This action cannot be done through a gateway");
|
||||
throw new Error("This action cannot be done through a public node");
|
||||
}
|
||||
const resPermission = await getUserPermission(
|
||||
{
|
||||
@ -3805,7 +3807,7 @@ export const getHostedData = async (data, isFromExtension) => {
|
||||
export const deleteHostedData = async (data, isFromExtension) => {
|
||||
const isGateway = await isRunningGateway();
|
||||
if (isGateway) {
|
||||
throw new Error("This action cannot be done through a gateway");
|
||||
throw new Error("This action cannot be done through a public node");
|
||||
}
|
||||
const requiredFields = ["hostedData"];
|
||||
const missingFields: string[] = [];
|
||||
@ -4378,4 +4380,97 @@ export const createGroupRequest = async (data, isFromExtension) => {
|
||||
} else {
|
||||
throw new Error("User declined request");
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserWalletTransactions = async (data, isFromExtension, appInfo) => {
|
||||
const requiredFields = ["coin"];
|
||||
const missingFields: string[] = [];
|
||||
requiredFields.forEach((field) => {
|
||||
if (!data[field]) {
|
||||
missingFields.push(field);
|
||||
}
|
||||
});
|
||||
if (missingFields.length > 0) {
|
||||
const missingFieldsString = missingFields.join(", ");
|
||||
const errorMsg = `Missing fields: ${missingFieldsString}`;
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const value =
|
||||
(await getPermission(
|
||||
`getUserWalletTransactions-${appInfo?.name}-${data.coin}`
|
||||
)) || false;
|
||||
let skip = false;
|
||||
if (value) {
|
||||
skip = true;
|
||||
}
|
||||
let resPermission;
|
||||
|
||||
if (!skip) {
|
||||
|
||||
resPermission = await getUserPermission(
|
||||
{
|
||||
text1:
|
||||
"Do you give this application permission to retrieve your wallet transactions",
|
||||
highlightedText: `coin: ${data.coin}`,
|
||||
checkbox1: {
|
||||
value: true,
|
||||
label: "Always allow wallet txs to be retrieved automatically",
|
||||
},
|
||||
},
|
||||
isFromExtension
|
||||
);
|
||||
}
|
||||
const { accepted = false, checkbox1 = false } = resPermission || {};
|
||||
|
||||
if (resPermission) {
|
||||
setPermission(
|
||||
`getUserWalletTransactions-${appInfo?.name}-${data.coin}`,
|
||||
checkbox1
|
||||
);
|
||||
}
|
||||
|
||||
if (accepted || skip) {
|
||||
const coin = data.coin;
|
||||
const walletKeys = await getUserWalletFunc(coin);
|
||||
let publicKey
|
||||
if(data?.coin === 'ARRR'){
|
||||
const resKeyPair = await getKeyPair();
|
||||
const parsedData = resKeyPair;
|
||||
publicKey = parsedData.arrrSeed58;
|
||||
} else {
|
||||
publicKey = walletKeys["publickey"]
|
||||
}
|
||||
|
||||
const _url = await createEndpoint(
|
||||
`/crosschain/` + data.coin.toLowerCase() + `/wallettransactions`
|
||||
);
|
||||
const _body = publicKey;
|
||||
try {
|
||||
const response = await fetch(_url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "*/*",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: _body,
|
||||
});
|
||||
if (!response?.ok) throw new Error("Unable to fetch wallet transactions");
|
||||
let res;
|
||||
try {
|
||||
res = await response.clone().json();
|
||||
} catch (e) {
|
||||
res = await response.text();
|
||||
}
|
||||
if (res?.error && res?.message) {
|
||||
throw new Error(res.message);
|
||||
}
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
throw new Error(error?.message || "Fetch Wallet Transactions Failed");
|
||||
}
|
||||
} else {
|
||||
throw new Error("User declined request");
|
||||
}
|
||||
};
|
@ -12,7 +12,7 @@ export function formatTimestamp(timestamp: number): string {
|
||||
} else if (elapsedTime < 1440) {
|
||||
return `${Math.floor(elapsedTime / 60)}h ago`
|
||||
} else {
|
||||
return timestampMoment.format('MMM D')
|
||||
return timestampMoment.format('MMM D, YYYY')
|
||||
}
|
||||
}
|
||||
export function formatTimestampForum(timestamp: number): string {
|
||||
|
Loading…
x
Reference in New Issue
Block a user