homepage, registername, userlookup, block, fixes

This commit is contained in:
PhilReact 2025-03-04 15:26:06 +02:00
parent f0d2080a5b
commit 164a380c28
52 changed files with 5441 additions and 1623 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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',

View File

@ -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>

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

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

View File

@ -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;
}

View File

@ -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;

View 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>
)
}

View 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% }
}

View File

@ -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)
}

View File

@ -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>
</>
)}

View File

@ -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}

View File

@ -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>
);

View 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>
)}
</>
);
};

View File

@ -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>

View File

@ -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>
)

View 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,
};
};

View File

@ -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'
];

View 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>
)
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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');

View File

@ -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>
}

View 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
};
};

View File

@ -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' }}>

View File

@ -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>

View File

@ -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}

View File

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

View File

@ -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>
);
};

View 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>
);
};

View File

@ -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}

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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 (

View File

@ -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>
)
}

View File

@ -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>
);

View File

@ -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'

View File

@ -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,

View 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>
);
};

View 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>
);
};

View File

@ -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>

View File

@ -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>
)
}

View 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>
)
}

View File

@ -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

View 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>
);
};

View File

@ -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>
)
}

View File

@ -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;

View File

@ -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;

View File

@ -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");
}
};

View File

@ -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 {