homepage, registername, userlookup, block, fixes

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

View File

@ -28,6 +28,8 @@ import {
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { decryptStoredWallet } from "./utils/decryptWallet"; import { decryptStoredWallet } from "./utils/decryptWallet";
import PersonSearchIcon from '@mui/icons-material/PersonSearch';
import { CountdownCircleTimer } from "react-countdown-circle-timer"; import { CountdownCircleTimer } from "react-countdown-circle-timer";
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";
@ -48,7 +50,8 @@ import 'react-json-view-lite/dist/index.css';
import HelpIcon from '@mui/icons-material/Help'; import HelpIcon from '@mui/icons-material/Help';
import EngineeringIcon from '@mui/icons-material/Engineering'; import EngineeringIcon from '@mui/icons-material/Engineering';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import { DrawerUserLookup } from "./components/Drawer/DrawerUserLookup";
import { UserLookup } from "./components/UserLookup.tsx/UserLookup";
import { import {
createAccount, createAccount,
@ -111,7 +114,7 @@ import { MainAvatar } from "./components/MainAvatar";
import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage"; import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage";
import { useQortalGetSaveSettings } from "./useQortalGetSaveSettings"; import { useQortalGetSaveSettings } from "./useQortalGetSaveSettings";
import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil"; import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil";
import { canSaveSettingToQdnAtom, fullScreenAtom, hasSettingsChangedAtom, isDisabledEditorEnterAtom, isUsingImportExportSettingsAtom, mailsAtom, oldPinnedAppsAtom, qMailLastEnteredTimestampAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from "./atoms/global"; import { canSaveSettingToQdnAtom, fullScreenAtom, groupsPropertiesAtom, hasSettingsChangedAtom, isDisabledEditorEnterAtom, isUsingImportExportSettingsAtom, mailsAtom, oldPinnedAppsAtom, qMailLastEnteredTimestampAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from "./atoms/global";
import { useAppFullScreen } from "./useAppFullscreen"; import { useAppFullScreen } from "./useAppFullscreen";
import { NotAuthenticated } from "./ExtStates/NotAuthenticated"; import { NotAuthenticated } from "./ExtStates/NotAuthenticated";
import { useFetchResources } from "./common/useFetchResources"; import { useFetchResources } from "./common/useFetchResources";
@ -127,6 +130,10 @@ import { Minting } from "./components/Minting/Minting";
import { isRunningGateway } from "./qortalRequests"; import { isRunningGateway } from "./qortalRequests";
import { QMailStatus } from "./components/QMailStatus"; import { QMailStatus } from "./components/QMailStatus";
import { GlobalActions } from "./components/GlobalActions/GlobalActions"; import { GlobalActions } from "./components/GlobalActions/GlobalActions";
import { RegisterName } from "./components/RegisterName";
import { BuyQortInformation } from "./components/BuyQortInformation";
import { WalletIcon } from "./assets/Icons/WalletIcon";
import { useBlockedAddresses } from "./components/Chat/useBlockUsers";
type extStates = type extStates =
| "not-authenticated" | "not-authenticated"
@ -310,6 +317,10 @@ function App() {
const [walletToBeDownloaded, setWalletToBeDownloaded] = useState<any>(null); const [walletToBeDownloaded, setWalletToBeDownloaded] = useState<any>(null);
const [walletToBeDownloadedPassword, setWalletToBeDownloadedPassword] = const [walletToBeDownloadedPassword, setWalletToBeDownloadedPassword] =
useState<string>(""); useState<string>("");
const {isUserBlocked,
addToBlockList,
removeBlockFromList, getAllBlockedUsers} = useBlockedAddresses()
const [isOpenDrawerLookup, setIsOpenDrawerLookup] = useState(false)
const [isMain, setIsMain] = useState<boolean>( const [isMain, setIsMain] = useState<boolean>(
window?.location?.href?.includes("?main=true") window?.location?.href?.includes("?main=true")
); );
@ -333,6 +344,11 @@ function App() {
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 { isShow: isShowUnsavedChanges, onCancel: onCancelUnsavedChanges, onOk: onOkUnsavedChanges, show: showUnsavedChanges, message: messageUnsavedChanges } = useModal(); const { isShow: isShowUnsavedChanges, onCancel: onCancelUnsavedChanges, onOk: onOkUnsavedChanges, show: showUnsavedChanges, message: messageUnsavedChanges } = useModal();
const balanceSetIntervalRef = useRef(null)
const [currentNode, setCurrentNode] = useState({
url: "http://127.0.0.1:12391",
});
const [useLocalNode, setUseLocalNode] = useState(false);
const {downloadResource} = useFetchResources() const {downloadResource} = useFetchResources()
const [showSeed, setShowSeed] = useState(false) const [showSeed, setShowSeed] = useState(false)
const [creationStep, setCreationStep] = useState(1) const [creationStep, setCreationStep] = useState(1)
@ -362,10 +378,6 @@ function App() {
message: messageQortalRequestExtension, message: messageQortalRequestExtension,
} = useModal(); } = useModal();
const [openRegisterName, setOpenRegisterName] = useState(false);
const registerNamePopoverRef = useRef(null);
const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false);
const [registerNameValue, setRegisterNameValue] = useState("");
const [infoSnack, setInfoSnack] = useState(null); const [infoSnack, setInfoSnack] = useState(null);
const [openSnack, setOpenSnack] = useState(false); const [openSnack, setOpenSnack] = useState(false);
const [hasLocalNode, setHasLocalNode] = useState(false); const [hasLocalNode, setHasLocalNode] = useState(false);
@ -418,6 +430,7 @@ function App() {
const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom); const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom);
const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom) const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom)
const resetAtomMailsAtom = useResetRecoilState(mailsAtom) const resetAtomMailsAtom = useResetRecoilState(mailsAtom)
const resetGroupPropertiesAtom = useResetRecoilState(groupsPropertiesAtom)
const resetAllRecoil = () => { const resetAllRecoil = () => {
resetAtomSortablePinnedAppsAtom(); resetAtomSortablePinnedAppsAtom();
resetAtomCanSaveSettingToQdnAtom(); resetAtomCanSaveSettingToQdnAtom();
@ -427,6 +440,7 @@ function App() {
resetAtomIsUsingImportExportSettingsAtom() resetAtomIsUsingImportExportSettingsAtom()
resetAtomQMailLastEnteredTimestampAtom() resetAtomQMailLastEnteredTimestampAtom()
resetAtomMailsAtom() resetAtomMailsAtom()
resetGroupPropertiesAtom()
}; };
useEffect(() => { useEffect(() => {
if (!isMobile) return; if (!isMobile) return;
@ -562,7 +576,7 @@ function App() {
} catch (e) { } catch (e) {
console.log(e); console.log(e);
error = e;
} }
}, },
}); });
@ -609,6 +623,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);
chrome?.runtime?.sendMessage({ action: "balance" }, (response) => { chrome?.runtime?.sendMessage({ action: "balance" }, (response) => {
@ -616,6 +654,8 @@ function App() {
setBalance(response); setBalance(response);
} }
setQortBalanceLoading(false); setQortBalanceLoading(false);
balanceSetInterval()
}); });
}; };
const getLtcBalanceFunc = () => { const getLtcBalanceFunc = () => {
@ -1176,6 +1216,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) {
@ -1332,106 +1375,91 @@ function App() {
}; };
}, []); }, []);
const registerName = async () => {
try {
if (!userInfo?.address) throw new Error("Your address was not found");
if(!registerNameValue) throw new Error('Enter a name')
const fee = await getFee("REGISTER_NAME");
await show({
message: "Would you like to register this name?",
publishFee: fee.fee + " QORT",
});
setIsLoadingRegisterName(true);
new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "registerName",
payload: {
name: registerNameValue,
},
},
(response) => {
if (!response?.error) {
res(response);
setIsLoadingRegisterName(false);
setInfoSnack({
type: "success",
message:
"Successfully registered. It may take a couple of minutes for the changes to propagate",
});
setOpenRegisterName(false);
setRegisterNameValue("");
setOpenSnack(true);
setTxList((prev) => [
{
...response,
type: "register-name",
label: `Registered name: awaiting confirmation. This may take a couple minutes.`,
labelDone: `Registered name: success!`,
done: false,
},
...prev.filter((item) => !item.done),
]);
return;
}
setInfoSnack({
type: "error",
message: response?.error,
});
setOpenSnack(true);
rej(response.error);
}
);
});
} catch (error) {
if (error?.message) {
setInfoSnack({
type: "error",
message: error?.message,
});
}
} finally {
setIsLoadingRegisterName(false);
}
};
const renderProfile = () => {
return (
<AuthenticatedContainer
sx={{
width: isMobile ? "100vw" : "auto",
display: "flex",
backgroundColor: "var(--bg-2)",
justifyContent: 'flex-end'
}}
>
{isMobile && (
<Box
sx={{
padding: "10px",
display: "flex",
justifyContent: "flex-end",
}}
>
<CloseIcon
onClick={() => {
setIsOpenDrawerProfile(false);
}}
sx={{
cursor: "pointer",
color: "white",
}}
/>
</Box>
)}
{desktopViewMode !== "apps" && desktopViewMode !== "dev" && desktopViewMode !== "chat" && ( const renderProfileLeft = ()=> {
<AuthenticatedContainerInnerLeft
return <AuthenticatedContainerInnerLeft
sx={{ sx={{
overflowY: isMobile && "auto", overflowY: isMobile && "auto",
padding: '0px 20px' padding: "0px 20px",
minWidth: "225px",
}} }}
> >
<Spacer height="20px" />
<Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'flex-start'
}}>
{authenticatedMode === "qort" && (
<Tooltip
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>LITECOIN WALLET</span>}
placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<img
onClick={() => {
setAuthenticatedMode("ltc");
}}
src={ltcLogo}
style={{
cursor: "pointer",
width: "20px",
height: "auto",
}}
/>
</Tooltip>
)}
{authenticatedMode === "ltc" && (
<Tooltip
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>QORTAL WALLET</span>}
placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<img
onClick={() => {
setAuthenticatedMode("qort");
}}
src={qortLogo}
style={{
cursor: "pointer",
width: "20px",
height: "auto",
}}
/>
</Tooltip>
)}
</Box>
<Spacer height="48px" /> <Spacer height="48px" />
{authenticatedMode === "ltc" ? ( {authenticatedMode === "ltc" ? (
@ -1480,7 +1508,7 @@ function App() {
</> </>
) : ( ) : (
<> <>
<MainAvatar myName={userInfo?.name} /> <MainAvatar setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} myName={userInfo?.name} balance={balance} />
<Spacer height="32px" /> <Spacer height="32px" />
<TextP <TextP
sx={{ sx={{
@ -1534,7 +1562,6 @@ function App() {
<Spacer height="35px" /> <Spacer height="35px" />
{userInfo && !userInfo?.name && ( {userInfo && !userInfo?.name && (
<TextP <TextP
ref={registerNamePopoverRef}
sx={{ sx={{
textAlign: "center", textAlign: "center",
lineHeight: 1.2, lineHeight: 1.2,
@ -1546,7 +1573,7 @@ function App() {
textDecoration: "underline", textDecoration: "underline",
}} }}
onClick={() => { onClick={() => {
setOpenRegisterName(true); executeEvent('openRegisterName', {})
}} }}
> >
REGISTER NAME REGISTER NAME
@ -1575,19 +1602,61 @@ function App() {
marginTop: "10px", marginTop: "10px",
textDecoration: "underline", textDecoration: "underline",
}} }}
onClick={() => { onClick={async () => {
executeEvent("addTab", { data: { service: 'APP', name: 'q-trade' } }); executeEvent("addTab", {
data: { service: "APP", name: "q-trade" },
});
executeEvent("open-apps-mode", {}); executeEvent("open-apps-mode", {});
}} }}
> >
Get QORT at Q-Trade Get QORT at Q-Trade
</TextP> </TextP>
</AuthenticatedContainerInnerLeft> </AuthenticatedContainerInnerLeft>
}
const renderProfile = () => {
return (
<AuthenticatedContainer
sx={{
width: isMobile ? "100vw" : "auto",
display: "flex",
backgroundColor: "var(--bg-2)",
justifyContent: "flex-end",
}}
>
{isMobile && (
<Box
sx={{
padding: "10px",
display: "flex",
justifyContent: "flex-end",
}}
>
<CloseIcon
onClick={() => {
setIsOpenDrawerProfile(false);
}}
sx={{
cursor: "pointer",
color: "white",
}}
/>
</Box>
)} )}
<AuthenticatedContainerInnerRight sx={{ {desktopViewMode !== "apps" &&
desktopViewMode !== "dev" &&
desktopViewMode !== "chat" && (
<>
{renderProfileLeft()}
</>
)}
<AuthenticatedContainerInnerRight
sx={{
height: "100%", height: "100%",
justifyContent: "space-between", justifyContent: "space-between",
}}> }}
>
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
@ -1601,6 +1670,25 @@ function App() {
{!isMobile && ( {!isMobile && (
<> <>
<Spacer height="20px" /> <Spacer height="20px" />
<Tooltip
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>LOG OUT</span>}
placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<img <img
src={Logout} src={Logout}
onClick={() => { onClick={() => {
@ -1609,8 +1697,11 @@ function App() {
}} }}
style={{ style={{
cursor: "pointer", cursor: "pointer",
width: '20px',
height: 'auto'
}} }}
/> />
</Tooltip>
</> </>
)} )}
<Spacer height="20px" /> <Spacer height="20px" />
@ -1619,17 +1710,124 @@ function App() {
onClick={() => { onClick={() => {
setIsSettingsOpen(true); setIsSettingsOpen(true);
}} }}
>
<Tooltip
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>SETTINGS</span>}
placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
> >
<SettingsIcon <SettingsIcon
sx={{ sx={{
color: "rgba(255, 255, 255, 0.5)", color: "rgba(255, 255, 255, 0.5)",
}} }}
/> />
</Tooltip>
</ButtonBase> </ButtonBase>
<Spacer height="20px" /> <Spacer height="20px" />
{authenticatedMode === "qort" && ( <ButtonBase
onClick={() => {
setIsOpenDrawerLookup(true);
}}
>
<Tooltip
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>USER LOOKUP</span>}
placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<PersonSearchIcon
sx={{
color: "rgba(255, 255, 255, 0.5)",
}}
/>
</Tooltip>
</ButtonBase>
{desktopViewMode !== 'home' && (
<>
<Spacer height="20px" />
<Tooltip
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>WALLET</span>}
placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<ButtonBase onClick={() => {
setIsOpenDrawerProfile(true);
}}>
<WalletIcon width={25} color="rgba(250, 250, 250, 0.5)" />
</ButtonBase>
</Tooltip>
</>
)}
{/* {authenticatedMode === "qort" && (
<Tooltip
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>LITECOIN WALLET</span>}
placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<img <img
onClick={() => { onClick={() => {
if(desktopViewMode !== 'home'){
setIsOpenDrawerProfile((prev)=> !prev)
}
setAuthenticatedMode("ltc"); setAuthenticatedMode("ltc");
}} }}
src={ltcLogo} src={ltcLogo}
@ -1639,8 +1837,28 @@ function App() {
height: "auto", height: "auto",
}} }}
/> />
</Tooltip>
)} )}
{authenticatedMode === "ltc" && ( {authenticatedMode === "ltc" && (
<Tooltip
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>QORTAL WALLET</span>}
placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<img <img
onClick={() => { onClick={() => {
setAuthenticatedMode("qort"); setAuthenticatedMode("qort");
@ -1652,7 +1870,8 @@ function App() {
height: "auto", height: "auto",
}} }}
/> />
)} </Tooltip>
)} */}
<Spacer height="20px" /> <Spacer height="20px" />
<CoreSyncStatus /> <CoreSyncStatus />
<Spacer height="20px" /> <Spacer height="20px" />
@ -1686,11 +1905,16 @@ function App() {
infoSnackCustom: infoSnack, infoSnackCustom: infoSnack,
setInfoSnackCustom: setInfoSnack, setInfoSnackCustom: setInfoSnack,
downloadResource, downloadResource,
getIndividualUserInfo getIndividualUserInfo,
isUserBlocked,
addToBlockList,
removeBlockFromList,
getAllBlockedUsers
}} }}
> >
<TaskManger getUserInfo={getUserInfo} /> <TaskManger getUserInfo={getUserInfo} />
<GlobalActions memberGroups={memberGroups} /> <GlobalActions memberGroups={memberGroups} />
</MyContext.Provider> </MyContext.Provider>
)} )}
<Spacer height="20px" /> <Spacer height="20px" />
@ -1708,30 +1932,82 @@ function App() {
}) })
} }
}}> }}>
<EngineeringIcon sx={{ <Tooltip
color: 'var(--unread)' title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>MINTING STATUS</span>}
}} /> placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<EngineeringIcon sx={{ color: 'var(--unread)' }} />
</Tooltip>
</ButtonBase> </ButtonBase>
<Spacer height="20px" /> <Spacer height="20px" />
{(desktopViewMode === "apps" || desktopViewMode === "home") && ( {(desktopViewMode === "apps" || desktopViewMode === "home") && (
<ButtonBase onClick={()=> { <ButtonBase onClick={()=> {
if(desktopViewMode === "apps"){ if(desktopViewMode === "apps"){
showTutorial('qapps', true) showTutorial('qapps', true)
} else { } else {
showTutorial('getting-started', true) showTutorial('getting-started', true)
} }
}} > }} >
<HelpIcon sx={{ <Tooltip
color: 'var(--unread)' title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>TUTORIAL</span>}
}} /> placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<HelpIcon sx={{ color: 'var(--unread)' }} />
</Tooltip>
</ButtonBase> </ButtonBase>
)} )}
<Spacer height="20px" /> <Spacer height="20px" />
<Tooltip
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>BACKUP WALLET</span>}
placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<img <img
onClick={() => { onClick={() => {
setExtstate("download-wallet"); setExtstate("download-wallet");
@ -1740,8 +2016,10 @@ function App() {
src={Download} src={Download}
style={{ style={{
cursor: "pointer", cursor: "pointer",
width: '20px'
}} }}
/> />
</Tooltip>
<Spacer height="40px" /> <Spacer height="40px" />
</Box> </Box>
</AuthenticatedContainerInnerRight> </AuthenticatedContainerInnerRight>
@ -1769,7 +2047,10 @@ function App() {
<Tutorials /> <Tutorials />
{extState === "not-authenticated" && ( {extState === "not-authenticated" && (
<NotAuthenticated getRootProps={getRootProps} getInputProps={getInputProps} setExtstate={setExtstate} apiKey={apiKey} globalApiKey={globalApiKey} setApiKey={setApiKey} handleSetGlobalApikey={handleSetGlobalApikey}/> <NotAuthenticated getRootProps={getRootProps} getInputProps={getInputProps} setExtstate={setExtstate} apiKey={apiKey} globalApiKey={globalApiKey} setApiKey={setApiKey} handleSetGlobalApikey={handleSetGlobalApikey} currentNode={currentNode}
setCurrentNode={setCurrentNode}
setUseLocalNode={setUseLocalNode}
useLocalNode={useLocalNode}/>
)} )}
{/* {extState !== "not-authenticated" && ( {/* {extState !== "not-authenticated" && (
<button onClick={logoutFunc}>logout</button> <button onClick={logoutFunc}>logout</button>
@ -1793,7 +2074,11 @@ function App() {
setOpenSnackGlobal: setOpenSnack, setOpenSnackGlobal: setOpenSnack,
infoSnackCustom: infoSnack, infoSnackCustom: infoSnack,
setInfoSnackCustom: setInfoSnack, setInfoSnackCustom: setInfoSnack,
getIndividualUserInfo getIndividualUserInfo,
isUserBlocked,
addToBlockList,
removeBlockFromList,
getAllBlockedUsers
}} }}
> >
<Box <Box
@ -1833,7 +2118,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" />
@ -2520,6 +2805,29 @@ function App() {
}} }}
ref={passwordRef} ref={passwordRef}
/> />
{useLocalNode ? (
<>
<Spacer height="20px" />
<Typography
sx={{
fontSize: "12px",
}}
>
{"Using node: "} {currentNode?.url}
</Typography>
</>
) : (
<>
<Spacer height="20px" />
<Typography
sx={{
fontSize: "12px",
}}
>
{"Using public node"}
</Typography>
</>
)}
<Spacer height="20px" /> <Spacer height="20px" />
<CustomButton onClick={authenticateWallet}> <CustomButton onClick={authenticateWallet}>
Authenticate Authenticate
@ -2855,7 +3163,7 @@ function App() {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
zIndex: 6, zIndex: 10000,
}} }}
> >
<Spacer height="48px" /> <Spacer height="48px" />
@ -2955,6 +3263,9 @@ function App() {
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>
@ -3027,7 +3338,7 @@ function App() {
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}
@ -3068,7 +3379,6 @@ function App() {
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'flex-start', justifyContent: 'flex-start',
minHeight: '400px',
maxHeight: '90vh', maxHeight: '90vh',
overflow: 'auto' overflow: 'auto'
}}> }}>
@ -3310,52 +3620,7 @@ function App() {
</Box> </Box>
</Dialog> </Dialog>
)} )}
<Popover
open={openRegisterName}
anchorEl={registerNamePopoverRef.current}
onClose={() => {
setOpenRegisterName(false);
setRegisterNameValue("");
}}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
style={{ marginTop: "8px" }}
>
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
}}
>
<Label>Choose a name</Label>
<Input
onChange={(e) => setRegisterNameValue(e.target.value)}
value={registerNameValue}
placeholder="Choose a name"
/>
<Spacer height="25px" />
<LoadingButton
loading={isLoadingRegisterName}
loadingPosition="start"
variant="contained"
disabled={!registerNameValue}
onClick={registerName}
>
Register Name
</LoadingButton>
</Box>
</Popover>
{isSettingsOpen && ( {isSettingsOpen && (
<Settings open={isSettingsOpen} setOpen={setIsSettingsOpen} /> <Settings open={isSettingsOpen} setOpen={setIsSettingsOpen} />
)} )}
@ -3369,8 +3634,11 @@ function App() {
open={isOpenDrawerProfile} open={isOpenDrawerProfile}
setOpen={setIsOpenDrawerProfile} setOpen={setIsOpenDrawerProfile}
> >
{renderProfile()} {renderProfileLeft()}
</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

@ -4,6 +4,7 @@ import { CustomButton, TextItalic, TextP, TextSpan } from "../App-styles";
import { import {
Box, Box,
Button, Button,
ButtonBase,
Checkbox, Checkbox,
Dialog, Dialog,
DialogActions, DialogActions,
@ -11,24 +12,24 @@ import {
DialogTitle, DialogTitle,
FormControlLabel, FormControlLabel,
Input, Input,
Switch,
Tooltip,
Typography,
ButtonBase,
styled, styled,
tooltipClasses, Switch,
TooltipProps Typography,
} from "@mui/material"; } from "@mui/material";
import Logo1 from "../assets/svgs/Logo1.svg"; import Logo1 from "../assets/svgs/Logo1.svg";
import Logo1Dark from "../assets/svgs/Logo1Dark.svg"; import Logo1Dark from "../assets/svgs/Logo1Dark.svg";
import Info from "../assets/svgs/Info.svg"; import Info from "../assets/svgs/Info.svg";
import HelpIcon from '@mui/icons-material/Help';
import { CustomizedSnackbars } from "../components/Snackbar/Snackbar"; import { CustomizedSnackbars } from "../components/Snackbar/Snackbar";
import { set } from "lodash"; import { set } from "lodash";
import { cleanUrl, isUsingLocal } from "../background"; import { cleanUrl, gateways, isUsingLocal } from "../background";
import HelpIcon from '@mui/icons-material/Help';
import { GlobalContext } from "../App"; import { GlobalContext } from "../App";
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
const manifestData = {
version: "0.5.2",
};
const manifestData = chrome?.runtime?.getManifest();
export const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => ( export const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} /> <Tooltip {...props} classes={{ popper: className }} />
@ -41,40 +42,47 @@ export const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => (
fontSize: theme.typography.pxToRem(12), fontSize: theme.typography.pxToRem(12),
}, },
})); }));
function removeTrailingSlash(url) {
return url.replace(/\/+$/, '');
}
export const NotAuthenticated = ({ export const NotAuthenticated = ({
getRootProps, getRootProps,
getInputProps, getInputProps,
setExtstate, setExtstate,
apiKey, apiKey,
setApiKey, setApiKey,
globalApiKey, globalApiKey,
handleSetGlobalApikey, handleSetGlobalApikey,
currentNode,
setCurrentNode,
useLocalNode,
setUseLocalNode
}) => { }) => {
const [isValidApiKey, setIsValidApiKey] = useState<boolean | null>(null); const [isValidApiKey, setIsValidApiKey] = useState<boolean | null>(null);
const [hasLocalNode, setHasLocalNode] = useState<boolean | null>(null); const [hasLocalNode, setHasLocalNode] = useState<boolean | null>(null);
const [useLocalNode, setUseLocalNode] = useState(false); // const [useLocalNode, setUseLocalNode] = useState(false);
const [openSnack, setOpenSnack] = React.useState(false); const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null); const [infoSnack, setInfoSnack] = React.useState(null);
const [show, setShow] = React.useState(false); const [show, setShow] = React.useState(false);
const [mode, setMode] = React.useState("list"); const [mode, setMode] = React.useState("list");
const [customNodes, setCustomNodes] = React.useState(null); const [customNodes, setCustomNodes] = React.useState(null);
const [currentNode, setCurrentNode] = React.useState({ // const [currentNode, setCurrentNode] = React.useState({
url: "http://127.0.0.1:12391", // url: "http://127.0.0.1:12391",
}); // });
const [importedApiKey, setImportedApiKey] = React.useState(null); const [importedApiKey, setImportedApiKey] = React.useState(null);
//add and edit states //add and edit states
const [url, setUrl] = React.useState("http://"); const [url, setUrl] = React.useState("https://");
const [customApikey, setCustomApiKey] = React.useState(""); const [customApikey, setCustomApiKey] = React.useState("");
const [customNodeToSaveIndex, setCustomNodeToSaveIndex] = const [customNodeToSaveIndex, setCustomNodeToSaveIndex] =
React.useState(null); React.useState(null);
const importedApiKeyRef = useRef(null)
const currentNodeRef = useRef(null)
const hasLocalNodeRef = useRef(null)
const { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext); const { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext);
const importedApiKeyRef = useRef(null);
const currentNodeRef = useRef(null);
const hasLocalNodeRef = useRef(null);
const isLocal = cleanUrl(currentNode?.url) === "127.0.0.1:12391"; const isLocal = cleanUrl(currentNode?.url) === "127.0.0.1:12391";
const handleFileChangeApiKey = (event) => { const handleFileChangeApiKey = (event) => {
const file = event.target.files[0]; // Get the selected file const file = event.target.files[0]; // Get the selected file
@ -84,13 +92,34 @@ export const NotAuthenticated = ({
const text = e.target.result; // Get the file content const text = e.target.result; // Get the file content
setImportedApiKey(text); // Store the file content in the state setImportedApiKey(text); // Store the file content in the state
if(customNodes){
setCustomNodes((prev)=> {
const copyPrev = [...prev]
const findLocalIndex = copyPrev?.findIndex((item)=> item?.url === 'http://127.0.0.1:12391')
if(findLocalIndex === -1){
copyPrev.unshift({
url: "http://127.0.0.1:12391",
apikey: text
})
} else {
copyPrev[findLocalIndex] = {
url: "http://127.0.0.1:12391",
apikey: text
}
}
chrome?.runtime?.sendMessage(
{ action: "setCustomNodes", copyPrev }
);
return copyPrev
})
}
}; };
reader.readAsText(file); // Read the file as text reader.readAsText(file); // Read the file as text
} }
}; };
const checkIfUserHasLocalNode = useCallback(async () => { const checkIfUserHasLocalNode = useCallback(async () => {
try { try {
const url = `http://127.0.0.1:12391/admin/status`; const url = `http://127.0.0.1:12391/admin/status`;
@ -103,49 +132,104 @@ 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 false
} catch (error) {
return false
} }
} catch (error) {}
}, []); }, []);
useEffect(() => { useEffect(() => {
checkIfUserHasLocalNode(); checkIfUserHasLocalNode();
}, []); }, []);
useEffect(() => { useEffect(() => {
chrome?.runtime?.sendMessage( chrome?.runtime?.sendMessage(
{ action: "getCustomNodesFromStorage" }, { action: "getCustomNodesFromStorage" },
(response) => { (response) => {
if (response) { if (response) {
setCustomNodes(response || []); setCustomNodes(response || []);
if(Array.isArray(response)){
const findLocal = response?.find((item)=> item?.url === 'http://127.0.0.1:12391')
if(findLocal && findLocal?.apikey){
setImportedApiKey(findLocal?.apikey)
}
}
} }
} }
); );
}, []); }, []);
useEffect(() => { useEffect(() => {
importedApiKeyRef.current = importedApiKey importedApiKeyRef.current = importedApiKey;
}, [importedApiKey]) }, [importedApiKey]);
useEffect(() => { useEffect(() => {
currentNodeRef.current = currentNode currentNodeRef.current = currentNode;
}, [currentNode]) }, [currentNode]);
useEffect(() => { useEffect(() => {
hasLocalNodeRef.current = hasLocalNode hasLocalNodeRef.current = hasLocalNode;
}, [hasLocalNode]) }, [hasLocalNode]);
const validateApiKey = useCallback(async (key, fromStartUp) => { const validateApiKey = useCallback(async (key, fromStartUp) => {
try { try {
if(!currentNodeRef.current) return if(key === "isGateway") return
const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391"; const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391";
if(isLocalKey && !hasLocalNodeRef.current && !fromStartUp){ if (fromStartUp && key?.url && key?.apikey && !isLocalKey && !gateways.some(gateway => key?.url?.includes(gateway))) {
throw new Error('Please turn on your local node') setCurrentNode({
url: key?.url,
apikey: key?.apikey,
});
let isValid = false
const url = `${key?.url}/admin/settings/localAuthBypassEnabled`;
const response = await fetch(url);
// Assuming the response is in plain text and will be 'true' or 'false'
const data = await response.text();
if(data && data === 'true'){
isValid = true
} else {
const url2 = `${key?.url}/admin/apikey/test?apiKey=${key?.apikey}`;
const response2 = await fetch(url2);
// Assuming the response is in plain text and will be 'true' or 'false'
const data2 = await response2.text();
if (data2 === "true") {
isValid = true
}
}
if (isValid) {
setIsValidApiKey(true);
setUseLocalNode(true);
return
}
} }
const isCurrentNodeLocal = cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391"; if (!currentNodeRef.current) return;
const stillHasLocal = await checkIfUserHasLocalNode()
if (isLocalKey && !stillHasLocal && !fromStartUp) {
throw new Error("Please turn on your local node");
}
//check custom nodes
// !gateways.some(gateway => apiKey?.url?.includes(gateway))
const isCurrentNodeLocal =
cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391";
if (isLocalKey && !isCurrentNodeLocal) { if (isLocalKey && !isCurrentNodeLocal) {
setIsValidApiKey(false); setIsValidApiKey(false);
setUseLocalNode(false); setUseLocalNode(false);
return return;
} }
let payload = {}; let payload = {};
@ -157,18 +241,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) {
chrome?.runtime?.sendMessage( chrome?.runtime?.sendMessage(
{ action: "setApiKey", payload }, { action: "setApiKey", payload },
(response) => { (response) => {
@ -177,28 +272,48 @@ export const NotAuthenticated = ({
setIsValidApiKey(true); setIsValidApiKey(true);
setUseLocalNode(true); setUseLocalNode(true);
if (!fromStartUp) { if (!fromStartUp) {
setApiKey(payload) setApiKey(payload);
} }
} }
} }
); )
} else { } else {
setIsValidApiKey(false); setIsValidApiKey(false);
setUseLocalNode(false); setUseLocalNode(false);
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) {
setCurrentNode({
url: "http://127.0.0.1:12391",
});
chrome?.runtime?.sendMessage(
{ action: "setApiKey", payload: "isGateway" },
(response) => {
if (response) {
setApiKey(null);
handleSetGlobalApikey(null);
}
}
)
return
}
if(!fromStartUp){
setInfoSnack({ setInfoSnack({
type: "error", type: "error",
message: error?.message || "Select a valid apikey", message: error?.message || "Select a valid apikey",
}); });
setOpenSnack(true); setOpenSnack(true);
}
console.error("Error validating API key:", error); console.error("Error validating API key:", error);
} }
}, []); }, []);
@ -212,22 +327,22 @@ export const NotAuthenticated = ({
const addCustomNode = () => { const addCustomNode = () => {
setMode("add-node"); setMode("add-node");
}; };
const saveCustomNodes = (myNodes, isFullListOfNodes) => {
const saveCustomNodes = (myNodes) => {
let nodes = [...(myNodes || [])]; let nodes = [...(myNodes || [])];
if (customNodeToSaveIndex !== null) { if (!isFullListOfNodes && customNodeToSaveIndex !== null) {
nodes.splice(customNodeToSaveIndex, 1, { nodes.splice(customNodeToSaveIndex, 1, {
url, url: removeTrailingSlash(url),
apikey: customApikey, apikey: customApikey,
}); });
} else if (url && customApikey) { } else if (!isFullListOfNodes && url) {
nodes.push({ nodes.push({
url, url: removeTrailingSlash(url),
apikey: customApikey, apikey: customApikey,
}); });
} }
setCustomNodes(nodes); setCustomNodes(nodes);
setCustomNodeToSaveIndex(null); setCustomNodeToSaveIndex(null);
if (!nodes) return; if (!nodes) return;
chrome?.runtime?.sendMessage( chrome?.runtime?.sendMessage(
@ -235,14 +350,14 @@ export const NotAuthenticated = ({
(response) => { (response) => {
if (response) { if (response) {
setMode("list"); setMode("list");
setUrl("http://"); setUrl("https://");
setCustomApiKey(""); setCustomApiKey("");
// add alert // add alert
} }
} }
); );
};
};
return ( return (
<> <>
@ -264,13 +379,12 @@ export const NotAuthenticated = ({
fontSize: '18px' fontSize: '18px'
}} }}
> >
WELCOME TO <TextItalic sx={{ WELCOME TO
fontSize: '18px'
}}>YOUR</TextItalic> <br></br>
<TextSpan sx={{ <TextSpan sx={{
fontSize: '18px' fontSize: '18px'
}}> QORTAL WALLET</TextSpan> }}> QORTAL</TextSpan>
</TextP> </TextP>
<Spacer height="30px" /> <Spacer height="30px" />
<Box <Box
sx={{ sx={{
@ -291,9 +405,13 @@ export const NotAuthenticated = ({
} }
> >
<CustomButton onClick={()=> setExtstate('wallets')}> <CustomButton onClick={()=> setExtstate('wallets')}>
Wallets {/* <input {...getInputProps()} /> */}
Accounts
</CustomButton> </CustomButton>
</HtmlTooltip> </HtmlTooltip>
{/* <Tooltip title="Authenticate by importing your Qortal JSON file" arrow>
<img src={Info} />
</Tooltip> */}
</Box> </Box>
<Spacer height="6px" /> <Spacer height="6px" />
@ -302,6 +420,7 @@ export const NotAuthenticated = ({
display: "flex", display: "flex",
gap: "10px", gap: "10px",
alignItems: "center", alignItems: "center",
}} }}
> >
<HtmlTooltip <HtmlTooltip
@ -333,7 +452,7 @@ export const NotAuthenticated = ({
} }
}} }}
> >
Create wallet Create account
</CustomButton> </CustomButton>
</HtmlTooltip> </HtmlTooltip>
@ -343,7 +462,7 @@ export const NotAuthenticated = ({
<Typography <Typography
sx={{ sx={{
fontSize: "12px", fontSize: "12px",
visibility: !useLocalNode && 'hidden' visibility: !useLocalNode && "hidden",
}} }}
> >
{"Using node: "} {currentNode?.url} {"Using node: "} {currentNode?.url}
@ -399,26 +518,24 @@ export const NotAuthenticated = ({
} else { } else {
setCurrentNode({ setCurrentNode({
url: "http://127.0.0.1:12391", url: "http://127.0.0.1:12391",
}) });
setUseLocalNode(false) setUseLocalNode(false);
chrome?.runtime?.sendMessage( chrome?.runtime?.sendMessage(
{ action: "setApiKey", payload: null }, { action: "setApiKey", payload: null },
(response) => { (response) => {
if (response) { if (response) {
setApiKey(null); setApiKey(null);
handleSetGlobalApikey(null); handleSetGlobalApikey(null);
} }
} }
); )
} }
}} }}
disabled={false} disabled={false}
defaultChecked defaultChecked
/> />
} }
label={`Use ${isLocal ? 'Local' : 'Custom'} Node`} label={`Use ${isLocal ? "Local" : "Custom"} Node`}
/> />
</Box> </Box>
{currentNode?.url === "http://127.0.0.1:12391" && ( {currentNode?.url === "http://127.0.0.1:12391" && (
@ -432,14 +549,12 @@ export const NotAuthenticated = ({
onChange={handleFileChangeApiKey} // File input handler onChange={handleFileChangeApiKey} // File input handler
/> />
</Button> </Button>
<Typography sx={{ <Typography
fontSize: '12px', sx={{
visibility: importedApiKey ? 'visible' : 'hidden' fontSize: "12px",
}}>{`api key : ${importedApiKey}`}</Typography> visibility: importedApiKey ? "visible" : "hidden",
}}
>{`api key : ${importedApiKey}`}</Typography>
</> </>
)} )}
<Button <Button
@ -453,10 +568,14 @@ export const NotAuthenticated = ({
Choose custom node Choose custom node
</Button> </Button>
</> </>
<Typography sx={{ <Typography
sx={{
color: "white", color: "white",
fontSize: '12px' fontSize: "12px",
}}>Build version: {manifestData?.version}</Typography> }}
>
Build version: {manifestData?.version}
</Typography>
</Box> </Box>
</> </>
<CustomizedSnackbars <CustomizedSnackbars
@ -483,7 +602,6 @@ export const NotAuthenticated = ({
flexDirection: "column", flexDirection: "column",
}} }}
> >
{mode === "list" && ( {mode === "list" && (
<Box <Box
sx={{ sx={{
@ -531,10 +649,9 @@ export const NotAuthenticated = ({
if (response) { if (response) {
setApiKey(null); setApiKey(null);
handleSetGlobalApikey(null); handleSetGlobalApikey(null);
} }
} }
); )
}} }}
variant="contained" variant="contained"
> >
@ -586,10 +703,9 @@ export const NotAuthenticated = ({
if (response) { if (response) {
setApiKey(null); setApiKey(null);
handleSetGlobalApikey(null); handleSetGlobalApikey(null);
} }
} }
); )
}} }}
variant="contained" variant="contained"
> >
@ -614,8 +730,7 @@ export const NotAuthenticated = ({
...(customNodes || []), ...(customNodes || []),
].filter((item) => item?.url !== node?.url); ].filter((item) => item?.url !== node?.url);
saveCustomNodes(nodesToSave, true);
saveCustomNodes(nodesToSave);
}} }}
variant="contained" variant="contained"
> >
@ -652,9 +767,7 @@ export const NotAuthenticated = ({
/> />
</Box> </Box>
)} )}
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
{mode === "list" && ( {mode === "list" && (
@ -690,7 +803,7 @@ export const NotAuthenticated = ({
<Button <Button
variant="contained" variant="contained"
disabled={!customApikey || !url} disabled={!url}
onClick={() => saveCustomNodes(customNodes)} onClick={() => saveCustomNodes(customNodes)}
autoFocus autoFocus
> >

View File

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

View File

@ -2,7 +2,7 @@ import React from 'react';
export const WalletIcon= ({ color, height, width }) => { export const WalletIcon= ({ color, height, width }) => {
return ( return (
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width={width || 30} height={width || 30} viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.0118 22.0891C18.0124 22.8671 16.6997 23.3391 15.2618 23.3391C13.8241 23.3391 12.5113 22.8671 11.5118 22.0891" stroke={color} stroke-width="2" stroke-linecap="round"/> <path d="M19.0118 22.0891C18.0124 22.8671 16.6997 23.3391 15.2618 23.3391C13.8241 23.3391 12.5113 22.8671 11.5118 22.0891" stroke={color} stroke-width="2" stroke-linecap="round"/>
<path d="M3.20108 17.356C2.7598 14.4844 2.53917 13.0486 3.08205 11.7758C3.62493 10.503 4.82938 9.63215 7.23827 7.89044L9.03808 6.58911C12.0347 4.42245 13.5331 3.33911 15.2618 3.33911C16.9907 3.33911 18.4889 4.42245 21.4856 6.58911L23.2854 7.89044C25.6943 9.63215 26.8988 10.503 27.4417 11.7758C27.9846 13.0486 27.7639 14.4844 27.3226 17.356L26.9463 19.8046C26.3208 23.8752 26.0079 25.9106 24.5481 27.1249C23.0882 28.3391 20.9539 28.3391 16.6853 28.3391H13.8383C9.56977 28.3391 7.43548 28.3391 5.97559 27.1249C4.5157 25.9106 4.20293 23.8752 3.57738 19.8046L3.20108 17.356Z" stroke={color} stroke-width="2" stroke-linejoin="round"/> <path d="M3.20108 17.356C2.7598 14.4844 2.53917 13.0486 3.08205 11.7758C3.62493 10.503 4.82938 9.63215 7.23827 7.89044L9.03808 6.58911C12.0347 4.42245 13.5331 3.33911 15.2618 3.33911C16.9907 3.33911 18.4889 4.42245 21.4856 6.58911L23.2854 7.89044C25.6943 9.63215 26.8988 10.503 27.4417 11.7758C27.9846 13.0486 27.7639 14.4844 27.3226 17.356L26.9463 19.8046C26.3208 23.8752 26.0079 25.9106 24.5481 27.1249C23.0882 28.3391 20.9539 28.3391 16.6853 28.3391H13.8383C9.56977 28.3391 7.43548 28.3391 5.97559 27.1249C4.5157 25.9106 4.20293 23.8752 3.57738 19.8046L3.20108 17.356Z" stroke={color} stroke-width="2" stroke-linejoin="round"/>
</svg> </svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -157,3 +157,8 @@ export const mailsAtom = atom({
key: 'mailsAtom', key: 'mailsAtom',
default: [], default: [],
}); });
export const groupsPropertiesAtom = atom({
key: 'groupsPropertiesAtom',
default: {},
});

View File

@ -1,8 +1,12 @@
import { getKeyPair, getLastRef, processTransactionVersion2 } from "./background"; import {
createEndpoint,
getKeyPair,
getLastRef,
processTransactionVersion2,
} from "./background";
import Base58 from "./deps/Base58"; import Base58 from "./deps/Base58";
import { createTransaction } from "./transactions/transactions"; import { createTransaction } from "./transactions/transactions";
export async function createRewardShareCase(data) { export async function createRewardShareCase(data) {
const { recipientPublicKey } = data; const { recipientPublicKey } = data;
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
@ -26,12 +30,10 @@ export async function createRewardShareCase(data) {
const res = await processTransactionVersion2(signedBytes); const res = await processTransactionVersion2(signedBytes);
if (!res?.signature) if (!res?.signature)
throw new Error("Transaction was not able to be processed"); throw new Error("Transaction was not able to be processed");
return res return res;
} }
export async function removeRewardShareCase(data) { export async function removeRewardShareCase(data) {
const { rewardShareKeyPairPublicKey, recipient, percentageShare } = data; const { rewardShareKeyPairPublicKey, recipient, percentageShare } = data;
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
const parsedData = JSON.parse(resKeyPair); const parsedData = JSON.parse(resKeyPair);
@ -55,13 +57,11 @@ export async function createRewardShareCase(data) {
const res = await processTransactionVersion2(signedBytes); const res = await processTransactionVersion2(signedBytes);
if (!res?.signature) if (!res?.signature)
throw new Error("Transaction was not able to be processed"); throw new Error("Transaction was not able to be processed");
return res return res;
} }
export async function getRewardSharePrivateKeyCase(data) { export async function getRewardSharePrivateKeyCase(data) {
const {recipientPublicKey} = data const { recipientPublicKey } = data;
const resKeyPair = await getKeyPair(); const resKeyPair = await getKeyPair();
const parsedData = JSON.parse(resKeyPair); const parsedData = JSON.parse(resKeyPair);
const uint8PrivateKey = Base58.decode(parsedData.privateKey); const uint8PrivateKey = Base58.decode(parsedData.privateKey);
@ -78,5 +78,65 @@ export async function createRewardShareCase(data) {
lastReference: lastRef, lastReference: lastRef,
}); });
return tx?._base58RewardShareSeed return tx?._base58RewardShareSeed;
}
export async function listActionsCase(data) {
const { type, listName = "", items = [] } = data;
let responseData;
if (type === "get") {
const url = await createEndpoint(`/lists/${listName}`);
const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch");
responseData = await response.json();
} else if (type === "remove") {
const url = await createEndpoint(`/lists/${listName}`);
const body = {
items: items,
};
const bodyToString = JSON.stringify(body);
const response = await fetch(url, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: bodyToString,
});
if (!response.ok) throw new Error("Failed to remove from list");
let res;
try {
res = await response.clone().json();
} catch (e) {
res = await response.text();
}
responseData = res;
} else if (type === "add") {
const url = await createEndpoint(`/lists/${listName}`);
const body = {
items: items,
};
const bodyToString = JSON.stringify(body);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: bodyToString,
});
if (!response.ok) throw new Error("Failed to add to list");
let res;
try {
res = await response.clone().json();
} catch (e) {
res = await response.text();
}
responseData = res;
}
return responseData;
} }

View File

@ -32,7 +32,7 @@ import { Sha256 } from "asmcrypto.js";
import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest"; import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest";
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes"; import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes";
import TradeBotRespondRequest from './transactions/TradeBotRespondRequest'; import TradeBotRespondRequest from './transactions/TradeBotRespondRequest';
import { createRewardShareCase, getRewardSharePrivateKeyCase, removeRewardShareCase } from './background-cases'; import { createRewardShareCase, getRewardSharePrivateKeyCase, listActionsCase, removeRewardShareCase } from './background-cases';
@ -551,6 +551,7 @@ const handleNotification = async (groups) => {
let mutedGroups = await getUserSettings({key: 'mutedGroups'}) || [] let mutedGroups = await getUserSettings({key: 'mutedGroups'}) || []
if(!isArray(mutedGroups)) mutedGroups = [] if(!isArray(mutedGroups)) mutedGroups = []
mutedGroups.push('0')
let isFocused; let isFocused;
const data = groups.filter((group) => group?.sender !== address && !mutedGroups.includes(group.groupId) && !isUpdateMsg(group?.data)); const data = groups.filter((group) => group?.sender !== address && !mutedGroups.includes(group.groupId) && !isUpdateMsg(group?.data));
@ -832,6 +833,7 @@ const checkNewMessages = async () => {
try { try {
let mutedGroups = await getUserSettings({key: 'mutedGroups'}) || [] let mutedGroups = await getUserSettings({key: 'mutedGroups'}) || []
if(!isArray(mutedGroups)) mutedGroups = [] if(!isArray(mutedGroups)) mutedGroups = []
mutedGroups.push('0')
let myName = ""; let myName = "";
const userData = await getUserInfo(); const userData = await getUserInfo();
if (userData?.name) { if (userData?.name) {
@ -997,7 +999,7 @@ export async function getNameInfoForOthers(address) {
return ""; return "";
} }
} }
async function getAddressInfo(address) { export async function getAddressInfo(address) {
const validApi = await getBaseApi(); const validApi = await getBaseApi();
const response = await fetch(validApi + "/addresses/" + address); const response = await fetch(validApi + "/addresses/" + address);
const data = await response.json(); const data = await response.json();
@ -1117,7 +1119,7 @@ export async function getBalanceInfo() {
const validApi = await getBaseApi(); const validApi = await getBaseApi();
const response = await fetch(validApi + "/addresses/balance/" + address); const response = await fetch(validApi + "/addresses/balance/" + address);
if (!response?.ok) throw new Error("Cannot fetch balance"); if (!response?.ok) throw new Error("0 QORT in your balance");
const data = await response.json(); const data = await response.json();
return data; return data;
} }
@ -1250,7 +1252,7 @@ export const getLastRef = async () => {
const response = await fetch( const response = await fetch(
validApi + "/addresses/lastreference/" + address validApi + "/addresses/lastreference/" + address
); );
if (!response?.ok) throw new Error("Cannot fetch balance"); if (!response?.ok) throw new Error("0 QORT in your balance");
const data = await response.text(); const data = await response.text();
return data; return data;
}; };
@ -3670,6 +3672,21 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
break; break;
case "listActions":
{
const data = request.payload;
listActionsCase(data)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
console.error(error.message);
});
}
break;
case "oauth": { case "oauth": {
const { nodeBaseUrl, senderAddress, senderPublicKey, timestamp } = const { nodeBaseUrl, senderAddress, senderPublicKey, timestamp } =
request.payload; request.payload;

View File

@ -0,0 +1,10 @@
import React from 'react'
import './barSpinner.css'
export const BarSpinner = ({width = '20px', color}) => {
return (
<div style={{
width,
color: color || 'green'
}} className="loader-bar"></div>
)
}

View File

@ -0,0 +1,19 @@
/* HTML: <div class="loader"></div> */
.loader-bar {
width: 45px;
aspect-ratio: .75;
--c:no-repeat linear-gradient(currentColor 0 0);
background:
var(--c) 0% 100%,
var(--c) 50% 100%,
var(--c) 100% 100%;
background-size: 20% 65%;
animation: l8 1s infinite linear;
}
@keyframes l8 {
16.67% {background-position: 0% 0% ,50% 100%,100% 100%}
33.33% {background-position: 0% 0% ,50% 0% ,100% 100%}
50% {background-position: 0% 0% ,50% 0% ,100% 0% }
66.67% {background-position: 0% 100%,50% 0% ,100% 0% }
83.33% {background-position: 0% 100%,50% 100%,100% 0% }
}

View File

@ -11,7 +11,7 @@ import { useQortalMessageListener } from "./useQortalMessageListener";
export const AppViewer = React.forwardRef(({ app , hide}, iframeRef) => { export const AppViewer = React.forwardRef(({ app , hide, isDevMode}, iframeRef) => {
const { rootHeight } = useContext(MyContext); const { rootHeight } = useContext(MyContext);
// const iframeRef = useRef(null); // const iframeRef = useRef(null);
const { document, window: frameWindow } = useFrame(); const { document, window: frameWindow } = useFrame();
@ -30,6 +30,17 @@ export const AppViewer = React.forwardRef(({ app , hide}, iframeRef) => {
const refreshAppFunc = (e) => { const refreshAppFunc = (e) => {
const {tabId} = e.detail const {tabId} = e.detail
if(tabId === app?.tabId){ if(tabId === app?.tabId){
if(isDevMode){
resetHistory()
if(!app?.isPreview || app?.isPrivate){
setUrl(app?.url + `?time=${Date.now()}`)
}
return
}
const constructUrl = `${getBaseApiReact()}/render/${app?.service}/${app?.name}${path != null ? path : ''}?theme=dark&identifier=${app?.identifier != null ? app?.identifier : ''}&time=${new Date().getMilliseconds()}` const constructUrl = `${getBaseApiReact()}/render/${app?.service}/${app?.name}${path != null ? path : ''}?theme=dark&identifier=${app?.identifier != null ? app?.identifier : ''}&time=${new Date().getMilliseconds()}`
setUrl(constructUrl) setUrl(constructUrl)
} }

View File

@ -394,7 +394,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
}}> }}>
<Spacer height="30px" /> <Spacer height="30px" />
<AppsHomeDesktop availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
</Box> </Box>
)} )}
@ -423,6 +423,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
isSelected={tab?.tabId === selectedTab?.tabId} isSelected={tab?.tabId === selectedTab?.tabId}
app={tab} app={tab}
ref={iframeRefs.current[tab.tabId]} ref={iframeRefs.current[tab.tabId]}
isDevMode={tab?.service ? false : true}
/> />
); );
})} })}
@ -438,7 +439,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
}}> }}>
<Spacer height="30px" /> <Spacer height="30px" />
<AppsHomeDesktop availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
</Box> </Box>
</> </>
)} )}

