homepage, block, registername, userlookup, tripple tap

This commit is contained in:
PhilReact 2025-03-04 23:39:33 +02:00
parent 3a39bd5e22
commit 74cdd3e34d
46 changed files with 5307 additions and 1603 deletions

View File

@ -117,9 +117,13 @@ import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil";
import { import {
canSaveSettingToQdnAtom, canSaveSettingToQdnAtom,
fullScreenAtom, fullScreenAtom,
groupsPropertiesAtom,
hasSettingsChangedAtom, hasSettingsChangedAtom,
isUsingImportExportSettingsAtom, isUsingImportExportSettingsAtom,
lastEnteredGroupIdAtom,
mailsAtom,
oldPinnedAppsAtom, oldPinnedAppsAtom,
qMailLastEnteredTimestampAtom,
settingsLocalLastUpdatedAtom, settingsLocalLastUpdatedAtom,
settingsQDNLastUpdatedAtom, settingsQDNLastUpdatedAtom,
sortablePinnedAppsAtom, sortablePinnedAppsAtom,
@ -137,6 +141,10 @@ import { useHandleUserInfo } from "./components/Group/useHandleUserInfo";
import { Minting } from "./components/Minting/Minting"; import { Minting } from "./components/Minting/Minting";
import { isRunningGateway } from "./qortalRequests"; import { isRunningGateway } from "./qortalRequests";
import { GlobalActions } from "./components/GlobalActions/GlobalActions"; import { GlobalActions } from "./components/GlobalActions/GlobalActions";
import { useBlockedAddresses } from "./components/Chat/useBlockUsers";
import { UserLookup } from "./components/UserLookup.tsx/UserLookup";
import { RegisterName } from "./components/RegisterName";
import { BuyQortInformation } from "./components/BuyQortInformation";
type extStates = type extStates =
@ -381,6 +389,8 @@ function App() {
const [requestBuyOrder, setRequestBuyOrder] = useState<any>(null); const [requestBuyOrder, setRequestBuyOrder] = useState<any>(null);
const [authenticatedMode, setAuthenticatedMode] = useState("qort"); const [authenticatedMode, setAuthenticatedMode] = useState("qort");
const [requestAuthentication, setRequestAuthentication] = useState<any>(null); const [requestAuthentication, setRequestAuthentication] = useState<any>(null);
const [isOpenDrawerLookup, setIsOpenDrawerLookup] = useState(false)
const [userInfo, setUserInfo] = useState<any>(null); const [userInfo, setUserInfo] = useState<any>(null);
const [balance, setBalance] = useState<any>(null); const [balance, setBalance] = useState<any>(null);
const [ltcBalance, setLtcBalance] = useState<any>(null); const [ltcBalance, setLtcBalance] = useState<any>(null);
@ -418,6 +428,9 @@ function App() {
const holdRefExtState = useRef<extStates>("not-authenticated"); const holdRefExtState = useRef<extStates>("not-authenticated");
const isFocusedRef = useRef<boolean>(true); const isFocusedRef = useRef<boolean>(true);
const { isShow, onCancel, onOk, show, message } = useModal(); const { isShow, onCancel, onOk, show, message } = useModal();
const {isUserBlocked,
addToBlockList,
removeBlockFromList, getAllBlockedUsers} = useBlockedAddresses()
const { const {
isShow: isShowUnsavedChanges, isShow: isShowUnsavedChanges,
onCancel: onCancelUnsavedChanges, onCancel: onCancelUnsavedChanges,
@ -473,6 +486,9 @@ function App() {
const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom); const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom);
const {getIndividualUserInfo} = useHandleUserInfo() const {getIndividualUserInfo} = useHandleUserInfo()
const balanceSetIntervalRef = useRef(null)
const { toggleFullScreen } = useAppFullScreen(setFullScreen); const { toggleFullScreen } = useAppFullScreen(setFullScreen);
const generatorRef = useRef(null) const generatorRef = useRef(null)
const exportSeedphrase = async ()=> { const exportSeedphrase = async ()=> {
@ -529,7 +545,10 @@ function App() {
); );
const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom); const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom);
const resetAtomIsUsingImportExportSettingsAtom = useResetRecoilState(isUsingImportExportSettingsAtom) const resetAtomIsUsingImportExportSettingsAtom = useResetRecoilState(isUsingImportExportSettingsAtom)
const resetGroupPropertiesAtom = useResetRecoilState(groupsPropertiesAtom)
const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom)
const resetAtomMailsAtom = useResetRecoilState(mailsAtom)
const resetLastEnteredGroupIdAtom = useResetRecoilState(lastEnteredGroupIdAtom)
const resetAllRecoil = () => { const resetAllRecoil = () => {
resetAtomSortablePinnedAppsAtom(); resetAtomSortablePinnedAppsAtom();
resetAtomCanSaveSettingToQdnAtom(); resetAtomCanSaveSettingToQdnAtom();
@ -537,6 +556,10 @@ function App() {
resetAtomSettingsLocalLastUpdatedAtom(); resetAtomSettingsLocalLastUpdatedAtom();
resetAtomOldPinnedAppsAtom(); resetAtomOldPinnedAppsAtom();
resetAtomIsUsingImportExportSettingsAtom(); resetAtomIsUsingImportExportSettingsAtom();
resetAtomQMailLastEnteredTimestampAtom()
resetAtomMailsAtom()
resetGroupPropertiesAtom()
resetLastEnteredGroupIdAtom()
}; };
useEffect(() => { useEffect(() => {
if (!isMobile) return; if (!isMobile) return;
@ -763,6 +786,30 @@ function App() {
}; };
}; };
const balanceSetInterval = ()=> {
try {
if(balanceSetIntervalRef?.current){
clearInterval(balanceSetIntervalRef?.current);
}
let isCalling = false;
balanceSetIntervalRef.current = setInterval(async () => {
if (isCalling) return;
isCalling = true;
chrome?.runtime?.sendMessage({ action: "balance" }, (response) => {
if (!response?.error && !isNaN(+response)) {
setBalance(response);
}
isCalling = false
});
}, 40000);
} catch (error) {
console.error(error)
}
}
const getBalanceFunc = () => { const getBalanceFunc = () => {
setQortBalanceLoading(true); setQortBalanceLoading(true);
window window
@ -772,6 +819,7 @@ function App() {
setBalance(response); setBalance(response);
} }
setQortBalanceLoading(false); setQortBalanceLoading(false);
balanceSetInterval()
}) })
.catch((error) => { .catch((error) => {
console.error("Failed to get balance:", error); console.error("Failed to get balance:", error);
@ -1188,6 +1236,9 @@ function App() {
resetAllRecoil(); resetAllRecoil();
setShowSeed(false) setShowSeed(false)
setCreationStep(1) setCreationStep(1)
if(balanceSetIntervalRef?.current){
clearInterval(balanceSetIntervalRef?.current);
}
}; };
function roundUpToDecimals(number, decimals = 8) { function roundUpToDecimals(number, decimals = 8) {
@ -1358,6 +1409,18 @@ function App() {
}; };
}, []); }, []);
const openUserProfile = (e) => {
setIsOpenDrawerProfile(true);
};
useEffect(() => {
subscribeToEvent("openUserProfile", openUserProfile);
return () => {
unsubscribeFromEvent("openUserProfile", openUserProfile);
};
}, []);
const openGlobalSnackBarFunc = (e) => { const openGlobalSnackBarFunc = (e) => {
const message = e.detail?.message; const message = e.detail?.message;
const type = e.detail?.type; const type = e.detail?.type;
@ -1595,7 +1658,7 @@ function App() {
textDecoration: "underline", textDecoration: "underline",
}} }}
onClick={() => { onClick={() => {
setOpenRegisterName(true); executeEvent('openRegisterName', {})
}} }}
> >
REGISTER NAME REGISTER NAME
@ -1784,7 +1847,11 @@ function App() {
setInfoSnackCustom: setInfoSnack, setInfoSnackCustom: setInfoSnack,
userInfo: userInfo, userInfo: userInfo,
downloadResource, downloadResource,
getIndividualUserInfo getIndividualUserInfo,
isUserBlocked,
addToBlockList,
removeBlockFromList,
getAllBlockedUsers
}} }}
> >
<Box <Box
@ -1835,7 +1902,7 @@ function App() {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
zIndex: 6, zIndex: 10000,
}} }}
> >
<Spacer height="22px" /> <Spacer height="22px" />
@ -2885,7 +2952,7 @@ await showInfo({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
zIndex: 6, zIndex: 10000,
}} }}
> >
<Spacer height="48px" /> <Spacer height="48px" />
@ -2985,6 +3052,9 @@ await showInfo({
open={isShow} open={isShow}
aria-labelledby="alert-dialog-title" aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
sx={{
zIndex: 10001
}}
> >
<DialogTitle id="alert-dialog-title">{message.paymentFee ? "Payment" : "Publish"}</DialogTitle> <DialogTitle id="alert-dialog-title">{message.paymentFee ? "Payment" : "Publish"}</DialogTitle>
<DialogContent> <DialogContent>
@ -3059,7 +3129,7 @@ await showInfo({
aria-labelledby="alert-dialog-title" aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
> >
<DialogTitle id="alert-dialog-title">{"Warning"}</DialogTitle> <DialogTitle id="alert-dialog-title">{"LOGOUT"}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText id="alert-dialog-description"> <DialogContentText id="alert-dialog-description">
{messageUnsavedChanges.message} {messageUnsavedChanges.message}
@ -3409,6 +3479,9 @@ await showInfo({
> >
{renderProfile()} {renderProfile()}
</DrawerComponent> </DrawerComponent>
<UserLookup isOpenDrawerLookup={isOpenDrawerLookup} setIsOpenDrawerLookup={setIsOpenDrawerLookup} />
<RegisterName balance={balance} show={show} setTxList={setTxList} userInfo={userInfo} setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack}/>
<BuyQortInformation balance={balance} />
</GlobalContext.Provider> </GlobalContext.Provider>
{extState === "create-wallet" && walletToBeDownloaded && ( {extState === "create-wallet" && walletToBeDownloaded && (
<ButtonBase onClick={()=> { <ButtonBase onClick={()=> {

View File

@ -12,41 +12,46 @@ import {
DialogTitle, DialogTitle,
FormControlLabel, FormControlLabel,
Input, Input,
styled,
Switch, Switch,
Tooltip,
Typography, 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, gateways, 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';
export const manifestData = { export const manifestData = {
version: "0.5.2", version: "0.5.2",
}; };
function removeTrailingSlash(url) { function removeTrailingSlash(url) {
return url.replace(/\/+$/, ''); return url.replace(/\/+$/, '');
} }
export const NotAuthenticated = ({ export const NotAuthenticated = ({
getRootProps, getRootProps,
getInputProps, getInputProps,
setExtstate, setExtstate,
currentNode,
setCurrentNode,
useLocalNode,
setUseLocalNode,
apiKey, apiKey,
setApiKey, setApiKey,
globalApiKey, globalApiKey,
handleSetGlobalApikey, handleSetGlobalApikey,
handleFilePick, 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);
@ -59,14 +64,14 @@ export const NotAuthenticated = ({
// 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 { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext);
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 { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext);
const importedApiKeyRef = useRef(null); const importedApiKeyRef = useRef(null);
const currentNodeRef = useRef(null); const currentNodeRef = useRef(null);
const hasLocalNodeRef = useRef(null); const hasLocalNodeRef = useRef(null);
@ -106,6 +111,7 @@ export const NotAuthenticated = ({
}) })
} }
}; };
reader.readAsText(file); // Read the file as text reader.readAsText(file); // Read the file as text
} }
@ -123,12 +129,14 @@ 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; return true
} }
return false; return false
} catch (error) { } catch (error) {
return false; return false
}
}
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -141,12 +149,16 @@ export const NotAuthenticated = ({
.then((response) => { .then((response) => {
setCustomNodes(response || []); setCustomNodes(response || []);
if(window?.electronAPI?.setAllowedDomains){
window.electronAPI.setAllowedDomains(response?.map((node)=> node.url))
}
if(Array.isArray(response)){ if(Array.isArray(response)){
const findLocal = response?.find((item)=> item?.url === 'http://127.0.0.1:12391') const findLocal = response?.find((item)=> item?.url === 'http://127.0.0.1:12391')
if(findLocal && findLocal?.apikey){ if(findLocal && findLocal?.apikey){
setImportedApiKey(findLocal?.apikey) setImportedApiKey(findLocal?.apikey)
} }
} }
}) })
.catch((error) => { .catch((error) => {
console.error( console.error(
@ -167,37 +179,54 @@ export const NotAuthenticated = ({
hasLocalNodeRef.current = hasLocalNode; hasLocalNodeRef.current = hasLocalNode;
}, [hasLocalNode]); }, [hasLocalNode]);
const validateApiKey = useCallback(async (key, fromStartUp) => { const validateApiKey = useCallback(async (key, fromStartUp) => {
try { try {
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(fromStartUp && key?.url && key?.apikey && !isLocalKey && !gateways.some(gateway => apiKey?.url?.includes(gateway))){ if (fromStartUp && key?.url && key?.apikey && !isLocalKey && !gateways.some(gateway => key?.url?.includes(gateway))) {
setCurrentNode({ setCurrentNode({
url: key?.url, url: key?.url,
apikey: key?.apikey, apikey: key?.apikey,
}); });
const url = `${key?.url}/admin/apikey/test`;
const response = await fetch(url, { let isValid = false
method: "GET",
headers: {
accept: "text/plain", const url = `${key?.url}/admin/settings/localAuthBypassEnabled`;
"X-API-KEY": key?.apikey, // Include the API key here const response = await fetch(url);
},
});
// 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'){
setIsValidApiKey(true); isValid = true
setUseLocalNode(true); } else {
return 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
}
} }
if (isValid) {
setIsValidApiKey(true);
setUseLocalNode(true);
return
}
} }
if (!currentNodeRef.current) return; if (!currentNodeRef.current) return;
const stillHasLocal = await checkIfUserHasLocalNode(); const stillHasLocal = await checkIfUserHasLocalNode()
if (isLocalKey && !stillHasLocal && !fromStartUp) { if (isLocalKey && !stillHasLocal && !fromStartUp) {
throw new Error("Please turn on your local node"); throw new Error("Please turn on your local node");
} }
//check custom nodes
// !gateways.some(gateway => apiKey?.url?.includes(gateway))
const isCurrentNodeLocal = const isCurrentNodeLocal =
cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391"; cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391";
if (isLocalKey && !isCurrentNodeLocal) { if (isLocalKey && !isCurrentNodeLocal) {
@ -215,18 +244,29 @@ export const NotAuthenticated = ({
} 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) {
window window
.sendMessage("setApiKey", payload) .sendMessage("setApiKey", payload)
.then((response) => { .then((response) => {
@ -248,21 +288,24 @@ export const NotAuthenticated = ({
} 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){ if (fromStartUp) {
setCurrentNode({ setCurrentNode({
url: "http://127.0.0.1:12391", url: "http://127.0.0.1:12391",
}); });
window window
.sendMessage("setApiKey", null) .sendMessage("setApiKey", "isGateway")
.then((response) => { .then((response) => {
if (response) { if (response) {
setApiKey(null); setApiKey(null);
@ -277,11 +320,13 @@ export const NotAuthenticated = ({
}); });
return 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);
} }
}, []); }, []);
@ -295,15 +340,14 @@ 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: removeTrailingSlash(url), url: removeTrailingSlash(url),
apikey: customApikey, apikey: customApikey,
}); });
} else if (url && customApikey) { } else if (!isFullListOfNodes && url) {
nodes.push({ nodes.push({
url: removeTrailingSlash(url), url: removeTrailingSlash(url),
apikey: customApikey, apikey: customApikey,
@ -311,6 +355,7 @@ export const NotAuthenticated = ({
} }
setCustomNodes(nodes); setCustomNodes(nodes);
setCustomNodeToSaveIndex(null); setCustomNodeToSaveIndex(null);
if (!nodes) return; if (!nodes) return;
window window
@ -318,8 +363,11 @@ export const NotAuthenticated = ({
.then((response) => { .then((response) => {
if (response) { if (response) {
setMode("list"); setMode("list");
setUrl("http://"); setUrl("https://");
setCustomApiKey(""); setCustomApiKey("");
if(window?.electronAPI?.setAllowedDomains){
window.electronAPI.setAllowedDomains(nodes?.map((node) => node.url))
}
// add alert if needed // add alert if needed
} }
}) })
@ -351,13 +399,12 @@ export const NotAuthenticated = ({
fontSize: '16px' fontSize: '16px'
}} }}
> >
WELCOME TO <TextItalic sx={{ WELCOME TO
fontSize: '18px'
}}>YOUR</TextItalic> <br></br>
<TextSpan sx={{ <TextSpan sx={{
fontSize: '18px' fontSize: '16px'
}}> QORTAL WALLET</TextSpan> }}> QORTAL</TextSpan>
</TextP> </TextP>
<Spacer height="30px" /> <Spacer height="30px" />
<Box <Box
sx={{ sx={{
@ -366,9 +413,15 @@ export const NotAuthenticated = ({
alignItems: "center", alignItems: "center",
}} }}
> >
<CustomButton onClick={() => setExtstate("wallets")}>
Wallets <CustomButton onClick={()=> setExtstate('wallets')}>
{/* <input {...getInputProps()} /> */}
Accounts
</CustomButton> </CustomButton>
{/* <Tooltip title="Authenticate by importing your Qortal JSON file" arrow>
<img src={Info} />
</Tooltip> */}
</Box> </Box>
<Spacer height="6px" /> <Spacer height="6px" />
@ -377,8 +430,10 @@ export const NotAuthenticated = ({
display: "flex", display: "flex",
gap: "10px", gap: "10px",
alignItems: "center", alignItems: "center",
}} }}
> >
<CustomButton <CustomButton
onClick={() => { onClick={() => {
setExtstate("create-wallet"); setExtstate("create-wallet");
@ -392,8 +447,10 @@ export const NotAuthenticated = ({
} }
}} }}
> >
Create wallet Create account
</CustomButton> </CustomButton>
</Box> </Box>
<Spacer height="15px" /> <Spacer height="15px" />
@ -432,6 +489,12 @@ export const NotAuthenticated = ({
}} }}
> >
<FormControlLabel <FormControlLabel
sx={{
"& .MuiFormControlLabel-label": {
fontSize: '14px'
}
}}
control={ control={
<Switch <Switch
sx={{ sx={{
@ -677,7 +740,7 @@ export const NotAuthenticated = ({
...(customNodes || []), ...(customNodes || []),
].filter((item) => item?.url !== node?.url); ].filter((item) => item?.url !== node?.url);
saveCustomNodes(nodesToSave); saveCustomNodes(nodesToSave, true);
}} }}
variant="contained" variant="contained"
> >
@ -750,7 +813,7 @@ export const NotAuthenticated = ({
<Button <Button
variant="contained" variant="contained"
disabled={!customApikey || !url} disabled={!url}
onClick={() => saveCustomNodes(customNodes)} onClick={() => saveCustomNodes(customNodes)}
autoFocus autoFocus
> >
@ -761,8 +824,8 @@ 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',
bottom: '25px', bottom: '25px',

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -157,4 +157,24 @@ export const addressInfoKeySelector = selectorFamily({
const userInfo = get(addressInfoControllerAtom); const userInfo = get(addressInfoControllerAtom);
return userInfo[key] || null; // Return the value for the key or null if not found return userInfo[key] || null; // Return the value for the key or null if not found
}, },
});
export const groupsPropertiesAtom = atom({
key: 'groupsPropertiesAtom',
default: {},
});
export const qMailLastEnteredTimestampAtom = atom({
key: 'qMailLastEnteredTimestampAtom',
default: null,
});
export const mailsAtom = atom({
key: 'mailsAtom',
default: [],
});
export const lastEnteredGroupIdAtom = atom({
key: 'lastEnteredGroupIdAtom',
default: null,
}); });

File diff suppressed because it is too large Load Diff

View File

@ -102,6 +102,7 @@ import {
createRewardShareCase, createRewardShareCase,
getRewardSharePrivateKeyCase, getRewardSharePrivateKeyCase,
removeRewardShareCase, removeRewardShareCase,
listActionsCase,
} from "./background-cases"; } from "./background-cases";
import { getData, removeKeysAndLogout, storeData } from "./utils/chromeStorage"; import { getData, removeKeysAndLogout, storeData } from "./utils/chromeStorage";
import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch'; import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch';
@ -778,7 +779,7 @@ export async function getNameInfoForOthers(address) {
} }
} }
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();
@ -3008,6 +3009,9 @@ function setupMessageListener() {
case "getEnteredQmailTimestamp": case "getEnteredQmailTimestamp":
getEnteredQmailTimestampCase(request, event); getEnteredQmailTimestampCase(request, event);
break; break;
case "listActions":
listActionsCase(request, event);
break;
case "logout": case "logout":
{ {
try { try {

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,25 +11,48 @@ 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();
const {path, history, changeCurrentIndex} = useQortalMessageListener(frameWindow, iframeRef, app?.tabId, app?.name, app?.service) const {path, history, changeCurrentIndex, resetHistory} = useQortalMessageListener(frameWindow, iframeRef, app?.tabId, isDevMode, app?.name, app?.service)
const [url, setUrl] = useState('') const [url, setUrl] = useState('')
useEffect(()=> { useEffect(()=> {
if(app?.isPreview) return
if(isDevMode){
setUrl(app?.url)
return
}
setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? `/${app?.path}` : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}`) setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? `/${app?.path}` : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}`)
}, [app?.service, app?.name, app?.identifier, app?.path]) }, [app?.service, app?.name, app?.identifier, app?.path, app?.isPreview])
useEffect(()=> {
if(app?.isPreview && app?.url){
resetHistory()
setUrl(app.url)
}
}, [app?.url, app?.isPreview])
const defaultUrl = useMemo(()=> { const defaultUrl = useMemo(()=> {
return url return url
}, [url]) }, [url, isDevMode])
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)
} }
@ -41,7 +64,7 @@ export const AppViewer = React.forwardRef(({ app , hide}, iframeRef) => {
return () => { return () => {
unsubscribeFromEvent("refreshApp", refreshAppFunc); unsubscribeFromEvent("refreshApp", refreshAppFunc);
}; };
}, [app, path]); }, [app, path, isDevMode]);
const removeTrailingSlash = (str) => str.replace(/\/$/, ''); const removeTrailingSlash = (str) => str.replace(/\/$/, '');
const copyLinkFunc = (e) => { const copyLinkFunc = (e) => {
@ -81,7 +104,7 @@ export const AppViewer = React.forwardRef(({ app , hide}, iframeRef) => {
const targetOrigin = iframeRef.current ? new URL(iframeRef.current.src).origin : "*"; const targetOrigin = iframeRef.current ? new URL(iframeRef.current.src).origin : "*";
// Signal non-manual navigation // Signal non-manual navigation
iframeRef.current.contentWindow.postMessage( iframeRef.current.contentWindow.postMessage(
{ action: 'PERFORMING_NON_MANUAL', currentIndex: previousPageIndex }, targetOrigin { action: 'PERFORMING_NON_MANUAL', currentIndex: previousPageIndex },targetOrigin
); );
// Update the current index locally // Update the current index locally
changeCurrentIndex(previousPageIndex); changeCurrentIndex(previousPageIndex);
@ -113,7 +136,10 @@ export const AppViewer = React.forwardRef(({ app , hide}, iframeRef) => {
try { try {
await navigationPromise; await navigationPromise;
} catch (error) { } catch (error) {
if(isDevMode){
setUrl(`${url}${previousPath != null ? previousPath : ''}?theme=dark&time=${new Date().getMilliseconds()}&isManualNavigation=false`)
return
}
setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${previousPath != null ? previousPath : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}&time=${new Date().getMilliseconds()}&isManualNavigation=false`) setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${previousPath != null ? previousPath : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}&time=${new Date().getMilliseconds()}&isManualNavigation=false`)
// iframeRef.current.contentWindow.location.href = previousPath; // Fallback URL update // iframeRef.current.contentWindow.location.href = previousPath; // Fallback URL update
} }
@ -140,8 +166,9 @@ export const AppViewer = React.forwardRef(({ app , hide}, iframeRef) => {
// Function to navigate back in iframe // Function to navigate back in iframe
const navigateForwardInIframe = async () => { const navigateForwardInIframe = async () => {
const targetOrigin = iframeRef.current ? new URL(iframeRef.current.src).origin : "*";
if (iframeRef.current && iframeRef.current.contentWindow) { if (iframeRef.current && iframeRef.current.contentWindow) {
const targetOrigin = iframeRef.current ? new URL(iframeRef.current.src).origin : "*";
iframeRef.current.contentWindow.postMessage( iframeRef.current.contentWindow.postMessage(
{ action: 'NAVIGATE_FORWARD'}, { action: 'NAVIGATE_FORWARD'},
targetOrigin targetOrigin
@ -162,7 +189,8 @@ export const AppViewer = React.forwardRef(({ app , hide}, iframeRef) => {
height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px )`, height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px )`,
border: 'none', border: 'none',
width: '100%' width: '100%'
}} id="browser-iframe" src={defaultUrl} sandbox="allow-scripts allow-same-origin allow-forms allow-modals" allow="fullscreen; clipboard-read; clipboard-write"> }} id="browser-iframe" src={defaultUrl} sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals"
allow="fullscreen; clipboard-read; clipboard-write">
</iframe> </iframe>
</Box> </Box>

View File

@ -3,7 +3,7 @@ import { AppViewer } from './AppViewer';
import Frame from 'react-frame-component'; import Frame from 'react-frame-component';
import { MyContext, isMobile } from '../../App'; import { MyContext, isMobile } from '../../App';
const AppViewerContainer = React.forwardRef(({ app, isSelected, hide, customHeight }, ref) => { const AppViewerContainer = React.forwardRef(({ app, isSelected, hide, customHeight, isDevMode }, ref) => {
const { rootHeight } = useContext(MyContext); const { rootHeight } = useContext(MyContext);
@ -42,7 +42,7 @@ const AppViewerContainer = React.forwardRef(({ app, isSelected, hide, customHeig
overflow: 'hidden', overflow: 'hidden',
}} }}
> >
<AppViewer app={app} ref={ref} hide={!isSelected || hide} /> <AppViewer app={app} ref={ref} hide={!isSelected || hide} isDevMode={isDevMode} />
</Frame> </Frame>
); );
}); });

View File

@ -297,7 +297,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
> >
{mode !== "viewer" && !selectedTab && <Spacer height="30px" />} {mode !== "viewer" && !selectedTab && <Spacer height="30px" />}
{mode === "home" && ( {mode === "home" && (
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
)} )}
<AppsLibrary <AppsLibrary
@ -326,6 +326,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
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}
/> />
); );
})} })}
@ -333,7 +334,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
{isNewTabWindow && mode === "viewer" && ( {isNewTabWindow && mode === "viewer" && (
<> <>
<Spacer height="30px" /> <Spacer height="30px" />
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
</> </>
)} )}
{mode !== "viewer" && !selectedTab && <Spacer height="180px" />} {mode !== "viewer" && !selectedTab && <Spacer height="180px" />}

View File

@ -18,8 +18,9 @@ import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
import { extractComponents } from "../Chat/MessageDisplay"; import { extractComponents } from "../Chat/MessageDisplay";
import HelpIcon from '@mui/icons-material/Help'; import HelpIcon from '@mui/icons-material/Help';
import { useHandleTutorials } from "../Tutorials/useHandleTutorials"; import { useHandleTutorials } from "../Tutorials/useHandleTutorials";
import { AppsPrivate } from "./AppsPrivate";
export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps }) => { export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps, myName }) => {
const [qortalUrl, setQortalUrl] = useState('') const [qortalUrl, setQortalUrl] = useState('')
const { showTutorial } = useContext(GlobalContext); const { showTutorial } = useContext(GlobalContext);
@ -145,7 +146,8 @@ export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps }) => {
<AppCircleLabel>Library</AppCircleLabel> <AppCircleLabel>Library</AppCircleLabel>
</AppCircleContainer> </AppCircleContainer>
</ButtonBase> </ButtonBase>
<AppsPrivate myName={myName} />
<SortablePinnedApps availableQapps={availableQapps} myWebsite={myWebsite} myApp={myApp} /> <SortablePinnedApps availableQapps={availableQapps} myWebsite={myWebsite} myApp={myApp} />
</AppsContainer> </AppsContainer>

View File

@ -132,10 +132,20 @@ export const AppsNavBar = ({appsMode}) => {
}; };
}, []); }, []);
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>
<AppsNavBarLeft> <AppsNavBarLeft>
@ -259,27 +269,54 @@ export const AppsNavBar = ({appsMode}) => {
onClick={() => { onClick={() => {
if (!selectedTab) return; if (!selectedTab) return;
setSortablePinnedApps((prev) => { setSortablePinnedApps((prev) => {
let updatedApps; let updatedApps;
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(
@ -320,9 +357,15 @@ export const AppsNavBar = ({appsMode}) => {
</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();
}} }}
> >
@ -350,6 +393,7 @@ export const AppsNavBar = ({appsMode}) => {
primary="Refresh" primary="Refresh"
/> />
</MenuItem> </MenuItem>
{!selectedTab?.isPrivate && (
<MenuItem <MenuItem
onClick={() => { onClick={() => {
executeEvent("copyLink", { executeEvent("copyLink", {
@ -382,6 +426,7 @@ export const AppsNavBar = ({appsMode}) => {
primary="Copy link" primary="Copy link"
/> />
</MenuItem> </MenuItem>
)}
</Menu> </Menu>
</AppsNavBarParent> </AppsNavBarParent>
); );

View File

@ -0,0 +1,550 @@
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 window.sendMessage(
"ENCRYPT_QORTAL_GROUP_DATA",
{
base64: object64,
groupId: selectedGroup,
}
);
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) => {
window
.sendMessage("publishOnQDN", {
data: decryptedData,
identifier: newPrivateAppValues?.identifier,
service: newPrivateAppValues?.service,
})
.then((response) => {
if (!response?.error) {
res(response);
return;
}
rej(response.error);
})
.catch((error) => {
rej(error.message || "An error occurred");
});
});
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,115 +1,173 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from "react";
import { DndContext, MouseSensor, closestCenter } from '@dnd-kit/core'; import { DndContext, MouseSensor, closestCenter } from "@dnd-kit/core";
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; import {
import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; arrayMove,
import { CSS } from '@dnd-kit/utilities'; SortableContext,
import { Avatar, ButtonBase } from '@mui/material'; sortableKeyboardCoordinates,
import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles'; useSortable,
import { getBaseApiReact } from '../../App'; } from "@dnd-kit/sortable";
import { executeEvent } from '../../utils/events'; import {
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; KeyboardSensor,
import { useRecoilState, useSetRecoilState } from 'recoil'; PointerSensor,
import { saveToLocalStorage } from './AppsNavBar'; TouchSensor,
import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps'; useSensor,
useSensors,
} from "@dnd-kit/core";
import { CSS } from "@dnd-kit/utilities";
import { Avatar, ButtonBase } from "@mui/material";
import { AppCircle, AppCircleContainer, AppCircleLabel } from "./Apps-styles";
import { getBaseApiReact } from "../../App";
import { executeEvent } from "../../utils/events";
import {
settingsLocalLastUpdatedAtom,
sortablePinnedAppsAtom,
} from "../../atoms/global";
import { useRecoilState, useSetRecoilState } from "recoil";
import { saveToLocalStorage } from "./AppsNavBar";
import { ContextMenuPinnedApps } from "../ContextMenuPinnedApps";
import LockIcon from "@mui/icons-material/Lock";
import { useHandlePrivateApps } from "./useHandlePrivateApps";
const SortableItem = ({ id, name, app, isDesktop }) => { const SortableItem = ({ id, name, app, isDesktop }) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); const { openApp } = useHandlePrivateApps();
const style = {
transform: CSS.Transform.toString(transform),
transition,
padding: '10px',
border: '1px solid #ccc',
marginBottom: '5px',
borderRadius: '4px',
backgroundColor: '#f9f9f9',
cursor: 'grab',
color: 'black'
};
return ( const { attributes, listeners, setNodeRef, transform, transition } =
<ContextMenuPinnedApps app={app} isMine={!!app?.isMine}> useSortable({ id });
<ButtonBase const style = {
ref={setNodeRef} {...attributes} {...listeners} transform: CSS.Transform.toString(transform),
sx={{ transition,
height: "80px", padding: "10px",
width: "60px", border: "1px solid #ccc",
transform: CSS.Transform.toString(transform), marginBottom: "5px",
transition, borderRadius: "4px",
}} backgroundColor: "#f9f9f9",
cursor: "grab",
onClick={()=> { color: "black",
executeEvent("addTab", { };
data: app
}) return (
}} <ContextMenuPinnedApps app={app} isMine={!!app?.isMine}>
> <ButtonBase
<AppCircleContainer sx={{ ref={setNodeRef}
border: "none", {...attributes}
gap: isDesktop ? '10px': '5px' {...listeners}
}}> sx={{
<AppCircle height: "80px",
sx={{ width: "60px",
border: "none", transform: CSS.Transform.toString(transform),
transition,
}}
onClick={async () => {
if (app?.isPrivate) {
try {
await openApp(app?.privateAppProperties);
} catch (error) {
console.error(error);
}
} else {
executeEvent("addTab", {
data: app,
});
}
}}
>
<AppCircleContainer
sx={{
border: "none",
gap: isDesktop ? "10px" : "5px",
}}
>
<AppCircle
sx={{
border: "none",
}}
>
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
<LockIcon
sx={{
height: "31px",
width: "31px",
}}
/>
) : (
<Avatar
sx={{
height: "31px",
width: "31px",
"& img": {
objectFit: "fill",
},
}}
alt={app?.metadata?.title || app?.name}
src={
app?.privateAppProperties?.logo
? app?.privateAppProperties?.logo
: `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`
}
>
<img
style={{
width: "31px",
height: "auto",
}} }}
> // src={LogoSelected}
<Avatar alt="center-icon"
sx={{ />
height: "31px", </Avatar>
width: "31px", )}
'& img': { </AppCircle>
objectFit: 'fill', {app?.isPrivate ? (
} <AppCircleLabel>
}} {`${app?.privateAppProperties?.appName || "Private"}`}
alt={app?.metadata?.title || app?.name} </AppCircleLabel>
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${ ) : (
app?.name <AppCircleLabel>{app?.metadata?.title || app?.name}</AppCircleLabel>
}/qortal_avatar?async=true`} )}
> </AppCircleContainer>
<img </ButtonBase>
style={{ </ContextMenuPinnedApps>
width: "31px", );
height: "auto",
}}
// src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
<AppCircleLabel>
{app?.metadata?.title || app?.name}
</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
</ContextMenuPinnedApps>
);
}; };
export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps = [] }) => { export const SortablePinnedApps = ({
isDesktop,
myWebsite,
myApp,
availableQapps = [],
}) => {
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom);
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); const setSettingsLocalLastUpdated = useSetRecoilState(
settingsLocalLastUpdatedAtom
);
const transformPinnedApps = useMemo(() => { const transformPinnedApps = useMemo(() => {
// Clone the existing pinned apps list // Clone the existing pinned apps list
let pinned = [...pinnedApps]; let pinned = [...pinnedApps];
// Function to add or update `isMine` property // Function to add or update `isMine` property
const addOrUpdateIsMine = (pinnedList, appToCheck) => { const addOrUpdateIsMine = (pinnedList, appToCheck) => {
if (!appToCheck) return pinnedList; if (!appToCheck) return pinnedList;
const existingIndex = pinnedList.findIndex( const existingIndex = pinnedList.findIndex(
(item) => item?.service === appToCheck?.service && item?.name === appToCheck?.name (item) =>
); item?.service === appToCheck?.service &&
item?.name === appToCheck?.name
if (existingIndex !== -1) { );
// If the app is already in the list, update it with `isMine: true`
pinnedList[existingIndex] = { ...pinnedList[existingIndex], isMine: true };
} else {
// If not in the list, add it with `isMine: true` at the beginning
pinnedList.unshift({ ...appToCheck, isMine: true });
}
return pinnedList; if (existingIndex !== -1) {
// If the app is already in the list, update it with `isMine: true`
pinnedList[existingIndex] = {
...pinnedList[existingIndex],
isMine: true,
};
} else {
// If not in the list, add it with `isMine: true` at the beginning
pinnedList.unshift({ ...appToCheck, isMine: true });
}
return pinnedList;
}; };
// Update or add `myWebsite` and `myApp` while preserving their positions // Update or add `myWebsite` and `myApp` while preserving their positions
@ -118,76 +176,77 @@ export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps
// Update pinned list based on availableQapps // Update pinned list based on availableQapps
pinned = pinned.map((pin) => { pinned = pinned.map((pin) => {
const findIndex = availableQapps?.findIndex( const findIndex = availableQapps?.findIndex(
(item) => item?.service === pin?.service && item?.name === pin?.name (item) => item?.service === pin?.service && item?.name === pin?.name
); );
if (findIndex !== -1) return { if (findIndex !== -1)
return {
...availableQapps[findIndex], ...availableQapps[findIndex],
...pin ...pin,
} };
return pin; return pin;
}); });
return pinned; return pinned;
}, [myApp, myWebsite, pinnedApps, availableQapps]); }, [myApp, myWebsite, pinnedApps, availableQapps]);
const sensors = useSensors( const sensors = useSensors(
useSensor(MouseSensor, { useSensor(MouseSensor, {
activationConstraint: { activationConstraint: {
distance: 10, distance: 10,
}, },
}), }),
useSensor(TouchSensor, { useSensor(TouchSensor, {
activationConstraint: { activationConstraint: {
delay: 500, // Delay in milliseconds before drag activates delay: 500, // Delay in milliseconds before drag activates
tolerance: 5, // Movement tolerance in pixels during the delay tolerance: 5, // Movement tolerance in pixels during the delay
}, },
}), }),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) })
); );
const handleDragEnd = (event) => { const handleDragEnd = (event) => {
const { active, over } = event; const { active, over } = event;
if (!over) return; if (!over) return;
if (active.id !== over.id) { if (active.id !== over.id) {
const oldIndex = transformPinnedApps.findIndex( const oldIndex = transformPinnedApps.findIndex(
(item) => `${item?.service}-${item?.name}` === active.id (item) => `${item?.service}-${item?.name}` === active.id
); );
const newIndex = transformPinnedApps.findIndex( const newIndex = transformPinnedApps.findIndex(
(item) => `${item?.service}-${item?.name}` === over.id (item) => `${item?.service}-${item?.name}` === over.id
); );
const newOrder = arrayMove(transformPinnedApps, oldIndex, newIndex); const newOrder = arrayMove(transformPinnedApps, oldIndex, newIndex);
setPinnedApps(newOrder); setPinnedApps(newOrder);
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', newOrder); saveToLocalStorage("ext_saved_settings", "sortablePinnedApps", newOrder);
setSettingsLocalLastUpdated(Date.now()); setSettingsLocalLastUpdated(Date.now());
} }
}; };
return ( return (
<DndContext <DndContext
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
>
<SortableContext
items={transformPinnedApps.map((app) => `${app?.service}-${app?.name}`)}
> >
<SortableContext items={transformPinnedApps.map((app) => `${app?.service}-${app?.name}`)}> {transformPinnedApps.map((app) => (
{transformPinnedApps.map((app) => ( <SortableItem
<SortableItem isDesktop={isDesktop}
isDesktop={isDesktop} key={`${app?.service}-${app?.name}`}
key={`${app?.service}-${app?.name}`} id={`${app?.service}-${app?.name}`}
id={`${app?.service}-${app?.name}`} name={app?.name}
name={app?.name} app={app}
app={app} />
/> ))}
))} </SortableContext>
</SortableContext> </DndContext>
</DndContext>
); );
}; };

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,237 @@
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 window.sendMessage(
"DECRYPT_QORTAL_GROUP_DATA",
{
base64: data,
groupId: privateAppProperties?.groupId,
}
);
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

@ -221,7 +221,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', 'SIGN_TRANSACTION', 'ADMIN_ACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK', 'DECRYPT_QORTAL_GROUP_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', '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', 'SIGN_TRANSACTION', 'ADMIN_ACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK', 'DECRYPT_QORTAL_GROUP_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', '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'
@ -55,6 +55,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
const editorRef = useRef(null); const editorRef = useRef(null);
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
const handleUpdateRef = useRef(null); const handleUpdateRef = useRef(null);
const {isUserBlocked} = useContext(MyContext)
const lastReadTimestamp = useRef(null) const lastReadTimestamp = useRef(null)
@ -166,10 +167,28 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
}) })
} }
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) => { 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;
@ -181,7 +200,12 @@ 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

@ -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, setMobileViewModeKeepOpen }) => { export const MessageDisplay = ({ htmlContent, isReply, setMobileViewModeKeepOpen }) => {
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();

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, ClickAwayListener, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material"; import { Avatar, Box, Button, ButtonBase, ClickAwayListener, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material";
@ -52,7 +52,7 @@ const getBadgeImg = (level)=> {
} }
} }
export const MessageItem = ({ export const MessageItem = React.memo(({
message, message,
onSeen, onSeen,
isLast, isLast,
@ -72,7 +72,6 @@ export const MessageItem = ({
setMobileViewModeKeepOpen setMobileViewModeKeepOpen
}) => { }) => {
const {getIndividualUserInfo} = useContext(MyContext) const {getIndividualUserInfo} = useContext(MyContext)
const userInfo = useRecoilValue(addressInfoKeySelector(message?.sender));
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleTooltipClose = () => { const handleTooltipClose = () => {
@ -85,32 +84,72 @@ export const MessageItem = ({
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",
@ -147,9 +186,7 @@ export const MessageItem = ({
color: "white", color: "white",
}} }}
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>
@ -176,7 +213,7 @@ export const MessageItem = ({
visibility: userInfo?.level !== undefined ? 'visible' : 'hidden', visibility: userInfo?.level !== undefined ? 'visible' : 'hidden',
width: '30px', width: '30px',
height: 'auto' height: 'auto'
}} src={getBadgeImg(userInfo?.level)} /> }} src={getBadgeImg(userInfo)} />
</Tooltip> </Tooltip>
</div> </div>
</ClickAwayListener> </ClickAwayListener>
@ -285,13 +322,7 @@ 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} setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
/> />
)} )}
@ -306,13 +337,7 @@ export const MessageItem = ({
)} )}
{message?.messageText && ( {message?.messageText && (
<MessageDisplay <MessageDisplay
htmlContent={generateHTML(message?.messageText, [ htmlContent={htmlText}
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])}
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
/> />
)} )}
@ -485,21 +510,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})=> {
@ -557,4 +572,37 @@ 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',
flexDirection: 'column',
alignItems: '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) => {
window.sendMessage("listActions", {
type: 'get',
listName: `blockedAddresses`,
})
.then((response) => {
if (response.error) {
rej(response?.message);
return;
} else {
res(response);
}
})
.catch((error) => {
console.error("Failed qortalRequest", error);
});
})
const blockedUsers = {}
response?.forEach((item)=> {
blockedUsers[item] = true
})
userBlockedRef.current = blockedUsers
const response2 = await new Promise((res, rej) => {
window.sendMessage("listActions", {
type: 'get',
listName: `blockedNames`,
})
.then((response) => {
if (response.error) {
rej(response?.message);
return;
} else {
res(response);
}
})
.catch((error) => {
console.error("Failed qortalRequest", error);
});
})
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) => {
window.sendMessage("listActions", {
type: 'remove',
items: name ? [name] : [address],
listName: name ? 'blockedNames' : 'blockedAddresses'
})
.then((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);
}
})
.catch((error) => {
console.error("Failed qortalRequest", error);
});
})
if(name && userBlockedRef.current[address]){
await new Promise((res, rej) => {
window.sendMessage("listActions", {
type: 'remove',
items: !name ? [name] : [address],
listName: !name ? 'blockedNames' : 'blockedAddresses'
})
.then((response) => {
if (response.error) {
rej(response?.message);
return;
} else {
const copyObject = {...userBlockedRef.current}
delete copyObject[address]
userBlockedRef.current = copyObject
res(response);
}
})
.catch((error) => {
console.error("Failed qortalRequest", error);
});
})
}
}, [])
const addToBlockList = useCallback(async (address, name)=> {
await new Promise((res, rej) => {
window.sendMessage("listActions", {
type: 'add',
items: name ? [name] : [address],
listName: name ? 'blockedNames' : 'blockedAddresses'
})
.then((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);
}
})
.catch((error) => {
console.error("Failed qortalRequest", error);
});
})
}, [])
return {
isUserBlocked,
addToBlockList,
removeBlockFromList,
getAllBlockedUsers
};
};

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: '100vw', height: '100%', maxWidth: '1000px' }} role="presentation">
{children}
</Box>
</Drawer>
</div>
);
}

View File

@ -0,0 +1,102 @@
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 = ({setMobileViewMode}) => {
return (
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
justifyContent: 'center'
}}
>
<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={()=> {
setMobileViewMode('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,117 @@
import React, { useState, useEffect, useRef } from 'react';
import { Box, Divider, Menu, MenuItem, Typography, styled } from '@mui/material';
import { executeEvent } from '../utils/events';
import { useRecoilState } from 'recoil';
import { lastEnteredGroupIdAtom } from '../atoms/global';
import CloseIcon from '@mui/icons-material/Close';
const CustomStyledMenu = styled(Menu)(({ theme }) => ({
'& .MuiPaper-root': {
backgroundColor: '#f9f9f9',
borderRadius: '12px',
padding: theme.spacing(1),
boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
},
'& .MuiMenuItem-root': {
fontSize: '14px',
color: '#444',
transition: '0.3s background-color',
'&:hover': {
backgroundColor: '#f0f0f0',
},
},
}));
export const GlobalTouchMenu = () => {
const [menuOpen, setMenuOpen] = useState(false);
const tapCount = useRef(0);
const lastTapTime = useRef(0);
const [menuPosition, setMenuPosition] = useState(null);
const [lastEnteredGroupId] = useRecoilState(lastEnteredGroupIdAtom)
useEffect(() => {
const handleTouchStart = (event) => {
const currentTime = new Date().getTime();
const tapGap = currentTime - lastTapTime.current;
const { clientX, clientY } = event.touches[0];
if (tapGap < 400) {
tapCount.current += 1;
} else {
tapCount.current = 1; // Reset if too much time has passed
}
lastTapTime.current = currentTime;
if (tapCount.current === 3) {
setMenuPosition({
top: clientY,
left: clientX,
});
setMenuOpen(true);
tapCount.current = 0; // Reset after activation
}
};
document.addEventListener('touchstart', handleTouchStart);
return () => {
document.removeEventListener('touchstart', handleTouchStart);
};
}, []);
const handleClose = () => {
setMenuOpen(false);
};
return (
<CustomStyledMenu
open={menuOpen}
anchorReference="anchorPosition"
anchorPosition={menuPosition ? { top: menuPosition?.top, left: menuPosition?.left } : undefined}
>
<MenuItem onClick={handleClose}>
<Box sx={{
display: 'flex',
alignItems: 'center',
gap: '10px'
}}>
<CloseIcon />
<Typography variant="inherit">Close Menu</Typography>
</Box>
</MenuItem>
<Divider />
<MenuItem onClick={()=> {
executeEvent('open-apps-mode', {})
handleClose()
}}>
<Typography variant="inherit">Apps</Typography>
</MenuItem>
<MenuItem onClick={()=> {
executeEvent("openGroupMessage", {
from: lastEnteredGroupId ,
});
handleClose()
}}>
<Typography variant="inherit">Group Chat</Typography>
</MenuItem>
<MenuItem onClick={()=> {
executeEvent('openUserLookupDrawer', {
addressOrName: ""
})
handleClose()
}}>
<Typography variant="inherit">User Lookup</Typography>
</MenuItem>
<MenuItem onClick={()=> {
executeEvent('openUserProfile',{})
handleClose()
}}>
<Typography variant="inherit">Wallet</Typography>
</MenuItem>
</CustomStyledMenu>
);
};

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

@ -19,6 +19,8 @@ import React, {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import BlockIcon from '@mui/icons-material/Block';
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
import { ChatGroup } from "../Chat/ChatGroup"; import { ChatGroup } from "../Chat/ChatGroup";
import { CreateCommonSecret } from "../Chat/CreateCommonSecret"; import { CreateCommonSecret } from "../Chat/CreateCommonSecret";
@ -95,9 +97,11 @@ import { AppsDesktop } from "../Apps/AppsDesktop";
import { formatEmailDate } from "./QMailMessages"; import { formatEmailDate } from "./QMailMessages";
import { useHandleMobileNativeBack } from "../../hooks/useHandleMobileNativeBack"; import { useHandleMobileNativeBack } from "../../hooks/useHandleMobileNativeBack";
import { AdminSpace } from "../Chat/AdminSpace"; import { AdminSpace } from "../Chat/AdminSpace";
import { useSetRecoilState } from "recoil"; import { useRecoilState, useSetRecoilState } from "recoil";
import { addressInfoControllerAtom, selectedGroupIdAtom } from "../../atoms/global"; import { addressInfoControllerAtom, groupsPropertiesAtom, lastEnteredGroupIdAtom, selectedGroupIdAtom } from "../../atoms/global";
import { sortArrayByTimestampAndGroupName } from "../../utils/time"; import { sortArrayByTimestampAndGroupName } from "../../utils/time";
import { BlockedUsersModal } from "./BlockedUsersModal";
import { GlobalTouchMenu } from "../GlobalTouchMenu";
// let touchStartY = 0; // let touchStartY = 0;
// let disablePullToRefresh = false; // let disablePullToRefresh = false;
@ -500,10 +504,12 @@ export const Group = ({
const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false) const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false)
const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false) const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false)
const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false) const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false)
const [groupsProperties, setGroupsProperties] = useState({}) const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom)
const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom); const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom);
const [isOpenBlockedUserModal, setIsOpenBlockedUserModal] = React.useState(false);
const setLastEnteredGroupIdAtom = useSetRecoilState(lastEnteredGroupIdAtom)
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
@ -906,7 +912,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]);
@ -997,7 +1006,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") {
window.sendMessage("addTimestampEnterChat", { window.sendMessage("addTimestampEnterChat", {
@ -1091,7 +1100,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);
@ -1441,7 +1450,8 @@ export const Group = ({
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");
setMobileViewMode('group')
return; return;
} }
if (findGroup) { if (findGroup) {
@ -1475,6 +1485,7 @@ export const Group = ({
setTimeout(() => { setTimeout(() => {
setSelectedGroup(findGroup); setSelectedGroup(findGroup);
setLastEnteredGroupIdAtom(findGroup?.groupId)
setMobileViewMode("group"); setMobileViewMode("group");
setDesktopSideView('groups') setDesktopSideView('groups')
setDesktopViewMode('home') setDesktopViewMode('home')
@ -1525,6 +1536,8 @@ export const Group = ({
setTimeout(() => { setTimeout(() => {
setSelectedGroup(findGroup); setSelectedGroup(findGroup);
setLastEnteredGroupIdAtom(findGroup?.groupId)
setMobileViewMode("group"); setMobileViewMode("group");
setDesktopSideView('groups') setDesktopSideView('groups')
setDesktopViewMode('home') setDesktopViewMode('home')
@ -1582,6 +1595,8 @@ export const Group = ({
setTimeout(() => { setTimeout(() => {
setSelectedGroup(findGroup); setSelectedGroup(findGroup);
setLastEnteredGroupIdAtom(findGroup?.groupId)
setMobileViewMode("group"); setMobileViewMode("group");
setDesktopSideView('groups') setDesktopSideView('groups')
setDesktopViewMode('home') setDesktopViewMode('home')
@ -1713,6 +1728,7 @@ export const Group = ({
borderRadius: !isMobile && '0px 15px 15px 0px' borderRadius: !isMobile && '0px 15px 15px 0px'
}} }}
> >
{isMobile && ( {isMobile && (
<Box <Box
sx={{ sx={{
@ -1978,6 +1994,8 @@ export const Group = ({
setIsOpenDrawer(false); setIsOpenDrawer(false);
setTimeout(() => { setTimeout(() => {
setSelectedGroup(group); setSelectedGroup(group);
setLastEnteredGroupIdAtom(group?.groupId)
// getTimestampEnterChat(); // getTimestampEnterChat();
}, 200); }, 200);
@ -2054,7 +2072,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: {
@ -2113,21 +2131,39 @@ export const Group = ({
width: "100%", width: "100%",
justifyContent: "center", justifyContent: "center",
padding: "10px", padding: "10px",
gap: '10px'
}} }}
> >
{chatMode === "groups" && ( {chatMode === "groups" && (
<CustomButton <>
onClick={() => { <CustomButton
setOpenAddGroup(true); onClick={() => {
}} setOpenAddGroup(true);
>
<AddCircleOutlineIcon
sx={{
color: "white",
}} }}
/> >
Group Mgmt <AddCircleOutlineIcon
</CustomButton> sx={{
color: "white",
}}
/>
Group Mgmt
</CustomButton>
<CustomButton
onClick={() => {
setIsOpenBlockedUserModal(true);
}}
sx={{
minWidth: 'unset',
padding: '10px'
}}
>
<BlockIcon
sx={{
color: "white",
}}
/>
</CustomButton>
</>
)} )}
{chatMode === "directs" && ( {chatMode === "directs" && (
<CustomButton <CustomButton
@ -2157,6 +2193,7 @@ export const Group = ({
myAddress={myAddress} myAddress={myAddress}
setIsLoadingGroups={setIsLoadingGroups} setIsLoadingGroups={setIsLoadingGroups}
/> />
<GlobalTouchMenu />
<CustomizedSnackbars <CustomizedSnackbars
open={openSnack} open={openSnack}
setOpen={setOpenSnack} setOpen={setOpenSnack}
@ -2383,7 +2420,7 @@ export const Group = ({
fontWeight: 600, fontWeight: 600,
}} }}
> >
{selectedGroup?.groupName} {selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName}
</Typography> </Typography>
<Box <Box
sx={{ sx={{
@ -2410,7 +2447,9 @@ export const Group = ({
)} )}
{isMobile && mobileViewMode === "group" && ( {isMobile && mobileViewMode === "group" && (
<> <div style={{
visibility: selectedGroup?.groupId === '0' ? 'hidden' : 'visibile'
}}>
<GroupMenu <GroupMenu
setGroupSection={setGroupSection} setGroupSection={setGroupSection}
groupSection={groupSection} groupSection={groupSection}
@ -2420,7 +2459,7 @@ export const Group = ({
hasUnreadAnnouncements={isUnread} hasUnreadAnnouncements={isUnread}
hasUnreadChat={isUnreadChat} hasUnreadChat={isUnreadChat}
/> />
</> </div>
)} )}
<Box <Box
sx={{ sx={{
@ -2610,7 +2649,11 @@ export const Group = ({
)} )}
</> </>
)} )}
{isOpenBlockedUserModal && (
<BlockedUsersModal close={()=> {
setIsOpenBlockedUserModal(false)
}} />
)}
{selectedDirect && !newChat && ( {selectedDirect && !newChat && (
<> <>
<Box <Box
@ -2690,6 +2733,7 @@ export const Group = ({
)} )}
{isMobile && mobileViewMode === "home" && ( {isMobile && mobileViewMode === "home" && (
<Home <Home
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: "100%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "row",
padding: "0px 20px", padding: "0px 20px",
gap: '10px',
justifyContent: 'flex-start',
marginBottom: '5px'
}} }}
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'
}} /> : (
<Box <ExpandMoreIcon sx={{
sx={{ marginLeft: 'auto'
width: "322px", }}/>
height: isMobile ? "165px" : "250px", )}
</ButtonBase>
display: "flex", <Collapse in={isExpanded} timeout="auto" unmountOnExit>
flexDirection: "column", <Box
bgcolor: "background.paper", sx={{
padding: "20px", width: "322px",
borderRadius: "19px", maxWidth: '100%',
}} display: "flex",
> flexDirection: "column",
{loading && groupsWithJoinRequests.length === 0 && ( bgcolor: "background.paper",
<Box padding: "20px",
borderRadius: "19px",
}}
>
{loading && groupsWithJoinRequests.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
<CustomLoader />
</Box>
)}
{!loading && groupsWithJoinRequests.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
>
<Typography
sx={{
fontSize: "11px",
fontWeight: 400,
color: "rgba(255, 255, 255, 0.2)",
}}
>
Nothing to display
</Typography>
</Box>
)}
<List
sx={{ 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 }) => { 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,30 +113,38 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
flexDirection: "column", flexDirection: "column",
alignItems: 'center' alignItems: 'center'
}}> }}>
<Box <ButtonBase
sx={{ sx={{
width: "322px", width: "100%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "row",
padding: '0px 20px', padding: '0px 20px',
gap: '10px',
justifyContent: 'flex-start',
marginBottom: '5px'
}} }}
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",
height: isMobile ? "165px" : "250px", maxWidth: '100%',
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@ -173,7 +184,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 (
@ -185,6 +196,9 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
getTimestampEnterChat() getTimestampEnterChat()
setGroupSection("announcement") setGroupSection("announcement")
setOpenManageMembers(true) setOpenManageMembers(true)
if(!isMobile){
setDesktopViewMode('chat')
}
setTimeout(() => { setTimeout(() => {
executeEvent("openGroupJoinRequest", {}); executeEvent("openGroupJoinRequest", {});
@ -225,6 +239,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
</List> </List>
</Box> </Box>
</Collapse>
</Box> </Box>
); );
}; };

View File

@ -1,4 +1,4 @@
import { Box, Button, ButtonBase, Typography } from "@mui/material"; import { Box, Button, ButtonBase, Divider, Typography } from "@mui/material";
import React, { useContext } from "react"; import React, { useContext } from "react";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched"; import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched";
@ -10,8 +10,13 @@ import { ListOfGroupPromotions } from "./ListOfGroupPromotions";
import HelpIcon from '@mui/icons-material/Help'; import HelpIcon from '@mui/icons-material/Help';
import { useHandleTutorials } from "../Tutorials/useHandleTutorials"; import { useHandleTutorials } from "../Tutorials/useHandleTutorials";
import { GlobalContext } from "../../App"; import { GlobalContext } from "../../App";
import { QortPrice } from "../Home/QortPrice";
import { QMailMessages } from "./QMailMessages";
import { Explore } from "../Explore/Explore";
import ExploreIcon from "@mui/icons-material/Explore";
export const Home = ({ export const Home = ({
name,
refreshHomeDataFunc, refreshHomeDataFunc,
myAddress, myAddress,
isLoadingGroups, isLoadingGroups,
@ -27,6 +32,30 @@ export const Home = ({
}) => { }) => {
const { showTutorial } = useContext(GlobalContext); const { showTutorial } = useContext(GlobalContext);
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={{
@ -101,19 +130,27 @@ export const Home = ({
display: "flex", display: "flex",
gap: "15px", gap: "15px",
flexWrap: "wrap", flexWrap: "wrap",
justifyContent: "center", alignItems: "center",
flexDirection: 'column',
width: '100%'
}} }}
> >
<ThingsToDoInitial <ThingsToDoInitial
balance={balance} balance={balance}
myAddress={myAddress} myAddress={myAddress}
name={userInfo?.name} name={userInfo?.name}
hasGroups={groups?.length !== 0} hasGroups={
groups?.filter((item) => item?.groupId !== "0").length !== 0
}
userInfo={userInfo} userInfo={userInfo}
/> />
<ListOfThreadPostsWatched /> {/* <ListOfThreadPostsWatched /> */}
<QortPrice />
{hasDoneNameAndBalanceAndIsLoaded && (
<>
<Spacer height="20px" />
<QMailMessages userAddress={userInfo?.address} userName={userInfo?.name} />
<GroupJoinRequests <GroupJoinRequests
setGroupSection={setGroupSection} setGroupSection={setGroupSection}
setSelectedGroup={setSelectedGroup} setSelectedGroup={setSelectedGroup}
@ -129,11 +166,43 @@ export const Home = ({
groups={groups} groups={groups}
setMobileViewMode={setMobileViewMode} setMobileViewMode={setMobileViewMode}
/> />
<ListOfGroupPromotions />
<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>
<Explore setMobileViewMode={setMobileViewMode} />
</>
)}
</Box> </Box>
)} )}
{!isLoadingGroups && (
<ListOfGroupPromotions />
)}
<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,8 +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;
@ -82,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);
@ -96,18 +101,16 @@ 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(
(index) => promotions[index]?.identifier, (index) => promotions[index]?.identifier,
[promotions] [promotions]
), ),
getScrollElement: () => listRef.current, getScrollElement: () => listRef.current,
estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed
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 {
@ -119,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, {
@ -170,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);
@ -179,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]);
@ -330,8 +336,6 @@ export const ListOfGroupPromotions = () => {
} }
}; };
return ( return (
<Box <Box
sx={{ sx={{
@ -339,91 +343,124 @@ export const ListOfGroupPromotions = () => {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
marginTop: "25px",
}} }}
> >
<Box <ButtonBase
sx={{ sx={{
width: isMobile ? "320px" : "750px", width: "100%",
maxWidth: "90%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "row",
padding: "0px 20px", padding: "0px 20px",
gap: '10px',
justifyContent: 'flex-start',
}} }}
> onClick={() => setIsExpanded((prev) => !prev)}
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
> >
<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={{
fontSize: "12px",
}}
>
Add Promotion
</Button>
</Box>
<Spacer height="10px" />
</Box>
<Box
sx={{
width: isMobile ? "320px" : "750px",
maxWidth: "90%",
maxHeight: "700px",
display: "flex",
flexDirection: "column",
bgcolor: "background.paper",
padding: "20px 0px",
borderRadius: "19px",
}}
>
{loading && promotions.length === 0 && (
<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={{ sx={{
fontSize: "11px", marginLeft: "auto",
fontWeight: 400, }}
color: "rgba(255, 255, 255, 0.2)", />
) : (
<ExpandMoreIcon
sx={{
marginLeft: "auto",
}}
/>
)}
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<>
<Box
sx={{
width: '100%',
display: "flex",
flexDirection: "column",
padding: "0px 20px",
marginTop: '15px',
width: "322px",
maxWidth: '100%'
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}} }}
> >
Nothing to display <Typography
</Typography> sx={{
fontSize: "13px",
fontWeight: 600,
}}
></Typography>
<Button
variant="contained"
onClick={() => setIsShowModal(true)}
sx={{
fontSize: "12px",
}}
>
Add Promotion
</Button>
</Box>
<Spacer height="10px" />
</Box> </Box>
)} <Box
sx={{
width: isMobile ? "320px" : "750px",
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 <div
style={{ style={{
height: "600px", height: "600px",
@ -462,7 +499,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
@ -481,237 +517,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
<Box </LoadingButton>
sx={{ </Box>
display: "flex", </Box>
alignItems: "center", </Popover>
justifyContent: "space-between",
width: "100%", <Box
}} sx={{
> display: "flex",
<Box alignItems: "center",
sx={{ justifyContent: "space-between",
display: "flex", width: "100%",
alignItems: "center", }}
gap: "15px", >
}} <Box
> sx={{
<Avatar display: "flex",
sx={{ alignItems: "center",
backgroundColor: "#27282c", gap: "15px",
color: "white", }}
}} >
alt={promotion?.name} <Avatar
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${ sx={{
promotion?.name backgroundColor: "#27282c",
}/qortal_avatar?async=true`} color: "white",
> }}
{promotion?.name?.charAt(0)} alt={promotion?.name}
</Avatar> src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
<Typography promotion?.name
sx={{ }/qortal_avatar?async=true`}
fontWight: 600, >
fontFamily: "Inter", {promotion?.name?.charAt(0)}
color: "cadetBlue", </Avatar>
}} <Typography
> sx={{
{promotion?.name} fontWight: 600,
</Typography> fontFamily: "Inter",
</Box> color: "cadetBlue",
}}
</Box> >
<Spacer height="20px"/> {promotion?.name}
<Typography </Typography>
sx={{ </Box>
fontWight: 600, <Typography
fontFamily: "Inter", sx={{
color: "cadetBlue", fontWight: 600,
}} fontFamily: "Inter",
> color: "cadetBlue",
{promotion?.groupName} }}
</Typography> >
<Spacer height="20px" /> {promotion?.groupName}
<Box sx={{ </Typography>
display: 'flex', </Box>
gap: '20px', <Spacer height="20px" />
alignItems: 'center' <Box
}}> sx={{
{promotion?.isOpen === false && ( display: "flex",
<LockIcon sx={{ gap: "20px",
color: 'var(--green)' alignItems: "center",
}} /> }}
)} >
{promotion?.isOpen === true && ( {promotion?.isOpen === false && (
<NoEncryptionGmailerrorredIcon sx={{ <LockIcon
color: 'var(--danger)' sx={{
}} /> color: "var(--green)",
)} }}
<Typography />
sx={{ )}
fontSize: "15px", {promotion?.isOpen === true && (
fontWeight: 600, <NoEncryptionGmailerrorredIcon
}} sx={{
> color: "var(--danger)",
{promotion?.isOpen ? 'Public group' : 'Private group' } }}
</Typography> />
</Box> )}
<Spacer height="20px" /> <Typography
<Typography sx={{
sx={{ fontSize: "15px",
fontWight: 600, fontWeight: 600,
fontFamily: "Inter", }}
color: "cadetBlue", >
}} {promotion?.isOpen
> ? "Public group"
{promotion?.data} : "Private group"}
</Typography> </Typography>
<Spacer height="20px" /> </Box>
<Box <Spacer height="20px" />
sx={{ <Typography
display: "flex", sx={{
justifyContent: "center", fontWight: 600,
width: "100%", fontFamily: "Inter",
}} color: "cadetBlue",
> }}
<Button >
onClick={(event) => handlePopoverOpen(event, promotion?.groupId)} {promotion?.data}
sx={{ </Typography>
fontSize: "12px", <Spacer height="20px" />
color: 'white' <Box
}} sx={{
> display: "flex",
Join Group: {` ${promotion?.groupName}`} justifyContent: "center",
</Button> width: "100%",
</Box> }}
</Box> >
<Spacer height="50px" /> <Button
// variant="contained"
onClick={(event) =>
handlePopoverOpen(event, promotion?.groupId)
}
sx={{
fontSize: "12px",
color: "white",
}}
>
Join Group: {` ${promotion?.groupName}`}
</Button>
</Box>
</Box>
<Spacer height="50px" />
</ErrorBoundary> </ErrorBoundary>
</div> </div>
); );
})} })}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</Box>
</Box> </>
</Collapse>
<Spacer height="20px" /> <Spacer height="20px" />
{isShowModal && ( {isShowModal && (
@ -747,6 +797,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';
@ -13,7 +13,12 @@ import MailIcon from '@mui/icons-material/Mail';
import MailOutlineIcon from '@mui/icons-material/MailOutline'; import MailOutlineIcon from '@mui/icons-material/MailOutline';
import { executeEvent } from '../../utils/events'; import { executeEvent } from '../../utils/events';
import { CustomLoader } from '../../common/CustomLoader'; import { CustomLoader } from '../../common/CustomLoader';
const isLessThanOneWeekOld = (timestamp) => { import { useRecoilState } from 'recoil';
import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import MarkEmailUnreadIcon from '@mui/icons-material/MarkEmailUnread';
export const isLessThanOneWeekOld = (timestamp) => {
// Current time in milliseconds // Current time in milliseconds
const now = Date.now(); const now = Date.now();
@ -39,8 +44,9 @@ export function formatEmailDate(timestamp: number) {
} }
} }
export const QMailMessages = ({userName, userAddress}) => { export const QMailMessages = ({userName, userAddress}) => {
const [mails, setMails] = useState([]) const [isExpanded, setIsExpanded] = useState(false)
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useState(null) 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 () => {
@ -97,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
@ -109,28 +124,43 @@ export const QMailMessages = ({userName, userAddress}) => {
}} }}
> >
<Box <ButtonBase
sx={{ sx={{
width: "322px", width: "100%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "row",
gap: '10px',
padding: "0px 20px", padding: "0px 20px",
justifyContent: 'flex-start',
marginBottom: '5px'
}} }}
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",
maxWidth: '100%',
height: isMobile ? "165px" : "250px", height: isMobile ? "165px" : "250px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@ -186,6 +216,8 @@ export const QMailMessages = ({userName, userAddress}) => {
onClick={()=> { onClick={()=> {
executeEvent("addTab", { data: { service: 'APP', name: 'q-mail' } }); executeEvent("addTab", { data: { service: 'APP', name: 'q-mail' } });
executeEvent("open-apps-mode", { }); executeEvent("open-apps-mode", { });
setLastEnteredTimestamp(Date.now())
}} }}
> >
<ListItemButton <ListItemButton
@ -219,7 +251,7 @@ export const QMailMessages = ({userName, userAddress}) => {
<MailOutlineIcon sx={{ <MailOutlineIcon sx={{
color: 'white' color: 'white'
}} /> }} />
): lastEnteredTimestamp < mail?.created ? ( ): (lastEnteredTimestamp < mail?.created) && isLessThanOneWeekOld(mail?.created) ? (
<MailIcon sx={{ <MailIcon sx={{
color: 'var(--unread)' color: 'var(--unread)'
}} /> }} />
@ -243,6 +275,7 @@ export const QMailMessages = ({userName, userAddress}) => {
</Box> </Box>
</Collapse>
</Box> </Box>
) )
} }

View File

@ -59,9 +59,7 @@ return false
}, [checked1, isLoaded, checked2]) }, [checked1, isLoaded, checked2])
if(hasDoneNameAndBalanceAndIsLoaded){ if(hasDoneNameAndBalanceAndIsLoaded){
return ( return null
<QMailMessages userAddress={userInfo?.address} userName={userInfo?.name} />
);
} }
return ( return (

View File

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

@ -0,0 +1,312 @@
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) => {
window
.sendMessage("registerName", {
name: registerNameValue,
})
.then((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) => {
setInfoSnack({
type: "error",
message: error.message || "An error occurred",
});
setOpenSnack(true);
rej(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

@ -24,7 +24,7 @@ export const CustomizedSnackbars = ({open, setOpen, info, setInfo, duration}) =
<div> <div>
<Snackbar sx={{ <Snackbar sx={{
zIndex: 15 zIndex: 15
}} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={duration || 6000} onClose={handleClose}> }} 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: 'unset',
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);
@ -119,8 +121,78 @@ 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

@ -41,6 +41,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,6 +1,6 @@
import { gateways, getApiKeyFromStorage } from "./background"; import { gateways, getApiKeyFromStorage } from "./background";
import { listOfAllQortalRequests } from "./components/Apps/useQortalMessageListener"; import { listOfAllQortalRequests } from "./components/Apps/useQortalMessageListener";
import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createGroupRequest, createPoll, 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, openNewTab, 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, createAndCopyEmbedLink, createBuyOrder, createGroupRequest, createPoll, 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, openNewTab, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll } from "./qortalRequests/get";
import { getData, storeData } from "./utils/chromeStorage"; import { getData, storeData } from "./utils/chromeStorage";
@ -1103,6 +1103,25 @@ export const isRunningGateway = async ()=> {
} }
break; break;
} }
case "GET_USER_WALLET_TRANSACTIONS": {
try {
const res = await getUserWalletTransactions(request.payload, isFromExtension, appInfo);
event.source.postMessage({
requestId: request.requestId,
action: request.action,
payload: res,
type: "backgroundMessageResponse",
}, event.origin);
} catch (error) {
event.source.postMessage({
requestId: request.requestId,
action: request.action,
error: error.message,
type: "backgroundMessageResponse",
}, event.origin);
}
break;
}
default: default:
break; break;
} }

View File

@ -4323,4 +4323,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 {