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,11 +129,13 @@ 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
} }
}, []); }, []);
@ -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,26 +179,40 @@ 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'){
isValid = true
} else {
const url2 = `${key?.url}/admin/apikey/test?apiKey=${key?.apikey}`;
const response2 = await fetch(url2);
// Assuming the response is in plain text and will be 'true' or 'false'
const data2 = await response2.text();
if (data2 === "true") {
isValid = true
}
}
if (isValid) {
setIsValidApiKey(true); setIsValidApiKey(true);
setUseLocalNode(true); setUseLocalNode(true);
return return
@ -194,10 +220,13 @@ export const NotAuthenticated = ({
} }
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);
if(!fromStartUp){
setInfoSnack({ setInfoSnack({
type: "error", type: "error",
message: "Select a valid apikey", 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
> >

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -158,3 +158,23 @@ export const addressInfoKeySelector = selectorFamily({
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,
});

View File

@ -12,6 +12,7 @@ import {
checkNewMessages, checkNewMessages,
checkThreads, checkThreads,
clearAllNotifications, clearAllNotifications,
createEndpoint,
createGroup, createGroup,
decryptDirectFunc, decryptDirectFunc,
decryptSingleForPublishes, decryptSingleForPublishes,
@ -56,7 +57,13 @@ import {
updateThreadActivity, updateThreadActivity,
walletVersion, walletVersion,
} from "./background"; } from "./background";
import { decryptGroupEncryption, encryptAndPublishSymmetricKeyGroupChat, encryptAndPublishSymmetricKeyGroupChatForAdmins, publishGroupEncryptedResource, publishOnQDN } from "./backgroundFunctions/encryption"; import {
decryptGroupEncryption,
encryptAndPublishSymmetricKeyGroupChat,
encryptAndPublishSymmetricKeyGroupChatForAdmins,
publishGroupEncryptedResource,
publishOnQDN,
} from "./backgroundFunctions/encryption";
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from "./constants/codes"; import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from "./constants/codes";
import Base58 from "./deps/Base58"; import Base58 from "./deps/Base58";
import { encryptSingle } from "./qdn/encryption/group-encryption"; import { encryptSingle } from "./qdn/encryption/group-encryption";
@ -81,8 +88,8 @@ export async function getWalletInfoCase(request, event) {
const response = await getKeyPair(); const response = await getKeyPair();
try { try {
const walletInfo = await getData('walletInfo').catch((error)=> null) const walletInfo = await getData("walletInfo").catch((error) => null);
if(walletInfo){ if (walletInfo) {
event.source.postMessage( event.source.postMessage(
{ {
requestId: request.requestId, requestId: request.requestId,
@ -103,7 +110,6 @@ export async function getWalletInfoCase(request, event) {
event.origin event.origin
); );
} }
} catch (error) { } catch (error) {
event.source.postMessage( event.source.postMessage(
{ {
@ -115,11 +121,10 @@ export async function getWalletInfoCase(request, event) {
event.origin event.origin
); );
} }
} catch (error) { } catch (error) {
try { try {
const walletInfo = await getData('walletInfo').catch((error)=> null) const walletInfo = await getData("walletInfo").catch((error) => null);
if(walletInfo){ if (walletInfo) {
event.source.postMessage( event.source.postMessage(
{ {
requestId: request.requestId, requestId: request.requestId,
@ -151,7 +156,6 @@ export async function getWalletInfoCase(request, event) {
event.origin event.origin
); );
} }
} }
} }
@ -236,7 +240,11 @@ export async function userInfoCase(request, event) {
export async function decryptWalletCase(request, event) { export async function decryptWalletCase(request, event) {
try { try {
const { password, wallet } = request.payload; const { password, wallet } = request.payload;
const response = await decryptWallet({password, wallet, walletVersion: wallet?.version || walletVersion}); const response = await decryptWallet({
password,
wallet,
walletVersion: wallet?.version || walletVersion,
});
event.source.postMessage( event.source.postMessage(
{ {
requestId: request.requestId, requestId: request.requestId,
@ -620,8 +628,7 @@ export async function banFromGroupCase(request, event) {
export async function addDataPublishesCase(request, event) { export async function addDataPublishesCase(request, event) {
try { try {
const { data, groupId, type } = request.payload; const { data, groupId, type } = request.payload;
const response = await addDataPublishes( data, groupId, type ); const response = await addDataPublishes(data, groupId, type);
event.source.postMessage( event.source.postMessage(
{ {
@ -648,8 +655,7 @@ export async function addDataPublishesCase(request, event) {
export async function getDataPublishesCase(request, event) { export async function getDataPublishesCase(request, event) {
try { try {
const { groupId, type } = request.payload; const { groupId, type } = request.payload;
const response = await getDataPublishes(groupId, type ); const response = await getDataPublishes(groupId, type);
event.source.postMessage( event.source.postMessage(
{ {
@ -902,7 +908,7 @@ export async function addTimestampEnterChatCase(request, event) {
export async function setApiKeyCase(request, event) { export async function setApiKeyCase(request, event) {
try { try {
const payload = request.payload; const payload = request.payload;
storeData('apiKey', payload) storeData("apiKey", payload);
event.source.postMessage( event.source.postMessage(
{ {
requestId: request.requestId, requestId: request.requestId,
@ -927,7 +933,7 @@ export async function setApiKeyCase(request, event) {
export async function setCustomNodesCase(request, event) { export async function setCustomNodesCase(request, event) {
try { try {
const nodes = request.payload; const nodes = request.payload;
storeData('customNodes', nodes) storeData("customNodes", nodes);
event.source.postMessage( event.source.postMessage(
{ {
@ -1039,7 +1045,7 @@ export async function addGroupNotificationTimestampCase(request, event) {
const response = await addTimestampGroupAnnouncement({ const response = await addTimestampGroupAnnouncement({
groupId, groupId,
timestamp, timestamp,
seenTimestamp: true seenTimestamp: true,
}); });
event.source.postMessage( event.source.postMessage(
@ -1264,7 +1270,7 @@ export async function encryptAndPublishSymmetricKeyGroupChatForAdminsCase(
await encryptAndPublishSymmetricKeyGroupChatForAdmins({ await encryptAndPublishSymmetricKeyGroupChatForAdmins({
groupId, groupId,
previousData, previousData,
admins admins,
}); });
event.source.postMessage( event.source.postMessage(
@ -1291,8 +1297,11 @@ export async function encryptAndPublishSymmetricKeyGroupChatForAdminsCase(
export async function publishGroupEncryptedResourceCase(request, event) { export async function publishGroupEncryptedResourceCase(request, event) {
try { try {
const {encryptedData, identifier} = request.payload; const { encryptedData, identifier } = request.payload;
const response = await publishGroupEncryptedResource({encryptedData, identifier}); const response = await publishGroupEncryptedResource({
encryptedData,
identifier,
});
event.source.postMessage( event.source.postMessage(
{ {
@ -1314,26 +1323,38 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function publishOnQDNCase(request, event) { export async function publishOnQDNCase(request, event) {
try { try {
const {data, identifier, service, title, const {
data,
identifier,
service,
title,
description, description,
category, category,
tag1, tag1,
tag2, tag2,
tag3, tag3,
tag4, tag4,
tag5, uploadType} = request.payload; tag5,
const response = await publishOnQDN({data, identifier, service, title, uploadType,
} = request.payload;
const response = await publishOnQDN({
data,
identifier,
service,
title,
description, description,
category, category,
tag1, tag1,
tag2, tag2,
tag3, tag3,
tag4, tag4,
tag5, uploadType}); tag5,
uploadType,
});
event.source.postMessage( event.source.postMessage(
{ {
@ -1349,18 +1370,18 @@ export async function publishGroupEncryptedResourceCase(request, event) {
{ {
requestId: request.requestId, requestId: request.requestId,
action: "publishOnQDN", action: "publishOnQDN",
error: error?.message || 'Unable to publish', error: error?.message || "Unable to publish",
type: "backgroundMessageResponse", type: "backgroundMessageResponse",
}, },
event.origin event.origin
); );
} }
} }
export async function handleActiveGroupDataFromSocketCase(request, event) { export async function handleActiveGroupDataFromSocketCase(request, event) {
try { try {
const {groups, directs} = request.payload; const { groups, directs } = request.payload;
const response = await handleActiveGroupDataFromSocket({groups, directs}); const response = await handleActiveGroupDataFromSocket({ groups, directs });
event.source.postMessage( event.source.postMessage(
{ {
@ -1382,11 +1403,11 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function getThreadActivityCase(request, event) { export async function getThreadActivityCase(request, event) {
try { try {
const response = await checkThreads(true) const response = await checkThreads(true);
event.source.postMessage( event.source.postMessage(
{ {
@ -1408,12 +1429,17 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function updateThreadActivityCase(request, event) { export async function updateThreadActivityCase(request, event) {
try { try {
const { threadId, qortalName, groupId, thread} = request.payload; const { threadId, qortalName, groupId, thread } = request.payload;
const response = await updateThreadActivity({ threadId, qortalName, groupId, thread }); const response = await updateThreadActivity({
threadId,
qortalName,
groupId,
thread,
});
event.source.postMessage( event.source.postMessage(
{ {
@ -1435,11 +1461,11 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function decryptGroupEncryptionCase(request, event) { export async function decryptGroupEncryptionCase(request, event) {
try { try {
const { data} = request.payload; const { data } = request.payload;
const response = await decryptGroupEncryption({ data }); const response = await decryptGroupEncryption({ data });
event.source.postMessage( event.source.postMessage(
{ {
@ -1461,12 +1487,16 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function encryptSingleCase(request, event) { export async function encryptSingleCase(request, event) {
try { try {
const { data, secretKeyObject, typeNumber} = request.payload; const { data, secretKeyObject, typeNumber } = request.payload;
const response = await encryptSingle({ data64: data, secretKeyObject, typeNumber }); const response = await encryptSingle({
data64: data,
secretKeyObject,
typeNumber,
});
event.source.postMessage( event.source.postMessage(
{ {
@ -1488,12 +1518,16 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function decryptSingleCase(request, event) { export async function decryptSingleCase(request, event) {
try { try {
const { data, secretKeyObject, skipDecodeBase64} = request.payload; const { data, secretKeyObject, skipDecodeBase64 } = request.payload;
const response = await decryptSingleFunc({ messages: data, secretKeyObject, skipDecodeBase64 }); const response = await decryptSingleFunc({
messages: data,
secretKeyObject,
skipDecodeBase64,
});
event.source.postMessage( event.source.postMessage(
{ {
requestId: request.requestId, requestId: request.requestId,
@ -1514,9 +1548,9 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function pauseAllQueuesCase(request, event) { export async function pauseAllQueuesCase(request, event) {
try { try {
await pauseAllQueues(); await pauseAllQueues();
@ -1540,9 +1574,9 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function resumeAllQueuesCase(request, event) { export async function resumeAllQueuesCase(request, event) {
try { try {
await resumeAllQueues(); await resumeAllQueues();
@ -1566,10 +1600,10 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function checkLocalCase(request, event) { export async function checkLocalCase(request, event) {
try { try {
const response = await checkLocalFunc() const response = await checkLocalFunc();
event.source.postMessage( event.source.postMessage(
{ {
requestId: request.requestId, requestId: request.requestId,
@ -1590,12 +1624,16 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function decryptSingleForPublishesCase(request, event) { export async function decryptSingleForPublishesCase(request, event) {
try { try {
const { data, secretKeyObject, skipDecodeBase64} = request.payload; const { data, secretKeyObject, skipDecodeBase64 } = request.payload;
const response = await decryptSingleForPublishes({ messages: data, secretKeyObject, skipDecodeBase64 }); const response = await decryptSingleForPublishes({
messages: data,
secretKeyObject,
skipDecodeBase64,
});
event.source.postMessage( event.source.postMessage(
{ {
@ -1617,12 +1655,15 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function decryptDirectCase(request, event) { export async function decryptDirectCase(request, event) {
try { try {
const { data, involvingAddress} = request.payload; const { data, involvingAddress } = request.payload;
const response = await decryptDirectFunc({ messages: data, involvingAddress }); const response = await decryptDirectFunc({
messages: data,
involvingAddress,
});
event.source.postMessage( event.source.postMessage(
{ {
@ -1644,14 +1685,21 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function sendChatGroupCase(request, event) { export async function sendChatGroupCase(request, event) {
try { try {
const { groupId, const {
groupId,
typeMessage = undefined, typeMessage = undefined,
chatReference = undefined, chatReference = undefined,
messageText} = request.payload; messageText,
const response = await sendChatGroup({ groupId, typeMessage, chatReference, messageText }); } = request.payload;
const response = await sendChatGroup({
groupId,
typeMessage,
chatReference,
messageText,
});
event.source.postMessage( event.source.postMessage(
{ {
@ -1673,23 +1721,27 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function sendChatDirectCase(request, event) { export async function sendChatDirectCase(request, event) {
try { try {
const { directTo, const {
directTo,
typeMessage = undefined, typeMessage = undefined,
chatReference = undefined, chatReference = undefined,
messageText, messageText,
publicKeyOfRecipient, publicKeyOfRecipient,
address, address,
otherData} = request.payload; otherData,
const response = await sendChatDirect({ directTo, } = request.payload;
const response = await sendChatDirect({
directTo,
chatReference, chatReference,
messageText, messageText,
typeMessage, typeMessage,
publicKeyOfRecipient, publicKeyOfRecipient,
address, address,
otherData }); otherData,
});
event.source.postMessage( event.source.postMessage(
{ {
@ -1711,11 +1763,10 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function setupGroupWebsocketCase(request, event) { export async function setupGroupWebsocketCase(request, event) {
try { try {
checkNewMessages(); checkNewMessages();
checkThreads(); checkThreads();
event.source.postMessage( event.source.postMessage(
@ -1738,9 +1789,9 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function addEnteredQmailTimestampCase(request, event) { export async function addEnteredQmailTimestampCase(request, event) {
try { try {
const response = await addEnteredQmailTimestamp(); const response = await addEnteredQmailTimestamp();
@ -1764,8 +1815,8 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function getEnteredQmailTimestampCase(request, event) { export async function getEnteredQmailTimestampCase(request, event) {
try { try {
const response = await getEnteredQmailTimestamp(); const response = await getEnteredQmailTimestamp();
@ -1773,7 +1824,7 @@ export async function publishGroupEncryptedResourceCase(request, event) {
{ {
requestId: request.requestId, requestId: request.requestId,
action: "getEnteredQmailTimestamp", action: "getEnteredQmailTimestamp",
payload: {timestamp: response}, payload: { timestamp: response },
type: "backgroundMessageResponse", type: "backgroundMessageResponse",
}, },
event.origin event.origin
@ -1789,9 +1840,9 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function getTimestampMentionCase(request, event) { export async function getTimestampMentionCase(request, event) {
try { try {
const response = await getTimestampMention(); const response = await getTimestampMention();
@ -1815,9 +1866,9 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function addTimestampMentionCase(request, event) { export async function addTimestampMentionCase(request, event) {
try { try {
const { groupId, timestamp } = request.payload; const { groupId, timestamp } = request.payload;
const response = await addTimestampMention({ groupId, timestamp }); const response = await addTimestampMention({ groupId, timestamp });
@ -1842,9 +1893,9 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function createPollCase(request, event) { export async function createPollCase(request, event) {
try { try {
const { pollName, pollDescription, pollOptions } = request.payload; const { pollName, pollDescription, pollOptions } = request.payload;
const resCreatePoll = await _createPoll( const resCreatePoll = await _createPoll(
@ -1877,12 +1928,11 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function voteOnPollCase(request, event) { export async function voteOnPollCase(request, event) {
try { try {
const res = await _voteOnPoll(request.payload, true, true); const res = await _voteOnPoll(request.payload, true, true);
event.source.postMessage( event.source.postMessage(
{ {
requestId: request.requestId, requestId: request.requestId,
@ -1903,11 +1953,11 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function createRewardShareCase(request, event) { export async function createRewardShareCase(request, event) {
try { try {
const {recipientPublicKey} = request.payload; const { recipientPublicKey } = request.payload;
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
const parsedData = resKeyPair; const parsedData = resKeyPair;
const uint8PrivateKey = Base58.decode(parsedData.privateKey); const uint8PrivateKey = Base58.decode(parsedData.privateKey);
@ -1949,11 +1999,12 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function removeRewardShareCase(request, event) { export async function removeRewardShareCase(request, event) {
try { try {
const {rewardShareKeyPairPublicKey, recipient, percentageShare} = request.payload; const { rewardShareKeyPairPublicKey, recipient, percentageShare } =
request.payload;
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
const parsedData = resKeyPair; const parsedData = resKeyPair;
const uint8PrivateKey = Base58.decode(parsedData.privateKey); const uint8PrivateKey = Base58.decode(parsedData.privateKey);
@ -1996,11 +2047,11 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
} }
export async function getRewardSharePrivateKeyCase(request, event) { export async function getRewardSharePrivateKeyCase(request, event) {
try { try {
const {recipientPublicKey} = request.payload; const { recipientPublicKey } = request.payload;
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
const parsedData = resKeyPair; const parsedData = resKeyPair;
const uint8PrivateKey = Base58.decode(parsedData.privateKey); const uint8PrivateKey = Base58.decode(parsedData.privateKey);
@ -2037,4 +2088,83 @@ export async function publishGroupEncryptedResourceCase(request, event) {
event.origin event.origin
); );
} }
}
export async function listActionsCase(request, event) {
try {
const { type, listName = "", items = [] } = request.payload;
let responseData;
if (type === "get") {
const url = await createEndpoint(`/lists/${listName}`);
const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch");
responseData = await response.json();
} else if (type === "remove") {
const url = await createEndpoint(`/lists/${listName}`);
const body = {
items: items,
};
const bodyToString = JSON.stringify(body);
const response = await fetch(url, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: bodyToString,
});
if (!response.ok) throw new Error("Failed to remove from list");
let res;
try {
res = await response.clone().json();
} catch (e) {
res = await response.text();
} }
responseData = res;
} else if (type === "add") {
const url = await createEndpoint(`/lists/${listName}`);
const body = {
items: items,
};
const bodyToString = JSON.stringify(body);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: bodyToString,
});
if (!response.ok) throw new Error("Failed to add to list");
let res;
try {
res = await response.clone().json();
} catch (e) {
res = await response.text();
}
responseData = res;
}
event.source.postMessage(
{
requestId: request.requestId,
action: "listActions",
payload: responseData,
type: "backgroundMessageResponse",
},
event.origin
);
} catch (error) {
event.source.postMessage(
{
requestId: request.requestId,
action: "listActions",
error: error?.message,
type: "backgroundMessageResponse",
},
event.origin
);
}
}

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,6 +146,7 @@ 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} />

View File

@ -132,10 +132,20 @@ export const AppsNavBar = ({appsMode}) => {
}; };
}, []); }, []);
const isSelectedAppPinned = !!sortablePinnedApps?.find( const isSelectedAppPinned = useMemo(()=> {
if(selectedTab?.isPrivate){
return !!sortablePinnedApps?.find(
(item) =>
item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name && item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service && item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier
);
} else {
return !!sortablePinnedApps?.find(
(item) => (item) =>
item?.name === selectedTab?.name && item?.service === selectedTab?.service item?.name === selectedTab?.name && item?.service === selectedTab?.service
); );
}
}, [selectedTab,sortablePinnedApps])
return ( return (
<AppsNavBarParent> <AppsNavBarParent>
<AppsNavBarLeft> <AppsNavBarLeft>
@ -264,6 +274,16 @@ export const AppsNavBar = ({appsMode}) => {
if (isSelectedAppPinned) { if (isSelectedAppPinned) {
// Remove the selected app if it is pinned // Remove the selected app if it is pinned
if(selectedTab?.isPrivate){
updatedApps = prev.filter(
(item) =>
!(
item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name &&
item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service &&
item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier
)
);
} else {
updatedApps = prev.filter( updatedApps = prev.filter(
(item) => (item) =>
!( !(
@ -271,8 +291,23 @@ export const AppsNavBar = ({appsMode}) => {
item?.service === selectedTab?.service item?.service === selectedTab?.service
) )
); );
}
} else { } else {
// Add the selected app if it is not pinned // Add the selected app if it is not pinned
if(selectedTab?.isPrivate){
updatedApps = [
...prev,
{
isPreview: true,
isPrivate: true,
privateAppProperties: {
...(selectedTab?.privateAppProperties || {})
}
},
];
} else {
updatedApps = [ updatedApps = [
...prev, ...prev,
{ {
@ -282,6 +317,8 @@ export const AppsNavBar = ({appsMode}) => {
]; ];
} }
}
saveToLocalStorage( saveToLocalStorage(
"ext_saved_settings", "ext_saved_settings",
"sortablePinnedApps", "sortablePinnedApps",
@ -320,9 +357,15 @@ export const AppsNavBar = ({appsMode}) => {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
if (selectedTab?.refreshFunc) {
selectedTab.refreshFunc(selectedTab?.tabId);
} else {
executeEvent("refreshApp", { executeEvent("refreshApp", {
tabId: selectedTab?.tabId, 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,69 +1,111 @@
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 { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
const style = { const style = {
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
padding: '10px', padding: "10px",
border: '1px solid #ccc', border: "1px solid #ccc",
marginBottom: '5px', marginBottom: "5px",
borderRadius: '4px', borderRadius: "4px",
backgroundColor: '#f9f9f9', backgroundColor: "#f9f9f9",
cursor: 'grab', cursor: "grab",
color: 'black' color: "black",
}; };
return ( return (
<ContextMenuPinnedApps app={app} isMine={!!app?.isMine}> <ContextMenuPinnedApps app={app} isMine={!!app?.isMine}>
<ButtonBase <ButtonBase
ref={setNodeRef} {...attributes} {...listeners} ref={setNodeRef}
{...attributes}
{...listeners}
sx={{ sx={{
height: "80px", height: "80px",
width: "60px", width: "60px",
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
}} }}
onClick={async () => {
onClick={()=> { if (app?.isPrivate) {
try {
await openApp(app?.privateAppProperties);
} catch (error) {
console.error(error);
}
} else {
executeEvent("addTab", { executeEvent("addTab", {
data: app data: app,
}) });
}
}} }}
> >
<AppCircleContainer sx={{ <AppCircleContainer
sx={{
border: "none", border: "none",
gap: isDesktop ? '10px': '5px' gap: isDesktop ? "10px" : "5px",
}}> }}
>
<AppCircle <AppCircle
sx={{ sx={{
border: "none", border: "none",
}} }}
> >
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
<LockIcon
sx={{
height: "31px",
width: "31px",
}}
/>
) : (
<Avatar <Avatar
sx={{ sx={{
height: "31px", height: "31px",
width: "31px", width: "31px",
'& img': { "& img": {
objectFit: 'fill', objectFit: "fill",
} },
}} }}
alt={app?.metadata?.title || app?.name} alt={app?.metadata?.title || app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${ src={
app?.privateAppProperties?.logo
? app?.privateAppProperties?.logo
: `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name app?.name
}/qortal_avatar?async=true`} }/qortal_avatar?async=true`
}
> >
<img <img
style={{ style={{
@ -74,22 +116,33 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
alt="center-icon" alt="center-icon"
/> />
</Avatar> </Avatar>
)}
</AppCircle> </AppCircle>
{app?.isPrivate ? (
<AppCircleLabel> <AppCircleLabel>
{app?.metadata?.title || app?.name} {`${app?.privateAppProperties?.appName || "Private"}`}
</AppCircleLabel> </AppCircleLabel>
) : (
<AppCircleLabel>{app?.metadata?.title || app?.name}</AppCircleLabel>
)}
</AppCircleContainer> </AppCircleContainer>
</ButtonBase> </ButtonBase>
</ContextMenuPinnedApps> </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];
@ -98,12 +151,17 @@ export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps
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 (existingIndex !== -1) {
// If the app is already in the list, update it with `isMine: true` // If the app is already in the list, update it with `isMine: true`
pinnedList[existingIndex] = { ...pinnedList[existingIndex], isMine: true }; pinnedList[existingIndex] = {
...pinnedList[existingIndex],
isMine: true,
};
} else { } else {
// If not in the list, add it with `isMine: true` at the beginning // If not in the list, add it with `isMine: true` at the beginning
pinnedList.unshift({ ...appToCheck, isMine: true }); pinnedList.unshift({ ...appToCheck, isMine: true });
@ -121,16 +179,17 @@ export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps
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, {
@ -164,7 +223,7 @@ export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps
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());
} }
}; };
@ -175,7 +234,9 @@ export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps
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}
@ -189,5 +250,3 @@ export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps
</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,13 +35,21 @@ const TabComponent = ({isSelected, app}) => {
} src={NavCloseTab}/> } src={NavCloseTab}/>
) } ) }
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
<LockIcon
sx={{
height: "28px",
width: "28px",
}}
/>
) : (
<Avatar <Avatar
sx={{ sx={{
height: "28px", height: "28px",
width: "28px", width: "28px",
}} }}
alt={app?.name} alt={app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${ src={app?.privateAppProperties?.logo ? app?.privateAppProperties?.logo :`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name app?.name
}/qortal_avatar?async=true`} }/qortal_avatar?async=true`}
> >
@ -53,6 +62,7 @@ const TabComponent = ({isSelected, app}) => {
alt="center-icon" alt="center-icon"
/> />
</Avatar> </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,8 +63,7 @@ function processText(input) {
return wrapper.innerHTML; return wrapper.innerHTML;
} }
export const MessageDisplay = ({ htmlContent, isReply, setMobileViewModeKeepOpen }) => { const linkify = (text) => {
const linkify = (text) => {
if (!text) return ""; // Return an empty string if text is null or undefined if (!text) return ""; // Return an empty string if text is null or undefined
let textFormatted = text; let textFormatted = text;
@ -74,10 +73,14 @@ export const MessageDisplay = ({ htmlContent, isReply, setMobileViewModeKeepOpen
return `<a href="${DOMPurify.sanitize(href)}" class="auto-link">${DOMPurify.sanitize(url)}</a>`; return `<a href="${DOMPurify.sanitize(href)}" class="auto-link">${DOMPurify.sanitize(url)}</a>`;
}); });
return processText(textFormatted); return processText(textFormatted);
}; };
export const MessageDisplay = ({ htmlContent, isReply, setMobileViewModeKeepOpen }) => {
const sanitizedContent = DOMPurify.sanitize(linkify(htmlContent), {
const sanitizedContent = useMemo(()=> {
return DOMPurify.sanitize(linkify(htmlContent), {
ALLOWED_TAGS: [ ALLOWED_TAGS: [
'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'div', 'span', 'img', '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' 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 's', 'hr'
@ -86,7 +89,8 @@ export const MessageDisplay = ({ htmlContent, isReply, setMobileViewModeKeepOpen
'href', 'target', 'rel', 'class', 'src', 'alt', 'title', 'href', 'target', 'rel', 'class', 'src', 'alt', 'title',
'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url' '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) { useEffect(()=> {
const getInfo = async ()=> {
if(!message?.sender) return
try {
const res = await getIndividualUserInfo(message?.sender)
if(!res) return null
setUserInfo(res)
} catch (error) {
//
}
}
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); onSeen(message.id);
} }, [message?.id])
}, [inView, message.id, isLast]);
useEffect(()=> {
if(message?.sender){
getIndividualUserInfo(message?.sender)
}
}, [message?.sender])
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})=> {
@ -558,3 +573,36 @@ export const ReplyPreview = ({message, isEdit})=> {
</Box> </Box>
) )
} }
const MessageWragger = ({lastMessage, onSeen, isLast, children})=> {
if(lastMessage){
return (
<WatchComponent onSeen={onSeen} isLast={isLast}>{children}</WatchComponent>
)
}
return children
}
const WatchComponent = ({onSeen, isLast, children})=> {
const { ref, inView } = useInView({
threshold: 0.7, // Fully visible
triggerOnce: true, // Only trigger once when it becomes visible
});
useEffect(() => {
if (inView && isLast && onSeen) {
onSeen();
}
}, [inView, isLast, onSeen]);
return <div ref={ref} style={{
width: '100%',
display: 'flex',
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);
if(selectedGroup?.groupId !== '0'){
getAdminsForPublic(selectedGroup) 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,9 +2131,11 @@ export const Group = ({
width: "100%", width: "100%",
justifyContent: "center", justifyContent: "center",
padding: "10px", padding: "10px",
gap: '10px'
}} }}
> >
{chatMode === "groups" && ( {chatMode === "groups" && (
<>
<CustomButton <CustomButton
onClick={() => { onClick={() => {
setOpenAddGroup(true); setOpenAddGroup(true);
@ -2128,6 +2148,22 @@ export const Group = ({
/> />
Group Mgmt Group Mgmt
</CustomButton> </CustomButton>
<CustomButton
onClick={() => {
setIsOpenBlockedUserModal(true);
}}
sx={{
minWidth: 'unset',
padding: '10px'
}}
>
<BlockIcon
sx={{
color: "white",
}}
/>
</CustomButton>
</>
)} )}
{chatMode === "directs" && ( {chatMode === "directs" && (
<CustomButton <CustomButton
@ -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,30 +57,38 @@ 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'
}} /> : (
<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",
bgcolor: "background.paper", bgcolor: "background.paper",
@ -101,16 +113,15 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
width: "100%", width: "100%",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
alignItems: 'center', alignItems: "center",
height: '100%', height: "100%",
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "11px", fontSize: "11px",
fontWeight: 400, fontWeight: 400,
color: 'rgba(255, 255, 255, 0.2)' color: "rgba(255, 255, 255, 0.2)",
}} }}
> >
Nothing to display Nothing to display
@ -125,6 +136,7 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
maxHeight: "300px", maxHeight: "300px",
overflow: "auto", overflow: "auto",
}} }}
className="scrollable-container"
> >
{groupsWithJoinRequests?.map((group) => { {groupsWithJoinRequests?.map((group) => {
return ( return (
@ -167,6 +179,7 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
})} })}
</List> </List>
</Box> </Box>
</Collapse>
</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);
@ -107,8 +112,6 @@ export const ListOfGroupPromotions = () => {
overscan: 10, // Number of items to render outside the visible area to improve smoothness overscan: 10, // Number of items to render outside the visible area to improve smoothness
}); });
useEffect(() => { useEffect(() => {
try { try {
(async () => { (async () => {
@ -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);
@ -181,7 +186,8 @@ export const ListOfGroupPromotions = () => {
const now = Date.now(); const now = Date.now();
const timeSinceLastFetch = now - promotionTimeInterval; const timeSinceLastFetch = now - promotionTimeInterval;
const initialDelay = timeSinceLastFetch >= THIRTY_MINUTES const initialDelay =
timeSinceLastFetch >= THIRTY_MINUTES
? 0 ? 0
: THIRTY_MINUTES - timeSinceLastFetch; : THIRTY_MINUTES - timeSinceLastFetch;
const initialTimeout = setTimeout(() => { const initialTimeout = setTimeout(() => {
@ -330,8 +336,6 @@ export const ListOfGroupPromotions = () => {
} }
}; };
return ( return (
<Box <Box
sx={{ sx={{
@ -339,16 +343,53 @@ export const ListOfGroupPromotions = () => {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
marginTop: "25px",
}} }}
> >
<ButtonBase
sx={{
width: "100%",
display: "flex",
flexDirection: "row",
padding: "0px 20px",
gap: '10px',
justifyContent: 'flex-start',
}}
onClick={() => setIsExpanded((prev) => !prev)}
>
<Typography
sx={{
fontSize: "1rem",
}}
>
Group promotions {promotions.length > 0 && ` (${promotions.length})`}
</Typography>
{isExpanded ? (
<ExpandLessIcon
sx={{
marginLeft: "auto",
}}
/>
) : (
<ExpandMoreIcon
sx={{
marginLeft: "auto",
}}
/>
)}
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<>
<Box <Box
sx={{ sx={{
width: isMobile ? "320px" : "750px", width: '100%',
maxWidth: "90%",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
padding: "0px 20px", padding: "0px 20px",
marginTop: '15px',
width: "322px",
maxWidth: '100%'
}} }}
> >
<Box <Box
@ -364,9 +405,7 @@ export const ListOfGroupPromotions = () => {
fontSize: "13px", fontSize: "13px",
fontWeight: 600, fontWeight: 600,
}} }}
> ></Typography>
Group Promotions
</Typography>
<Button <Button
variant="contained" variant="contained"
onClick={() => setIsShowModal(true)} onClick={() => setIsShowModal(true)}
@ -379,11 +418,9 @@ export const ListOfGroupPromotions = () => {
</Box> </Box>
<Spacer height="10px" /> <Spacer height="10px" />
</Box> </Box>
<Box <Box
sx={{ sx={{
width: isMobile ? "320px" : "750px", width: isMobile ? "320px" : "750px",
maxWidth: "90%",
maxHeight: "700px", maxHeight: "700px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@ -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
@ -542,7 +578,8 @@ export const ListOfGroupPromotions = () => {
fontWeight: 600, fontWeight: 600,
}} }}
> >
Number of members: {` ${promotion?.memberCount}`} Number of members:{" "}
{` ${promotion?.memberCount}`}
</Typography> </Typography>
{promotion?.description && ( {promotion?.description && (
<Typography <Typography
@ -561,8 +598,9 @@ export const ListOfGroupPromotions = () => {
fontWeight: 600, fontWeight: 600,
}} }}
> >
*This is a closed/private group, so you will need to wait *This is a closed/private group, so you
until an admin accepts your request will need to wait until an admin accepts
your request
</Typography> </Typography>
)} )}
<Spacer height="5px" /> <Spacer height="5px" />
@ -588,7 +626,10 @@ export const ListOfGroupPromotions = () => {
loadingPosition="start" loadingPosition="start"
variant="contained" variant="contained"
onClick={() => onClick={() =>
handleJoinGroup(promotion, promotion?.isOpen) handleJoinGroup(
promotion,
promotion?.isOpen
)
} }
> >
Join Join
@ -634,9 +675,6 @@ export const ListOfGroupPromotions = () => {
{promotion?.name} {promotion?.name}
</Typography> </Typography>
</Box> </Box>
</Box>
<Spacer height="20px"/>
<Typography <Typography
sx={{ sx={{
fontWight: 600, fontWight: 600,
@ -646,21 +684,28 @@ export const ListOfGroupPromotions = () => {
> >
{promotion?.groupName} {promotion?.groupName}
</Typography> </Typography>
</Box>
<Spacer height="20px" /> <Spacer height="20px" />
<Box sx={{ <Box
display: 'flex', sx={{
gap: '20px', display: "flex",
alignItems: 'center' gap: "20px",
}}> alignItems: "center",
}}
>
{promotion?.isOpen === false && ( {promotion?.isOpen === false && (
<LockIcon sx={{ <LockIcon
color: 'var(--green)' sx={{
}} /> color: "var(--green)",
}}
/>
)} )}
{promotion?.isOpen === true && ( {promotion?.isOpen === true && (
<NoEncryptionGmailerrorredIcon sx={{ <NoEncryptionGmailerrorredIcon
color: 'var(--danger)' sx={{
}} /> color: "var(--danger)",
}}
/>
)} )}
<Typography <Typography
sx={{ sx={{
@ -668,7 +713,9 @@ export const ListOfGroupPromotions = () => {
fontWeight: 600, fontWeight: 600,
}} }}
> >
{promotion?.isOpen ? 'Public group' : 'Private group' } {promotion?.isOpen
? "Public group"
: "Private group"}
</Typography> </Typography>
</Box> </Box>
<Spacer height="20px" /> <Spacer height="20px" />
@ -690,10 +737,13 @@ export const ListOfGroupPromotions = () => {
}} }}
> >
<Button <Button
onClick={(event) => handlePopoverOpen(event, promotion?.groupId)} // variant="contained"
onClick={(event) =>
handlePopoverOpen(event, promotion?.groupId)
}
sx={{ sx={{
fontSize: "12px", fontSize: "12px",
color: 'white' color: "white",
}} }}
> >
Join Group: {` ${promotion?.groupName}`} Join Group: {` ${promotion?.groupName}`}
@ -703,15 +753,15 @@ export const ListOfGroupPromotions = () => {
<Spacer height="50px" /> <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

@ -4324,3 +4324,96 @@ export const createGroupRequest = async (data, isFromExtension) => {
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 {