View File

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

View File

@ -8,7 +8,6 @@ import NavBack from "../../assets/svgs/NavBack.svg";
import NavAdd from "../../assets/svgs/NavAdd.svg"; import NavAdd from "../../assets/svgs/NavAdd.svg";
import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg"; import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg";
import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { import {
ButtonBase, ButtonBase,
ListItemIcon, ListItemIcon,
@ -119,7 +118,6 @@ export const AppsNavBarDesktop = ({disableBack}) => {
const setTabsToNav = (e) => { const setTabsToNav = (e) => {
const { tabs, selectedTab, isNewTabWindow } = e.detail?.data; const { tabs, selectedTab, isNewTabWindow } = e.detail?.data;
setTabs([...tabs]); setTabs([...tabs]);
setSelectedTab(!selectedTab ? null : { ...selectedTab }); setSelectedTab(!selectedTab ? null : { ...selectedTab });
setIsNewTabWindow(isNewTabWindow); setIsNewTabWindow(isNewTabWindow);
@ -135,10 +133,20 @@ export const AppsNavBarDesktop = ({disableBack}) => {
const isSelectedAppPinned = !!sortablePinnedApps?.find( const isSelectedAppPinned = useMemo(()=> {
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
sx={{ sx={{
@ -283,6 +291,16 @@ export const AppsNavBarDesktop = ({disableBack}) => {
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) =>
!( !(
@ -290,8 +308,23 @@ export const AppsNavBarDesktop = ({disableBack}) => {
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,
{ {
@ -301,6 +334,8 @@ export const AppsNavBarDesktop = ({disableBack}) => {
]; ];
} }
}
saveToLocalStorage( saveToLocalStorage(
"ext_saved_settings", "ext_saved_settings",
"sortablePinnedApps", "sortablePinnedApps",
@ -322,7 +357,7 @@ export const AppsNavBarDesktop = ({disableBack}) => {
<PushPinIcon <PushPinIcon
height={20} height={20}
sx={{ sx={{
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)", color: isSelectedAppPinned ? "var(--danger)" : "rgba(250, 250, 250, 0.5)",
}} }}
/> />
</ListItemIcon> </ListItemIcon>
@ -331,7 +366,7 @@ export const AppsNavBarDesktop = ({disableBack}) => {
"& .MuiTypography-root": { "& .MuiTypography-root": {
fontSize: "12px", fontSize: "12px",
fontWeight: 600, fontWeight: 600,
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)", color: isSelectedAppPinned ? "var(--danger)" : "rgba(250, 250, 250, 0.5)",
}, },
}} }}
primary={`${isSelectedAppPinned ? "Unpin app" : "Pin app"}`} primary={`${isSelectedAppPinned ? "Unpin app" : "Pin app"}`}
@ -339,9 +374,15 @@ export const AppsNavBarDesktop = ({disableBack}) => {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
if (selectedTab?.refreshFunc) {
selectedTab.refreshFunc(selectedTab?.tabId);
} else {
executeEvent("refreshApp", { executeEvent("refreshApp", {
tabId: selectedTab?.tabId, tabId: selectedTab?.tabId,
}); });
}
handleClose(); handleClose();
}} }}
> >
@ -369,6 +410,7 @@ export const AppsNavBarDesktop = ({disableBack}) => {
primary="Refresh" primary="Refresh"
/> />
</MenuItem> </MenuItem>
{!selectedTab?.isPrivate && (
<MenuItem <MenuItem
onClick={() => { onClick={() => {
executeEvent("copyLink", { executeEvent("copyLink", {
@ -401,6 +443,7 @@ export const AppsNavBarDesktop = ({disableBack}) => {
primary="Copy link" primary="Copy link"
/> />
</MenuItem> </MenuItem>
)}
</Menu> </Menu>
</AppsNavBarParent> </AppsNavBarParent>
); );

View File

@ -0,0 +1,564 @@
import React, { useContext, useMemo, useState } from "react";
import {
Avatar,
Box,
Button,
ButtonBase,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Input,
MenuItem,
Select,
Tab,
Tabs,
Typography,
} from "@mui/material";
import { useDropzone } from "react-dropzone";
import { useHandlePrivateApps } from "./useHandlePrivateApps";
import { useRecoilState, useSetRecoilState } from "recoil";
import { groupsPropertiesAtom, myGroupsWhereIAmAdminAtom } from "../../atoms/global";
import { Label } from "../Group/AddGroup";
import { Spacer } from "../../common/Spacer";
import {
Add,
AppCircle,
AppCircleContainer,
AppCircleLabel,
PublishQAppChoseFile,
PublishQAppInfo,
} from "./Apps-styles";
import ImageUploader from "../../common/ImageUploader";
import { isMobile, MyContext } from "../../App";
import { fileToBase64 } from "../../utils/fileReading";
import { objectToBase64 } from "../../qdn/encryption/group-encryption";
import { getFee } from "../../background";
const maxFileSize = 50 * 1024 * 1024; // 50MB
export const AppsPrivate = ({myName}) => {
const { openApp } = useHandlePrivateApps();
const [file, setFile] = useState(null);
const [logo, setLogo] = useState(null);
const [qortalUrl, setQortalUrl] = useState("");
const [selectedGroup, setSelectedGroup] = useState(0);
const [groupsProperties] = useRecoilState(groupsPropertiesAtom)
const [valueTabPrivateApp, setValueTabPrivateApp] = useState(0);
const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState(
myGroupsWhereIAmAdminAtom
);
const myGroupsWhereIAmAdmin = useMemo(()=> {
return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false)
}, [myGroupsWhereIAmAdminFromGlobal, groupsProperties])
const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false);
const { show, setInfoSnackCustom, setOpenSnackGlobal, memberGroups } = useContext(MyContext);
const myGroupsPrivate = useMemo(()=> {
return memberGroups?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false)
}, [memberGroups, groupsProperties])
const [privateAppValues, setPrivateAppValues] = useState({
name: "",
service: "DOCUMENT",
identifier: "",
groupId: 0,
});
const [newPrivateAppValues, setNewPrivateAppValues] = useState({
service: "DOCUMENT",
identifier: "",
name: "",
});
const { getRootProps, getInputProps } = useDropzone({
accept: {
"application/zip": [".zip"], // Only accept zip files
},
maxSize: maxFileSize,
multiple: false, // Disable multiple file uploads
onDrop: (acceptedFiles) => {
if (acceptedFiles.length > 0) {
setFile(acceptedFiles[0]); // Set the file name
}
},
onDropRejected: (fileRejections) => {
fileRejections.forEach(({ file, errors }) => {
errors.forEach((error) => {
if (error.code === "file-too-large") {
console.error(
`File ${file.name} is too large. Max size allowed is ${
maxFileSize / (1024 * 1024)
} MB.`
);
}
});
});
},
});
const addPrivateApp = async () => {
try {
if (privateAppValues?.groupId === 0) return;
await openApp(privateAppValues, true);
} catch (error) {
console.error(error)
}
};
const clearFields = () => {
setPrivateAppValues({
name: "",
service: "DOCUMENT",
identifier: "",
groupId: 0,
});
setNewPrivateAppValues({
service: "DOCUMENT",
identifier: "",
name: "",
});
setFile(null);
setValueTabPrivateApp(0);
setSelectedGroup(0);
setLogo(null);
};
const publishPrivateApp = async () => {
try {
if (selectedGroup === 0) return;
if (!logo) throw new Error("Please select an image for a logo");
if (!myName) throw new Error("You need a Qortal name to publish");
if (!newPrivateAppValues?.name) throw new Error("Your app needs a name");
const base64Logo = await fileToBase64(logo);
const base64App = await fileToBase64(file);
const objectToSave = {
app: base64App,
logo: base64Logo,
name: newPrivateAppValues.name,
};
const object64 = await objectToBase64(objectToSave);
const decryptedData = await new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "ENCRYPT_QORTAL_GROUP_DATA",
type: "qortalRequest",
payload: {
base64: object64,
groupId: selectedGroup,
},
},
(response) => {
if (response.error) {
rej(response?.message);
return;
} else {
res(response);
}
}
);
});
if (decryptedData?.error) {
throw new Error(
decryptedData?.error || "Unable to encrypt app. App not published"
);
}
const fee = await getFee("ARBITRARY");
await show({
message: "Would you like to publish this app?",
publishFee: fee.fee + " QORT",
});
await new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "publishOnQDN",
payload: {
data: decryptedData,
identifier: newPrivateAppValues?.identifier,
service: newPrivateAppValues?.service,
},
},
(response) => {
if (!response?.error) {
res(response);
return;
}
rej(response.error);
}
);
});
openApp(
{
identifier: newPrivateAppValues?.identifier,
service: newPrivateAppValues?.service,
name: myName,
groupId: selectedGroup,
},
true
);
clearFields();
} catch (error) {
setOpenSnackGlobal(true)
setInfoSnackCustom({
type: "error",
message: error?.message || "Unable to publish app",
});
}
};
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValueTabPrivateApp(newValue);
};
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`,
};
}
return (
<>
<ButtonBase
onClick={() => {
setIsOpenPrivateModal(true);
}}
sx={{
width: "80px",
}}
>
<AppCircleContainer
sx={{
gap: !isMobile ? "10px" : "5px",
}}
>
<AppCircle>
<Add>+</Add>
</AppCircle>
<AppCircleLabel>Private</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
{isOpenPrivateModal && (
<Dialog
open={isOpenPrivateModal}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
onKeyDown={(e) => {
if (e.key === "Enter") {
if (valueTabPrivateApp === 0) {
if (
!privateAppValues.name ||
!privateAppValues.service ||
!privateAppValues.identifier ||
!privateAppValues?.groupId
)
return;
addPrivateApp();
}
}
}}
maxWidth="md"
fullWidth={true}
>
<DialogTitle id="alert-dialog-title">
{valueTabPrivateApp === 0
? "Access private app"
: "Publish private app"}
</DialogTitle>
<Box>
<Tabs
value={valueTabPrivateApp}
onChange={handleChange}
aria-label="basic tabs example"
variant={isMobile ? "scrollable" : "fullWidth"} // Scrollable on mobile, full width on desktop
scrollButtons="auto"
allowScrollButtonsMobile
sx={{
"& .MuiTabs-indicator": {
backgroundColor: "white",
},
}}
>
<Tab
label="Access app"
{...a11yProps(0)}
sx={{
"&.Mui-selected": {
color: "white",
},
fontSize: isMobile ? "0.75rem" : "1rem", // Adjust font size for mobile
}}
/>
<Tab
label="Publish app"
{...a11yProps(1)}
sx={{
"&.Mui-selected": {
color: "white",
},
fontSize: isMobile ? "0.75rem" : "1rem", // Adjust font size for mobile
}}
/>
</Tabs>
</Box>
{valueTabPrivateApp === 0 && (
<>
<DialogContent>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
<Label>Select a group</Label>
<Label>Only private groups will be shown</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={privateAppValues?.groupId}
label="Groups"
onChange={(e) => {
setPrivateAppValues((prev) => {
return {
...prev,
groupId: e.target.value,
};
});
}}
>
<MenuItem value={0}>No group selected</MenuItem>
{myGroupsPrivate
?.filter((item) => !item?.isOpen)
.map((group) => {
return (
<MenuItem key={group?.groupId} value={group?.groupId}>
{group?.groupName}
</MenuItem>
);
})}
</Select>
</Box>
<Spacer height="10px" />
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
marginTop: "15px",
}}
>
<Label>name</Label>
<Input
placeholder="name"
value={privateAppValues?.name}
onChange={(e) =>
setPrivateAppValues((prev) => {
return {
...prev,
name: e.target.value,
};
})
}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
marginTop: "15px",
}}
>
<Label>identifier</Label>
<Input
placeholder="identifier"
value={privateAppValues?.identifier}
onChange={(e) =>
setPrivateAppValues((prev) => {
return {
...prev,
identifier: e.target.value,
};
})
}
/>
</Box>
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={() => {
setIsOpenPrivateModal(false);
}}
>
Close
</Button>
<Button
disabled={
!privateAppValues.name ||
!privateAppValues.service ||
!privateAppValues.identifier ||
!privateAppValues?.groupId
}
variant="contained"
onClick={() => addPrivateApp()}
autoFocus
>
Access
</Button>
</DialogActions>
</>
)}
{valueTabPrivateApp === 1 && (
<>
<DialogContent>
<PublishQAppInfo
sx={{
fontSize: "14px",
}}
>
Select .zip file containing static content:{" "}
</PublishQAppInfo>
<Spacer height="10px" />
<PublishQAppInfo
sx={{
fontSize: "14px",
}}
>{`
50mb MB maximum`}</PublishQAppInfo>
{file && (
<>
<Spacer height="5px" />
<PublishQAppInfo>{`Selected: (${file?.name})`}</PublishQAppInfo>
</>
)}
<Spacer height="18px" />
<PublishQAppChoseFile {...getRootProps()}>
{" "}
<input {...getInputProps()} />
{file ? "Change" : "Choose"} File
</PublishQAppChoseFile>
<Spacer height="20px" />
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
<Label>Select a group</Label>
<Label>
Only groups where you are an admin will be shown
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={selectedGroup}
label="Groups where you are an admin"
onChange={(e) => setSelectedGroup(e.target.value)}
>
<MenuItem value={0}>No group selected</MenuItem>
{myGroupsWhereIAmAdmin
?.filter((item) => !item?.isOpen)
.map((group) => {
return (
<MenuItem key={group?.groupId} value={group?.groupId}>
{group?.groupName}
</MenuItem>
);
})}
</Select>
</Box>
<Spacer height="20px" />
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
marginTop: "15px",
}}
>
<Label>identifier</Label>
<Input
placeholder="identifier"
value={newPrivateAppValues?.identifier}
onChange={(e) =>
setNewPrivateAppValues((prev) => {
return {
...prev,
identifier: e.target.value,
};
})
}
/>
</Box>
<Spacer height="10px" />
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
marginTop: "15px",
}}
>
<Label>App name</Label>
<Input
placeholder="App name"
value={newPrivateAppValues?.name}
onChange={(e) =>
setNewPrivateAppValues((prev) => {
return {
...prev,
name: e.target.value,
};
})
}
/>
</Box>
<Spacer height="10px" />
<ImageUploader onPick={(file) => setLogo(file)}>
<Button variant="contained">Choose logo</Button>
</ImageUploader>
{logo?.name}
<Spacer height="25px" />
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={() => {
setIsOpenPrivateModal(false);
clearFields();
}}
>
Close
</Button>
<Button
disabled={
!newPrivateAppValues.name ||
!newPrivateAppValues.service ||
!newPrivateAppValues.identifier ||
!selectedGroup
}
variant="contained"
onClick={() => publishPrivateApp()}
autoFocus
>
Publish
</Button>
</DialogActions>
</>
)}
</Dialog>
)}
</>
);
};

View File

@ -1,18 +1,21 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { DndContext, closestCenter } from '@dnd-kit/core'; import { DndContext, closestCenter } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable';
import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { Avatar, ButtonBase } from '@mui/material'; import { Avatar, ButtonBase } from '@mui/material';
import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles'; import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles';
import { getBaseApiReact } from '../../App'; import { getBaseApiReact, MyContext } from '../../App';
import { executeEvent } from '../../utils/events'; import { executeEvent } from '../../utils/events';
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global';
import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilState, useSetRecoilState } from 'recoil';
import { saveToLocalStorage } from './AppsNavBar'; import { saveToLocalStorage } from './AppsNavBar';
import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps'; import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps';
import LockIcon from "@mui/icons-material/Lock";
import { useHandlePrivateApps } from './useHandlePrivateApps';
const SortableItem = ({ id, name, app, isDesktop }) => { const SortableItem = ({ id, name, app, isDesktop }) => {
const {openApp} = useHandlePrivateApps()
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const style = { const style = {
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
@ -35,10 +38,20 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
}} }}
onClick={()=> { onClick={async ()=> {
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={{
@ -50,6 +63,14 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
border: "none", border: "none",
}} }}
> >
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
<LockIcon
sx={{
height: "42px",
width: "42px",
}}
/>
) : (
<Avatar <Avatar
sx={{ sx={{
height: "42px", height: "42px",
@ -59,7 +80,7 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
} }
}} }}
alt={app?.metadata?.title || app?.name} alt={app?.metadata?.title || app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${ src={ app?.privateAppProperties?.logo ? app?.privateAppProperties?.logo :`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name app?.name
}/qortal_avatar?async=true`} }/qortal_avatar?async=true`}
> >
@ -72,10 +93,19 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
alt="center-icon" alt="center-icon"
/> />
</Avatar> </Avatar>
)}
</AppCircle> </AppCircle>
{app?.isPrivate ? (
<AppCircleLabel>
{`${app?.privateAppProperties?.appName || "Private"}`}
</AppCircleLabel>
) : (
<AppCircleLabel> <AppCircleLabel>
{app?.metadata?.title || app?.name} {app?.metadata?.title || app?.name}
</AppCircleLabel> </AppCircleLabel>
)}
</AppCircleContainer> </AppCircleContainer>
</ButtonBase> </ButtonBase>
</ContextMenuPinnedApps> </ContextMenuPinnedApps>

View File

@ -5,6 +5,7 @@ import { getBaseApiReact } from '../../App';
import { Avatar, ButtonBase } from '@mui/material'; import { Avatar, ButtonBase } from '@mui/material';
import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { executeEvent } from '../../utils/events'; import { executeEvent } from '../../utils/events';
import LockIcon from "@mui/icons-material/Lock";
const TabComponent = ({isSelected, app}) => { const TabComponent = ({isSelected, app}) => {
return ( return (
@ -34,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,251 @@
import React, { useContext, useState } from "react";
import { executeEvent } from "../../utils/events";
import { getBaseApiReact, MyContext } from "../../App";
import { createEndpoint } from "../../background";
import { useRecoilState, useSetRecoilState } from "recoil";
import {
settingsLocalLastUpdatedAtom,
sortablePinnedAppsAtom,
} from "../../atoms/global";
import { saveToLocalStorage } from "./AppsNavBarDesktop";
import { base64ToBlobUrl } from "../../utils/fileReading";
import { base64ToUint8Array } from "../../qdn/encryption/group-encryption";
import { uint8ArrayToObject } from "../../backgroundFunctions/encryption";
export const useHandlePrivateApps = () => {
const [status, setStatus] = useState("");
const {
openSnackGlobal,
setOpenSnackGlobal,
infoSnackCustom,
setInfoSnackCustom,
} = useContext(MyContext);
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(
sortablePinnedAppsAtom
);
const setSettingsLocalLastUpdated = useSetRecoilState(
settingsLocalLastUpdatedAtom
);
const openApp = async (
privateAppProperties,
addToPinnedApps,
setLoadingStatePrivateApp
) => {
try {
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp(`Downloading and decrypting private app.`);
}
setOpenSnackGlobal(true);
setInfoSnackCustom({
type: "info",
message: "Fetching app data",
duration: null
});
const urlData = `${getBaseApiReact()}/arbitrary/${
privateAppProperties?.service
}/${privateAppProperties?.name}/${
privateAppProperties?.identifier
}?encoding=base64`;
let data;
try {
const responseData = await fetch(urlData, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if(!responseData?.ok){
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp("Error! Unable to download private app.");
}
throw new Error("Unable to fetch app");
}
data = await responseData.text();
if (data?.error) {
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp("Error! Unable to download private app.");
}
throw new Error("Unable to fetch app");
}
} catch (error) {
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp("Error! Unable to download private app.");
}
throw error;
}
let decryptedData;
// eslint-disable-next-line no-useless-catch
try {
decryptedData = await new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "DECRYPT_QORTAL_GROUP_DATA",
type: "qortalRequest",
payload: {
base64: data,
groupId: privateAppProperties?.groupId,
},
},
(response) => {
if (response.error) {
rej(response?.message);
return;
} else {
res(response);
}
}
);
});
if (decryptedData?.error) {
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp("Error! Unable to decrypt private app.");
}
throw new Error(decryptedData?.error);
}
} catch (error) {
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp("Error! Unable to decrypt private app.");
}
throw error;
}
try {
const convertToUint = base64ToUint8Array(decryptedData);
const UintToObject = uint8ArrayToObject(convertToUint);
if (decryptedData) {
setInfoSnackCustom({
type: "info",
message: "Building app",
});
const endpoint = await createEndpoint(
`/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true`
);
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "text/plain",
},
body: UintToObject?.app,
});
const previewPath = await response.text();
const refreshfunc = async (tabId, privateAppProperties) => {
const checkIfPreviewLinkStillWorksUrl = await createEndpoint(
`/render/hash/HmtnZpcRPwisMfprUXuBp27N2xtv5cDiQjqGZo8tbZS?secret=E39WTiG4qBq3MFcMPeRZabtQuzyfHg9ZuR5SgY7nW1YH`
);
const res = await fetch(checkIfPreviewLinkStillWorksUrl);
if (res.ok) {
executeEvent("refreshApp", {
tabId: tabId,
});
} else {
const endpoint = await createEndpoint(
`/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true`
);
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "text/plain",
},
body: UintToObject?.app,
});
const previewPath = await response.text();
executeEvent("updateAppUrl", {
tabId: tabId,
url: await createEndpoint(previewPath),
});
setTimeout(() => {
executeEvent("refreshApp", {
tabId: tabId,
});
}, 300);
}
};
const appName = UintToObject?.name;
const logo = UintToObject?.logo
? `data:image/png;base64,${UintToObject?.logo}`
: null;
const dataBody = {
url: await createEndpoint(previewPath),
isPreview: true,
isPrivate: true,
privateAppProperties: { ...privateAppProperties, logo, appName },
filePath: "",
refreshFunc: (tabId) => {
refreshfunc(tabId, privateAppProperties);
},
};
executeEvent("addTab", {
data: dataBody,
});
setInfoSnackCustom({
type: "success",
message: "Opened",
});
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp(``);
}
if (addToPinnedApps) {
setSortablePinnedApps((prev) => {
const updatedApps = [
...prev,
{
isPrivate: true,
isPreview: true,
privateAppProperties: {
...privateAppProperties,
logo,
appName,
},
},
];
saveToLocalStorage(
"ext_saved_settings",
"sortablePinnedApps",
updatedApps
);
return updatedApps;
});
setSettingsLocalLastUpdated(Date.now());
}
}
} catch (error) {
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp(`Error! ${error?.message || 'Unable to build private app.'}`);
}
throw error
}
}
catch (error) {
setInfoSnackCustom({
type: "error",
message: error?.message || "Unable to fetch app",
});
}
};
return {
openApp,
status,
};
};

