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 { import {
Box, Box,
Button, Button,
ButtonBase,
Checkbox, Checkbox,
Dialog, Dialog,
DialogActions, DialogActions,
@ -11,24 +12,24 @@ import {
DialogTitle, DialogTitle,
FormControlLabel, FormControlLabel,
Input, Input,
Switch,
Tooltip,
Typography,
ButtonBase,
styled, styled,
tooltipClasses, Switch,
TooltipProps Typography,
} from "@mui/material"; } from "@mui/material";
import Logo1 from "../assets/svgs/Logo1.svg"; import Logo1 from "../assets/svgs/Logo1.svg";
import Logo1Dark from "../assets/svgs/Logo1Dark.svg"; import Logo1Dark from "../assets/svgs/Logo1Dark.svg";
import Info from "../assets/svgs/Info.svg"; import Info from "../assets/svgs/Info.svg";
import HelpIcon from '@mui/icons-material/Help';
import { CustomizedSnackbars } from "../components/Snackbar/Snackbar"; import { CustomizedSnackbars } from "../components/Snackbar/Snackbar";
import { set } from "lodash"; import { set } from "lodash";
import { cleanUrl, isUsingLocal } from "../background"; import { cleanUrl, gateways, isUsingLocal } from "../background";
import HelpIcon from '@mui/icons-material/Help';
import { GlobalContext } from "../App"; 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) => ( export const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} /> <Tooltip {...props} classes={{ popper: className }} />
@ -41,40 +42,47 @@ export const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
fontSize: theme.typography.pxToRem(12), fontSize: theme.typography.pxToRem(12),
}, },
})); }));
function removeTrailingSlash(url) {
return url.replace(/\/+$/, '');
}
export const NotAuthenticated = ({ export const NotAuthenticated = ({
getRootProps, getRootProps,
getInputProps, getInputProps,
setExtstate, setExtstate,
apiKey, apiKey,
setApiKey, setApiKey,
globalApiKey, globalApiKey,
handleSetGlobalApikey, handleSetGlobalApikey,
currentNode,
setCurrentNode,
useLocalNode,
setUseLocalNode
}) => { }) => {
const [isValidApiKey, setIsValidApiKey] = useState<boolean | null>(null); const [isValidApiKey, setIsValidApiKey] = useState<boolean | null>(null);
const [hasLocalNode, setHasLocalNode] = 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 [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null); const [infoSnack, setInfoSnack] = React.useState(null);
const [show, setShow] = React.useState(false); const [show, setShow] = React.useState(false);
const [mode, setMode] = React.useState("list"); const [mode, setMode] = React.useState("list");
const [customNodes, setCustomNodes] = React.useState(null); const [customNodes, setCustomNodes] = React.useState(null);
const [currentNode, setCurrentNode] = React.useState({ // const [currentNode, setCurrentNode] = React.useState({
url: "http://127.0.0.1:12391", // url: "http://127.0.0.1:12391",
}); // });
const [importedApiKey, setImportedApiKey] = React.useState(null); const [importedApiKey, setImportedApiKey] = React.useState(null);
//add and edit states //add and edit states
const [url, setUrl] = React.useState("http://"); const [url, setUrl] = React.useState("https://");
const [customApikey, setCustomApiKey] = React.useState(""); const [customApikey, setCustomApiKey] = React.useState("");
const [customNodeToSaveIndex, setCustomNodeToSaveIndex] = const [customNodeToSaveIndex, setCustomNodeToSaveIndex] =
React.useState(null); React.useState(null);
const importedApiKeyRef = useRef(null) const { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext);
const currentNodeRef = useRef(null)
const hasLocalNodeRef = useRef(null)
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 isLocal = cleanUrl(currentNode?.url) === "127.0.0.1:12391";
const handleFileChangeApiKey = (event) => { const handleFileChangeApiKey = (event) => {
const file = event.target.files[0]; // Get the selected file 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 const text = e.target.result; // Get the file content
setImportedApiKey(text); // Store the file content in the state 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 reader.readAsText(file); // Read the file as text
} }
}; };
const checkIfUserHasLocalNode = useCallback(async () => { const checkIfUserHasLocalNode = useCallback(async () => {
try { try {
const url = `http://127.0.0.1:12391/admin/status`; const url = `http://127.0.0.1:12391/admin/status`;
@ -103,50 +132,105 @@ export const NotAuthenticated = ({
const data = await response.json(); const data = await response.json();
if (data?.height) { if (data?.height) {
setHasLocalNode(true); setHasLocalNode(true);
return true
} }
} catch (error) {} return false
} catch (error) {
return false
}
}, []); }, []);
useEffect(() => { useEffect(() => {
checkIfUserHasLocalNode(); checkIfUserHasLocalNode();
}, []); }, []);
useEffect(() => { useEffect(() => {
chrome?.runtime?.sendMessage( chrome?.runtime?.sendMessage(
{ action: "getCustomNodesFromStorage" }, { action: "getCustomNodesFromStorage" },
(response) => { (response) => {
if (response) { if (response) {
setCustomNodes(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(()=> { useEffect(() => {
importedApiKeyRef.current = importedApiKey importedApiKeyRef.current = importedApiKey;
}, [importedApiKey]) }, [importedApiKey]);
useEffect(()=> { useEffect(() => {
currentNodeRef.current = currentNode currentNodeRef.current = currentNode;
}, [currentNode]) }, [currentNode]);
useEffect(() => {
hasLocalNodeRef.current = hasLocalNode;
}, [hasLocalNode]);
useEffect(()=> {
hasLocalNodeRef.current = hasLocalNode
}, [hasLocalNode])
const validateApiKey = useCallback(async (key, fromStartUp) => { const validateApiKey = useCallback(async (key, fromStartUp) => {
try { try {
if(!currentNodeRef.current) return if(key === "isGateway") return
const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391"; const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391";
if(isLocalKey && !hasLocalNodeRef.current && !fromStartUp){ if (fromStartUp && key?.url && key?.apikey && !isLocalKey && !gateways.some(gateway => key?.url?.includes(gateway))) {
throw new Error('Please turn on your local node') 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) { if (isValid) {
setIsValidApiKey(false); setIsValidApiKey(true);
setUseLocalNode(false); setUseLocalNode(true);
return 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 = {}; let payload = {};
if (currentNodeRef.current?.url === "http://127.0.0.1:12391") { if (currentNodeRef.current?.url === "http://127.0.0.1:12391") {
@ -154,21 +238,32 @@ export const NotAuthenticated = ({
apikey: importedApiKeyRef.current || key?.apikey, apikey: importedApiKeyRef.current || key?.apikey,
url: currentNodeRef.current?.url, url: currentNodeRef.current?.url,
}; };
} else if(currentNodeRef.current) { } else if (currentNodeRef.current) {
payload = currentNodeRef.current; payload = currentNodeRef.current;
} }
const url = `${payload?.url}/admin/apikey/test`; let isValid = false
const response = await fetch(url, {
method: "GET",
headers: { const url = `${payload?.url}/admin/settings/localAuthBypassEnabled`;
accept: "text/plain", const response = await fetch(url);
"X-API-KEY": payload?.apikey, // Include the API key here
},
});
// Assuming the response is in plain text and will be 'true' or 'false' // Assuming the response is in plain text and will be 'true' or 'false'
const data = await response.text(); 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( chrome?.runtime?.sendMessage(
{ action: "setApiKey", payload }, { action: "setApiKey", payload },
(response) => { (response) => {
@ -176,29 +271,49 @@ export const NotAuthenticated = ({
handleSetGlobalApikey(payload); handleSetGlobalApikey(payload);
setIsValidApiKey(true); setIsValidApiKey(true);
setUseLocalNode(true); setUseLocalNode(true);
if(!fromStartUp){ if (!fromStartUp) {
setApiKey(payload) setApiKey(payload);
} }
} }
} }
); )
} else { } else {
setIsValidApiKey(false); setIsValidApiKey(false);
setUseLocalNode(false); setUseLocalNode(false);
setInfoSnack({ if(!fromStartUp){
type: "error", setInfoSnack({
message: "Select a valid apikey", type: "error",
}); message: "Select a valid apikey",
setOpenSnack(true); });
setOpenSnack(true);
}
} }
} catch (error) { } catch (error) {
setIsValidApiKey(false); setIsValidApiKey(false);
setUseLocalNode(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({ setInfoSnack({
type: "error", type: "error",
message: error?.message || "Select a valid apikey", message: error?.message || "Select a valid apikey",
}); });
setOpenSnack(true); setOpenSnack(true);
}
console.error("Error validating API key:", error); console.error("Error validating API key:", error);
} }
}, []); }, []);
@ -212,22 +327,22 @@ export const NotAuthenticated = ({
const addCustomNode = () => { const addCustomNode = () => {
setMode("add-node"); setMode("add-node");
}; };
const saveCustomNodes = (myNodes, isFullListOfNodes) => {
const saveCustomNodes = (myNodes) => {
let nodes = [...(myNodes || [])]; let nodes = [...(myNodes || [])];
if (customNodeToSaveIndex !== null) { if (!isFullListOfNodes && customNodeToSaveIndex !== null) {
nodes.splice(customNodeToSaveIndex, 1, { nodes.splice(customNodeToSaveIndex, 1, {
url, url: removeTrailingSlash(url),
apikey: customApikey, apikey: customApikey,
}); });
} else if (url && customApikey) { } else if (!isFullListOfNodes && url) {
nodes.push({ nodes.push({
url, url: removeTrailingSlash(url),
apikey: customApikey, apikey: customApikey,
}); });
} }
setCustomNodes(nodes); setCustomNodes(nodes);
setCustomNodeToSaveIndex(null); setCustomNodeToSaveIndex(null);
if (!nodes) return; if (!nodes) return;
chrome?.runtime?.sendMessage( chrome?.runtime?.sendMessage(
@ -235,14 +350,14 @@ export const NotAuthenticated = ({
(response) => { (response) => {
if (response) { if (response) {
setMode("list"); setMode("list");
setUrl("http://"); setUrl("https://");
setCustomApiKey(""); setCustomApiKey("");
// add alert // add alert
} }
} }
); );
};
};
return ( return (
<> <>
@ -254,7 +369,7 @@ export const NotAuthenticated = ({
height: "154px", height: "154px",
}} }}
> >
<img src={Logo1Dark} className="base-image" /> <img src={Logo1Dark} className="base-image" />
</div> </div>
<Spacer height="30px" /> <Spacer height="30px" />
<TextP <TextP
@ -264,13 +379,12 @@ export const NotAuthenticated = ({
fontSize: '18px' fontSize: '18px'
}} }}
> >
WELCOME TO <TextItalic sx={{ WELCOME TO
fontSize: '18px'
}}>YOUR</TextItalic> <br></br>
<TextSpan sx={{ <TextSpan sx={{
fontSize: '18px' fontSize: '18px'
}}> QORTAL WALLET</TextSpan> }}> QORTAL</TextSpan>
</TextP> </TextP>
<Spacer height="30px" /> <Spacer height="30px" />
<Box <Box
sx={{ sx={{
@ -291,9 +405,13 @@ export const NotAuthenticated = ({
} }
> >
<CustomButton onClick={()=> setExtstate('wallets')}> <CustomButton onClick={()=> setExtstate('wallets')}>
Wallets {/* <input {...getInputProps()} /> */}
Accounts
</CustomButton> </CustomButton>
</HtmlTooltip> </HtmlTooltip>
{/* <Tooltip title="Authenticate by importing your Qortal JSON file" arrow>
<img src={Info} />
</Tooltip> */}
</Box> </Box>
<Spacer height="6px" /> <Spacer height="6px" />
@ -302,9 +420,10 @@ export const NotAuthenticated = ({
display: "flex", display: "flex",
gap: "10px", gap: "10px",
alignItems: "center", alignItems: "center",
}} }}
> >
<HtmlTooltip <HtmlTooltip
disableHoverListener={hasSeenGettingStarted === true} disableHoverListener={hasSeenGettingStarted === true}
placement="right" placement="right"
title={ title={
@ -333,21 +452,21 @@ export const NotAuthenticated = ({
} }
}} }}
> >
Create wallet Create account
</CustomButton> </CustomButton>
</HtmlTooltip> </HtmlTooltip>
</Box> </Box>
<Spacer height="15px" /> <Spacer height="15px" />
<Typography <Typography
sx={{ sx={{
fontSize: "12px", fontSize: "12px",
visibility: !useLocalNode && 'hidden' visibility: !useLocalNode && "hidden",
}} }}
> >
{"Using node: "} {currentNode?.url} {"Using node: "} {currentNode?.url}
</Typography> </Typography>
<> <>
<Spacer height="15px" /> <Spacer height="15px" />
<Box <Box
@ -375,7 +494,7 @@ export const NotAuthenticated = ({
}} }}
> >
<FormControlLabel <FormControlLabel
sx={{ sx={{
"& .MuiFormControlLabel-label": { "& .MuiFormControlLabel-label": {
fontSize: '14px' fontSize: '14px'
} }
@ -398,27 +517,25 @@ export const NotAuthenticated = ({
validateApiKey(currentNode); validateApiKey(currentNode);
} else { } else {
setCurrentNode({ setCurrentNode({
url: "http://127.0.0.1:12391", url: "http://127.0.0.1:12391",
}) });
setUseLocalNode(false) setUseLocalNode(false);
chrome?.runtime?.sendMessage( chrome?.runtime?.sendMessage(
{ action: "setApiKey", payload:null }, { action: "setApiKey", payload: null },
(response) => { (response) => {
if (response) { if (response) {
setApiKey(null); setApiKey(null);
handleSetGlobalApikey(null); handleSetGlobalApikey(null);
}
} }
); }
)
} }
}} }}
disabled={false} disabled={false}
defaultChecked defaultChecked
/> />
} }
label={`Use ${isLocal ? 'Local' : 'Custom'} Node`} label={`Use ${isLocal ? "Local" : "Custom"} Node`}
/> />
</Box> </Box>
{currentNode?.url === "http://127.0.0.1:12391" && ( {currentNode?.url === "http://127.0.0.1:12391" && (
@ -432,31 +549,33 @@ export const NotAuthenticated = ({
onChange={handleFileChangeApiKey} // File input handler onChange={handleFileChangeApiKey} // File input handler
/> />
</Button> </Button>
<Typography sx={{ <Typography
fontSize: '12px', sx={{
visibility: importedApiKey ? 'visible' : 'hidden' fontSize: "12px",
}}>{`api key : ${importedApiKey}`}</Typography> visibility: importedApiKey ? "visible" : "hidden",
}}
>{`api key : ${importedApiKey}`}</Typography>
</> </>
)} )}
<Button <Button
size="small" size="small"
onClick={() => { onClick={() => {
setShow(true); setShow(true);
}} }}
variant="contained" variant="contained"
component="label" component="label"
> >
Choose custom node Choose custom node
</Button> </Button>
</> </>
<Typography sx={{ <Typography
color: "white", sx={{
fontSize: '12px' color: "white",
}}>Build version: {manifestData?.version}</Typography> fontSize: "12px",
}}
>
Build version: {manifestData?.version}
</Typography>
</Box> </Box>
</> </>
<CustomizedSnackbars <CustomizedSnackbars
@ -483,7 +602,6 @@ export const NotAuthenticated = ({
flexDirection: "column", flexDirection: "column",
}} }}
> >
{mode === "list" && ( {mode === "list" && (
<Box <Box
sx={{ sx={{
@ -525,16 +643,15 @@ export const NotAuthenticated = ({
setMode("list"); setMode("list");
setShow(false); setShow(false);
setUseLocalNode(false); setUseLocalNode(false);
chrome?.runtime?.sendMessage( chrome?.runtime?.sendMessage(
{ action: "setApiKey", payload:null }, { action: "setApiKey", payload: null },
(response) => { (response) => {
if (response) { if (response) {
setApiKey(null); setApiKey(null);
handleSetGlobalApikey(null); handleSetGlobalApikey(null);
}
} }
); }
)
}} }}
variant="contained" variant="contained"
> >
@ -579,17 +696,16 @@ export const NotAuthenticated = ({
setMode("list"); setMode("list");
setShow(false); setShow(false);
setIsValidApiKey(false); setIsValidApiKey(false);
setUseLocalNode(false); setUseLocalNode(false);
chrome?.runtime?.sendMessage( chrome?.runtime?.sendMessage(
{ action: "setApiKey", payload:null }, { action: "setApiKey", payload: null },
(response) => { (response) => {
if (response) { if (response) {
setApiKey(null); setApiKey(null);
handleSetGlobalApikey(null); handleSetGlobalApikey(null);
}
} }
} )
);
}} }}
variant="contained" variant="contained"
> >
@ -613,9 +729,8 @@ export const NotAuthenticated = ({
const nodesToSave = [ const nodesToSave = [
...(customNodes || []), ...(customNodes || []),
].filter((item) => item?.url !== node?.url); ].filter((item) => item?.url !== node?.url);
saveCustomNodes(nodesToSave); saveCustomNodes(nodesToSave, true);
}} }}
variant="contained" variant="contained"
> >
@ -652,9 +767,7 @@ export const NotAuthenticated = ({
/> />
</Box> </Box>
)} )}
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
{mode === "list" && ( {mode === "list" && (
@ -690,7 +803,7 @@ export const NotAuthenticated = ({
<Button <Button
variant="contained" variant="contained"
disabled={!customApikey || !url} disabled={!url}
onClick={() => saveCustomNodes(customNodes)} onClick={() => saveCustomNodes(customNodes)}
autoFocus autoFocus
> >
@ -701,7 +814,7 @@ export const NotAuthenticated = ({
</DialogActions> </DialogActions>
</Dialog> </Dialog>
)} )}
<ButtonBase onClick={()=> { <ButtonBase onClick={()=> {
showTutorial('create-account', true) showTutorial('create-account', true)
}} sx={{ }} sx={{
position: 'fixed', position: 'fixed',

View File

@ -134,11 +134,11 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
setPassword('') setPassword('')
setSeedError('') setSeedError('')
} else { } else {
setSeedError('Could not create wallet.') setSeedError('Could not create account.')
} }
} catch (error) { } catch (error) {
setSeedError(error?.message || 'Could not create wallet.') setSeedError(error?.message || 'Could not create account.')
} finally { } finally {
setIsLoadingEncryptSeed(false) setIsLoadingEncryptSeed(false)
} }
@ -176,19 +176,19 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
{(wallets?.length === 0 || {(wallets?.length === 0 ||
!wallets) ? ( !wallets) ? (
<> <>
<Typography>No wallets saved</Typography> <Typography>No accounts saved</Typography>
<Spacer height="75px" /> <Spacer height="75px" />
</> </>
): ( ): (
<> <>
<Typography>Your saved wallets</Typography> <Typography>Your saved accounts</Typography>
<Spacer height="30px" /> <Spacer height="30px" />
</> </>
)} )}
{rawWallet && ( {rawWallet && (
<Box> <Box>
<Typography>Selected Wallet:</Typography> <Typography>Selected Account:</Typography>
{rawWallet?.name && <Typography>{rawWallet.name}</Typography>} {rawWallet?.name && <Typography>{rawWallet.name}</Typography>}
{rawWallet?.address0 && ( {rawWallet?.address0 && (
<Typography>{rawWallet?.address0}</Typography> <Typography>{rawWallet?.address0}</Typography>
@ -267,7 +267,7 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
padding: '10px' padding: '10px'
}} {...getRootProps()}> }} {...getRootProps()}>
<input {...getInputProps()} /> <input {...getInputProps()} />
Add wallets Add account
</CustomButton> </CustomButton>
</HtmlTooltip> </HtmlTooltip>
</Box> </Box>

View File

@ -2,7 +2,7 @@ import React from 'react';
export const WalletIcon= ({ color, height, width }) => { export const WalletIcon= ({ color, height, width }) => {
return ( 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="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"/> <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> </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({ export const mailsAtom = atom({
key: 'mailsAtom', key: 'mailsAtom',
default: [], 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 Base58 from "./deps/Base58";
import { createTransaction } from "./transactions/transactions"; import { createTransaction } from "./transactions/transactions";
export async function createRewardShareCase(data) { export async function createRewardShareCase(data) {
const {recipientPublicKey} = data; const { recipientPublicKey } = data;
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
const parsedData = JSON.parse(resKeyPair); const parsedData = JSON.parse(resKeyPair);
const uint8PrivateKey = Base58.decode(parsedData.privateKey); const uint8PrivateKey = Base58.decode(parsedData.privateKey);
const uint8PublicKey = Base58.decode(parsedData.publicKey); const uint8PublicKey = Base58.decode(parsedData.publicKey);
const keyPair = { const keyPair = {
privateKey: uint8PrivateKey, privateKey: uint8PrivateKey,
publicKey: uint8PublicKey, publicKey: uint8PublicKey,
}; };
let lastRef = await getLastRef(); let lastRef = await getLastRef();
const tx = await createTransaction(38, keyPair, {
recipientPublicKey,
percentageShare: 0,
lastReference: lastRef,
});
const signedBytes = Base58.encode(tx.signedBytes); const tx = await createTransaction(38, keyPair, {
recipientPublicKey,
const res = await processTransactionVersion2(signedBytes); percentageShare: 0,
if (!res?.signature) lastReference: lastRef,
throw new Error("Transaction was not able to be processed"); });
return res
}
export async function removeRewardShareCase(data) { const signedBytes = Base58.encode(tx.signedBytes);
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 res = await processTransactionVersion2(signedBytes);
if (!res?.signature)
const res = await processTransactionVersion2(signedBytes); throw new Error("Transaction was not able to be processed");
if (!res?.signature) return res;
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 tx = await createTransaction(381, keyPair, {
const {recipientPublicKey} = data rewardShareKeyPairPublicKey,
const resKeyPair = await getKeyPair(); recipient,
const parsedData = JSON.parse(resKeyPair); percentageShare,
const uint8PrivateKey = Base58.decode(parsedData.privateKey); lastReference: lastRef,
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 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 { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest";
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes"; import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes";
import TradeBotRespondRequest from './transactions/TradeBotRespondRequest'; 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'}) || [] let mutedGroups = await getUserSettings({key: 'mutedGroups'}) || []
if(!isArray(mutedGroups)) mutedGroups = [] if(!isArray(mutedGroups)) mutedGroups = []
mutedGroups.push('0')
let isFocused; let isFocused;
const data = groups.filter((group) => group?.sender !== address && !mutedGroups.includes(group.groupId) && !isUpdateMsg(group?.data)); 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)); const dataWithUpdates = groups.filter((group) => group?.sender !== address && !mutedGroups.includes(group.groupId));
@ -832,6 +833,7 @@ const checkNewMessages = async () => {
try { try {
let mutedGroups = await getUserSettings({key: 'mutedGroups'}) || [] let mutedGroups = await getUserSettings({key: 'mutedGroups'}) || []
if(!isArray(mutedGroups)) mutedGroups = [] if(!isArray(mutedGroups)) mutedGroups = []
mutedGroups.push('0')
let myName = ""; let myName = "";
const userData = await getUserInfo(); const userData = await getUserInfo();
if (userData?.name) { if (userData?.name) {
@ -997,7 +999,7 @@ export async function getNameInfoForOthers(address) {
return ""; return "";
} }
} }
async function getAddressInfo(address) { export async function getAddressInfo(address) {
const validApi = await getBaseApi(); const validApi = await getBaseApi();
const response = await fetch(validApi + "/addresses/" + address); const response = await fetch(validApi + "/addresses/" + address);
const data = await response.json(); const data = await response.json();
@ -1117,7 +1119,7 @@ export async function getBalanceInfo() {
const validApi = await getBaseApi(); const validApi = await getBaseApi();
const response = await fetch(validApi + "/addresses/balance/" + address); 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(); const data = await response.json();
return data; return data;
} }
@ -1250,7 +1252,7 @@ export const getLastRef = async () => {
const response = await fetch( const response = await fetch(
validApi + "/addresses/lastreference/" + address 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(); const data = await response.text();
return data; return data;
}; };
@ -3670,6 +3672,21 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
break; 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": { case "oauth": {
const { nodeBaseUrl, senderAddress, senderPublicKey, timestamp } = const { nodeBaseUrl, senderAddress, senderPublicKey, timestamp } =
request.payload; 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 { rootHeight } = useContext(MyContext);
// const iframeRef = useRef(null); // const iframeRef = useRef(null);
const { document, window: frameWindow } = useFrame(); const { document, window: frameWindow } = useFrame();
@ -30,6 +30,17 @@ export const AppViewer = React.forwardRef(({ app , hide}, iframeRef) => {
const refreshAppFunc = (e) => { const refreshAppFunc = (e) => {
const {tabId} = e.detail const {tabId} = e.detail
if(tabId === app?.tabId){ 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()}` 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) setUrl(constructUrl)
} }

View File

@ -394,7 +394,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
}}> }}>
<Spacer height="30px" /> <Spacer height="30px" />
<AppsHomeDesktop availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
</Box> </Box>
)} )}
@ -423,6 +423,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
isSelected={tab?.tabId === selectedTab?.tabId} isSelected={tab?.tabId === selectedTab?.tabId}
app={tab} app={tab}
ref={iframeRefs.current[tab.tabId]} 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" /> <Spacer height="30px" />
<AppsHomeDesktop availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
</Box> </Box>
</> </>
)} )}

View File

@ -16,12 +16,14 @@ import { SortablePinnedApps } from "./SortablePinnedApps";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { extractComponents } from "../Chat/MessageDisplay"; import { extractComponents } from "../Chat/MessageDisplay";
import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward'; import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
import { AppsPrivate } from "./AppsPrivate";
export const AppsHomeDesktop = ({ export const AppsHomeDesktop = ({
setMode, setMode,
myApp, myApp,
myWebsite, myWebsite,
availableQapps, availableQapps,
myName
}) => { }) => {
const [qortalUrl, setQortalUrl] = useState('') const [qortalUrl, setQortalUrl] = useState('')
@ -136,7 +138,7 @@ export const AppsHomeDesktop = ({
<AppCircleLabel>Library</AppCircleLabel> <AppCircleLabel>Library</AppCircleLabel>
</AppCircleContainer> </AppCircleContainer>
</ButtonBase> </ButtonBase>
<AppsPrivate myName={myName} />
<SortablePinnedApps <SortablePinnedApps
isDesktop={true} isDesktop={true}
availableQapps={availableQapps} availableQapps={availableQapps}

View File

@ -8,7 +8,6 @@ import NavBack from "../../assets/svgs/NavBack.svg";
import NavAdd from "../../assets/svgs/NavAdd.svg"; import NavAdd from "../../assets/svgs/NavAdd.svg";
import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg"; import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg";
import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { import {
ButtonBase, ButtonBase,
ListItemIcon, ListItemIcon,
@ -119,7 +118,6 @@ export const AppsNavBarDesktop = ({disableBack}) => {
const setTabsToNav = (e) => { const setTabsToNav = (e) => {
const { tabs, selectedTab, isNewTabWindow } = e.detail?.data; const { tabs, selectedTab, isNewTabWindow } = e.detail?.data;
setTabs([...tabs]); setTabs([...tabs]);
setSelectedTab(!selectedTab ? null : { ...selectedTab }); setSelectedTab(!selectedTab ? null : { ...selectedTab });
setIsNewTabWindow(isNewTabWindow); setIsNewTabWindow(isNewTabWindow);
@ -135,10 +133,20 @@ export const AppsNavBarDesktop = ({disableBack}) => {
const isSelectedAppPinned = !!sortablePinnedApps?.find( const isSelectedAppPinned = useMemo(()=> {
(item) => if(selectedTab?.isPrivate){
item?.name === selectedTab?.name && item?.service === selectedTab?.service 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 ( return (
<AppsNavBarParent <AppsNavBarParent
sx={{ sx={{
@ -283,22 +291,49 @@ export const AppsNavBarDesktop = ({disableBack}) => {
if (isSelectedAppPinned) { if (isSelectedAppPinned) {
// Remove the selected app if it is pinned // Remove the selected app if it is pinned
updatedApps = prev.filter( if(selectedTab?.isPrivate){
(item) => updatedApps = prev.filter(
!( (item) =>
item?.name === selectedTab?.name && !(
item?.service === selectedTab?.service 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 { } else {
// Add the selected app if it is not pinned // Add the selected app if it is not pinned
updatedApps = [ if(selectedTab?.isPrivate){
updatedApps = [
...prev, ...prev,
{ {
name: selectedTab?.name, isPreview: true,
service: selectedTab?.service, isPrivate: true,
privateAppProperties: {
...(selectedTab?.privateAppProperties || {})
}
}, },
]; ];
} else {
updatedApps = [
...prev,
{
name: selectedTab?.name,
service: selectedTab?.service,
},
];
}
} }
saveToLocalStorage( saveToLocalStorage(
@ -322,7 +357,7 @@ export const AppsNavBarDesktop = ({disableBack}) => {
<PushPinIcon <PushPinIcon
height={20} height={20}
sx={{ sx={{
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)", color: isSelectedAppPinned ? "var(--danger)" : "rgba(250, 250, 250, 0.5)",
}} }}
/> />
</ListItemIcon> </ListItemIcon>
@ -331,7 +366,7 @@ export const AppsNavBarDesktop = ({disableBack}) => {
"& .MuiTypography-root": { "& .MuiTypography-root": {
fontSize: "12px", fontSize: "12px",
fontWeight: 600, 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"}`} primary={`${isSelectedAppPinned ? "Unpin app" : "Pin app"}`}
@ -339,9 +374,15 @@ export const AppsNavBarDesktop = ({disableBack}) => {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
executeEvent("refreshApp", { if (selectedTab?.refreshFunc) {
tabId: selectedTab?.tabId, selectedTab.refreshFunc(selectedTab?.tabId);
});
} else {
executeEvent("refreshApp", {
tabId: selectedTab?.tabId,
});
}
handleClose(); handleClose();
}} }}
> >
@ -369,38 +410,40 @@ export const AppsNavBarDesktop = ({disableBack}) => {
primary="Refresh" primary="Refresh"
/> />
</MenuItem> </MenuItem>
<MenuItem {!selectedTab?.isPrivate && (
onClick={() => { <MenuItem
executeEvent("copyLink", { onClick={() => {
tabId: selectedTab?.tabId, executeEvent("copyLink", {
}); tabId: selectedTab?.tabId,
handleClose(); });
}} handleClose();
>
<ListItemIcon
sx={{
minWidth: "24px !important",
marginRight: "5px",
}} }}
> >
<ContentCopyIcon <ListItemIcon
height={20}
sx={{ 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> </MenuItem>
<ListItemText )}
sx={{
"& .MuiTypography-root": {
fontSize: "12px",
fontWeight: 600,
color: "rgba(250, 250, 250, 0.5)",
},
}}
primary="Copy link"
/>
</MenuItem>
</Menu> </Menu>
</AppsNavBarParent> </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 { DndContext, closestCenter } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable';
import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { Avatar, ButtonBase } from '@mui/material'; import { Avatar, ButtonBase } from '@mui/material';
import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles'; import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles';
import { getBaseApiReact } from '../../App'; import { getBaseApiReact, MyContext } from '../../App';
import { executeEvent } from '../../utils/events'; import { executeEvent } from '../../utils/events';
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global';
import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilState, useSetRecoilState } from 'recoil';
import { saveToLocalStorage } from './AppsNavBar'; import { saveToLocalStorage } from './AppsNavBar';
import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps'; import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps';
import LockIcon from "@mui/icons-material/Lock";
import { useHandlePrivateApps } from './useHandlePrivateApps';
const SortableItem = ({ id, name, app, isDesktop }) => { const SortableItem = ({ id, name, app, isDesktop }) => {
const {openApp} = useHandlePrivateApps()
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const style = { const style = {
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
@ -28,17 +31,27 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
return ( return (
<ContextMenuPinnedApps app={app} isMine={!!app?.isMine}> <ContextMenuPinnedApps app={app} isMine={!!app?.isMine}>
<ButtonBase <ButtonBase
ref={setNodeRef} {...attributes} {...listeners} ref={setNodeRef} {...attributes} {...listeners}
sx={{ sx={{
width: "80px", width: "80px",
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
}} }}
onClick={()=> { onClick={async ()=> {
executeEvent("addTab", { if(app?.isPrivate){
data: app try {
}) await openApp(app?.privateAppProperties)
} catch (error) {
console.error(error)
}
} else {
executeEvent("addTab", {
data: app
})
}
}} }}
> >
<AppCircleContainer sx={{ <AppCircleContainer sx={{
@ -50,7 +63,15 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
border: "none", border: "none",
}} }}
> >
<Avatar {app?.isPrivate && !app?.privateAppProperties?.logo ? (
<LockIcon
sx={{
height: "42px",
width: "42px",
}}
/>
) : (
<Avatar
sx={{ sx={{
height: "42px", height: "42px",
width: "42px", width: "42px",
@ -59,7 +80,7 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
} }
}} }}
alt={app?.metadata?.title || app?.name} alt={app?.metadata?.title || app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${ src={ app?.privateAppProperties?.logo ? app?.privateAppProperties?.logo :`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name app?.name
}/qortal_avatar?async=true`} }/qortal_avatar?async=true`}
> >
@ -72,10 +93,19 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
alt="center-icon" alt="center-icon"
/> />
</Avatar> </Avatar>
)}
</AppCircle> </AppCircle>
<AppCircleLabel> {app?.isPrivate ? (
<AppCircleLabel>
{`${app?.privateAppProperties?.appName || "Private"}`}
</AppCircleLabel>
) : (
<AppCircleLabel>
{app?.metadata?.title || app?.name} {app?.metadata?.title || app?.name}
</AppCircleLabel> </AppCircleLabel>
)}
</AppCircleContainer> </AppCircleContainer>
</ButtonBase> </ButtonBase>
</ContextMenuPinnedApps> </ContextMenuPinnedApps>

View File

@ -5,6 +5,7 @@ import { getBaseApiReact } from '../../App';
import { Avatar, ButtonBase } from '@mui/material'; import { Avatar, ButtonBase } from '@mui/material';
import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { executeEvent } from '../../utils/events'; import { executeEvent } from '../../utils/events';
import LockIcon from "@mui/icons-material/Lock";
const TabComponent = ({isSelected, app}) => { const TabComponent = ({isSelected, app}) => {
return ( return (
@ -34,25 +35,34 @@ const TabComponent = ({isSelected, app}) => {
} src={NavCloseTab}/> } src={NavCloseTab}/>
) } ) }
<Avatar {app?.isPrivate && !app?.privateAppProperties?.logo ? (
sx={{ <LockIcon
height: "28px", sx={{
width: "28px", height: "28px",
}} width: "28px",
alt={app?.name} }}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${ />
app?.name ) : (
}/qortal_avatar?async=true`} <Avatar
> sx={{
<img height: "28px",
style={{ width: "28px",
width: "28px", }}
height: "auto", alt={app?.name}
}} src={app?.privateAppProperties?.logo ? app?.privateAppProperties?.logo :`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
src={LogoSelected} app?.name
alt="center-icon" }/qortal_avatar?async=true`}
/> >
</Avatar> <img
style={{
width: "28px",
height: "auto",
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
)}
</TabParent> </TabParent>
</ButtonBase> </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_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE',
'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER', 'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER',
'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER', '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 { CreateCommonSecret } from './CreateCommonSecret'
import { reusableGet } from '../../qdn/publish/pubish' import { reusableGet } from '../../qdn/publish/pubish'
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption' import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'
@ -10,11 +10,11 @@ import Tiptap from './TipTap'
import { CustomButton } from '../../App-styles' import { CustomButton } from '../../App-styles'
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar' 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 { CustomizedSnackbars } from '../Snackbar/Snackbar'
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes' import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes'
import { useMessageQueue } from '../../MessageQueueContext' 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 { Box, ButtonBase, Divider, Typography } from '@mui/material'
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { ReplyPreview } from './MessageItem' import { ReplyPreview } from './MessageItem'
@ -47,6 +47,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
const [isOpenQManager, setIsOpenQManager] = useState(null) const [isOpenQManager, setIsOpenQManager] = useState(null)
const [onEditMessage, setOnEditMessage] = useState(null) const [onEditMessage, setOnEditMessage] = useState(null)
const [messageSize, setMessageSize] = useState(0) const [messageSize, setMessageSize] = useState(0)
const {isUserBlocked} = useContext(MyContext)
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
const [, forceUpdate] = useReducer((x) => x + 1, 0); 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 { try {
if (hasInitialized.current) { if (hasInitialized.current) {
decryptMessages(data, true); const dataRemovedBlock = data?.filter((item)=> !isUserBlocked(item?.sender, item?.senderName))
decryptMessages(dataRemovedBlock, true);
return; return;
} }
hasInitialized.current = true; hasInitialized.current = true;
@ -182,7 +201,11 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
}, },
}); });
const responseData = await response.json(); const responseData = await response.json();
decryptMessages(responseData, false); const dataRemovedBlock = responseData?.filter((item)=> {
return !isUserBlocked(item?.sender, item?.senderName)
})
decryptMessages(dataRemovedBlock, false);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

@ -5,6 +5,7 @@ import {
InputBase, InputBase,
MenuItem, MenuItem,
Select, Select,
Tooltip,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
@ -584,49 +585,89 @@ export const ChatOptions = ({
minHeight: "200px", minHeight: "200px",
}} }}
> >
<ButtonBase <ButtonBase onClick={() => {
onClick={() => { setMode("search")
setMode("search"); }}>
}} <Tooltip
> title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>SEARCH</span>}
<SearchIcon /> placement="left"
</ButtonBase> arrow
<ButtonBase sx={{ fontSize: "24" }}
onClick={() => { slotProps={{
setMode("default"); tooltip: {
setSearchValue(""); sx: {
setSelectedMember(0); color: "#ffffff",
openQManager(); backgroundColor: "#444444",
}} },
> },
<InsertLinkIcon arrow: {
sx={{ sx: {
color: "white", color: "#444444",
}} },
/> },
</ButtonBase>
<ContextMenuMentions
getTimestampMention={getTimestampMention}
groupId={selectedGroup}
>
<ButtonBase
onClick={() => {
setMode("mentions");
setSearchValue("");
setSelectedMember(0);
}} }}
> >
<AlternateEmailIcon <SearchIcon />
sx={{ </Tooltip>
color: </ButtonBase>
mentionList?.length > 0 && <ButtonBase onClick={() => {
(!lastMentionTimestamp || setMode("default")
lastMentionTimestamp < mentionList[0]?.timestamp) setSearchValue('')
? "var(--unread)" setSelectedMember(0)
: "white", openQManager()
}} }}>
/> <Tooltip
</ButtonBase> 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> </ContextMenuMentions>
</Box> </Box>
</Box> </Box>

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useMemo } from 'react';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import './styles.css'; import './styles.css';
import { executeEvent } from '../../utils/events'; import { executeEvent } from '../../utils/events';
@ -63,30 +63,34 @@ function processText(input) {
return wrapper.innerHTML; return wrapper.innerHTML;
} }
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 }) => { 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), { const sanitizedContent = useMemo(()=> {
ALLOWED_TAGS: [ return DOMPurify.sanitize(linkify(htmlContent), {
'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'div', 'span', 'img', ALLOWED_TAGS: [
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td','s', 'hr' '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', ALLOWED_ATTR: [
'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url' '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, '');; ],
}).replace(/<span[^>]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, '');
}, [htmlContent])
const handleClick = async (e) => { const handleClick = async (e) => {
e.preventDefault(); e.preventDefault();
@ -94,7 +98,15 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
const target = e.target; const target = e.target;
if (target.tagName === 'A') { if (target.tagName === 'A') {
const href = target.getAttribute('href'); 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')) { } else if (target.getAttribute('data-url')) {
const url = 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 { Message } from "@chatscope/chat-ui-kit-react";
import React, { useContext, useEffect, useState } from "react"; import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { MessageDisplay } from "./MessageDisplay"; import { MessageDisplay } from "./MessageDisplay";
import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material"; import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material";
@ -8,6 +8,7 @@ import { getBaseApi } from "../../background";
import { MyContext, getBaseApiReact } from "../../App"; import { MyContext, getBaseApiReact } from "../../App";
import { generateHTML } from "@tiptap/react"; import { generateHTML } from "@tiptap/react";
import Highlight from "@tiptap/extension-highlight"; import Highlight from "@tiptap/extension-highlight";
import Mention from "@tiptap/extension-mention";
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline"; import Underline from "@tiptap/extension-underline";
import { executeEvent } from "../../utils/events"; import { executeEvent } from "../../utils/events";
@ -17,7 +18,6 @@ import { Spacer } from "../../common/Spacer";
import { ReactionPicker } from "../ReactionPicker"; import { ReactionPicker } from "../ReactionPicker";
import KeyOffIcon from '@mui/icons-material/KeyOff'; import KeyOffIcon from '@mui/icons-material/KeyOff';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import Mention from "@tiptap/extension-mention";
import TextStyle from '@tiptap/extension-text-style'; import TextStyle from '@tiptap/extension-text-style';
import { addressInfoKeySelector } from "../../atoms/global"; import { addressInfoKeySelector } from "../../atoms/global";
import { useRecoilValue } from "recoil"; import { useRecoilValue } from "recoil";
@ -50,8 +50,7 @@ const getBadgeImg = (level)=> {
default: return level0Img default: return level0Img
} }
} }
export const MessageItem = React.memo(({
export const MessageItem = ({
message, message,
onSeen, onSeen,
isLast, isLast,
@ -67,40 +66,80 @@ export const MessageItem = ({
isUpdating, isUpdating,
lastSignature, lastSignature,
onEdit, onEdit,
isPrivate, isPrivate
setMobileViewModeKeepOpen
}) => { }) => {
const {getIndividualUserInfo} = useContext(MyContext)
const userInfo = useRecoilValue(addressInfoKeySelector(message?.sender));
const {getIndividualUserInfo} = useContext(MyContext)
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const [selectedReaction, setSelectedReaction] = useState(null); const [selectedReaction, setSelectedReaction] = useState(null);
const { ref, inView } = useInView({ const [userInfo, setUserInfo] = useState(null)
threshold: 0.7, // Fully visible
triggerOnce: false, // Only trigger once when it becomes visible
});
useEffect(() => {
if (inView && isLast && onSeen) {
onSeen(message.id);
}
}, [inView, message.id, isLast]);
useEffect(()=> { useEffect(()=> {
if(message?.sender){ const getInfo = async ()=> {
getIndividualUserInfo(message?.sender) if(!message?.sender) return
try {
const res = await getIndividualUserInfo(message?.sender)
if(!res) return null
setUserInfo(res)
} catch (error) {
//
} }
}, [message?.sender]) }
getInfo()
}, [message?.sender, getIndividualUserInfo])
const htmlText = useMemo(()=> {
if(message?.messageText){
return generateHTML(message?.messageText, [
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])
}
}, [])
const htmlReply = useMemo(()=> {
if(reply?.messageText){
return generateHTML(reply?.messageText, [
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])
}
}, [])
const userAvatarUrl = useMemo(()=> {
return message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
message?.senderName
}/qortal_avatar?async=true` : ''
}, [])
const onSeenFunc = useCallback(()=> {
onSeen(message.id);
}, [message?.id])
return ( return (
<> <MessageWragger lastMessage={lastSignature === message?.signature} isLast={isLast} onSeen={onSeenFunc}>
{message?.divide && ( {message?.divide && (
<div className="unread-divider" id="unread-divider-id"> <div className="unread-divider" id="unread-divider-id">
Unread messages below Unread messages below
</div> </div>
)} )}
<div <div
ref={lastSignature === message?.signature ? ref : null}
style={{ style={{
padding: "10px", padding: "10px",
backgroundColor: "#232428", backgroundColor: "#232428",
@ -135,25 +174,25 @@ export const MessageItem = ({
sx={{ sx={{
backgroundColor: "#27282c", backgroundColor: "#27282c",
color: "white", color: "white",
height: '40px',
width: '40px'
}} }}
alt={message?.senderName} alt={message?.senderName}
src={message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${ src={userAvatarUrl}
message?.senderName
}/qortal_avatar?async=true` : ''}
> >
{message?.senderName?.charAt(0)} {message?.senderName?.charAt(0)}
</Avatar> </Avatar>
</WrapperUserAction> </WrapperUserAction>
<Tooltip disableFocusListener title={`level ${userInfo?.level}`}> <Tooltip disableFocusListener title={`level ${userInfo}`}>
<img style={{ <img style={{
visibility: userInfo?.level !== undefined ? 'visible' : 'hidden', visibility: userInfo !== undefined ? 'visible' : 'hidden',
width: '30px', width: '30px',
height: 'auto' height: 'auto'
}} src={getBadgeImg(userInfo?.level)} /> }} src={getBadgeImg(userInfo)} />
</Tooltip> </Tooltip>
</Box> </Box>
)} )}
@ -195,7 +234,7 @@ export const MessageItem = ({
gap: '10px', gap: '10px',
alignItems: 'center' alignItems: 'center'
}}> }}>
{message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && ( {message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && (
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
onEdit(message); onEdit(message);
@ -260,41 +299,27 @@ export const MessageItem = ({
}}>Replied to {reply?.senderName || reply?.senderAddress}</Typography> }}>Replied to {reply?.senderName || reply?.senderAddress}</Typography>
{reply?.messageText && ( {reply?.messageText && (
<MessageDisplay <MessageDisplay
htmlContent={generateHTML(reply?.messageText, [ htmlContent={htmlReply}
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])}
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
/> />
)} )}
{reply?.decryptedData?.type === "notification" ? ( {reply?.decryptedData?.type === "notification" ? (
<MessageDisplay htmlContent={reply.decryptedData?.data?.message} /> <MessageDisplay htmlContent={reply.decryptedData?.data?.message} />
) : ( ) : (
<MessageDisplay setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} isReply htmlContent={reply.text} /> <MessageDisplay isReply htmlContent={reply.text} />
)} )}
</Box> </Box>
</Box> </Box>
</> </>
)} )}
{message?.messageText && (
<MessageDisplay <MessageDisplay
htmlContent={generateHTML(message?.messageText, [ htmlContent={htmlText}
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])}
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
/> />
)}
{message?.decryptedData?.type === "notification" ? ( {message?.decryptedData?.type === "notification" ? (
<MessageDisplay htmlContent={message.decryptedData?.data?.message} /> <MessageDisplay htmlContent={message.decryptedData?.data?.message} />
) : ( ) : (
<MessageDisplay setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} htmlContent={message.text} /> <MessageDisplay htmlContent={message.text} />
)} )}
<Box <Box
sx={{ sx={{
@ -319,11 +344,13 @@ export const MessageItem = ({
background: 'var(--bg-2)', background: 'var(--bg-2)',
borderRadius: '7px' borderRadius: '7px'
}} onClick={(event) => { }} onClick={(event) => {
event.stopPropagation(); // Prevent event bubbling event.stopPropagation(); // Prevent event bubbling
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
setSelectedReaction(reaction); setSelectedReaction(reaction);
}}> }}>
<div>{reaction}</div> {numberOfReactions > 1 && ( <div style={{
fontSize: '16px'
}}>{reaction}</div> {numberOfReactions > 1 && (
<Typography sx={{ <Typography sx={{
marginLeft: '4px' marginLeft: '4px'
}}>{' '} {numberOfReactions}</Typography> }}>{' '} {numberOfReactions}</Typography>
@ -361,7 +388,7 @@ export const MessageItem = ({
</Typography> </Typography>
<List sx={{ <List sx={{
overflow: 'auto', overflow: 'auto',
maxWidth: '80vw', maxWidth: '300px',
maxHeight: '300px' maxHeight: '300px'
}}> }}>
{reactions[selectedReaction]?.map((reactionItem) => ( {reactions[selectedReaction]?.map((reactionItem) => (
@ -404,14 +431,14 @@ export const MessageItem = ({
alignItems: 'center', alignItems: 'center',
gap: '15px' gap: '15px'
}}> }}>
{message?.isNotEncrypted && isPrivate && ( {message?.isNotEncrypted && isPrivate && (
<KeyOffIcon sx={{ <KeyOffIcon sx={{
color: 'white', color: 'white',
marginLeft: '10px' marginLeft: '10px'
}} /> }} />
)} )}
{isUpdating ? ( {isUpdating ? (
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: "14px",
@ -460,21 +487,11 @@ export const MessageItem = ({
</Box> </Box>
</Box> </Box>
{/* <Message
model={{
direction: 'incoming',
message: message.text,
position: 'single',
sender: message.senderName,
sentTime: message.timestamp
}}
></Message> */}
{/* {!message.unread && <span style={{ color: 'green' }}> Seen</span>} */}
</div> </div>
</> </MessageWragger>
); );
}; });
export const ReplyPreview = ({message, isEdit})=> { export const ReplyPreview = ({message, isEdit})=> {
@ -501,7 +518,7 @@ export const ReplyPreview = ({message, isEdit})=> {
<Box sx={{ <Box sx={{
padding: '5px' padding: '5px'
}}> }}>
{isEdit ? ( {isEdit ? (
<Typography sx={{ <Typography sx={{
fontSize: '12px', fontSize: '12px',
fontWeight: 600 fontWeight: 600
@ -531,5 +548,38 @@ export const ReplyPreview = ({message, isEdit})=> {
)} )}
</Box> </Box>
</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) => { <MenuItem onClick={(e) => {
handleClose(e); handleClose(e);
setSortablePinnedApps((prev) => { setSortablePinnedApps((prev) => {
const updatedApps = prev.filter( if(app?.isPrivate){
(item) => !(item?.name === app?.name && item?.service === app?.service) 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; 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' }}> <ListItemIcon sx={{ minWidth: '32px' }}>

View File

@ -97,7 +97,7 @@ export const CoreSyncStatus = ({imageSize, position}) => {
<h4 className="lineHeight">{message}</h4> <h4 className="lineHeight">{message}</h4>
<h4 className="lineHeight">Block Height: <span style={{ color: '#03a9f4' }}>{height || ''}</span></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">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> <i></i>
</div> </div>
</div> </div>

View File

@ -18,9 +18,9 @@ import { NotificationIcon2 } from "../../assets/Icons/NotificationIcon2";
import { ChatIcon } from "../../assets/Icons/ChatIcon"; import { ChatIcon } from "../../assets/Icons/ChatIcon";
import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon"; import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon";
import { MembersIcon } from "../../assets/Icons/MembersIcon"; import { MembersIcon } from "../../assets/Icons/MembersIcon";
import { AdminsIcon } from "../../assets/Icons/AdminsIcon";
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from '@mui/icons-material/Lock';
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
import { AdminsIcon } from "../../assets/Icons/AdminsIcon";
const IconWrapper = ({ children, label, color, selected, selectColor, customHeight }) => { const IconWrapper = ({ children, label, color, selected, selectColor, customHeight }) => {
return ( return (
@ -98,7 +98,7 @@ export const DesktopHeader = ({
padding: "10px", padding: "10px",
}} }}
> >
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',
gap: '10px' gap: '10px'
}}> }}>
@ -118,7 +118,7 @@ export const DesktopHeader = ({
fontWeight: 600, fontWeight: 600,
}} }}
> >
{selectedGroup?.groupName} {selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName}
</Typography> </Typography>
</Box> </Box>
<Box <Box
@ -126,9 +126,10 @@ export const DesktopHeader = ({
display: "flex", display: "flex",
gap: "20px", gap: "20px",
alignItems: "center", alignItems: "center",
visibility: selectedGroup?.groupId === '0' ? 'hidden' : 'visibile'
}} }}
> >
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
goToAnnouncements() goToAnnouncements()
@ -139,6 +140,7 @@ export const DesktopHeader = ({
label="ANN" label="ANN"
selected={isAnnouncement} selected={isAnnouncement}
selectColor="#09b6e8" selectColor="#09b6e8"
customHeight="55px"
> >
<NotificationIcon2 <NotificationIcon2
height={25} 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 { formatEmailDate } from "./QMailMessages";
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from '@mui/icons-material/Lock';
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
import { useSetRecoilState } from "recoil"; import { useRecoilState, useSetRecoilState } from "recoil";
import { addressInfoControllerAtom, selectedGroupIdAtom } from "../../atoms/global"; import { addressInfoControllerAtom, groupsPropertiesAtom, selectedGroupIdAtom } from "../../atoms/global";
import { sortArrayByTimestampAndGroupName } from "../../utils/time"; import { sortArrayByTimestampAndGroupName } from "../../utils/time";
import { AdminSpace } from "../Chat/AdminSpace"; import { AdminSpace } from "../Chat/AdminSpace";
import { HubsIcon } from "../../assets/Icons/HubsIcon"; import { HubsIcon } from "../../assets/Icons/HubsIcon";
import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
import { DesktopSideBar } from "../DesktopSideBar"; import { DesktopSideBar } from "../DesktopSideBar";
import BlockIcon from '@mui/icons-material/Block';
import { BlockedUsersModal } from "./BlockedUsersModal";
// let touchStartY = 0; // let touchStartY = 0;
// let disablePullToRefresh = false; // let disablePullToRefresh = false;
@ -480,6 +483,7 @@ export const Group = ({
const [mobileViewMode, setMobileViewMode] = useState("home"); const [mobileViewMode, setMobileViewMode] = useState("home");
const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState(""); const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState("");
const isFocusedRef = useRef(true); const isFocusedRef = useRef(true);
const [isOpenBlockedUserModal, setIsOpenBlockedUserModal] = React.useState(false);
const timestampEnterDataRef = useRef({}); const timestampEnterDataRef = useRef({});
const selectedGroupRef = useRef(null); const selectedGroupRef = useRef(null);
const selectedDirectRef = useRef(null); const selectedDirectRef = useRef(null);
@ -497,9 +501,11 @@ export const Group = ({
const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false) const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false)
const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom) const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom)
const [groupsProperties, setGroupsProperties] = useState({}) const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom)
const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom); const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom);
const isPrivate = useMemo(()=> { const isPrivate = useMemo(()=> {
if(selectedGroup?.groupId === '0') return false
if(!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null if(!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null
if(groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false if(groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false
if(groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true if(groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true
@ -899,7 +905,10 @@ export const Group = ({
} }
if(isPrivate === false){ if(isPrivate === false){
setTriedToFetchSecretKey(true); setTriedToFetchSecretKey(true);
getAdminsForPublic(selectedGroup) if(selectedGroup?.groupId !== '0'){
getAdminsForPublic(selectedGroup)
}
} }
}, [selectedGroup, isPrivate]); }, [selectedGroup, isPrivate]);
@ -988,7 +997,7 @@ export const Group = ({
// Update the component state with the received 'sendqort' state // Update the component state with the received 'sendqort' state
setGroups(sortArrayByTimestampAndGroupName(message.payload)); setGroups(sortArrayByTimestampAndGroupName(message.payload));
getLatestRegularChat(message.payload) getLatestRegularChat(message.payload)
setMemberGroups(message.payload); setMemberGroups(message.payload?.filter((item)=> item?.groupId !== '0'));
if (selectedGroupRef.current && groupSectionRef.current === "chat") { if (selectedGroupRef.current && groupSectionRef.current === "chat") {
chrome?.runtime?.sendMessage({ chrome?.runtime?.sendMessage({
@ -1081,7 +1090,7 @@ export const Group = ({
!initiatedGetMembers.current && !initiatedGetMembers.current &&
selectedGroup?.groupId && selectedGroup?.groupId &&
secretKey && secretKey &&
admins.includes(myAddress) admins.includes(myAddress) && selectedGroup?.groupId !== '0'
) { ) {
// getAdmins(selectedGroup?.groupId); // getAdmins(selectedGroup?.groupId);
getMembers(selectedGroup?.groupId); getMembers(selectedGroup?.groupId);
@ -1432,11 +1441,11 @@ export const Group = ({
if (isLoadingOpenSectionFromNotification.current) return; if (isLoadingOpenSectionFromNotification.current) return;
const groupId = e.detail?.from; const groupId = e.detail?.from;
const findGroup = groups?.find((group) => +group?.groupId === +groupId); const findGroup = groups?.find((group) => +group?.groupId === +groupId);
if (findGroup?.groupId === selectedGroup?.groupId) { if (findGroup?.groupId === selectedGroup?.groupId) {
isLoadingOpenSectionFromNotification.current = false; isLoadingOpenSectionFromNotification.current = false;
setChatMode("groups");
setDesktopViewMode('chat')
return; return;
} }
if (findGroup) { if (findGroup) {
@ -2159,7 +2168,7 @@ export const Group = ({
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={group.groupName} primary={group.groupId === '0' ? 'General' : group.groupName}
secondary={!group?.timestamp ? 'no messages' :`last message: ${formatEmailDate(group?.timestamp)}`} secondary={!group?.timestamp ? 'no messages' :`last message: ${formatEmailDate(group?.timestamp)}`}
primaryTypographyProps={{ primaryTypographyProps={{
style: { style: {
@ -2218,9 +2227,11 @@ export const Group = ({
width: "100%", width: "100%",
justifyContent: "center", justifyContent: "center",
padding: "10px", padding: "10px",
gap: '10px'
}} }}
> >
{chatMode === "groups" && ( {chatMode === "groups" && (
<>
<CustomButton <CustomButton
onClick={() => { onClick={() => {
setOpenAddGroup(true); setOpenAddGroup(true);
@ -2231,8 +2242,24 @@ export const Group = ({
color: "white", color: "white",
}} }}
/> />
Group Mgmt Group Mgmt
</CustomButton> </CustomButton>
<CustomButton
onClick={() => {
setIsOpenBlockedUserModal(true);
}}
sx={{
minWidth: 'unset',
padding: '10px'
}}
>
<BlockIcon
sx={{
color: "white",
}}
/>
</CustomButton>
</>
)} )}
{chatMode === "directs" && ( {chatMode === "directs" && (
<CustomButton <CustomButton
@ -2742,7 +2769,11 @@ export const Group = ({
)} )}
</div> </div>
)} )}
{isOpenBlockedUserModal && (
<BlockedUsersModal close={()=> {
setIsOpenBlockedUserModal(false)
}} />
)}
{selectedDirect && !newChat && ( {selectedDirect && !newChat && (
<> <>
<Box <Box
@ -2815,6 +2846,7 @@ export const Group = ({
{!isMobile && ( {!isMobile && (
<HomeDesktop <HomeDesktop
name={userInfo?.name}
refreshHomeDataFunc={refreshHomeDataFunc} refreshHomeDataFunc={refreshHomeDataFunc}
myAddress={myAddress} myAddress={myAddress}
isLoadingGroups={isLoadingGroups} isLoadingGroups={isLoadingGroups}

View File

@ -10,16 +10,20 @@ import CommentIcon from "@mui/icons-material/Comment";
import InfoIcon from "@mui/icons-material/Info"; import InfoIcon from "@mui/icons-material/Info";
import GroupAddIcon from "@mui/icons-material/GroupAdd"; import GroupAddIcon from "@mui/icons-material/GroupAdd";
import { executeEvent } from "../../utils/events"; 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 { Spacer } from "../../common/Spacer";
import { getGroupNames } from "./UserListOfInvites"; import { getGroupNames } from "./UserListOfInvites";
import { CustomLoader } from "../../common/CustomLoader"; import { CustomLoader } from "../../common/CustomLoader";
import { getBaseApiReact, isMobile } from "../../App"; import { getBaseApiReact, isMobile } from "../../App";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState( const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState(
[] []
); );
const [isExpanded, setIsExpanded] = React.useState(false);
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const getJoinRequests = async () => { const getJoinRequests = async () => {
@ -53,120 +57,129 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
alignItems: "center", alignItems: "center",
}} }}
> >
<Box <ButtonBase
sx={{ sx={{
width: "322px", width: "322px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "row",
padding: "0px 20px", padding: "0px 20px",
gap: '10px',
justifyContent: 'flex-start'
}} }}
onClick={()=> setIsExpanded((prev)=> !prev)}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: "1rem",
fontWeight: 600,
}} }}
> >
Group Invites: Group Invites {groupsWithJoinRequests?.length > 0 && ` (${groupsWithJoinRequests?.length})`}
</Typography> </Typography>
<Spacer height="10px" /> {isExpanded ? <ExpandLessIcon sx={{
</Box> marginLeft: 'auto'
}} /> : (
<ExpandMoreIcon sx={{
marginLeft: 'auto'
}}/>
)}
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box
sx={{
width: "322px",
height: isMobile ? "165px" : "250px",
<Box display: "flex",
sx={{ flexDirection: "column",
width: "322px", bgcolor: "background.paper",
height: isMobile ? "165px" : "250px", padding: "20px",
borderRadius: "19px",
display: "flex", }}
flexDirection: "column", >
bgcolor: "background.paper", {loading && groupsWithJoinRequests.length === 0 && (
padding: "20px", <Box
borderRadius: "19px", sx={{
}} width: "100%",
> display: "flex",
{loading && groupsWithJoinRequests.length === 0 && ( justifyContent: "center",
<Box }}
>
<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={{ sx={{
width: "100%", width: "100%",
display: "flex", maxWidth: 360,
justifyContent: "center", bgcolor: "background.paper",
maxHeight: "300px",
overflow: "auto",
}} }}
className="scrollable-container"
> >
<CustomLoader /> {groupsWithJoinRequests?.map((group) => {
</Box> return (
)} <ListItem
{!loading && groupsWithJoinRequests.length === 0 && ( sx={{
<Box marginBottom: "20px",
sx={{ }}
width: "100%", key={group?.groupId}
display: "flex", onClick={() => {
justifyContent: "center", setOpenAddGroup(true);
alignItems: 'center', setTimeout(() => {
height: '100%', executeEvent("openGroupInvitesRequest", {});
}, 300);
}} }}
> disablePadding
<Typography secondaryAction={
sx={{ <IconButton edge="end" aria-label="comments">
fontSize: "11px", <GroupAddIcon
fontWeight: 400, sx={{
color: 'rgba(255, 255, 255, 0.2)' color: "white",
}} fontSize: "18px",
> }}
Nothing to display />
</Typography> </IconButton>
</Box> }
)} >
<List <ListItemButton disableRipple role={undefined} dense>
sx={{ <ListItemText
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
sx={{ sx={{
color: "white", "& .MuiTypography-root": {
fontSize: "18px", fontSize: "13px",
fontWeight: 400,
},
}} }}
primary={`${group?.groupName} has invited you`}
/> />
</IconButton> </ListItemButton>
} </ListItem>
> );
<ListItemButton disableRipple role={undefined} dense> })}
<ListItemText </List>
sx={{ </Box>
"& .MuiTypography-root": { </Collapse>
fontSize: "13px",
fontWeight: 400,
},
}}
primary={`${group?.groupName} has invited you`}
/>
</ListItemButton>
</ListItem>
);
})}
</List>
</Box>
</Box> </Box>
); );
}; };

View File

@ -11,16 +11,20 @@ import InfoIcon from "@mui/icons-material/Info";
import { RequestQueueWithPromise } from "../../utils/queue/queue"; import { RequestQueueWithPromise } from "../../utils/queue/queue";
import GroupAddIcon from '@mui/icons-material/GroupAdd'; import GroupAddIcon from '@mui/icons-material/GroupAdd';
import { executeEvent } from "../../utils/events"; 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 { Spacer } from "../../common/Spacer";
import { CustomLoader } from "../../common/CustomLoader"; import { CustomLoader } from "../../common/CustomLoader";
import { getBaseApi } from "../../background"; import { getBaseApi } from "../../background";
import { MyContext, getBaseApiReact, isMobile } from "../../App"; import { MyContext, getBaseApiReact, isMobile } from "../../App";
import { myGroupsWhereIAmAdminAtom } from "../../atoms/global"; import { myGroupsWhereIAmAdminAtom } from "../../atoms/global";
import { useSetRecoilState } from "recoil"; 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 requestQueueGroupJoinRequests = new RequestQueueWithPromise(2)
export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection, setMobileViewMode, setDesktopViewMode }) => { export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection, setMobileViewMode, setDesktopViewMode }) => {
const [isExpanded, setIsExpanded] = React.useState(false)
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([]) const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([])
const [loading, setLoading] = React.useState(true) const [loading, setLoading] = React.useState(true)
const {txList, setTxList} = React.useContext(MyContext) const {txList, setTxList} = React.useContext(MyContext)
@ -34,7 +38,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
setLoading(true) setLoading(true)
let groupsAsAdmin = [] let groupsAsAdmin = []
const getAllGroupsAsAdmin = groups.map(async (group)=> { const getAllGroupsAsAdmin = groups.filter((item)=> item.groupId !== '0').map(async (group)=> {
const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(()=> { const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
return fetch( return fetch(
@ -55,7 +59,6 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
await Promise.all(getAllGroupsAsAdmin) await Promise.all(getAllGroupsAsAdmin)
setMyGroupsWhereIAmAdmin(groupsAsAdmin) setMyGroupsWhereIAmAdmin(groupsAsAdmin)
const res = await Promise.all(groupsAsAdmin.map(async (group)=> { const res = await Promise.all(groupsAsAdmin.map(async (group)=> {
const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> { const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
@ -110,26 +113,33 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
flexDirection: "column", flexDirection: "column",
alignItems: 'center' alignItems: 'center'
}}> }}>
<Box <ButtonBase
sx={{ sx={{
width: "322px", width: "322px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "row",
padding: '0px 20px', padding: '0px 20px',
gap: '10px',
justifyContent: 'flex-start'
}} }}
onClick={()=> setIsExpanded((prev)=> !prev)}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: "1rem",
fontWeight: 600,
}} }}
> >
Join Requests: Join Requests {filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length > 0 && ` (${filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length})`}
</Typography> </Typography>
<Spacer height="10px" /> {isExpanded ? <ExpandLessIcon sx={{
</Box> marginLeft: 'auto'
}} /> : (
<ExpandMoreIcon sx={{
marginLeft: 'auto'
}}/>
)}
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box <Box
sx={{ sx={{
width: "322px", width: "322px",
@ -173,7 +183,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
</Typography> </Typography>
</Box> </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)=> { {filteredJoinRequests?.map((group)=> {
if(group?.data?.length === 0) return null if(group?.data?.length === 0) return null
return ( return (
@ -228,6 +238,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
</List> </List>
</Box> </Box>
</Collapse>
</Box> </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 React from "react";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched"; import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched";
@ -7,10 +7,14 @@ import { GroupJoinRequests } from "./GroupJoinRequests";
import { GroupInvites } from "./GroupInvites"; import { GroupInvites } from "./GroupInvites";
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from "@mui/icons-material/Refresh";
import { ListOfGroupPromotions } from "./ListOfGroupPromotions"; 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 = ({ export const HomeDesktop = ({
refreshHomeDataFunc, refreshHomeDataFunc,
myAddress, myAddress,
name,
isLoadingGroups, isLoadingGroups,
balance, balance,
userInfo, userInfo,
@ -22,140 +26,217 @@ export const HomeDesktop = ({
setOpenAddGroup, setOpenAddGroup,
setMobileViewMode, setMobileViewMode,
setDesktopViewMode, 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 ( return (
<Box <Box
sx={{ sx={{
display: desktopViewMode === 'home' ? 'flex' : 'none', display: desktopViewMode === "home" ? "flex" : "none",
width: "100%", width: "100%",
flexDirection: "column", flexDirection: "column",
height: "100%", height: "100%",
overflow: "auto", overflow: "auto",
alignItems: "center", alignItems: "center",
}} }}
> >
<Spacer height="20px" /> <Spacer height="20px" />
<Box sx={{ <Box
display: "flex",
width: "100%",
flexDirection: "column",
height: "100%",
alignItems: "flex-start",
maxWidth: '1036px'
}}>
<Typography
sx={{ sx={{
color: "rgba(255, 255, 255, 1)", display: "flex",
fontWeight: 400, width: "100%",
fontSize: userInfo?.name?.length > 15 ? "16px" : "20px", flexDirection: "column",
padding: '10px' height: "100%",
alignItems: "flex-start",
maxWidth: "1036px",
}} }}
> >
Welcome <Typography
{userInfo?.name ? (
<span
style={{
fontStyle: "italic",
}}
>{`, ${userInfo?.name}`}</span>
) : null}
</Typography>
<Spacer height="30px" />
{!isLoadingGroups && (
<Box
sx={{ sx={{
display: "flex", color: "rgba(255, 255, 255, 1)",
gap: "15px", fontWeight: 400,
flexWrap: "wrap", fontSize: userInfo?.name?.length > 15 ? "16px" : "20px",
justifyContent: "center", padding: "10px",
}} }}
> >
<Box sx={{ Welcome
width: '330px', {userInfo?.name ? (
display: 'flex', <span
alignItems: 'center', style={{
justifyContent: 'center' fontStyle: "italic",
}}> }}
<ThingsToDoInitial >{`, ${userInfo?.name}`}</span>
balance={balance} ) : null}
myAddress={myAddress} </Typography>
name={userInfo?.name} <Spacer height="30px" />
hasGroups={groups?.length !== 0} {!isLoadingGroups && (
userInfo={userInfo} <Box
/> sx={{
</Box> display: "flex",
{desktopViewMode === 'home' && ( gap: "20px",
<> flexWrap: "wrap",
<Box sx={{ width: "100%",
width: '330px', justifyContent: "center",
display: 'flex', }}
alignItems: 'center', >
justifyContent: 'center' <Box
}}> sx={{
<ListOfThreadPostsWatched /> display: "flex",
</Box> gap: "20px",
<Box sx={{ flexWrap: "wrap",
width: '330px', flexDirection: "column",
display: 'flex', }}
alignItems: 'center', >
justifyContent: 'center' <Box
}}>
<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
sx={{ sx={{
width: "330px",
display: "flex", display: "flex",
width: "100%", alignItems: "center",
justifyContent: "flex-start", justifyContent: "center",
}} }}
> >
<Button <ThingsToDoInitial
variant="outlined" balance={balance}
startIcon={<RefreshIcon />} myAddress={myAddress}
onClick={refreshHomeDataFunc} name={userInfo?.name}
sx={{ userInfo={userInfo}
color: "white", hasGroups={
}} groups?.filter((item) => item?.groupId !== "0").length !== 0
> }
Refresh home data />
</Button> </Box>
</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" /> <Spacer height="180px" />
</Box> </Box>
); );

View File

@ -9,6 +9,8 @@ import {
Avatar, Avatar,
Box, Box,
Button, Button,
ButtonBase,
Collapse,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
@ -28,8 +30,8 @@ import {
import { getNameInfo } from "./Group"; import { getNameInfo } from "./Group";
import { getBaseApi, getFee } from "../../background"; import { getBaseApi, getFee } from "../../background";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from "@mui/icons-material/Lock";
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; import NoEncryptionGmailerrorredIcon from "@mui/icons-material/NoEncryptionGmailerrorred";
import { import {
MyContext, MyContext,
getArbitraryEndpointReact, getArbitraryEndpointReact,
@ -40,7 +42,11 @@ import { Spacer } from "../../common/Spacer";
import { CustomLoader } from "../../common/CustomLoader"; import { CustomLoader } from "../../common/CustomLoader";
import { RequestQueueWithPromise } from "../../utils/queue/queue"; import { RequestQueueWithPromise } from "../../utils/queue/queue";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { myGroupsWhereIAmAdminAtom, promotionTimeIntervalAtom, promotionsAtom } from "../../atoms/global"; import {
myGroupsWhereIAmAdminAtom,
promotionTimeIntervalAtom,
promotionsAtom,
} from "../../atoms/global";
import { Label } from "./AddGroup"; import { Label } from "./AddGroup";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { CustomizedSnackbars } from "../Snackbar/Snackbar";
@ -48,7 +54,8 @@ import { getGroupNames } from "./UserListOfInvites";
import { WrapperUserAction } from "../WrapperUserAction"; import { WrapperUserAction } from "../WrapperUserAction";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import ErrorBoundary from "../../common/ErrorBoundary"; 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 const requestQueuePromos = new RequestQueueWithPromise(20);
export function utf8ToBase64(inputString: string): string { export function utf8ToBase64(inputString: string): string {
@ -65,7 +72,6 @@ export function utf8ToBase64(inputString: string): string {
const uid = new ShortUniqueId({ length: 8 }); const uid = new ShortUniqueId({ length: 8 });
export function getGroupId(str) { export function getGroupId(str) {
const match = str.match(/group-(\d+)-/); const match = str.match(/group-(\d+)-/);
return match ? match[1] : null; return match ? match[1] : null;
@ -81,12 +87,12 @@ export const ListOfGroupPromotions = () => {
const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState( const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState(
myGroupsWhereIAmAdminAtom myGroupsWhereIAmAdminAtom
); );
const [promotions, setPromotions] = useRecoilState( const [promotions, setPromotions] = useRecoilState(promotionsAtom);
promotionsAtom
);
const [promotionTimeInterval, setPromotionTimeInterval] = useRecoilState( const [promotionTimeInterval, setPromotionTimeInterval] = useRecoilState(
promotionTimeIntervalAtom promotionTimeIntervalAtom
); );
const [isExpanded, setIsExpanded] = React.useState(false);
const [openSnack, setOpenSnack] = useState(false); const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null); const [infoSnack, setInfoSnack] = useState(null);
const [fee, setFee] = useState(null); const [fee, setFee] = useState(null);
@ -95,7 +101,6 @@ export const ListOfGroupPromotions = () => {
const { show, setTxList } = useContext(MyContext); const { show, setTxList } = useContext(MyContext);
const listRef = useRef(); const listRef = useRef();
const rowVirtualizer = useVirtualizer({ const rowVirtualizer = useVirtualizer({
count: promotions.length, count: promotions.length,
getItemKey: React.useCallback( getItemKey: React.useCallback(
@ -107,7 +112,6 @@ export const ListOfGroupPromotions = () => {
overscan: 10, // Number of items to render outside the visible area to improve smoothness overscan: 10, // Number of items to render outside the visible area to improve smoothness
}); });
useEffect(() => { useEffect(() => {
try { try {
(async () => { (async () => {
@ -118,7 +122,7 @@ export const ListOfGroupPromotions = () => {
}, []); }, []);
const getPromotions = useCallback(async () => { const getPromotions = useCallback(async () => {
try { try {
setPromotionTimeInterval(Date.now()) setPromotionTimeInterval(Date.now());
const identifier = `group-promotions-ui24-`; const identifier = `group-promotions-ui24-`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`;
const response = await fetch(url, { const response = await fetch(url, {
@ -169,7 +173,9 @@ export const ListOfGroupPromotions = () => {
}); });
await Promise.all(getPromos); 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); setPromotions(groupWithInfo);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -178,22 +184,23 @@ export const ListOfGroupPromotions = () => {
useEffect(() => { useEffect(() => {
const now = Date.now(); const now = Date.now();
const timeSinceLastFetch = now - promotionTimeInterval; const timeSinceLastFetch = now - promotionTimeInterval;
const initialDelay = timeSinceLastFetch >= THIRTY_MINUTES const initialDelay =
? 0 timeSinceLastFetch >= THIRTY_MINUTES
: THIRTY_MINUTES - timeSinceLastFetch; ? 0
: THIRTY_MINUTES - timeSinceLastFetch;
const initialTimeout = setTimeout(() => { const initialTimeout = setTimeout(() => {
getPromotions(); getPromotions();
// Start a 30-minute interval // Start a 30-minute interval
const interval = setInterval(() => { const interval = setInterval(() => {
getPromotions(); getPromotions();
}, THIRTY_MINUTES); }, THIRTY_MINUTES);
return () => clearInterval(interval); return () => clearInterval(interval);
}, initialDelay); }, initialDelay);
return () => clearTimeout(initialTimeout); return () => clearTimeout(initialTimeout);
}, [getPromotions, promotionTimeInterval]); }, [getPromotions, promotionTimeInterval]);
@ -321,103 +328,144 @@ export const ListOfGroupPromotions = () => {
}; };
return ( return (
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
display: "flex", display: "flex",
marginTop: "20px",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
marginTop: "25px", justifyContent: "center",
}} }}
> >
<Box <Box sx={{
sx={{ display: 'flex',
width: isMobile ? "320px" : "750px", gap: '20px',
maxWidth: "90%", width: '100%',
display: "flex", justifyContent: 'space-between'
flexDirection: "column", }}>
padding: "0px 20px", <ButtonBase
}}
>
<Box
sx={{ sx={{
width: "100%",
display: "flex", display: "flex",
justifyContent: "space-between", flexDirection: "row",
alignItems: "center", padding: `0px ${isExpanded ? "24px" : "20px"}`,
gap: "10px",
justifyContent: "flex-start",
alignSelf: isExpanded && "flex-start",
}} }}
onClick={() => setIsExpanded((prev) => !prev)}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: "1rem",
fontWeight: 600,
}} }}
> >
Group Promotions Group promotions {promotions.length > 0 && ` (${promotions.length})`}
</Typography> </Typography>
<Button {isExpanded ? (
variant="contained" <ExpandLessIcon
onClick={() => setIsShowModal(true)} sx={{
sx={{ marginLeft: "auto",
fontSize: "12px", }}
}} />
> ) : (
Add Promotion <ExpandMoreIcon
</Button> sx={{
</Box> marginLeft: "auto",
<Spacer height="10px" /> }}
/>
)}
</ButtonBase>
<Box
style={{
width: "330px",
}}
/>
</Box> </Box>
<Box <Collapse in={isExpanded} timeout="auto" unmountOnExit>
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 <Box
sx={{ sx={{
width: "100%", width: isMobile ? "320px" : "750px",
maxWidth: "90%",
display: "flex", display: "flex",
justifyContent: "center", flexDirection: "column",
padding: "0px 20px",
}} }}
> >
<CustomLoader /> <Box
</Box>
)}
{!loading && promotions.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
>
<Typography
sx={{ sx={{
fontSize: "11px", width: "100%",
fontWeight: 400, display: "flex",
color: "rgba(255, 255, 255, 0.2)", 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> </Box>
)} <Box
<div 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={{ style={{
height: "600px", height: "600px",
position: "relative", position: "relative",
@ -455,7 +503,6 @@ export const ListOfGroupPromotions = () => {
const index = virtualRow.index; const index = virtualRow.index;
const promotion = promotions[index]; const promotion = promotions[index];
return ( return (
<div <div
data-index={virtualRow.index} //needed for dynamic row height measurement data-index={virtualRow.index} //needed for dynamic row height measurement
ref={rowVirtualizer.measureElement} //measure dynamic row height ref={rowVirtualizer.measureElement} //measure dynamic row height
@ -474,235 +521,251 @@ export const ListOfGroupPromotions = () => {
gap: "5px", gap: "5px",
}} }}
> >
<ErrorBoundary <ErrorBoundary
fallback={ fallback={
<Typography> <Typography>
Error loading content: Invalid Data Error loading content: Invalid Data
</Typography> </Typography>
} }
> >
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
width: "100%", width: "100%",
padding: "0px 20px", padding: "0px 20px",
}} }}
> >
<Popover <Popover
open={openPopoverIndex === promotion?.groupId} open={openPopoverIndex === promotion?.groupId}
anchorEl={popoverAnchor} anchorEl={popoverAnchor}
onClose={(event, reason) => { onClose={(event, reason) => {
if (reason === "backdropClick") { if (reason === "backdropClick") {
// Prevent closing on backdrop click // Prevent closing on backdrop click
return; return;
} }
handlePopoverClose(); // Close only on other events like Esc key press handlePopoverClose(); // Close only on other events like Esc key press
}} }}
anchorOrigin={{ anchorOrigin={{
vertical: "top", vertical: "top",
horizontal: "center", horizontal: "center",
}} }}
transformOrigin={{ transformOrigin={{
vertical: "bottom", vertical: "bottom",
horizontal: "center", horizontal: "center",
}} }}
style={{ marginTop: "8px" }} style={{ marginTop: "8px" }}
> >
<Box <Box
sx={{ sx={{
width: "325px", width: "325px",
height: "auto", height: "auto",
maxHeight: "400px", maxHeight: "400px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
gap: "10px", gap: "10px",
padding: "10px", padding: "10px",
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: "13px",
fontWeight: 600, fontWeight: 600,
}} }}
> >
Group name: {` ${promotion?.groupName}`} Group name: {` ${promotion?.groupName}`}
</Typography> </Typography>
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: "13px",
fontWeight: 600, fontWeight: 600,
}} }}
> >
Number of members: {` ${promotion?.memberCount}`} Number of members:{" "}
</Typography> {` ${promotion?.memberCount}`}
{promotion?.description && ( </Typography>
<Typography {promotion?.description && (
sx={{ <Typography
fontSize: "13px", sx={{
fontWeight: 600, fontSize: "13px",
}} fontWeight: 600,
> }}
{promotion?.description} >
</Typography> {promotion?.description}
)} </Typography>
{promotion?.isOpen === false && ( )}
<Typography {promotion?.isOpen === false && (
sx={{ <Typography
fontSize: "13px", sx={{
fontWeight: 600, fontSize: "13px",
}} fontWeight: 600,
> }}
*This is a closed/private group, so you will need to wait >
until an admin accepts your request *This is a closed/private group, so you
</Typography> will need to wait until an admin accepts
)} your request
<Spacer height="5px" /> </Typography>
<Box )}
sx={{ <Spacer height="5px" />
display: "flex", <Box
gap: "20px", sx={{
alignItems: "center", display: "flex",
width: "100%", gap: "20px",
justifyContent: "center", alignItems: "center",
}} width: "100%",
> justifyContent: "center",
<LoadingButton }}
loading={isLoadingJoinGroup} >
loadingPosition="start" <LoadingButton
variant="contained" loading={isLoadingJoinGroup}
onClick={handlePopoverClose} loadingPosition="start"
> variant="contained"
Close onClick={handlePopoverClose}
</LoadingButton> >
<LoadingButton Close
loading={isLoadingJoinGroup} </LoadingButton>
loadingPosition="start" <LoadingButton
variant="contained" loading={isLoadingJoinGroup}
onClick={() => loadingPosition="start"
handleJoinGroup(promotion, promotion?.isOpen) variant="contained"
} onClick={() =>
> handleJoinGroup(
Join promotion,
</LoadingButton> promotion?.isOpen
</Box> )
</Box> }
</Popover> >
Join
</LoadingButton>
</Box>
</Box>
</Popover>
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
width: "100%", width: "100%",
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: "15px", gap: "15px",
}} }}
> >
<Avatar <Avatar
sx={{ sx={{
backgroundColor: "#27282c", backgroundColor: "#27282c",
color: "white", color: "white",
}} }}
alt={promotion?.name} alt={promotion?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${ src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
promotion?.name promotion?.name
}/qortal_avatar?async=true`} }/qortal_avatar?async=true`}
> >
{promotion?.name?.charAt(0)} {promotion?.name?.charAt(0)}
</Avatar> </Avatar>
<Typography <Typography
sx={{ sx={{
fontWight: 600, fontWight: 600,
fontFamily: "Inter", fontFamily: "Inter",
color: "cadetBlue", color: "cadetBlue",
}} }}
> >
{promotion?.name} {promotion?.name}
</Typography> </Typography>
</Box> </Box>
<Typography <Typography
sx={{ sx={{
fontWight: 600, fontWight: 600,
fontFamily: "Inter", fontFamily: "Inter",
color: "cadetBlue", color: "cadetBlue",
}} }}
> >
{promotion?.groupName} {promotion?.groupName}
</Typography> </Typography>
</Box> </Box>
<Spacer height="20px" /> <Spacer height="20px" />
<Box sx={{ <Box
display: 'flex', sx={{
gap: '20px', display: "flex",
alignItems: 'center' gap: "20px",
}}> alignItems: "center",
{promotion?.isOpen === false && ( }}
<LockIcon sx={{ >
color: 'var(--green)' {promotion?.isOpen === false && (
}} /> <LockIcon
)} sx={{
{promotion?.isOpen === true && ( color: "var(--green)",
<NoEncryptionGmailerrorredIcon sx={{ }}
color: 'var(--danger)' />
}} /> )}
)} {promotion?.isOpen === true && (
<Typography <NoEncryptionGmailerrorredIcon
sx={{ sx={{
fontSize: "15px", color: "var(--danger)",
fontWeight: 600, }}
}} />
> )}
{promotion?.isOpen ? 'Public group' : 'Private group' } <Typography
</Typography> sx={{
</Box> fontSize: "15px",
<Spacer height="20px" /> fontWeight: 600,
<Typography }}
sx={{ >
fontWight: 600, {promotion?.isOpen
fontFamily: "Inter", ? "Public group"
color: "cadetBlue", : "Private group"}
}} </Typography>
> </Box>
{promotion?.data} <Spacer height="20px" />
</Typography> <Typography
<Spacer height="20px" /> sx={{
<Box fontWight: 600,
sx={{ fontFamily: "Inter",
display: "flex", color: "cadetBlue",
justifyContent: "center", }}
width: "100%", >
}} {promotion?.data}
> </Typography>
<Button <Spacer height="20px" />
// variant="contained" <Box
onClick={(event) => handlePopoverOpen(event, promotion?.groupId)} sx={{
sx={{ display: "flex",
fontSize: "12px", justifyContent: "center",
color: 'white' width: "100%",
}} }}
> >
Join Group: {` ${promotion?.groupName}`} <Button
</Button> // variant="contained"
</Box> onClick={(event) =>
</Box> handlePopoverOpen(event, promotion?.groupId)
<Spacer height="50px" /> }
sx={{
fontSize: "12px",
color: "white",
}}
>
Join Group: {` ${promotion?.groupName}`}
</Button>
</Box>
</Box>
<Spacer height="50px" />
</ErrorBoundary> </ErrorBoundary>
</div> </div>
); );
})} })}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</Box> </Box>
</>
</Collapse>
<Spacer height="20px" /> <Spacer height="20px" />
{isShowModal && ( {isShowModal && (
@ -712,7 +775,7 @@ export const ListOfGroupPromotions = () => {
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
> >
<DialogTitle id="alert-dialog-title"> <DialogTitle id="alert-dialog-title">
{"Promote your group to non-members"} {"Promote your group to non-members"}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText id="alert-dialog-description"> <DialogContentText id="alert-dialog-description">
@ -738,6 +801,7 @@ export const ListOfGroupPromotions = () => {
value={selectedGroup} value={selectedGroup}
label="Groups where you are an admin" label="Groups where you are an admin"
onChange={(e) => setSelectedGroup(e.target.value)} onChange={(e) => setSelectedGroup(e.target.value)}
variant="outlined"
> >
{myGroupsWhereIAmAdmin?.map((group) => { {myGroupsWhereIAmAdmin?.map((group) => {
return ( 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 List from "@mui/material/List";
import ListItem from "@mui/material/ListItem"; import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton"; import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText"; import ListItemText from "@mui/material/ListItemText";
import moment from 'moment' import moment from 'moment'
import { Box, Typography } from "@mui/material"; import { Box, ButtonBase, Collapse, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { getBaseApiReact, isMobile } from "../../App"; import { getBaseApiReact, isMobile } from "../../App";
import { MessagingIcon } from '../../assets/Icons/MessagingIcon'; import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
@ -15,6 +15,9 @@ import { executeEvent } from '../../utils/events';
import { CustomLoader } from '../../common/CustomLoader'; import { CustomLoader } from '../../common/CustomLoader';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global'; 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) => { export const isLessThanOneWeekOld = (timestamp) => {
// Current time in milliseconds // Current time in milliseconds
const now = Date.now(); const now = Date.now();
@ -41,8 +44,9 @@ export function formatEmailDate(timestamp: number) {
} }
} }
export const QMailMessages = ({userName, userAddress}) => { export const QMailMessages = ({userName, userAddress}) => {
const [mails, setMails] = useRecoilState(mailsAtom) const [isExpanded, setIsExpanded] = useState(false)
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom) const [mails, setMails] = useRecoilState(mailsAtom)
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const getMails = useCallback(async () => { const getMails = useCallback(async () => {
@ -99,7 +103,16 @@ export const QMailMessages = ({userName, userAddress}) => {
}, [getMails, 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 ( return (
<Box <Box
@ -111,26 +124,39 @@ export const QMailMessages = ({userName, userAddress}) => {
}} }}
> >
<Box <ButtonBase
sx={{ sx={{
width: "322px", width: "322px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "row",
gap: '10px',
padding: "0px 20px", padding: "0px 20px",
justifyContent: 'flex-start'
}} }}
onClick={()=> setIsExpanded((prev)=> !prev)}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: "1rem",
fontWeight: 600,
}} }}
> >
Latest Q-Mails Latest Q-Mails
</Typography> </Typography>
<Spacer height="10px" /> <MarkEmailUnreadIcon sx={{
</Box> 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 <Box
className="scrollable-container"
sx={{ sx={{
width: "322px", width: "322px",
height: isMobile ? "165px" : "250px", height: isMobile ? "165px" : "250px",
@ -247,6 +273,7 @@ export const QMailMessages = ({userName, userAddress}) => {
</Box> </Box>
</Collapse>
</Box> </Box>
) )
} }

View File

@ -12,27 +12,17 @@ import { Box, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { isMobile } from "../../App"; import { isMobile } from "../../App";
import { QMailMessages } from "./QMailMessages"; import { QMailMessages } from "./QMailMessages";
import { executeEvent } from "../../utils/events";
export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInfo }) => { export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInfo }) => {
const [checked1, setChecked1] = React.useState(false); const [checked1, setChecked1] = React.useState(false);
const [checked2, setChecked2] = 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) => { // React.useEffect(() => {
// const response = await fetch(getBaseApiReact() + "/addresses/" + address); // if (hasGroups) setChecked3(true);
// const data = await response.json(); // }, [hasGroups]);
// if (data.error && data.error === 124) {
// setChecked1(false);
// } else if (data.address) {
// setChecked1(true);
// }
// };
// const checkInfo = async () => {
// try {
// getAddressInfo(myAddress);
// } catch (error) {}
// };
React.useEffect(() => { React.useEffect(() => {
if (balance && +balance >= 6) { if (balance && +balance >= 6) {
@ -40,9 +30,6 @@ export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInf
} }
}, [balance]); }, [balance]);
React.useEffect(() => {
if (hasGroups) setChecked3(true);
}, [hasGroups]);
React.useEffect(() => { React.useEffect(() => {
if (name) setChecked2(true); if (name) setChecked2(true);
@ -50,20 +37,21 @@ export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInf
const isLoaded = React.useMemo(()=> { const isLoaded = React.useMemo(()=> {
if(userInfo !== null) return true if(userInfo !== null) return true
return false return false
}, [userInfo]) }, [ userInfo])
const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> { const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> {
if(isLoaded && checked1 && checked2) return true if(isLoaded && checked1 && checked2) return true
return false return false
}, [checked1, isLoaded, checked2]) }, [checked1, isLoaded, checked2])
if(hasDoneNameAndBalanceAndIsLoaded){ if(hasDoneNameAndBalanceAndIsLoaded){
return ( return (
<QMailMessages userAddress={userInfo?.address} userName={userInfo?.name} /> <QMailMessages userAddress={userInfo?.address} userName={userInfo?.name} />
); );
} }
if(!isLoaded) return null
return ( return (
<Box <Box
@ -84,12 +72,11 @@ return (
> >
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: "1rem",
fontWeight: 600, fontWeight: 600,
}} }}
> >
{!isLoaded ? 'Loading...' : 'Getting Started' } {!isLoaded ? 'Loading...' : 'Getting Started' }
</Typography> </Typography>
<Spacer height="10px" /> <Spacer height="10px" />
</Box> </Box>
@ -97,7 +84,6 @@ return (
<Box <Box
sx={{ sx={{
width: "322px", width: "322px",
height: isMobile ? "165px" : "250px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
bgcolor: "background.paper", bgcolor: "background.paper",
@ -105,149 +91,140 @@ return (
borderRadius: "19px", borderRadius: "19px",
}} }}
> >
<List sx={{ width: "100%", maxWidth: 360 }}> {isLoaded && (
<ListItem <List sx={{ width: "100%", maxWidth: 360 }}>
// secondaryAction={ <ListItem
// <IconButton edge="end" aria-label="comments">
// <InfoIcon disablePadding
// sx={{ sx={{
// color: "white", marginBottom: '20px'
// }} }}
// /> >
// </IconButton> <ListItemButton
// } sx={{
disablePadding padding: "0px",
sx={{ }}
marginBottom: '20px' disableRipple
}} role={undefined}
> dense
<ListItemButton onClick={()=> {
sx={{ executeEvent("openBuyQortInfo", {})
padding: "0px", }}
}} >
disableRipple <ListItemText
role={undefined} sx={{
dense "& .MuiTypography-root": {
> fontSize: "1rem",
<ListItemText fontWeight: 400,
sx={{ },
"& .MuiTypography-root": { }}
fontSize: "13px", primary={`Have at least 6 QORT in your wallet`}
fontWeight: 400, />
}, <ListItemIcon
}} sx={{
primary={`Have at least 6 QORT in your wallet`} justifyContent: "flex-end",
/> }}
<ListItemIcon >
sx={{ <Box
justifyContent: "flex-end", sx={{
}} height: "18px",
> width: "18px",
<Box borderRadius: "50%",
sx={{ backgroundColor: checked1 ? "rgba(9, 182, 232, 1)" : "transparent",
height: "18px", outline: "1px solid rgba(9, 182, 232, 1)",
width: "18px", }}
borderRadius: "50%", />
backgroundColor: checked1 ? "rgba(9, 182, 232, 1)" : "transparent", {/* <Checkbox
outline: "1px solid rgba(9, 182, 232, 1)", edge="start"
}} checked={checked1}
/> tabIndex={-1}
{/* <Checkbox disableRipple
edge="start" disabled={true}
checked={checked1} sx={{
tabIndex={-1} "&.Mui-checked": {
disableRipple color: "white", // Customize the color when checked
disabled={true} },
sx={{ "& .MuiSvgIcon-root": {
"&.Mui-checked": { color: "white",
color: "white", // Customize the color when checked },
}, }}
"& .MuiSvgIcon-root": { /> */}
color: "white", </ListItemIcon>
}, </ListItemButton>
}} </ListItem>
/> */} <ListItem
</ListItemIcon> sx={{
</ListItemButton> marginBottom: '20px'
</ListItem> }}
<ListItem // secondaryAction={
sx={{ // <IconButton edge="end" aria-label="comments">
marginBottom: '20px' // <InfoIcon
}} // sx={{
// secondaryAction={ // color: "white",
// <IconButton edge="end" aria-label="comments"> // }}
// <InfoIcon // />
// sx={{ // </IconButton>
// color: "white", // }
// }} disablePadding
// /> >
// </IconButton> <ListItemButton sx={{
// } padding: "0px",
disablePadding }} disableRipple role={undefined} dense>
>
<ListItemButton sx={{ <ListItemText onClick={() => {
padding: "0px", executeEvent('openRegisterName', {})
}} disableRipple role={undefined} dense> }} sx={{
"& .MuiTypography-root": {
<ListItemText sx={{ fontSize: "1rem",
"& .MuiTypography-root": { fontWeight: 400,
fontSize: "13px", },
fontWeight: 400, }} primary={`Register a name`} />
}, <ListItemIcon sx={{
}} primary={`Register a name`} /> justifyContent: "flex-end",
<ListItemIcon sx={{ }}>
justifyContent: "flex-end", <Box
}}> sx={{
<Box height: "18px",
sx={{ width: "18px",
height: "18px", borderRadius: "50%",
width: "18px", backgroundColor: checked2 ? "rgba(9, 182, 232, 1)" : "transparent",
borderRadius: "50%", outline: "1px solid rgba(9, 182, 232, 1)",
backgroundColor: checked2 ? "rgba(9, 182, 232, 1)" : "transparent", }}
outline: "1px solid rgba(9, 182, 232, 1)", />
}} </ListItemIcon>
/> </ListItemButton>
</ListItemIcon> </ListItem>
</ListItemButton> {/* <ListItem
</ListItem> disablePadding
<ListItem >
// secondaryAction={ <ListItemButton sx={{
// <IconButton edge="end" aria-label="comments"> padding: "0px",
// <InfoIcon }} disableRipple role={undefined} dense>
// sx={{
// color: "white", <ListItemText sx={{
// }} "& .MuiTypography-root": {
// /> fontSize: "13px",
// </IconButton> fontWeight: 400,
// } },
disablePadding }} primary={`Join a group`} />
> <ListItemIcon sx={{
<ListItemButton sx={{ justifyContent: "flex-end",
padding: "0px", }}>
}} disableRipple role={undefined} dense> <Box
sx={{
<ListItemText sx={{ height: "18px",
"& .MuiTypography-root": { width: "18px",
fontSize: "13px", borderRadius: "50%",
fontWeight: 400, backgroundColor: checked3 ? "rgba(9, 182, 232, 1)" : "transparent",
}, outline: "1px solid rgba(9, 182, 232, 1)",
}} primary={`Join a group hub`} /> }}
<ListItemIcon sx={{ />
justifyContent: "flex-end", </ListItemIcon>
}}> </ListItemButton>
<Box </ListItem> */}
sx={{ </List>
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>
</Box> </Box>
); );

View File

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

View File

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

View File

@ -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 { getFee } from "../background";
import { fileToBase64 } from "../utils/fileReading"; import { fileToBase64 } from "../utils/fileReading";
import { LoadingButton } from "@mui/lab"; 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 [hasAvatar, setHasAvatar] = useState(false);
const [avatarFile, setAvatarFile] = useState(null); const [avatarFile, setAvatarFile] = useState(null);
const [tempAvatar, setTempAvatar] = useState(null) const [tempAvatar, setTempAvatar] = useState(null)
@ -52,10 +53,11 @@ const [isLoading, setIsLoading] = useState(false)
checkIfAvatarExists(); checkIfAvatarExists();
}, [myName]); }, [myName]);
const publishAvatar = async ()=> { const publishAvatar = async ()=> {
try { try {
const fee = await getFee('ARBITRARY') const fee = await getFee('ARBITRARY')
if(+balance < +fee.fee) throw new Error(`Publishing an Avatar requires ${fee.fee}`)
await show({ await show({
message: "Would you like to publish an avatar?" , message: "Would you like to publish an avatar?" ,
publishFee: fee.fee + ' QORT' publishFee: fee.fee + ' QORT'
@ -63,30 +65,36 @@ const [isLoading, setIsLoading] = useState(false)
setIsLoading(true); setIsLoading(true);
const avatarBase64 = await fileToBase64(avatarFile) const avatarBase64 = await fileToBase64(avatarFile)
await new Promise((res, rej) => { await new Promise((res, rej) => {
chrome?.runtime?.sendMessage( chrome?.runtime?.sendMessage(
{ {
action: "publishOnQDN", action: "publishOnQDN",
payload: { payload: {
data: avatarBase64, data: avatarBase64,
identifier: "qortal_avatar", identifier: "qortal_avatar",
service: 'THUMBNAIL' service: 'THUMBNAIL'
},
}, },
(response) => { },
(response) => {
if (!response?.error) {
res(response); if (!response?.error) {
return res(response);
} return
rej(response.error);
} }
); rej(response.error);
}); }
);
});
setAvatarFile(null); setAvatarFile(null);
setTempAvatar(`data:image/webp;base64,${avatarBase64}`) setTempAvatar(`data:image/webp;base64,${avatarBase64}`)
handleClose() handleClose()
} catch (error) { } catch (error) {
if (error?.message) {
setOpenSnack(true)
setInfoSnack({
type: "error",
message: error?.message,
});
}
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -115,7 +123,7 @@ const [isLoading, setIsLoading] = useState(false)
change avatar change avatar
</Typography> </Typography>
</ButtonBase> </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 change avatar
</Typography> </Typography>
</ButtonBase> </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 set avatar
</Typography> </Typography>
</ButtonBase> </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 ( return (
<Popover <Popover
id={id} id={id}
@ -196,8 +204,21 @@ const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose
</ImageUploader> </ImageUploader>
{avatarFile?.name} {avatarFile?.name}
<Spacer height="25px" /> <Spacer height="25px" />
{!myName && (
<LoadingButton loading={isLoading} disabled={!avatarFile} onClick={publishAvatar} variant="contained"> <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 Publish avatar
</LoadingButton> </LoadingButton>
</Box> </Box>

View File

@ -3,7 +3,7 @@ import QMailLogo from '../assets/QMailLogo.png'
import { useRecoilState } from 'recoil' import { useRecoilState } from 'recoil'
import { mailsAtom, qMailLastEnteredTimestampAtom } from '../atoms/global' import { mailsAtom, qMailLastEnteredTimestampAtom } from '../atoms/global'
import { isLessThanOneWeekOld } from './Group/QMailMessages' import { isLessThanOneWeekOld } from './Group/QMailMessages'
import { ButtonBase } from '@mui/material' import { ButtonBase, Tooltip } from '@mui/material'
import { executeEvent } from '../utils/events' import { executeEvent } from '../utils/events'
export const QMailStatus = () => { export const QMailStatus = () => {
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom) const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom)
@ -35,9 +35,28 @@ export const QMailStatus = () => {
borderRadius: '50%', borderRadius: '50%',
outline: '1px solid white' outline: '1px solid white'
}} /> }} />
)}<img style={{ )}
width: '24px', <Tooltip
height: 'auto' title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>Q-MAIL</span>}
}} src={QMailLogo} /></ButtonBase> 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 if(!open) return null
return ( return (
<div> <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 <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 React, { useContext, useEffect, useState } from 'react';
import { Popover, Button, Box } from '@mui/material'; import { Popover, Button, Box, CircularProgress } from '@mui/material';
import { executeEvent } from '../utils/events'; import { executeEvent } from '../utils/events';
import { BlockedUsersModal } from './Group/BlockedUsersModal';
import { MyContext } from '../App';
export const WrapperUserAction = ({ children, address, name, disabled }) => { export const WrapperUserAction = ({ children, address, name, disabled }) => {
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
@ -46,6 +48,7 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => {
</Box> </Box>
{/* Popover */} {/* Popover */}
{open && (
<Popover <Popover
id={id} id={id}
open={open} open={open}
@ -119,8 +122,81 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => {
> >
Copy address Copy address
</Button> </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> </Box>
</Popover> </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 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; export default theme;

View File

@ -1,5 +1,5 @@
import { banFromGroup, gateways, getApiKeyFromStorage } from "./background"; 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 = [ const listOfAllQortalRequests = [
'GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS',
@ -756,6 +756,20 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
}); });
break; 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; return true;

View File

@ -657,7 +657,7 @@ export const decryptData = async (data) => {
export const getListItems = async (data, isFromExtension) => { export const getListItems = async (data, isFromExtension) => {
const isGateway = await isRunningGateway() const isGateway = await isRunningGateway()
if(isGateway){ 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 requiredFields = ["list_name"];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -711,7 +711,7 @@ export const getListItems = async (data, isFromExtension) => {
export const addListItems = async (data, isFromExtension) => { export const addListItems = async (data, isFromExtension) => {
const isGateway = await isRunningGateway() const isGateway = await isRunningGateway()
if(isGateway){ 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 requiredFields = ["list_name", "items"];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -766,7 +766,7 @@ export const addListItems = async (data, isFromExtension) => {
export const deleteListItems = async (data, isFromExtension) => { export const deleteListItems = async (data, isFromExtension) => {
const isGateway = await isRunningGateway() const isGateway = await isRunningGateway()
if(isGateway){ 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 requiredFields = ["list_name"];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -2280,7 +2280,7 @@ export const getTxActivitySummary = async (data) => {
export const updateForeignFee = async (data) => { export const updateForeignFee = async (data) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { 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 requiredFields = ['coin', 'type', 'value'];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -2379,7 +2379,7 @@ export const getTxActivitySummary = async (data) => {
export const setCurrentForeignServer = async (data) => { export const setCurrentForeignServer = async (data) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { 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 requiredFields = ['coin'];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -2440,7 +2440,7 @@ export const getTxActivitySummary = async (data) => {
export const addForeignServer = async (data) => { export const addForeignServer = async (data) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { 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 requiredFields = ['coin'];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -2500,7 +2500,7 @@ export const getTxActivitySummary = async (data) => {
export const removeForeignServer = async (data) => { export const removeForeignServer = async (data) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { 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 requiredFields = ['coin'];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -3053,7 +3053,7 @@ const crosschainAtInfo = await Promise.all(atPromises);
}, 0) }, 0)
)} )}
${` ${crosschainAtInfo?.[0]?.foreignBlockchain}`}`, ${` ${crosschainAtInfo?.[0]?.foreignBlockchain}`}`,
highlightedText: `Is using gateway: ${isGateway}`, highlightedText: `Is using public node: ${isGateway}`,
fee: '', fee: '',
foreignFee: `${sellerForeignFee[foreignBlockchain].value} ${sellerForeignFee[foreignBlockchain].ticker}` foreignFee: `${sellerForeignFee[foreignBlockchain].value} ${sellerForeignFee[foreignBlockchain].ticker}`
}, isFromExtension); }, isFromExtension);
@ -3224,13 +3224,15 @@ export const createSellOrder = async (data, isFromExtension) => {
throw new Error(errorMsg); throw new Error(errorMsg);
} }
const parsedForeignAmount = Number(data.foreignAmount)?.toFixed(8)
const receivingAddress = await getUserWalletFunc(data.foreignBlockchain) const receivingAddress = await getUserWalletFunc(data.foreignBlockchain)
try { try {
const resPermission = await getUserPermission({ const resPermission = await getUserPermission({
text1: "Do you give this application permission to perform a sell order?", text1: "Do you give this application permission to perform a sell order?",
text2: `${data.qortAmount}${" "} text2: `${data.qortAmount}${" "}
${`QORT`}`, ${`QORT`}`,
text3: `FOR ${data.foreignAmount} ${data.foreignBlockchain}`, text3: `FOR ${parsedForeignAmount} ${data.foreignBlockchain}`,
fee: '0.02' fee: '0.02'
}, isFromExtension); }, isFromExtension);
const { accepted } = resPermission; const { accepted } = resPermission;
@ -3247,12 +3249,12 @@ const receivingAddress = await getUserWalletFunc(data.foreignBlockchain)
}; };
const response = await tradeBotCreateRequest({ const response = await tradeBotCreateRequest({
creatorPublicKey: userPublicKey, creatorPublicKey: userPublicKey,
qortAmount: parseFloat(data.qortAmount), qortAmount: parseFloat(data.qortAmount),
fundingQortAmount: parseFloat(data.qortAmount) + 0.001, fundingQortAmount: parseFloat(data.qortAmount) + 0.01,
foreignBlockchain: data.foreignBlockchain, foreignBlockchain: data.foreignBlockchain,
foreignAmount: parseFloat(data.foreignAmount), foreignAmount: parseFloat(parsedForeignAmount),
tradeTimeout: 120, tradeTimeout: 120,
receivingAddress: receivingAddress.address receivingAddress: receivingAddress.address
}, keyPair) }, keyPair)
return response return response
@ -3353,7 +3355,7 @@ export const adminAction = async (data, isFromExtension) => {
} }
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { 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 = ""; let apiEndpoint = "";
@ -3769,7 +3771,7 @@ url
export const getHostedData = async (data, isFromExtension) => { export const getHostedData = async (data, isFromExtension) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { 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( const resPermission = await getUserPermission(
{ {
@ -3805,7 +3807,7 @@ export const getHostedData = async (data, isFromExtension) => {
export const deleteHostedData = async (data, isFromExtension) => { export const deleteHostedData = async (data, isFromExtension) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { 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 requiredFields = ["hostedData"];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -4378,4 +4380,97 @@ export const createGroupRequest = async (data, isFromExtension) => {
} else { } else {
throw new Error("User declined request"); 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) { } else if (elapsedTime < 1440) {
return `${Math.floor(elapsedTime / 60)}h ago` return `${Math.floor(elapsedTime / 60)}h ago`
} else { } else {
return timestampMoment.format('MMM D') return timestampMoment.format('MMM D, YYYY')
} }
} }
export function formatTimestampForum(timestamp: number): string { export function formatTimestampForum(timestamp: number): string {