View File

@ -243,7 +243,7 @@ const UIQortalRequests = [
'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE', 'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE',
'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER', 'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER',
'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER', 'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER',
'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_PUBLIC_NODE', 'ADMIN_ACTION', 'SIGN_TRANSACTION', 'DECRYPT_QORTAL_GROUP_DATA', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'SHOW_ACTIONS', 'REGISTER_NAME', 'UPDATE_NAME', 'LEAVE_GROUP', 'INVITE_TO_GROUP', 'KICK_FROM_GROUP', 'BAN_FROM_GROUP', 'CANCEL_GROUP_BAN', 'ADD_GROUP_ADMIN','REMOVE_GROUP_ADMIN','DECRYPT_AESGCM', 'CANCEL_GROUP_INVITE', 'CREATE_GROUP' 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_PUBLIC_NODE', 'ADMIN_ACTION', 'SIGN_TRANSACTION', 'DECRYPT_QORTAL_GROUP_DATA', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'SHOW_ACTIONS', 'REGISTER_NAME', 'UPDATE_NAME', 'LEAVE_GROUP', 'INVITE_TO_GROUP', 'KICK_FROM_GROUP', 'BAN_FROM_GROUP', 'CANCEL_GROUP_BAN', 'ADD_GROUP_ADMIN','REMOVE_GROUP_ADMIN','DECRYPT_AESGCM', 'CANCEL_GROUP_INVITE', 'CREATE_GROUP', 'GET_USER_WALLET_TRANSACTIONS'
]; ];

View File

@ -0,0 +1,154 @@
import React, { useCallback, useContext, useEffect, useState } from 'react'
import {
Avatar,
Box,
Button,
ButtonBase,
Collapse,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Input,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemIcon,
ListItemText,
List,
MenuItem,
Popover,
Select,
TextField,
Typography,
} from "@mui/material";
import { Label } from './Group/AddGroup';
import { Spacer } from '../common/Spacer';
import { LoadingButton } from '@mui/lab';
import { getBaseApiReact, MyContext } from '../App';
import { getFee } from '../background';
import qTradeLogo from "../assets/Icons/q-trade-logo.webp";
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../utils/events';
import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner';
export const BuyQortInformation = ({balance}) => {
const [isOpen, setIsOpen] = useState(false)
const openBuyQortInfoFunc = useCallback((e) => {
setIsOpen(true)
}, [ setIsOpen]);
useEffect(() => {
subscribeToEvent("openBuyQortInfo", openBuyQortInfoFunc);
return () => {
unsubscribeFromEvent("openBuyQortInfo", openBuyQortInfoFunc);
};
}, [openBuyQortInfoFunc]);
return (
<Dialog
open={isOpen}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Get QORT"}
</DialogTitle>
<DialogContent>
<Box
sx={{
width: "400px",
maxWidth: '90vw',
height: "400px",
maxHeight: '90vh',
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
}}
>
<Typography>Get QORT using Qortal's crosschain trade portal</Typography>
<ButtonBase
sx={{
"&:hover": { backgroundColor: "secondary.main" },
transition: "all 0.1s ease-in-out",
padding: "5px",
borderRadius: "5px",
gap: "5px",
}}
onClick={async () => {
executeEvent("addTab", {
data: { service: "APP", name: "q-trade" },
});
executeEvent("open-apps-mode", {});
setIsOpen(false)
}}
>
<img
style={{
borderRadius: "50%",
height: '30px'
}}
src={qTradeLogo}
/>
<Typography
sx={{
fontSize: "1rem",
}}
>
Trade QORT
</Typography>
</ButtonBase>
<Spacer height='40px' />
<Typography sx={{
textDecoration: 'underline'
}}>Benefits of having QORT</Typography>
<List
sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}
aria-label="contacts"
>
<ListItem disablePadding>
<ListItemIcon>
<RadioButtonCheckedIcon sx={{
color: 'white'
}} />
</ListItemIcon>
<ListItemText primary="Create transactions on the Qortal Blockchain" />
</ListItem>
<ListItem disablePadding>
<ListItemIcon>
<RadioButtonCheckedIcon sx={{
color: 'white'
}} />
</ListItemIcon>
<ListItemText primary="Having at least 4 QORT in your balance allows you to send chat messages at near instant speed." />
</ListItem>
</List>
</Box>
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={() => {
setIsOpen(false)
}}
>
Close
</Button>
</DialogActions>
</Dialog>
)
}

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' import React, { useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { CreateCommonSecret } from './CreateCommonSecret' import { CreateCommonSecret } from './CreateCommonSecret'
import { reusableGet } from '../../qdn/publish/pubish' import { reusableGet } from '../../qdn/publish/pubish'
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption' import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'
@ -10,11 +10,11 @@ import Tiptap from './TipTap'
import { CustomButton } from '../../App-styles' import { CustomButton } from '../../App-styles'
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar' import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'
import { getBaseApiReact, getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App' import { getBaseApiReact, getBaseApiReactSocket, isMobile, MyContext, pauseAllQueues, resumeAllQueues } from '../../App'
import { CustomizedSnackbars } from '../Snackbar/Snackbar' import { CustomizedSnackbars } from '../Snackbar/Snackbar'
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes' import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes'
import { useMessageQueue } from '../../MessageQueueContext' import { useMessageQueue } from '../../MessageQueueContext'
import { executeEvent } from '../../utils/events' import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'
import { Box, ButtonBase, Divider, Typography } from '@mui/material' import { Box, ButtonBase, Divider, Typography } from '@mui/material'
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { ReplyPreview } from './MessageItem' import { ReplyPreview } from './MessageItem'
@ -47,6 +47,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
const [isOpenQManager, setIsOpenQManager] = useState(null) const [isOpenQManager, setIsOpenQManager] = useState(null)
const [onEditMessage, setOnEditMessage] = useState(null) const [onEditMessage, setOnEditMessage] = useState(null)
const [messageSize, setMessageSize] = useState(0) const [messageSize, setMessageSize] = useState(0)
const {isUserBlocked} = useContext(MyContext)
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
@ -167,10 +168,28 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
}) })
} }
const 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;
@ -182,7 +201,11 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
}, },
}); });
const responseData = await response.json(); const responseData = await response.json();
decryptMessages(responseData, false); const dataRemovedBlock = responseData?.filter((item)=> {
return !isUserBlocked(item?.sender, item?.senderName)
})
decryptMessages(dataRemovedBlock, false);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

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

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useMemo } from 'react';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import './styles.css'; import './styles.css';
import { executeEvent } from '../../utils/events'; import { executeEvent } from '../../utils/events';
@ -63,7 +63,6 @@ function processText(input) {
return wrapper.innerHTML; return wrapper.innerHTML;
} }
export const MessageDisplay = ({ htmlContent, isReply }) => {
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
@ -76,8 +75,12 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
return processText(textFormatted); return processText(textFormatted);
}; };
export const MessageDisplay = ({ htmlContent, isReply }) => {
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 }) => {
'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();
@ -94,7 +98,15 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
const target = e.target; const target = e.target;
if (target.tagName === 'A') { if (target.tagName === 'A') {
const href = target.getAttribute('href'); const href = target.getAttribute('href');
window.electronAPI.openExternal(href); if (chrome && chrome.tabs) {
chrome.tabs.create({ url: href }, (tab) => {
if (chrome.runtime.lastError) {
console.error("Error opening tab:", chrome.runtime.lastError);
} else {
console.log("Tab opened successfully:", tab);
}
});
}
} else if (target.getAttribute('data-url')) { } else if (target.getAttribute('data-url')) {
const url = target.getAttribute('data-url'); const url = target.getAttribute('data-url');

View File

@ -1,5 +1,5 @@
import { Message } from "@chatscope/chat-ui-kit-react"; import { Message } from "@chatscope/chat-ui-kit-react";
import React, { useContext, useEffect, useState } from "react"; import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { MessageDisplay } from "./MessageDisplay"; import { MessageDisplay } from "./MessageDisplay";
import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material"; import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material";
@ -8,6 +8,7 @@ import { getBaseApi } from "../../background";
import { MyContext, getBaseApiReact } from "../../App"; import { MyContext, getBaseApiReact } from "../../App";
import { generateHTML } from "@tiptap/react"; import { generateHTML } from "@tiptap/react";
import Highlight from "@tiptap/extension-highlight"; import Highlight from "@tiptap/extension-highlight";
import Mention from "@tiptap/extension-mention";
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline"; import Underline from "@tiptap/extension-underline";
import { executeEvent } from "../../utils/events"; import { executeEvent } from "../../utils/events";
@ -17,7 +18,6 @@ import { Spacer } from "../../common/Spacer";
import { ReactionPicker } from "../ReactionPicker"; import { ReactionPicker } from "../ReactionPicker";
import KeyOffIcon from '@mui/icons-material/KeyOff'; import KeyOffIcon from '@mui/icons-material/KeyOff';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import Mention from "@tiptap/extension-mention";
import TextStyle from '@tiptap/extension-text-style'; import TextStyle from '@tiptap/extension-text-style';
import { addressInfoKeySelector } from "../../atoms/global"; import { addressInfoKeySelector } from "../../atoms/global";
import { useRecoilValue } from "recoil"; import { useRecoilValue } from "recoil";
@ -50,8 +50,7 @@ const getBadgeImg = (level)=> {
default: return level0Img default: return level0Img
} }
} }
export const MessageItem = React.memo(({
export const MessageItem = ({
message, message,
onSeen, onSeen,
isLast, isLast,
@ -67,40 +66,80 @@ export const MessageItem = ({
isUpdating, isUpdating,
lastSignature, lastSignature,
onEdit, onEdit,
isPrivate, isPrivate
setMobileViewModeKeepOpen
}) => { }) => {
const {getIndividualUserInfo} = useContext(MyContext)
const userInfo = useRecoilValue(addressInfoKeySelector(message?.sender));
const {getIndividualUserInfo} = useContext(MyContext)
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const [selectedReaction, setSelectedReaction] = useState(null); const [selectedReaction, setSelectedReaction] = useState(null);
const { ref, inView } = useInView({ const [userInfo, setUserInfo] = useState(null)
threshold: 0.7, // Fully visible
triggerOnce: false, // Only trigger once when it becomes visible
});
useEffect(()=> { useEffect(()=> {
if (inView && isLast && onSeen) { 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",
@ -135,25 +174,25 @@ export const MessageItem = ({
sx={{ sx={{
backgroundColor: "#27282c", backgroundColor: "#27282c",
color: "white", color: "white",
height: '40px',
width: '40px'
}} }}
alt={message?.senderName} alt={message?.senderName}
src={message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${ src={userAvatarUrl}
message?.senderName
}/qortal_avatar?async=true` : ''}
> >
{message?.senderName?.charAt(0)} {message?.senderName?.charAt(0)}
</Avatar> </Avatar>
</WrapperUserAction> </WrapperUserAction>
<Tooltip disableFocusListener title={`level ${userInfo?.level}`}> <Tooltip disableFocusListener title={`level ${userInfo}`}>
<img style={{ <img style={{
visibility: userInfo?.level !== undefined ? 'visible' : 'hidden', visibility: userInfo !== undefined ? 'visible' : 'hidden',
width: '30px', width: '30px',
height: 'auto' height: 'auto'
}} src={getBadgeImg(userInfo?.level)} /> }} src={getBadgeImg(userInfo)} />
</Tooltip> </Tooltip>
</Box> </Box>
)} )}
@ -260,41 +299,27 @@ export const MessageItem = ({
}}>Replied to {reply?.senderName || reply?.senderAddress}</Typography> }}>Replied to {reply?.senderName || reply?.senderAddress}</Typography>
{reply?.messageText && ( {reply?.messageText && (
<MessageDisplay <MessageDisplay
htmlContent={generateHTML(reply?.messageText, [ htmlContent={htmlReply}
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])}
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
/> />
)} )}
{reply?.decryptedData?.type === "notification" ? ( {reply?.decryptedData?.type === "notification" ? (
<MessageDisplay htmlContent={reply.decryptedData?.data?.message} /> <MessageDisplay htmlContent={reply.decryptedData?.data?.message} />
) : ( ) : (
<MessageDisplay setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} isReply htmlContent={reply.text} /> <MessageDisplay isReply htmlContent={reply.text} />
)} )}
</Box> </Box>
</Box> </Box>
</> </>
)} )}
{message?.messageText && (
<MessageDisplay <MessageDisplay
htmlContent={generateHTML(message?.messageText, [ htmlContent={htmlText}
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])}
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
/> />
)}
{message?.decryptedData?.type === "notification" ? ( {message?.decryptedData?.type === "notification" ? (
<MessageDisplay htmlContent={message.decryptedData?.data?.message} /> <MessageDisplay htmlContent={message.decryptedData?.data?.message} />
) : ( ) : (
<MessageDisplay setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} htmlContent={message.text} /> <MessageDisplay htmlContent={message.text} />
)} )}
<Box <Box
sx={{ sx={{
@ -323,7 +348,9 @@ export const MessageItem = ({
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
setSelectedReaction(reaction); setSelectedReaction(reaction);
}}> }}>
<div>{reaction}</div> {numberOfReactions > 1 && ( <div style={{
fontSize: '16px'
}}>{reaction}</div> {numberOfReactions > 1 && (
<Typography sx={{ <Typography sx={{
marginLeft: '4px' marginLeft: '4px'
}}>{' '} {numberOfReactions}</Typography> }}>{' '} {numberOfReactions}</Typography>
@ -361,7 +388,7 @@ export const MessageItem = ({
</Typography> </Typography>
<List sx={{ <List sx={{
overflow: 'auto', overflow: 'auto',
maxWidth: '80vw', maxWidth: '300px',
maxHeight: '300px' maxHeight: '300px'
}}> }}>
{reactions[selectedReaction]?.map((reactionItem) => ( {reactions[selectedReaction]?.map((reactionItem) => (
@ -460,21 +487,11 @@ export const MessageItem = ({
</Box> </Box>
</Box> </Box>
{/* <Message
model={{
direction: 'incoming',
message: message.text,
position: 'single',
sender: message.senderName,
sentTime: message.timestamp
}}
></Message> */}
{/* {!message.unread && <span style={{ color: 'green' }}> Seen</span>} */}
</div> </div>
</> </MessageWragger>
); );
}; });
export const ReplyPreview = ({message, isEdit})=> { export const ReplyPreview = ({message, isEdit})=> {
@ -531,5 +548,38 @@ export const ReplyPreview = ({message, isEdit})=> {
)} )}
</Box> </Box>
</Box> </Box>
) )
} }
const MessageWragger = ({lastMessage, onSeen, isLast, children})=> {
if(lastMessage){
return (
<WatchComponent onSeen={onSeen} isLast={isLast}>{children}</WatchComponent>
)
}
return children
}
const WatchComponent = ({onSeen, isLast, children})=> {
const { ref, inView } = useInView({
threshold: 0.7, // Fully visible
triggerOnce: true, // Only trigger once when it becomes visible
});
useEffect(() => {
if (inView && isLast && onSeen) {
onSeen();
}
}, [inView, isLast, onSeen]);
return <div ref={ref} style={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
{children}
</div>
}

View File

@ -0,0 +1,192 @@
import React, { useCallback, useEffect, useRef } from "react";
import { getBaseApiReact } from "../../App";
import { truncate } from "lodash";
export const useBlockedAddresses = () => {
const userBlockedRef = useRef({})
const userNamesBlockedRef = useRef({})
const getAllBlockedUsers = useCallback(()=> {
return {
names: userNamesBlockedRef.current,
addresses: userBlockedRef.current
}
}, [])
const isUserBlocked = useCallback((address, name)=> {
try {
if(!address) return false
if(userBlockedRef.current[address] || userNamesBlockedRef.current[name]) return true
return false
} catch (error) {
//error
}
}, [])
useEffect(()=> {
const fetchBlockedList = async ()=> {
try {
const response = await new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "listActions",
payload: {
type: 'get',
listName: `blockedAddresses`,
},
},
(response) => {
if (response.error) {
rej(response?.message);
return;
} else {
res(response);
}
}
);
})
const blockedUsers = {}
response?.forEach((item)=> {
blockedUsers[item] = true
})
userBlockedRef.current = blockedUsers
const response2 = await new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "listActions",
payload: {
type: 'get',
listName: `blockedNames`,
},
},
(response) => {
if (response.error) {
rej(response?.message);
return;
} else {
res(response);
}
}
);
})
const blockedUsers2 = {}
response2?.forEach((item)=> {
blockedUsers2[item] = true
})
userNamesBlockedRef.current = blockedUsers2
} catch (error) {
console.error(error)
}
}
fetchBlockedList()
}, [])
const removeBlockFromList = useCallback(async (address, name)=> {
await new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "listActions",
payload: {
type: 'remove',
items: name ? [name] : [address],
listName: name ? 'blockedNames' : 'blockedAddresses'
},
},
(response) => {
if (response.error) {
rej(response?.message);
return;
} else {
if(!name){
const copyObject = {...userBlockedRef.current}
delete copyObject[address]
userBlockedRef.current = copyObject
} else {
const copyObject = {...userNamesBlockedRef.current}
delete copyObject[name]
userNamesBlockedRef.current = copyObject
}
res(response);
}
}
);
})
if(name && userBlockedRef.current[address]){
await new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "listActions",
payload: {
type: 'remove',
items: !name ? [name] : [address],
listName: !name ? 'blockedNames' : 'blockedAddresses'
},
},
(response) => {
if (response.error) {
rej(response?.message);
return;
} else {
const copyObject = {...userBlockedRef.current}
delete copyObject[address]
userBlockedRef.current = copyObject
res(response);
}
}
);
})
}
}, [])
const addToBlockList = useCallback(async (address, name)=> {
await new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "listActions",
payload: {
type: 'add',
items: name ? [name] : [address],
listName: name ? 'blockedNames' : 'blockedAddresses'
},
},
(response) => {
console.log('response', response)
if (response.error) {
rej(response?.message);
return;
} else {
if(name){
const copyObject = {...userNamesBlockedRef.current}
copyObject[name] = true
userNamesBlockedRef.current = copyObject
}else {
const copyObject = {...userBlockedRef.current}
copyObject[address] = true
userBlockedRef.current = copyObject
}
res(response);
}
}
)
})
}, [])
return {
isUserBlocked,
addToBlockList,
removeBlockFromList,
getAllBlockedUsers
};
};

View File

@ -124,11 +124,19 @@ export const ContextMenuPinnedApps = ({ children, app, isMine }) => {
<MenuItem onClick={(e) => { <MenuItem onClick={(e) => {
handleClose(e); handleClose(e);
setSortablePinnedApps((prev) => { setSortablePinnedApps((prev) => {
if(app?.isPrivate){
const updatedApps = prev.filter(
(item) => !(item?.privateAppProperties?.name === app?.privateAppProperties?.name && item?.privateAppProperties?.service === app?.privateAppProperties?.service && item?.privateAppProperties?.identifier === app?.privateAppProperties?.identifier)
);
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps);
return updatedApps;
} else {
const updatedApps = prev.filter( const updatedApps = prev.filter(
(item) => !(item?.name === app?.name && item?.service === app?.service) (item) => !(item?.name === app?.name && item?.service === app?.service)
); );
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps); saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps);
return updatedApps; return updatedApps;
}
}); });
}}> }}>
<ListItemIcon sx={{ minWidth: '32px' }}> <ListItemIcon sx={{ minWidth: '32px' }}>

View File

@ -97,7 +97,7 @@ export const CoreSyncStatus = ({imageSize, position}) => {
<h4 className="lineHeight">{message}</h4> <h4 className="lineHeight">{message}</h4>
<h4 className="lineHeight">Block Height: <span style={{ color: '#03a9f4' }}>{height || ''}</span></h4> <h4 className="lineHeight">Block Height: <span style={{ color: '#03a9f4' }}>{height || ''}</span></h4>
<h4 className="lineHeight">Connected Peers: <span style={{ color: '#03a9f4' }}>{numberOfConnections || ''}</span></h4> <h4 className="lineHeight">Connected Peers: <span style={{ color: '#03a9f4' }}>{numberOfConnections || ''}</span></h4>
<h4 className="lineHeight">Using gateway: <span style={{ color: '#03a9f4' }}>{isUsingGateway?.toString()}</span></h4> <h4 className="lineHeight">Using public node: <span style={{ color: '#03a9f4' }}>{isUsingGateway?.toString()}</span></h4>
<i></i> <i></i>
</div> </div>
</div> </div>

View File

@ -18,9 +18,9 @@ import { NotificationIcon2 } from "../../assets/Icons/NotificationIcon2";
import { ChatIcon } from "../../assets/Icons/ChatIcon"; import { ChatIcon } from "../../assets/Icons/ChatIcon";
import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon"; import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon";
import { MembersIcon } from "../../assets/Icons/MembersIcon"; import { MembersIcon } from "../../assets/Icons/MembersIcon";
import { AdminsIcon } from "../../assets/Icons/AdminsIcon";
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from '@mui/icons-material/Lock';
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
import { AdminsIcon } from "../../assets/Icons/AdminsIcon";
const IconWrapper = ({ children, label, color, selected, selectColor, customHeight }) => { const IconWrapper = ({ children, label, color, selected, selectColor, customHeight }) => {
return ( return (
@ -118,7 +118,7 @@ export const DesktopHeader = ({
fontWeight: 600, fontWeight: 600,
}} }}
> >
{selectedGroup?.groupName} {selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName}
</Typography> </Typography>
</Box> </Box>
<Box <Box
@ -126,6 +126,7 @@ export const DesktopHeader = ({
display: "flex", display: "flex",
gap: "20px", gap: "20px",
alignItems: "center", alignItems: "center",
visibility: selectedGroup?.groupId === '0' ? 'hidden' : 'visibile'
}} }}
> >
@ -139,6 +140,7 @@ export const DesktopHeader = ({
label="ANN" label="ANN"
selected={isAnnouncement} selected={isAnnouncement}
selectColor="#09b6e8" selectColor="#09b6e8"
customHeight="55px"
> >
<NotificationIcon2 <NotificationIcon2
height={25} height={25}

View File

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

View File

@ -0,0 +1,101 @@
import { Box, ButtonBase, Typography } from "@mui/material";
import React from "react";
import ChatIcon from "@mui/icons-material/Chat";
import qTradeLogo from "../../assets/Icons/q-trade-logo.webp";
import AppsIcon from "@mui/icons-material/Apps";
import { executeEvent } from "../../utils/events";
export const Explore = ({setDesktopViewMode}) => {
return (
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
}}
>
<ButtonBase
sx={{
"&:hover": { backgroundColor: "secondary.main" },
transition: "all 0.1s ease-in-out",
padding: "5px",
borderRadius: "5px",
gap: "5px",
}}
onClick={async () => {
executeEvent("addTab", {
data: { service: "APP", name: "q-trade" },
});
executeEvent("open-apps-mode", {});
}}
>
<img
style={{
borderRadius: "50%",
height: '30px'
}}
src={qTradeLogo}
/>
<Typography
sx={{
fontSize: "1rem",
}}
>
Trade QORT
</Typography>
</ButtonBase>
<ButtonBase
sx={{
"&:hover": { backgroundColor: "secondary.main" },
transition: "all 0.1s ease-in-out",
padding: "5px",
borderRadius: "5px",
gap: "5px",
}}
onClick={()=> {
setDesktopViewMode('apps')
}}
>
<AppsIcon
sx={{
color: "white",
}}
/>
<Typography
sx={{
fontSize: "1rem",
}}
>
See Apps
</Typography>
</ButtonBase>
<ButtonBase
sx={{
"&:hover": { backgroundColor: "secondary.main" },
transition: "all 0.1s ease-in-out",
padding: "5px",
borderRadius: "5px",
gap: "5px",
}}
onClick={async () => {
executeEvent("openGroupMessage", {
from: "0" ,
});
}}
>
<ChatIcon
sx={{
color: "white",
}}
/>
<Typography
sx={{
fontSize: "1rem",
}}
>
General Chat
</Typography>
</ButtonBase>
</Box>
);
};

View File

@ -0,0 +1,190 @@
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
TextField,
Typography,
} from "@mui/material";
import React, { useContext, useEffect, useState } from "react";
import { MyContext } from "../../App";
import { Spacer } from "../../common/Spacer";
import { executeEvent } from "../../utils/events";
export const BlockedUsersModal = ({ close }) => {
const [hasChanged, setHasChanged] = useState(false);
const [value, setValue] = useState("");
const { getAllBlockedUsers, removeBlockFromList, addToBlockList } = useContext(MyContext);
const [blockedUsers, setBlockedUsers] = useState({
addresses: {},
names: {},
});
const fetchBlockedUsers = () => {
setBlockedUsers(getAllBlockedUsers());
};
useEffect(() => {
fetchBlockedUsers();
}, []);
return (
<Dialog
open={true}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle>Blocked Users</DialogTitle>
<DialogContent sx={{
padding: '20px'
}}>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
<TextField
placeholder="Name"
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
<Button variant="contained" onClick={async ()=> {
try {
if(!value) return
await addToBlockList(undefined, value)
fetchBlockedUsers()
setHasChanged(true)
} catch (error) {
console.error(error)
}
}}>Block</Button>
</Box>
{Object.entries(blockedUsers?.addresses).length > 0 && (
<>
<Spacer height="20px" />
<DialogContentText id="alert-dialog-description">
Blocked Users for Chat ( addresses )
</DialogContentText>
<Spacer height="10px" />
</>
)}
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: '10px'
}}>
{Object.entries(blockedUsers?.addresses || {})?.map(
([key, value]) => {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
width: '100%',
justifyContent: 'space-between'
}}
>
<Typography>{key}</Typography>
<Button
variant="contained"
onClick={async () => {
try {
await removeBlockFromList(key, undefined);
setHasChanged(true);
setValue('')
fetchBlockedUsers();
} catch (error) {
console.error(error);
}
}}
>
Unblock
</Button>
</Box>
);
}
)}
</Box>
{Object.entries(blockedUsers?.names).length > 0 && (
<>
<Spacer height="20px" />
<DialogContentText id="alert-dialog-description">
Blocked Users for QDN and Chat (names)
</DialogContentText>
<Spacer height="10px" />
</>
)}
<Box sx={{
display: 'flex',
flexDirection: 'column',
gap: '10px'
}}>
{Object.entries(blockedUsers?.names || {})?.map(([key, value]) => {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
width: '100%',
justifyContent: 'space-between'
}}
>
<Typography>{key}</Typography>
<Button
variant="contained"
onClick={async () => {
try {
await removeBlockFromList(undefined, key);
setHasChanged(true);
fetchBlockedUsers();
} catch (error) {
console.error(error);
}
}}
>
Unblock
</Button>
</Box>
);
})}
</Box>
</DialogContent>
<DialogActions>
<Button
sx={{
backgroundColor: "var(--green)",
color: "black",
fontWeight: "bold",
opacity: 0.7,
"&:hover": {
backgroundColor: "var(--green)",
color: "black",
opacity: 1,
},
}}
variant="contained"
onClick={()=> {
if(hasChanged){
executeEvent('updateChatMessagesWithBlocks', true)
}
close()
}}
>
close
</Button>
</DialogActions>
</Dialog>
);
};

View File

@ -94,13 +94,16 @@ import { AppsDesktop } from "../Apps/AppsDesktop";
import { formatEmailDate } from "./QMailMessages"; import { formatEmailDate } from "./QMailMessages";
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from '@mui/icons-material/Lock';
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
import { useSetRecoilState } from "recoil"; import { useRecoilState, useSetRecoilState } from "recoil";
import { addressInfoControllerAtom, selectedGroupIdAtom } from "../../atoms/global"; import { addressInfoControllerAtom, groupsPropertiesAtom, selectedGroupIdAtom } from "../../atoms/global";
import { sortArrayByTimestampAndGroupName } from "../../utils/time"; import { sortArrayByTimestampAndGroupName } from "../../utils/time";
import { AdminSpace } from "../Chat/AdminSpace"; import { AdminSpace } from "../Chat/AdminSpace";
import { HubsIcon } from "../../assets/Icons/HubsIcon"; import { HubsIcon } from "../../assets/Icons/HubsIcon";
import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
import { DesktopSideBar } from "../DesktopSideBar"; import { DesktopSideBar } from "../DesktopSideBar";
import BlockIcon from '@mui/icons-material/Block';
import { BlockedUsersModal } from "./BlockedUsersModal";
// let touchStartY = 0; // let touchStartY = 0;
// let disablePullToRefresh = false; // let disablePullToRefresh = false;
@ -480,6 +483,7 @@ export const Group = ({
const [mobileViewMode, setMobileViewMode] = useState("home"); const [mobileViewMode, setMobileViewMode] = useState("home");
const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState(""); const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState("");
const isFocusedRef = useRef(true); const isFocusedRef = useRef(true);
const [isOpenBlockedUserModal, setIsOpenBlockedUserModal] = React.useState(false);
const timestampEnterDataRef = useRef({}); const timestampEnterDataRef = useRef({});
const selectedGroupRef = useRef(null); const selectedGroupRef = useRef(null);
const selectedDirectRef = useRef(null); const selectedDirectRef = useRef(null);
@ -497,9 +501,11 @@ export const Group = ({
const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false) const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false)
const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom) const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom)
const [groupsProperties, setGroupsProperties] = useState({}) const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom)
const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom); const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom);
const isPrivate = useMemo(()=> { const isPrivate = useMemo(()=> {
if(selectedGroup?.groupId === '0') return false
if(!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null if(!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null
if(groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false if(groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false
if(groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true if(groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true
@ -899,7 +905,10 @@ export const Group = ({
} }
if(isPrivate === false){ if(isPrivate === false){
setTriedToFetchSecretKey(true); setTriedToFetchSecretKey(true);
if(selectedGroup?.groupId !== '0'){
getAdminsForPublic(selectedGroup) getAdminsForPublic(selectedGroup)
}
} }
}, [selectedGroup, isPrivate]); }, [selectedGroup, isPrivate]);
@ -988,7 +997,7 @@ export const Group = ({
// Update the component state with the received 'sendqort' state // Update the component state with the received 'sendqort' state
setGroups(sortArrayByTimestampAndGroupName(message.payload)); setGroups(sortArrayByTimestampAndGroupName(message.payload));
getLatestRegularChat(message.payload) getLatestRegularChat(message.payload)
setMemberGroups(message.payload); setMemberGroups(message.payload?.filter((item)=> item?.groupId !== '0'));
if (selectedGroupRef.current && groupSectionRef.current === "chat") { if (selectedGroupRef.current && groupSectionRef.current === "chat") {
chrome?.runtime?.sendMessage({ chrome?.runtime?.sendMessage({
@ -1081,7 +1090,7 @@ export const Group = ({
!initiatedGetMembers.current && !initiatedGetMembers.current &&
selectedGroup?.groupId && selectedGroup?.groupId &&
secretKey && secretKey &&
admins.includes(myAddress) admins.includes(myAddress) && selectedGroup?.groupId !== '0'
) { ) {
// getAdmins(selectedGroup?.groupId); // getAdmins(selectedGroup?.groupId);
getMembers(selectedGroup?.groupId); getMembers(selectedGroup?.groupId);
@ -1432,11 +1441,11 @@ export const Group = ({
if (isLoadingOpenSectionFromNotification.current) return; if (isLoadingOpenSectionFromNotification.current) return;
const groupId = e.detail?.from; const groupId = e.detail?.from;
const findGroup = groups?.find((group) => +group?.groupId === +groupId); const findGroup = groups?.find((group) => +group?.groupId === +groupId);
if (findGroup?.groupId === selectedGroup?.groupId) { if (findGroup?.groupId === selectedGroup?.groupId) {
isLoadingOpenSectionFromNotification.current = false; isLoadingOpenSectionFromNotification.current = false;
setChatMode("groups");
setDesktopViewMode('chat')
return; return;
} }
if (findGroup) { if (findGroup) {
@ -2159,7 +2168,7 @@ export const Group = ({
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={group.groupName} primary={group.groupId === '0' ? 'General' : group.groupName}
secondary={!group?.timestamp ? 'no messages' :`last message: ${formatEmailDate(group?.timestamp)}`} secondary={!group?.timestamp ? 'no messages' :`last message: ${formatEmailDate(group?.timestamp)}`}
primaryTypographyProps={{ primaryTypographyProps={{
style: { style: {
@ -2218,9 +2227,11 @@ export const Group = ({
width: "100%", width: "100%",
justifyContent: "center", justifyContent: "center",
padding: "10px", padding: "10px",
gap: '10px'
}} }}
> >
{chatMode === "groups" && ( {chatMode === "groups" && (
<>
<CustomButton <CustomButton
onClick={() => { onClick={() => {
setOpenAddGroup(true); setOpenAddGroup(true);
@ -2233,6 +2244,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
@ -2742,7 +2769,11 @@ export const Group = ({
)} )}
</div> </div>
)} )}
{isOpenBlockedUserModal && (
<BlockedUsersModal close={()=> {
setIsOpenBlockedUserModal(false)
}} />
)}
{selectedDirect && !newChat && ( {selectedDirect && !newChat && (
<> <>
<Box <Box
@ -2815,6 +2846,7 @@ export const Group = ({
{!isMobile && ( {!isMobile && (
<HomeDesktop <HomeDesktop
name={userInfo?.name}
refreshHomeDataFunc={refreshHomeDataFunc} refreshHomeDataFunc={refreshHomeDataFunc}
myAddress={myAddress} myAddress={myAddress}
isLoadingGroups={isLoadingGroups} isLoadingGroups={isLoadingGroups}

View File

@ -10,16 +10,20 @@ import CommentIcon from "@mui/icons-material/Comment";
import InfoIcon from "@mui/icons-material/Info"; import InfoIcon from "@mui/icons-material/Info";
import GroupAddIcon from "@mui/icons-material/GroupAdd"; import GroupAddIcon from "@mui/icons-material/GroupAdd";
import { executeEvent } from "../../utils/events"; import { executeEvent } from "../../utils/events";
import { Box, Typography } from "@mui/material"; import { Box, ButtonBase, Collapse, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { getGroupNames } from "./UserListOfInvites"; import { getGroupNames } from "./UserListOfInvites";
import { CustomLoader } from "../../common/CustomLoader"; import { CustomLoader } from "../../common/CustomLoader";
import { getBaseApiReact, isMobile } from "../../App"; import { getBaseApiReact, isMobile } from "../../App";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState( const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState(
[] []
); );
const [isExpanded, setIsExpanded] = React.useState(false);
const [loading, setLoading] = React.useState(true); const [loading, setLoading] = React.useState(true);
const getJoinRequests = async () => { const getJoinRequests = async () => {
@ -53,25 +57,33 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
alignItems: "center", alignItems: "center",
}} }}
> >
<Box <ButtonBase
sx={{ sx={{
width: "322px", width: "322px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "row",
padding: "0px 20px", padding: "0px 20px",
gap: '10px',
justifyContent: 'flex-start'
}} }}
onClick={()=> setIsExpanded((prev)=> !prev)}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: "1rem",
fontWeight: 600,
}} }}
> >
Group Invites: Group Invites {groupsWithJoinRequests?.length > 0 && ` (${groupsWithJoinRequests?.length})`}
</Typography> </Typography>
<Spacer height="10px" /> {isExpanded ? <ExpandLessIcon sx={{
</Box> marginLeft: 'auto'
}} /> : (
<ExpandMoreIcon sx={{
marginLeft: 'auto'
}}/>
)}
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box <Box
sx={{ sx={{
width: "322px", width: "322px",
@ -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, setDesktopViewMode }) => { export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection, setMobileViewMode, setDesktopViewMode }) => {
const [isExpanded, setIsExpanded] = React.useState(false)
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([]) const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([])
const [loading, setLoading] = React.useState(true) const [loading, setLoading] = React.useState(true)
const {txList, setTxList} = React.useContext(MyContext) const {txList, setTxList} = React.useContext(MyContext)
@ -34,7 +38,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
setLoading(true) setLoading(true)
let groupsAsAdmin = [] let groupsAsAdmin = []
const getAllGroupsAsAdmin = groups.map(async (group)=> { const getAllGroupsAsAdmin = groups.filter((item)=> item.groupId !== '0').map(async (group)=> {
const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(()=> { const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
return fetch( return fetch(
@ -55,7 +59,6 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
await Promise.all(getAllGroupsAsAdmin) await Promise.all(getAllGroupsAsAdmin)
setMyGroupsWhereIAmAdmin(groupsAsAdmin) setMyGroupsWhereIAmAdmin(groupsAsAdmin)
const res = await Promise.all(groupsAsAdmin.map(async (group)=> { const res = await Promise.all(groupsAsAdmin.map(async (group)=> {
const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> { const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
@ -110,26 +113,33 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
flexDirection: "column", flexDirection: "column",
alignItems: 'center' alignItems: 'center'
}}> }}>
<Box <ButtonBase
sx={{ sx={{
width: "322px", width: "322px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "row",
padding: '0px 20px', padding: '0px 20px',
gap: '10px',
justifyContent: 'flex-start'
}} }}
onClick={()=> setIsExpanded((prev)=> !prev)}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: "1rem",
fontWeight: 600,
}} }}
> >
Join Requests: Join Requests {filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length > 0 && ` (${filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length})`}
</Typography> </Typography>
<Spacer height="10px" /> {isExpanded ? <ExpandLessIcon sx={{
</Box> marginLeft: 'auto'
}} /> : (
<ExpandMoreIcon sx={{
marginLeft: 'auto'
}}/>
)}
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box <Box
sx={{ sx={{
width: "322px", width: "322px",
@ -173,7 +183,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
</Typography> </Typography>
</Box> </Box>
)} )}
<List sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper", maxHeight: '300px', overflow: 'auto' }}> <List className="scrollable-container" sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper", maxHeight: '300px', overflow: 'auto' }}>
{filteredJoinRequests?.map((group)=> { {filteredJoinRequests?.map((group)=> {
if(group?.data?.length === 0) return null if(group?.data?.length === 0) return null
return ( return (
@ -228,6 +238,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
</List> </List>
</Box> </Box>
</Collapse>
</Box> </Box>
); );
}; };

View File

@ -1,4 +1,4 @@
import { Box, Button, Typography } from "@mui/material"; import { Box, Button, Divider, Typography } from "@mui/material";
import React from "react"; import React from "react";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched"; import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched";
@ -7,10 +7,14 @@ import { GroupJoinRequests } from "./GroupJoinRequests";
import { GroupInvites } from "./GroupInvites"; import { GroupInvites } from "./GroupInvites";
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from "@mui/icons-material/Refresh";
import { ListOfGroupPromotions } from "./ListOfGroupPromotions"; import { ListOfGroupPromotions } from "./ListOfGroupPromotions";
import { QortPrice } from "../Home/QortPrice";
import ExploreIcon from "@mui/icons-material/Explore";
import { Explore } from "../Explore/Explore";
import { NewUsersCTA } from "../Home/NewUsersCTA";
export const HomeDesktop = ({ export const HomeDesktop = ({
refreshHomeDataFunc, refreshHomeDataFunc,
myAddress, myAddress,
name,
isLoadingGroups, isLoadingGroups,
balance, balance,
userInfo, userInfo,
@ -22,35 +26,61 @@ export const HomeDesktop = ({
setOpenAddGroup, setOpenAddGroup,
setMobileViewMode, setMobileViewMode,
setDesktopViewMode, setDesktopViewMode,
desktopViewMode desktopViewMode,
}) => { }) => {
const [checked1, setChecked1] = React.useState(false);
const [checked2, setChecked2] = React.useState(false);
React.useEffect(() => {
if (balance && +balance >= 6) {
setChecked1(true);
}
}, [balance]);
React.useEffect(() => {
if (name) setChecked2(true);
}, [name]);
const isLoaded = React.useMemo(()=> {
if(userInfo !== null) return true
return false
}, [ userInfo])
const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> {
if(isLoaded && checked1 && checked2) return true
return false
}, [checked1, isLoaded, checked2])
return ( return (
<Box <Box
sx={{ sx={{
display: desktopViewMode === 'home' ? 'flex' : 'none', display: desktopViewMode === "home" ? "flex" : "none",
width: "100%", width: "100%",
flexDirection: "column", flexDirection: "column",
height: "100%", height: "100%",
overflow: "auto", overflow: "auto",
alignItems: "center", alignItems: "center",
}} }}
> >
<Spacer height="20px" /> <Spacer height="20px" />
<Box sx={{ <Box
sx={{
display: "flex", display: "flex",
width: "100%", width: "100%",
flexDirection: "column", flexDirection: "column",
height: "100%", height: "100%",
alignItems: "flex-start", alignItems: "flex-start",
maxWidth: '1036px' maxWidth: "1036px",
}}> }}
>
<Typography <Typography
sx={{ sx={{
color: "rgba(255, 255, 255, 1)", color: "rgba(255, 255, 255, 1)",
fontWeight: 400, fontWeight: 400,
fontSize: userInfo?.name?.length > 15 ? "16px" : "20px", fontSize: userInfo?.name?.length > 15 ? "16px" : "20px",
padding: '10px' padding: "10px",
}} }}
> >
Welcome Welcome
@ -67,41 +97,52 @@ export const HomeDesktop = ({
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
gap: "15px", gap: "20px",
flexWrap: "wrap", flexWrap: "wrap",
width: "100%",
justifyContent: "center",
}}
>
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
flexDirection: "column",
}}
>
<Box
sx={{
width: "330px",
display: "flex",
alignItems: "center",
justifyContent: "center", justifyContent: "center",
}} }}
> >
<Box sx={{
width: '330px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<ThingsToDoInitial <ThingsToDoInitial
balance={balance} balance={balance}
myAddress={myAddress} myAddress={myAddress}
name={userInfo?.name} name={userInfo?.name}
hasGroups={groups?.length !== 0}
userInfo={userInfo} userInfo={userInfo}
hasGroups={
groups?.filter((item) => item?.groupId !== "0").length !== 0
}
/> />
</Box> </Box>
{desktopViewMode === 'home' && (
{desktopViewMode === "home" && (
<> <>
<Box sx={{
width: '330px', {hasDoneNameAndBalanceAndIsLoaded && (
display: 'flex', <>
alignItems: 'center', <Box
justifyContent: 'center' sx={{
}}> width: "330px",
<ListOfThreadPostsWatched /> display: "flex",
</Box> alignItems: "center",
<Box sx={{ justifyContent: "center",
width: '330px', }}
display: 'flex', >
alignItems: 'center',
justifyContent: 'center'
}}>
<GroupJoinRequests <GroupJoinRequests
setGroupSection={setGroupSection} setGroupSection={setGroupSection}
setSelectedGroup={setSelectedGroup} setSelectedGroup={setSelectedGroup}
@ -113,12 +154,14 @@ export const HomeDesktop = ({
setDesktopViewMode={setDesktopViewMode} setDesktopViewMode={setDesktopViewMode}
/> />
</Box> </Box>
<Box sx={{ <Box
width: '330px', sx={{
display: 'flex', width: "330px",
alignItems: 'center', display: "flex",
justifyContent: 'center' alignItems: "center",
}}> justifyContent: "center",
}}
>
<GroupInvites <GroupInvites
setOpenAddGroup={setOpenAddGroup} setOpenAddGroup={setOpenAddGroup}
myAddress={myAddress} myAddress={myAddress}
@ -128,33 +171,71 @@ export const HomeDesktop = ({
</Box> </Box>
</> </>
)} )}
</>
)}
</Box>
<QortPrice />
</Box> </Box>
)} )}
{!isLoadingGroups && ( {!isLoadingGroups && (
<ListOfGroupPromotions /> <>
)} <Spacer height="60px" />
</Box> <Divider
color="secondary"
<Spacer height="26px" />
{/* <Box
sx={{ sx={{
display: "flex",
width: "100%", width: "100%",
justifyContent: "flex-start",
}} }}
> >
<Button <Box
variant="outlined" sx={{
startIcon={<RefreshIcon />} display: "flex",
onClick={refreshHomeDataFunc} gap: "10px",
alignItems: "center",
}}
>
<ExploreIcon
sx={{ sx={{
color: "white", color: "white",
}} }}
/>{" "}
<Typography
sx={{
fontSize: "1rem",
}}
> >
Refresh home data Explore
</Button> </Typography>{" "}
</Box> */} </Box>
</Divider>
{!hasDoneNameAndBalanceAndIsLoaded && (
<Spacer height="40px" />
)}
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
width: "100%",
justifyContent: "center",
}}
>
{hasDoneNameAndBalanceAndIsLoaded && (
<ListOfGroupPromotions />
)}
<Explore setDesktopViewMode={setDesktopViewMode} />
</Box>
<NewUsersCTA balance={balance} />
</>
)}
</Box>
<Spacer height="26px" />
<Spacer height="180px" /> <Spacer height="180px" />
</Box> </Box>

View File

@ -9,6 +9,8 @@ import {
Avatar, Avatar,
Box, Box,
Button, Button,
ButtonBase,
Collapse,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
@ -28,8 +30,8 @@ import {
import { getNameInfo } from "./Group"; import { getNameInfo } from "./Group";
import { getBaseApi, getFee } from "../../background"; import { getBaseApi, getFee } from "../../background";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from "@mui/icons-material/Lock";
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; import NoEncryptionGmailerrorredIcon from "@mui/icons-material/NoEncryptionGmailerrorred";
import { import {
MyContext, MyContext,
getArbitraryEndpointReact, getArbitraryEndpointReact,
@ -40,7 +42,11 @@ import { Spacer } from "../../common/Spacer";
import { CustomLoader } from "../../common/CustomLoader"; import { CustomLoader } from "../../common/CustomLoader";
import { RequestQueueWithPromise } from "../../utils/queue/queue"; import { RequestQueueWithPromise } from "../../utils/queue/queue";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { myGroupsWhereIAmAdminAtom, promotionTimeIntervalAtom, promotionsAtom } from "../../atoms/global"; import {
myGroupsWhereIAmAdminAtom,
promotionTimeIntervalAtom,
promotionsAtom,
} from "../../atoms/global";
import { Label } from "./AddGroup"; import { Label } from "./AddGroup";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { CustomizedSnackbars } from "../Snackbar/Snackbar";
@ -48,7 +54,8 @@ import { getGroupNames } from "./UserListOfInvites";
import { WrapperUserAction } from "../WrapperUserAction"; import { WrapperUserAction } from "../WrapperUserAction";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import ErrorBoundary from "../../common/ErrorBoundary"; import ErrorBoundary from "../../common/ErrorBoundary";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
export const requestQueuePromos = new RequestQueueWithPromise(20); export const requestQueuePromos = new RequestQueueWithPromise(20);
export function utf8ToBase64(inputString: string): string { export function utf8ToBase64(inputString: string): string {
@ -65,7 +72,6 @@ export function utf8ToBase64(inputString: string): string {
const uid = new ShortUniqueId({ length: 8 }); const uid = new ShortUniqueId({ length: 8 });
export function getGroupId(str) { export function getGroupId(str) {
const match = str.match(/group-(\d+)-/); const match = str.match(/group-(\d+)-/);
return match ? match[1] : null; return match ? match[1] : null;
@ -81,12 +87,12 @@ export const ListOfGroupPromotions = () => {
const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState( const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState(
myGroupsWhereIAmAdminAtom myGroupsWhereIAmAdminAtom
); );
const [promotions, setPromotions] = useRecoilState( const [promotions, setPromotions] = useRecoilState(promotionsAtom);
promotionsAtom
);
const [promotionTimeInterval, setPromotionTimeInterval] = useRecoilState( const [promotionTimeInterval, setPromotionTimeInterval] = useRecoilState(
promotionTimeIntervalAtom promotionTimeIntervalAtom
); );
const [isExpanded, setIsExpanded] = React.useState(false);
const [openSnack, setOpenSnack] = useState(false); const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null); const [infoSnack, setInfoSnack] = useState(null);
const [fee, setFee] = useState(null); const [fee, setFee] = useState(null);
@ -95,7 +101,6 @@ export const ListOfGroupPromotions = () => {
const { show, setTxList } = useContext(MyContext); const { show, setTxList } = useContext(MyContext);
const listRef = useRef(); const listRef = useRef();
const rowVirtualizer = useVirtualizer({ const rowVirtualizer = useVirtualizer({
count: promotions.length, count: promotions.length,
getItemKey: React.useCallback( getItemKey: React.useCallback(
@ -107,7 +112,6 @@ export const ListOfGroupPromotions = () => {
overscan: 10, // Number of items to render outside the visible area to improve smoothness overscan: 10, // Number of items to render outside the visible area to improve smoothness
}); });
useEffect(() => { useEffect(() => {
try { try {
(async () => { (async () => {
@ -118,7 +122,7 @@ export const ListOfGroupPromotions = () => {
}, []); }, []);
const getPromotions = useCallback(async () => { const getPromotions = useCallback(async () => {
try { try {
setPromotionTimeInterval(Date.now()) setPromotionTimeInterval(Date.now());
const identifier = `group-promotions-ui24-`; const identifier = `group-promotions-ui24-`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`;
const response = await fetch(url, { const response = await fetch(url, {
@ -169,7 +173,9 @@ export const ListOfGroupPromotions = () => {
}); });
await Promise.all(getPromos); await Promise.all(getPromos);
const groupWithInfo = await getGroupNames(data.sort((a, b) => b.created - a.created)); const groupWithInfo = await getGroupNames(
data.sort((a, b) => b.created - a.created)
);
setPromotions(groupWithInfo); setPromotions(groupWithInfo);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -180,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(() => {
@ -321,20 +328,64 @@ export const ListOfGroupPromotions = () => {
}; };
return ( return (
<Box <Box
sx={{ sx={{
width: "100%", width: "100%",
display: "flex", display: "flex",
marginTop: "20px",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
marginTop: "25px", justifyContent: "center",
}} }}
> >
<Box sx={{
display: 'flex',
gap: '20px',
width: '100%',
justifyContent: 'space-between'
}}>
<ButtonBase
sx={{
display: "flex",
flexDirection: "row",
padding: `0px ${isExpanded ? "24px" : "20px"}`,
gap: "10px",
justifyContent: "flex-start",
alignSelf: isExpanded && "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>
<Box
style={{
width: "330px",
}}
/>
</Box>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<>
<Box <Box
sx={{ sx={{
width: isMobile ? "320px" : "750px", width: isMobile ? "320px" : "750px",
@ -357,9 +408,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)}
@ -372,7 +421,6 @@ 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",
@ -455,7 +503,6 @@ export const ListOfGroupPromotions = () => {
const index = virtualRow.index; const index = virtualRow.index;
const promotion = promotions[index]; const promotion = promotions[index];
return ( return (
<div <div
data-index={virtualRow.index} //needed for dynamic row height measurement data-index={virtualRow.index} //needed for dynamic row height measurement
ref={rowVirtualizer.measureElement} //measure dynamic row height ref={rowVirtualizer.measureElement} //measure dynamic row height
@ -535,7 +582,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
@ -554,8 +602,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" />
@ -581,7 +630,10 @@ export const ListOfGroupPromotions = () => {
loadingPosition="start" loadingPosition="start"
variant="contained" variant="contained"
onClick={() => onClick={() =>
handleJoinGroup(promotion, promotion?.isOpen) handleJoinGroup(
promotion,
promotion?.isOpen
)
} }
> >
Join Join
@ -638,20 +690,26 @@ export const ListOfGroupPromotions = () => {
</Typography> </Typography>
</Box> </Box>
<Spacer height="20px" /> <Spacer height="20px" />
<Box sx={{ <Box
display: 'flex', sx={{
gap: '20px', display: "flex",
alignItems: 'center' gap: "20px",
}}> alignItems: "center",
}}
>
{promotion?.isOpen === false && ( {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={{
@ -659,7 +717,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" />
@ -682,10 +742,12 @@ export const ListOfGroupPromotions = () => {
> >
<Button <Button
// variant="contained" // variant="contained"
onClick={(event) => handlePopoverOpen(event, promotion?.groupId)} onClick={(event) =>
handlePopoverOpen(event, promotion?.groupId)
}
sx={{ sx={{
fontSize: "12px", fontSize: "12px",
color: 'white' color: "white",
}} }}
> >
Join Group: {` ${promotion?.groupName}`} Join Group: {` ${promotion?.groupName}`}
@ -695,7 +757,6 @@ export const ListOfGroupPromotions = () => {
<Spacer height="50px" /> <Spacer height="50px" />
</ErrorBoundary> </ErrorBoundary>
</div> </div>
); );
})} })}
</div> </div>
@ -703,6 +764,8 @@ export const ListOfGroupPromotions = () => {
</div> </div>
</div> </div>
</Box> </Box>
</>
</Collapse>
<Spacer height="20px" /> <Spacer height="20px" />
{isShowModal && ( {isShowModal && (
@ -738,6 +801,7 @@ export const ListOfGroupPromotions = () => {
value={selectedGroup} value={selectedGroup}
label="Groups where you are an admin" label="Groups where you are an admin"
onChange={(e) => setSelectedGroup(e.target.value)} onChange={(e) => setSelectedGroup(e.target.value)}
variant="outlined"
> >
{myGroupsWhereIAmAdmin?.map((group) => { {myGroupsWhereIAmAdmin?.map((group) => {
return ( return (

View File

@ -1,11 +1,11 @@
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import List from "@mui/material/List"; import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem"; import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton"; import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText"; import ListItemText from "@mui/material/ListItemText";
import moment from 'moment' import moment from 'moment'
import { Box, Typography } from "@mui/material"; import { Box, ButtonBase, Collapse, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { getBaseApiReact, isMobile } from "../../App"; import { getBaseApiReact, isMobile } from "../../App";
import { MessagingIcon } from '../../assets/Icons/MessagingIcon'; import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
@ -15,6 +15,9 @@ import { executeEvent } from '../../utils/events';
import { CustomLoader } from '../../common/CustomLoader'; import { CustomLoader } from '../../common/CustomLoader';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global'; import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import MarkEmailUnreadIcon from '@mui/icons-material/MarkEmailUnread';
export const isLessThanOneWeekOld = (timestamp) => { export const isLessThanOneWeekOld = (timestamp) => {
// Current time in milliseconds // Current time in milliseconds
const now = Date.now(); const now = Date.now();
@ -41,6 +44,7 @@ export function formatEmailDate(timestamp: number) {
} }
} }
export const QMailMessages = ({userName, userAddress}) => { export const QMailMessages = ({userName, userAddress}) => {
const [isExpanded, setIsExpanded] = useState(false)
const [mails, setMails] = useRecoilState(mailsAtom) const [mails, setMails] = useRecoilState(mailsAtom)
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom) const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@ -99,7 +103,16 @@ export const QMailMessages = ({userName, userAddress}) => {
}, [getMails, userName, userAddress]); }, [getMails, userName, userAddress]);
const anyUnread = useMemo(()=> {
let unread = false
mails.forEach((mail)=> {
if(lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created)){
unread = true
}
})
return unread
}, [mails, lastEnteredTimestamp])
return ( return (
<Box <Box
@ -111,26 +124,39 @@ export const QMailMessages = ({userName, userAddress}) => {
}} }}
> >
<Box <ButtonBase
sx={{ sx={{
width: "322px", width: "322px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "row",
gap: '10px',
padding: "0px 20px", padding: "0px 20px",
justifyContent: 'flex-start'
}} }}
onClick={()=> setIsExpanded((prev)=> !prev)}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: "1rem",
fontWeight: 600,
}} }}
> >
Latest Q-Mails Latest Q-Mails
</Typography> </Typography>
<Spacer height="10px" /> <MarkEmailUnreadIcon sx={{
</Box> color: anyUnread ? '--unread' : 'white'
}}/>
{isExpanded ? <ExpandLessIcon sx={{
marginLeft: 'auto'
}} /> : (
<ExpandMoreIcon sx={{
color: anyUnread ? '--unread' : 'white',
marginLeft: 'auto'
}} />
)}
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box <Box
className="scrollable-container"
sx={{ sx={{
width: "322px", width: "322px",
height: isMobile ? "165px" : "250px", height: isMobile ? "165px" : "250px",
@ -247,6 +273,7 @@ export const QMailMessages = ({userName, userAddress}) => {
</Box> </Box>
</Collapse>
</Box> </Box>
) )
} }

View File

@ -12,27 +12,17 @@ import { Box, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { isMobile } from "../../App"; import { isMobile } from "../../App";
import { QMailMessages } from "./QMailMessages"; import { QMailMessages } from "./QMailMessages";
import { executeEvent } from "../../utils/events";
export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInfo }) => { export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInfo }) => {
const [checked1, setChecked1] = React.useState(false); const [checked1, setChecked1] = React.useState(false);
const [checked2, setChecked2] = React.useState(false); const [checked2, setChecked2] = React.useState(false);
const [checked3, setChecked3] = React.useState(false); // const [checked3, setChecked3] = React.useState(false);
// const getAddressInfo = async (address) => { // React.useEffect(() => {
// const response = await fetch(getBaseApiReact() + "/addresses/" + address); // if (hasGroups) setChecked3(true);
// const data = await response.json(); // }, [hasGroups]);
// if (data.error && data.error === 124) {
// setChecked1(false);
// } else if (data.address) {
// setChecked1(true);
// }
// };
// const checkInfo = async () => {
// try {
// getAddressInfo(myAddress);
// } catch (error) {}
// };
React.useEffect(() => { React.useEffect(() => {
if (balance && +balance >= 6) { if (balance && +balance >= 6) {
@ -40,9 +30,6 @@ export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInf
} }
}, [balance]); }, [balance]);
React.useEffect(() => {
if (hasGroups) setChecked3(true);
}, [hasGroups]);
React.useEffect(() => { React.useEffect(() => {
if (name) setChecked2(true); if (name) setChecked2(true);
@ -64,6 +51,7 @@ return (
<QMailMessages userAddress={userInfo?.address} userName={userInfo?.name} /> <QMailMessages userAddress={userInfo?.address} userName={userInfo?.name} />
); );
} }
if(!isLoaded) return null
return ( return (
<Box <Box
@ -84,12 +72,11 @@ return (
> >
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: "1rem",
fontWeight: 600, fontWeight: 600,
}} }}
> >
{!isLoaded ? 'Loading...' : 'Getting Started' } {!isLoaded ? 'Loading...' : 'Getting Started' }
</Typography> </Typography>
<Spacer height="10px" /> <Spacer height="10px" />
</Box> </Box>
@ -97,7 +84,6 @@ return (
<Box <Box
sx={{ sx={{
width: "322px", width: "322px",
height: isMobile ? "165px" : "250px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
bgcolor: "background.paper", bgcolor: "background.paper",
@ -105,17 +91,10 @@ return (
borderRadius: "19px", borderRadius: "19px",
}} }}
> >
{isLoaded && (
<List sx={{ width: "100%", maxWidth: 360 }}> <List sx={{ width: "100%", maxWidth: 360 }}>
<ListItem <ListItem
// secondaryAction={
// <IconButton edge="end" aria-label="comments">
// <InfoIcon
// sx={{
// color: "white",
// }}
// />
// </IconButton>
// }
disablePadding disablePadding
sx={{ sx={{
marginBottom: '20px' marginBottom: '20px'
@ -128,11 +107,14 @@ return (
disableRipple disableRipple
role={undefined} role={undefined}
dense dense
onClick={()=> {
executeEvent("openBuyQortInfo", {})
}}
> >
<ListItemText <ListItemText
sx={{ sx={{
"& .MuiTypography-root": { "& .MuiTypography-root": {
fontSize: "13px", fontSize: "1rem",
fontWeight: 400, fontWeight: 400,
}, },
}} }}
@ -189,9 +171,11 @@ return (
padding: "0px", padding: "0px",
}} disableRipple role={undefined} dense> }} disableRipple role={undefined} dense>
<ListItemText sx={{ <ListItemText onClick={() => {
executeEvent('openRegisterName', {})
}} sx={{
"& .MuiTypography-root": { "& .MuiTypography-root": {
fontSize: "13px", fontSize: "1rem",
fontWeight: 400, fontWeight: 400,
}, },
}} primary={`Register a name`} /> }} primary={`Register a name`} />
@ -210,16 +194,7 @@ return (
</ListItemIcon> </ListItemIcon>
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem {/* <ListItem
// secondaryAction={
// <IconButton edge="end" aria-label="comments">
// <InfoIcon
// sx={{
// color: "white",
// }}
// />
// </IconButton>
// }
disablePadding disablePadding
> >
<ListItemButton sx={{ <ListItemButton sx={{
@ -231,7 +206,7 @@ return (
fontSize: "13px", fontSize: "13px",
fontWeight: 400, fontWeight: 400,
}, },
}} primary={`Join a group hub`} /> }} primary={`Join a group`} />
<ListItemIcon sx={{ <ListItemIcon sx={{
justifyContent: "flex-end", justifyContent: "flex-end",
}}> }}>
@ -246,8 +221,10 @@ return (
/> />
</ListItemIcon> </ListItemIcon>
</ListItemButton> </ListItemButton>
</ListItem> </ListItem> */}
</List> </List>
)}
</Box> </Box>
</Box> </Box>
); );

View File

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

View File

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

View File

@ -0,0 +1,93 @@
import { Box, ButtonBase, Typography } from "@mui/material";
import React from "react";
import { Spacer } from "../../common/Spacer";
export const NewUsersCTA = ({ balance }) => {
if (balance === undefined || +balance > 0) return null;
return (
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Spacer height="40px" />
<Box
sx={{
width: "320px",
justifyContent: "center",
flexDirection: "column",
alignItems: "center",
padding: "15px",
outline: "1px solid gray",
borderRadius: "4px",
}}
>
<Typography
sx={{
textAlign: "center",
fontSize: "1.2rem",
fontWeight: "bold",
}}
>
Are you a new user?
</Typography>
<Spacer height="20px" />
<Typography>
Please message us on Telegram or Discord if you need 4 QORT to start
chatting without any limitations
</Typography>
<Spacer height="20px" />
<Box
sx={{
width: "100%",
display: "flex",
gap: "10px",
justifyContent: "center",
}}
>
<ButtonBase
sx={{
textDecoration: "underline",
}}
onClick={() => {
if (chrome && chrome.tabs) {
chrome.tabs.create({ url: "https://link.qortal.dev/telegram-invite" }, (tab) => {
if (chrome.runtime.lastError) {
console.error("Error opening tab:", chrome.runtime.lastError);
} else {
console.log("Tab opened successfully:", tab);
}
});
}
}}
>
Telegram
</ButtonBase>
<ButtonBase
sx={{
textDecoration: "underline",
}}
onClick={() => {
if (chrome && chrome.tabs) {
chrome.tabs.create({ url: "https://link.qortal.dev/discord-invite" }, (tab) => {
if (chrome.runtime.lastError) {
console.error("Error opening tab:", chrome.runtime.lastError);
} else {
console.log("Tab opened successfully:", tab);
}
});
}
}}
>
Discord
</ButtonBase>
</Box>
</Box>
</Box>
);
};

View File

@ -0,0 +1,257 @@
import React, { useCallback, useEffect, useState } from "react";
import { getBaseApiReact } from "../../App";
import { Box, Tooltip, Typography } from "@mui/material";
import { BarSpinner } from "../../common/Spinners/BarSpinner/BarSpinner";
import { formatDate } from "../../utils/time";
function getAverageLtcPerQort(trades) {
let totalQort = 0;
let totalLtc = 0;
trades.forEach((trade) => {
const qort = parseFloat(trade.qortAmount);
const ltc = parseFloat(trade.foreignAmount);
totalQort += qort;
totalLtc += ltc;
});
// Avoid division by zero
if (totalQort === 0) return 0;
// Weighted average price
return parseFloat((totalLtc / totalQort).toFixed(8));
}
function getTwoWeeksAgoTimestamp() {
const now = new Date();
now.setDate(now.getDate() - 14); // Subtract 14 days
return now.getTime(); // Get timestamp in milliseconds
}
function formatWithCommasAndDecimals(number) {
return Number(number).toLocaleString();
}
export const QortPrice = () => {
const [ltcPerQort, setLtcPerQort] = useState(null);
const [supply, setSupply] = useState(null);
const [lastBlock, setLastBlock] = useState(null);
const [loading, setLoading] = useState(true);
const getPrice = useCallback(async () => {
try {
setLoading(true);
const response = await fetch(
`${getBaseApiReact()}/crosschain/trades?foreignBlockchain=LITECOIN&minimumTimestamp=${getTwoWeeksAgoTimestamp()}&limit=20&reverse=true`
);
const data = await response.json();
setLtcPerQort(getAverageLtcPerQort(data));
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}, []);
const getLastBlock = useCallback(async () => {
try {
setLoading(true);
const response = await fetch(`${getBaseApiReact()}/blocks/last`);
const data = await response.json();
setLastBlock(data);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}, []);
const getSupplyInCirculation = useCallback(async () => {
try {
setLoading(true);
const response = await fetch(
`${getBaseApiReact()}/stats/supply/circulating`
);
const data = await response.text();
formatWithCommasAndDecimals(data);
setSupply(formatWithCommasAndDecimals(data));
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
getPrice();
getSupplyInCirculation();
getLastBlock();
const interval = setInterval(() => {
getPrice();
getSupplyInCirculation();
getLastBlock();
}, 900000);
return () => clearInterval(interval);
}, [getPrice]);
return (
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
flexDirection: "column",
width: "322px",
}}
>
<Tooltip
title={
<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>
Based on the latest 20 trades
</span>
}
placement="bottom"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<Box
sx={{
width: "322px",
display: "flex",
flexDirection: "row",
gap: "10px",
justifyContent: "space-between",
}}
>
<Typography
sx={{
fontSize: "1rem",
fontWeight: "bold",
}}
>
Price
</Typography>
{!ltcPerQort ? (
<BarSpinner width="16px" color="white" />
) : (
<Typography
sx={{
fontSize: "1rem",
}}
>
{ltcPerQort} LTC/QORT
</Typography>
)}
</Box>
</Tooltip>
<Box
sx={{
width: "322px",
display: "flex",
flexDirection: "row",
gap: "10px",
justifyContent: "space-between",
}}
>
<Typography
sx={{
fontSize: "1rem",
fontWeight: "bold",
}}
>
Supply
</Typography>
{!supply ? (
<BarSpinner width="16px" color="white" />
) : (
<Typography
sx={{
fontSize: "1rem",
}}
>
{supply} QORT
</Typography>
)}
</Box>
<Tooltip
title={
<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>
{lastBlock?.timestamp && formatDate(lastBlock?.timestamp)}
</span>
}
placement="bottom"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<Box
sx={{
width: "322px",
display: "flex",
flexDirection: "row",
gap: "10px",
justifyContent: "space-between",
}}
>
<Typography
sx={{
fontSize: "1rem",
fontWeight: "bold",
}}
>
Last height
</Typography>
{!lastBlock?.height ? (
<BarSpinner width="16px" color="white" />
) : (
<Typography
sx={{
fontSize: "1rem",
}}
>
{lastBlock?.height}
</Typography>
)}
</Box>
</Tooltip>
</Box>
);
};

View File

@ -7,8 +7,9 @@ import ImageUploader from "../common/ImageUploader";
import { getFee } from "../background"; import { getFee } from "../background";
import { fileToBase64 } from "../utils/fileReading"; import { fileToBase64 } from "../utils/fileReading";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import ErrorIcon from '@mui/icons-material/Error';
export const MainAvatar = ({ myName }) => { export const MainAvatar = ({ myName, balance, setOpenSnack, setInfoSnack }) => {
const [hasAvatar, setHasAvatar] = useState(false); const [hasAvatar, setHasAvatar] = useState(false);
const [avatarFile, setAvatarFile] = useState(null); const [avatarFile, setAvatarFile] = useState(null);
const [tempAvatar, setTempAvatar] = useState(null) const [tempAvatar, setTempAvatar] = useState(null)
@ -52,10 +53,11 @@ const [isLoading, setIsLoading] = useState(false)
checkIfAvatarExists(); checkIfAvatarExists();
}, [myName]); }, [myName]);
const publishAvatar = async ()=> { const publishAvatar = async ()=> {
try { try {
const fee = await getFee('ARBITRARY') const fee = await getFee('ARBITRARY')
if(+balance < +fee.fee) throw new Error(`Publishing an Avatar requires ${fee.fee}`)
await show({ await show({
message: "Would you like to publish an avatar?" , message: "Would you like to publish an avatar?" ,
publishFee: fee.fee + ' QORT' publishFee: fee.fee + ' QORT'
@ -86,7 +88,13 @@ const [isLoading, setIsLoading] = useState(false)
setTempAvatar(`data:image/webp;base64,${avatarBase64}`) setTempAvatar(`data:image/webp;base64,${avatarBase64}`)
handleClose() handleClose()
} catch (error) { } catch (error) {
if (error?.message) {
setOpenSnack(true)
setInfoSnack({
type: "error",
message: error?.message,
});
}
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -115,7 +123,7 @@ const [isLoading, setIsLoading] = useState(false)
change avatar change avatar
</Typography> </Typography>
</ButtonBase> </ButtonBase>
<PopoverComp avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} /> <PopoverComp myName={myName} avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} />
</> </>
); );
} }
@ -143,7 +151,7 @@ const [isLoading, setIsLoading] = useState(false)
change avatar change avatar
</Typography> </Typography>
</ButtonBase> </ButtonBase>
<PopoverComp avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} /> <PopoverComp myName={myName} avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} />
</> </>
); );
} }
@ -161,13 +169,13 @@ const [isLoading, setIsLoading] = useState(false)
set avatar set avatar
</Typography> </Typography>
</ButtonBase> </ButtonBase>
<PopoverComp avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} /> <PopoverComp myName={myName} avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} />
</> </>
); );
}; };
const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose, publishAvatar, isLoading}) => { const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose, publishAvatar, isLoading, myName}) => {
return ( return (
<Popover <Popover
id={id} id={id}
@ -196,8 +204,21 @@ const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose
</ImageUploader> </ImageUploader>
{avatarFile?.name} {avatarFile?.name}
<Spacer height="25px" /> <Spacer height="25px" />
{!myName && (
<Box sx={{
display: 'flex',
gap: '5px',
alignItems: 'center'
}}>
<ErrorIcon sx={{
color: 'white'
}} />
<Typography>A registered name is required to set an avatar</Typography>
</Box>
)}
<LoadingButton loading={isLoading} disabled={!avatarFile} onClick={publishAvatar} variant="contained"> <Spacer height="25px" />
<LoadingButton loading={isLoading} disabled={!avatarFile || !myName} onClick={publishAvatar} variant="contained">
Publish avatar Publish avatar
</LoadingButton> </LoadingButton>
</Box> </Box>

View File

@ -3,7 +3,7 @@ import QMailLogo from '../assets/QMailLogo.png'
import { useRecoilState } from 'recoil' import { useRecoilState } from 'recoil'
import { mailsAtom, qMailLastEnteredTimestampAtom } from '../atoms/global' import { mailsAtom, qMailLastEnteredTimestampAtom } from '../atoms/global'
import { isLessThanOneWeekOld } from './Group/QMailMessages' import { isLessThanOneWeekOld } from './Group/QMailMessages'
import { ButtonBase } from '@mui/material' import { ButtonBase, Tooltip } from '@mui/material'
import { executeEvent } from '../utils/events' import { executeEvent } from '../utils/events'
export const QMailStatus = () => { export const QMailStatus = () => {
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom) const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom)
@ -35,9 +35,28 @@ export const QMailStatus = () => {
borderRadius: '50%', borderRadius: '50%',
outline: '1px solid white' outline: '1px solid white'
}} /> }} />
)}<img style={{ )}
width: '24px', <Tooltip
height: 'auto' title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>Q-MAIL</span>}
}} src={QMailLogo} /></ButtonBase> placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<img style={{ width: '24px', height: 'auto' }} src={QMailLogo} />
</Tooltip>
</ButtonBase>
) )
} }

View File

@ -0,0 +1,308 @@
import React, { useCallback, useContext, useEffect, useState } from 'react'
import {
Avatar,
Box,
Button,
ButtonBase,
Collapse,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Input,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemIcon,
ListItemText,
List,
MenuItem,
Popover,
Select,
TextField,
Typography,
} from "@mui/material";
import { Label } from './Group/AddGroup';
import { Spacer } from '../common/Spacer';
import { LoadingButton } from '@mui/lab';
import { getBaseApiReact, MyContext } from '../App';
import { getFee } from '../background';
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
import { subscribeToEvent, unsubscribeFromEvent } from '../utils/events';
import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner';
import CheckIcon from '@mui/icons-material/Check';
import ErrorIcon from '@mui/icons-material/Error';
enum Availability {
NULL = 'null',
LOADING = 'loading',
AVAILABLE = 'available',
NOT_AVAILABLE = 'not-available'
}
export const RegisterName = ({setOpenSnack, setInfoSnack, userInfo, show, setTxList, balance}) => {
const [isOpen, setIsOpen] = useState(false)
const [registerNameValue, setRegisterNameValue] = useState('')
const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false)
const [isNameAvailable, setIsNameAvailable] = useState<Availability>(Availability.NULL)
const [nameFee, setNameFee] = useState(null)
const checkIfNameExisits = async (name)=> {
if(!name?.trim()){
setIsNameAvailable(Availability.NULL)
return
}
setIsNameAvailable(Availability.LOADING)
try {
const res = await fetch(`${getBaseApiReact()}/names/` + name);
const data = await res.json()
if(data?.message === 'name unknown'){
setIsNameAvailable(Availability.AVAILABLE)
} else {
setIsNameAvailable(Availability.NOT_AVAILABLE)
}
} catch (error) {
console.error(error)
} finally {
}
}
// Debounce logic
useEffect(() => {
const handler = setTimeout(() => {
checkIfNameExisits(registerNameValue);
}, 500);
// Cleanup timeout if searchValue changes before the timeout completes
return () => {
clearTimeout(handler);
};
}, [registerNameValue]);
const openRegisterNameFunc = useCallback((e) => {
setIsOpen(true)
}, [ setIsOpen]);
useEffect(() => {
subscribeToEvent("openRegisterName", openRegisterNameFunc);
return () => {
unsubscribeFromEvent("openRegisterName", openRegisterNameFunc);
};
}, [openRegisterNameFunc]);
useEffect(()=> {
const nameRegistrationFee = async ()=> {
try {
const fee = await getFee("REGISTER_NAME");
setNameFee(fee?.fee)
} catch (error) {
console.error(error)
}
}
nameRegistrationFee()
}, [])
const registerName = async () => {
try {
if (!userInfo?.address) throw new Error("Your address was not found");
if(!registerNameValue) throw new Error('Enter a name')
const fee = await getFee("REGISTER_NAME");
await show({
message: "Would you like to register this name?",
publishFee: fee.fee + " QORT",
});
setIsLoadingRegisterName(true);
new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "registerName",
payload: {
name: registerNameValue,
},
},
(response) => {
if (!response?.error) {
res(response);
setIsLoadingRegisterName(false);
setInfoSnack({
type: "success",
message:
"Successfully registered. It may take a couple of minutes for the changes to propagate",
});
setIsOpen(false);
setRegisterNameValue("");
setOpenSnack(true);
setTxList((prev) => [
{
...response,
type: "register-name",
label: `Registered name: awaiting confirmation. This may take a couple minutes.`,
labelDone: `Registered name: success!`,
done: false,
},
...prev.filter((item) => !item.done),
]);
return;
}
setInfoSnack({
type: "error",
message: response?.error,
});
setOpenSnack(true);
rej(response.error);
}
);
});
} catch (error) {
if (error?.message) {
setOpenSnack(true)
setInfoSnack({
type: "error",
message: error?.message,
});
}
} finally {
setIsLoadingRegisterName(false);
}
};
return (
<Dialog
open={isOpen}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Register name"}
</DialogTitle>
<DialogContent>
<Box
sx={{
width: "400px",
maxWidth: '90vw',
height: "500px",
maxHeight: '90vh',
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
}}
>
<Label>Choose a name</Label>
<TextField
autoComplete='off'
autoFocus
onChange={(e) => setRegisterNameValue(e.target.value)}
value={registerNameValue}
placeholder="Choose a name"
/>
{(!balance || (nameFee && balance && balance < nameFee))&& (
<>
<Spacer height="10px" />
<Box sx={{
display: 'flex',
gap: '5px',
alignItems: 'center'
}}>
<ErrorIcon sx={{
color: 'white'
}} />
<Typography>Your balance is {balance ?? 0} QORT. A name registration requires a {nameFee} QORT fee</Typography>
</Box>
<Spacer height="10px" />
</>
)}
<Spacer height="5px" />
{isNameAvailable === Availability.AVAILABLE && (
<Box sx={{
display: 'flex',
gap: '5px',
alignItems: 'center'
}}>
<CheckIcon sx={{
color: 'white'
}} />
<Typography>{registerNameValue} is available</Typography>
</Box>
)}
{isNameAvailable === Availability.NOT_AVAILABLE && (
<Box sx={{
display: 'flex',
gap: '5px',
alignItems: 'center'
}}>
<ErrorIcon sx={{
color: 'white'
}} />
<Typography>{registerNameValue} is unavailable</Typography>
</Box>
)}
{isNameAvailable === Availability.LOADING && (
<Box sx={{
display: 'flex',
gap: '5px',
alignItems: 'center'
}}>
<BarSpinner width="16px" color="white" />
<Typography>Checking if name already existis</Typography>
</Box>
)}
<Spacer height="25px" />
<Typography sx={{
textDecoration: 'underline'
}}>Benefits of a name</Typography>
<List
sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}
aria-label="contacts"
>
<ListItem disablePadding>
<ListItemIcon>
<RadioButtonCheckedIcon sx={{
color: 'white'
}} />
</ListItemIcon>
<ListItemText primary="Publish data to Qortal: anything from apps to videos. Fully decentralized!" />
</ListItem>
<ListItem disablePadding>
<ListItemIcon>
<RadioButtonCheckedIcon sx={{
color: 'white'
}} />
</ListItemIcon>
<ListItemText primary="Secure ownership of data published by your name. You can even sell your name, along with your data to a third party." />
</ListItem>
</List>
</Box>
</DialogContent>
<DialogActions>
<Button
disabled={isLoadingRegisterName}
variant="contained"
onClick={() => {
setIsOpen(false)
setRegisterNameValue('')
}}
>
Close
</Button>
<Button
disabled={!registerNameValue.trim() ||isLoadingRegisterName || isNameAvailable !== Availability.AVAILABLE || !balance || ((balance && nameFee) && +balance < +nameFee)}
variant="contained"
onClick={registerName}
autoFocus
>
Register Name
</Button>
</DialogActions>
</Dialog>
)
}

View File

@ -22,7 +22,7 @@ export const CustomizedSnackbars = ({open, setOpen, info, setInfo, duration}) =
if(!open) return null if(!open) return null
return ( return (
<div> <div>
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={duration || 6000} onClose={handleClose}> <Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={info?.duration === null ? null : (duration || 6000)} onClose={handleClose}>
<Alert <Alert

View File

@ -0,0 +1,507 @@
import React, { useCallback, useEffect, useState } from "react";
import { DrawerUserLookup } from "../Drawer/DrawerUserLookup";
import {
Avatar,
Box,
Button,
ButtonBase,
Card,
Divider,
TableBody,
TableCell,
TableHead,
TableRow,
TextField,
Tooltip,
Typography,
Table,
CircularProgress,
} from "@mui/material";
import { getAddressInfo, getNameOrAddress } from "../../background";
import { getBaseApiReact } from "../../App";
import { getNameInfo } from "../Group/Group";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import { Spacer } from "../../common/Spacer";
import { formatTimestamp } from "../../utils/time";
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
import SearchIcon from '@mui/icons-material/Search';
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
function formatAddress(str) {
if (str.length <= 12) return str;
const first6 = str.slice(0, 6);
const last6 = str.slice(-6);
return `${first6}....${last6}`;
}
export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
const [nameOrAddress, setNameOrAddress] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const [addressInfo, setAddressInfo] = useState(null);
const [isLoadingUser, setIsLoadingUser] = useState(false);
const [isLoadingPayments, setIsLoadingPayments] = useState(false);
const [payments, setPayments] = useState([]);
const lookupFunc = useCallback(async (messageAddressOrName) => {
try {
setErrorMessage('')
setIsLoadingUser(true)
setPayments([])
setAddressInfo(null)
const inputAddressOrName = messageAddressOrName || nameOrAddress
if (!inputAddressOrName?.trim())
throw new Error("Please insert a name or address");
const owner = await getNameOrAddress(inputAddressOrName);
if (!owner) throw new Error("Name does not exist");
const addressInfoRes = await getAddressInfo(owner);
if (!addressInfoRes?.publicKey) {
throw new Error("Address does not exist on blockchain");
}
const name = await getNameInfo(owner);
const balanceRes = await fetch(
`${getBaseApiReact()}/addresses/balance/${owner}`
);
const balanceData = await balanceRes.json();
setAddressInfo({
...addressInfoRes,
balance: balanceData,
name,
});
setIsLoadingUser(false)
setIsLoadingPayments(true)
const getPayments = await fetch(
`${getBaseApiReact()}/transactions/search?txType=PAYMENT&address=${owner}&confirmationStatus=CONFIRMED&limit=20&reverse=true`
);
const paymentsData = await getPayments.json();
setPayments(paymentsData);
} catch (error) {
setErrorMessage(error?.message)
console.error(error);
} finally {
setIsLoadingUser(false)
setIsLoadingPayments(false)
}
}, [nameOrAddress]);
const openUserLookupDrawerFunc = useCallback((e) => {
setIsOpenDrawerLookup(true)
const message = e.detail?.addressOrName;
if(message){
lookupFunc(message)
}
}, [lookupFunc, setIsOpenDrawerLookup]);
useEffect(() => {
subscribeToEvent("openUserLookupDrawer", openUserLookupDrawerFunc);
return () => {
unsubscribeFromEvent("openUserLookupDrawer", openUserLookupDrawerFunc);
};
}, [openUserLookupDrawerFunc]);
const onClose = ()=> {
setIsOpenDrawerLookup(false)
setNameOrAddress('')
setErrorMessage('')
setPayments([])
setIsLoadingUser(false)
setIsLoadingPayments(false)
setAddressInfo(null)
}
return (
<DrawerUserLookup open={isOpenDrawerLookup} setOpen={setIsOpenDrawerLookup}>
<Box
sx={{
display: "flex",
flexDirection: "column",
padding: "15px",
height: "100vh",
overflow: "hidden",
}}
>
<Box
sx={{
display: "flex",
gap: "5px",
alignItems: "center",
flexShrink: 0,
}}
>
<TextField
autoFocus
value={nameOrAddress}
onChange={(e) => setNameOrAddress(e.target.value)}
size="small"
placeholder="Address or Name"
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter" && nameOrAddress) {
lookupFunc();
}
}}
/>
<ButtonBase onClick={()=> {
lookupFunc();
}} >
<SearchIcon sx={{
color: 'white',
marginRight: '20px'
}} />
</ButtonBase>
<ButtonBase sx={{
marginLeft: 'auto',
}} onClick={()=> {
onClose()
}}>
<CloseFullscreenIcon sx={{
color: 'white'
}} />
</ButtonBase>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
overflow: "auto",
}}
>
{!isLoadingUser && errorMessage && (
<Box sx={{
display: 'flex',
width: '100%',
justifyContent: 'center',
marginTop: '40px'
}}>
<Typography>{errorMessage}</Typography>
</Box>
)}
{isLoadingUser && (
<Box sx={{
display: 'flex',
width: '100%',
justifyContent: 'center',
marginTop: '40px'
}}>
<CircularProgress sx={{
color: 'white'
}} />
</Box>
)}
{!isLoadingUser && addressInfo && (
<>
<Spacer height="30px" />
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
flexDirection: "row",
width: "100%",
justifyContent: "center",
}}
>
<Card
sx={{
padding: "15px",
minWidth: "320px",
alignItems: "center",
minHeight: "200px",
background: "var(--bg-primary)",
display: "flex",
flexDirection: "column",
}}
>
<Typography
sx={{
textAlign: "center",
}}
>
{addressInfo?.name ?? "Name not registered"}
</Typography>
<Spacer height="20px" />
<Divider>
{addressInfo?.name ? (
<Avatar
sx={{
height: "50px",
width: "50px",
"& img": {
objectFit: "fill",
},
}}
alt={addressInfo?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
addressInfo?.name
}/qortal_avatar?async=true`}
>
<AccountCircleIcon
sx={{
fontSize: "50px",
}}
/>
</Avatar>
) : (
<AccountCircleIcon
sx={{
fontSize: "50px",
}}
/>
)}
</Divider>
<Spacer height="20px" />
<Typography
sx={{
textAlign: "center",
}}
>
Level {addressInfo?.level}
</Typography>
</Card>
<Card
sx={{
padding: "15px",
minWidth: "320px",
minHeight: "200px",
gap: "20px",
display: "flex",
flexDirection: "column",
background: "var(--bg-primary)",
}}
>
<Box
sx={{
display: "flex",
gap: "20px",
justifyContent: "space-between",
width: "100%",
}}
>
<Box
sx={{
display: "flex",
flexShrink: 0,
}}
>
<Typography>Address</Typography>
</Box>
<Tooltip
title={
<span
style={{
color: "white",
fontSize: "14px",
fontWeight: 700,
}}
>
copy address
</span>
}
placement="bottom"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<ButtonBase
onClick={() => {
navigator.clipboard.writeText(addressInfo?.address);
}}
>
<Typography
sx={{
textAlign: "end",
}}
>
{addressInfo?.address}
</Typography>
</ButtonBase>
</Tooltip>
</Box>
<Box
sx={{
display: "flex",
gap: "20px",
justifyContent: "space-between",
width: "100%",
}}
>
<Typography>Balance</Typography>
<Typography>{addressInfo?.balance}</Typography>
</Box>
<Spacer height="20px" />
<Button variant="contained" onClick={()=> {
executeEvent('openPaymentInternal', {
address: addressInfo?.address,
name: addressInfo?.name,
});
}}>Send QORT</Button>
</Card>
</Box>
</>
)}
<Spacer height="40px" />
{isLoadingPayments && (
<Box sx={{
display: 'flex',
width: '100%',
justifyContent: 'center'
}}>
<CircularProgress sx={{
color: 'white'
}} />
</Box>
)}
{!isLoadingPayments && addressInfo && (
<Card
sx={{
padding: "15px",
overflow: "auto",
display: "flex",
flexDirection: "column",
background: "var(--bg-primary)",
}}
>
<Typography>20 most recent payments</Typography>
<Spacer height="20px" />
{!isLoadingPayments && payments?.length === 0 && (
<Box sx={{
display: 'flex',
width: '100%',
justifyContent: 'center'
}}>
<Typography>No payments</Typography>
</Box>
)}
<Table>
<TableHead>
<TableRow>
<TableCell>Sender</TableCell>
<TableCell>Reciver</TableCell>
<TableCell>Amount</TableCell>
<TableCell>Time</TableCell>
</TableRow>
</TableHead>
<TableBody>
{payments.map((payment, index) => (
<TableRow key={payment?.signature}>
<TableCell>
<Tooltip
title={
<span
style={{
color: "white",
fontSize: "14px",
fontWeight: 700,
}}
>
copy address
</span>
}
placement="bottom"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<ButtonBase
onClick={() => {
navigator.clipboard.writeText(
payment?.creatorAddress
);
}}
>
{formatAddress(payment?.creatorAddress)}
</ButtonBase>
</Tooltip>
</TableCell>
<TableCell>
<Tooltip
title={
<span
style={{
color: "white",
fontSize: "14px",
fontWeight: 700,
}}
>
copy address
</span>
}
placement="bottom"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<ButtonBase
onClick={() => {
navigator.clipboard.writeText(payment?.recipient);
}}
>
{formatAddress(payment?.recipient)}
</ButtonBase>
</Tooltip>
</TableCell>
<TableCell>
{payment?.amount}
</TableCell>
<TableCell>{formatTimestamp(payment?.timestamp)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
)}
</Box>
</Box>
</DrawerUserLookup>
);
};

View File

@ -1,6 +1,8 @@
import React, { useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { Popover, Button, Box } from '@mui/material'; import { Popover, Button, Box, CircularProgress } from '@mui/material';
import { executeEvent } from '../utils/events'; import { executeEvent } from '../utils/events';
import { BlockedUsersModal } from './Group/BlockedUsersModal';
import { MyContext } from '../App';
export const WrapperUserAction = ({ children, address, name, disabled }) => { export const WrapperUserAction = ({ children, address, name, disabled }) => {
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
@ -46,6 +48,7 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => {
</Box> </Box>
{/* Popover */} {/* Popover */}
{open && (
<Popover <Popover
id={id} id={id}
open={open} open={open}
@ -119,8 +122,81 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => {
> >
Copy address Copy address
</Button> </Button>
<Button
variant="text"
onClick={() => {
executeEvent('openUserLookupDrawer', {
addressOrName: name || address
})
handleClose();
}}
sx={{
color: 'white',
justifyContent: 'flex-start'
}}
>
User lookup
</Button>
<BlockUser handleClose={handleClose} address={address} name={name} />
</Box> </Box>
</Popover> </Popover>
)}
</> </>
); );
}; };
const BlockUser = ({address, name, handleClose})=> {
const [isAlreadyBlocked, setIsAlreadyBlocked] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const {isUserBlocked,
addToBlockList,
removeBlockFromList} = useContext(MyContext)
useEffect(()=> {
if(!address) return
setIsAlreadyBlocked(isUserBlocked(address, name))
}, [address, setIsAlreadyBlocked, isUserBlocked, name])
return (
<Button
variant="text"
onClick={async () => {
try {
setIsLoading(true)
if(isAlreadyBlocked === true){
await removeBlockFromList(address, name)
} else if(isAlreadyBlocked === false) {
await addToBlockList(address, name)
}
executeEvent('updateChatMessagesWithBlocks', true)
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
handleClose();
}
}}
sx={{
color: 'white',
justifyContent: 'flex-start',
gap: '10px'
}}
>
{(isAlreadyBlocked === null || isLoading) && (
<CircularProgress color="secondary" size={24} />
)}
{isAlreadyBlocked && (
'Unblock name'
)}
{isAlreadyBlocked === false && (
'Block name'
)}
</Button>
)
}

View File

@ -40,6 +40,24 @@ const theme = createTheme({
color: '#b0b0b0', // Lighter text for body2, often used for secondary text color: '#b0b0b0', // Lighter text for body2, often used for secondary text
}, },
}, },
components: {
MuiOutlinedInput: {
styleOverrides: {
root: {
".MuiOutlinedInput-notchedOutline": {
borderColor: "white", // ⚪ Default outline color
},
},
},
},
MuiSelect: {
styleOverrides: {
icon: {
color: "white", // ✅ Caret (dropdown arrow) color
},
},
},
},
}); });
export default theme; export default theme;

View File

@ -1,5 +1,5 @@
import { banFromGroup, gateways, getApiKeyFromStorage } from "./background"; import { banFromGroup, gateways, getApiKeyFromStorage } from "./background";
import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createBuyOrder, createGroupRequest, createPoll, createSellOrder, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll } from "./qortalRequests/get"; import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createBuyOrder, createGroupRequest, createPoll, createSellOrder, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getUserWalletTransactions, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll } from "./qortalRequests/get";
const listOfAllQortalRequests = [ const listOfAllQortalRequests = [
'GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', 'GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS',
@ -756,6 +756,20 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
}); });
break; break;
} }
case "GET_USER_WALLET_TRANSACTIONS" : {
const data = request.payload;
getUserWalletTransactions(data, isFromExtension, appInfo)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
} }
} }
return true; return true;

View File

@ -657,7 +657,7 @@ export const decryptData = async (data) => {
export const getListItems = async (data, isFromExtension) => { export const getListItems = async (data, isFromExtension) => {
const isGateway = await isRunningGateway() const isGateway = await isRunningGateway()
if(isGateway){ if(isGateway){
throw new Error('This action cannot be done through a gateway') throw new Error("This action cannot be done through a public node");
} }
const requiredFields = ["list_name"]; const requiredFields = ["list_name"];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -711,7 +711,7 @@ export const getListItems = async (data, isFromExtension) => {
export const addListItems = async (data, isFromExtension) => { export const addListItems = async (data, isFromExtension) => {
const isGateway = await isRunningGateway() const isGateway = await isRunningGateway()
if(isGateway){ if(isGateway){
throw new Error('This action cannot be done through a gateway') throw new Error("This action cannot be done through a public node");
} }
const requiredFields = ["list_name", "items"]; const requiredFields = ["list_name", "items"];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -766,7 +766,7 @@ export const addListItems = async (data, isFromExtension) => {
export const deleteListItems = async (data, isFromExtension) => { export const deleteListItems = async (data, isFromExtension) => {
const isGateway = await isRunningGateway() const isGateway = await isRunningGateway()
if(isGateway){ if(isGateway){
throw new Error('This action cannot be done through a gateway') throw new Error("This action cannot be done through a public node");
} }
const requiredFields = ["list_name"]; const requiredFields = ["list_name"];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -2280,7 +2280,7 @@ export const getTxActivitySummary = async (data) => {
export const updateForeignFee = async (data) => { export const updateForeignFee = async (data) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { if (isGateway) {
throw new Error("This action cannot be done through a gateway"); throw new Error("This action cannot be done through a public node");
} }
const requiredFields = ['coin', 'type', 'value']; const requiredFields = ['coin', 'type', 'value'];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -2379,7 +2379,7 @@ export const getTxActivitySummary = async (data) => {
export const setCurrentForeignServer = async (data) => { export const setCurrentForeignServer = async (data) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { if (isGateway) {
throw new Error("This action cannot be done through a gateway"); throw new Error("This action cannot be done through a public node");
} }
const requiredFields = ['coin']; const requiredFields = ['coin'];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -2440,7 +2440,7 @@ export const getTxActivitySummary = async (data) => {
export const addForeignServer = async (data) => { export const addForeignServer = async (data) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { if (isGateway) {
throw new Error("This action cannot be done through a gateway"); throw new Error("This action cannot be done through a public node");
} }
const requiredFields = ['coin']; const requiredFields = ['coin'];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -2500,7 +2500,7 @@ export const getTxActivitySummary = async (data) => {
export const removeForeignServer = async (data) => { export const removeForeignServer = async (data) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { if (isGateway) {
throw new Error("This action cannot be done through a gateway"); throw new Error("This action cannot be done through a public node");
} }
const requiredFields = ['coin']; const requiredFields = ['coin'];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -3053,7 +3053,7 @@ const crosschainAtInfo = await Promise.all(atPromises);
}, 0) }, 0)
)} )}
${` ${crosschainAtInfo?.[0]?.foreignBlockchain}`}`, ${` ${crosschainAtInfo?.[0]?.foreignBlockchain}`}`,
highlightedText: `Is using gateway: ${isGateway}`, highlightedText: `Is using public node: ${isGateway}`,
fee: '', fee: '',
foreignFee: `${sellerForeignFee[foreignBlockchain].value} ${sellerForeignFee[foreignBlockchain].ticker}` foreignFee: `${sellerForeignFee[foreignBlockchain].value} ${sellerForeignFee[foreignBlockchain].ticker}`
}, isFromExtension); }, isFromExtension);
@ -3224,13 +3224,15 @@ export const createSellOrder = async (data, isFromExtension) => {
throw new Error(errorMsg); throw new Error(errorMsg);
} }
const parsedForeignAmount = Number(data.foreignAmount)?.toFixed(8)
const receivingAddress = await getUserWalletFunc(data.foreignBlockchain) const receivingAddress = await getUserWalletFunc(data.foreignBlockchain)
try { try {
const resPermission = await getUserPermission({ const resPermission = await getUserPermission({
text1: "Do you give this application permission to perform a sell order?", text1: "Do you give this application permission to perform a sell order?",
text2: `${data.qortAmount}${" "} text2: `${data.qortAmount}${" "}
${`QORT`}`, ${`QORT`}`,
text3: `FOR ${data.foreignAmount} ${data.foreignBlockchain}`, text3: `FOR ${parsedForeignAmount} ${data.foreignBlockchain}`,
fee: '0.02' fee: '0.02'
}, isFromExtension); }, isFromExtension);
const { accepted } = resPermission; const { accepted } = resPermission;
@ -3248,9 +3250,9 @@ const receivingAddress = await getUserWalletFunc(data.foreignBlockchain)
const response = await tradeBotCreateRequest({ const response = await tradeBotCreateRequest({
creatorPublicKey: userPublicKey, creatorPublicKey: userPublicKey,
qortAmount: parseFloat(data.qortAmount), qortAmount: parseFloat(data.qortAmount),
fundingQortAmount: parseFloat(data.qortAmount) + 0.001, fundingQortAmount: parseFloat(data.qortAmount) + 0.01,
foreignBlockchain: data.foreignBlockchain, foreignBlockchain: data.foreignBlockchain,
foreignAmount: parseFloat(data.foreignAmount), foreignAmount: parseFloat(parsedForeignAmount),
tradeTimeout: 120, tradeTimeout: 120,
receivingAddress: receivingAddress.address receivingAddress: receivingAddress.address
}, keyPair) }, keyPair)
@ -3353,7 +3355,7 @@ export const adminAction = async (data, isFromExtension) => {
} }
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { if (isGateway) {
throw new Error("This action cannot be done through a gateway"); throw new Error("This action cannot be done through a public node");
} }
let apiEndpoint = ""; let apiEndpoint = "";
@ -3769,7 +3771,7 @@ url
export const getHostedData = async (data, isFromExtension) => { export const getHostedData = async (data, isFromExtension) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { if (isGateway) {
throw new Error("This action cannot be done through a gateway"); throw new Error("This action cannot be done through a public node");
} }
const resPermission = await getUserPermission( const resPermission = await getUserPermission(
{ {
@ -3805,7 +3807,7 @@ export const getHostedData = async (data, isFromExtension) => {
export const deleteHostedData = async (data, isFromExtension) => { export const deleteHostedData = async (data, isFromExtension) => {
const isGateway = await isRunningGateway(); const isGateway = await isRunningGateway();
if (isGateway) { if (isGateway) {
throw new Error("This action cannot be done through a gateway"); throw new Error("This action cannot be done through a public node");
} }
const requiredFields = ["hostedData"]; const requiredFields = ["hostedData"];
const missingFields: string[] = []; const missingFields: string[] = [];
@ -4379,3 +4381,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 {