From 164a380c2807d482e810a89f30f0a516f495c55f Mon Sep 17 00:00:00 2001 From: PhilReact Date: Tue, 4 Mar 2025 15:26:06 +0200 Subject: [PATCH] homepage, registername, userlookup, block, fixes --- src/App.tsx | 984 +++++++++++------- src/ExtStates/NotAuthenticated.tsx | 411 +++++--- src/Wallets.tsx | 12 +- src/assets/Icons/WalletIcon.tsx | 2 +- src/assets/Icons/q-trade-logo.webp | Bin 0 -> 1072 bytes src/atoms/global.ts | 5 + src/background-cases.ts | 204 ++-- src/background.ts | 27 +- src/common/Spinners/BarSpinner/BarSpinner.tsx | 10 + src/common/Spinners/BarSpinner/barSpinner.css | 19 + src/components/Apps/AppViewer.tsx | 13 +- src/components/Apps/AppsDesktop.tsx | 5 +- src/components/Apps/AppsHomeDesktop.tsx | 4 +- src/components/Apps/AppsNavBarDesktop.tsx | 139 ++- src/components/Apps/AppsPrivate.tsx | 564 ++++++++++ src/components/Apps/SortablePinnedApps.tsx | 52 +- src/components/Apps/TabComponent.tsx | 48 +- src/components/Apps/useHandlePrivateApps.tsx | 251 +++++ .../Apps/useQortalMessageListener.tsx | 2 +- src/components/BuyQortInformation.tsx | 154 +++ src/components/Chat/ChatGroup.tsx | 35 +- src/components/Chat/ChatOptions.tsx | 123 ++- src/components/Chat/MessageDisplay.tsx | 58 +- src/components/Chat/MessageItem.tsx | 194 ++-- src/components/Chat/useBlockUsers.tsx | 192 ++++ src/components/ContextMenuPinnedApps.tsx | 18 +- src/components/CoreSyncStatus.tsx | 2 +- src/components/Desktop/DesktopHeader.tsx | 10 +- src/components/Drawer/DrawerUserLookup.tsx | 22 + src/components/Explore/Explore.tsx | 101 ++ src/components/Group/BlockedUsersModal.tsx | 190 ++++ src/components/Group/Group.tsx | 56 +- src/components/Group/GroupInvites.tsx | 205 ++-- src/components/Group/GroupJoinRequests.tsx | 37 +- src/components/Group/HomeDesktop.tsx | 307 ++++-- .../Group/ListOfGroupPromotions.tsx | 682 ++++++------ src/components/Group/QMailMessages.tsx | 51 +- src/components/Group/ThingsToDoInitial.tsx | 325 +++--- src/components/Group/WebsocketActive.tsx | 10 +- src/components/Group/useHandleUserInfo.tsx | 24 +- src/components/Home/NewUsersCTA.tsx | 93 ++ src/components/Home/QortPrice.tsx | 257 +++++ src/components/MainAvatar.tsx | 73 +- src/components/QMailStatus.tsx | 29 +- src/components/RegisterName.tsx | 308 ++++++ src/components/Snackbar/Snackbar.tsx | 2 +- src/components/UserLookup.tsx/UserLookup.tsx | 507 +++++++++ src/components/WrapperUserAction.tsx | 80 +- src/main.tsx | 18 + src/qortalRequests.ts | 16 +- src/qortalRequests/get.ts | 131 ++- src/utils/time.ts | 2 +- 52 files changed, 5441 insertions(+), 1623 deletions(-) create mode 100644 src/assets/Icons/q-trade-logo.webp create mode 100644 src/common/Spinners/BarSpinner/BarSpinner.tsx create mode 100644 src/common/Spinners/BarSpinner/barSpinner.css create mode 100644 src/components/Apps/AppsPrivate.tsx create mode 100644 src/components/Apps/useHandlePrivateApps.tsx create mode 100644 src/components/BuyQortInformation.tsx create mode 100644 src/components/Chat/useBlockUsers.tsx create mode 100644 src/components/Drawer/DrawerUserLookup.tsx create mode 100644 src/components/Explore/Explore.tsx create mode 100644 src/components/Group/BlockedUsersModal.tsx create mode 100644 src/components/Home/NewUsersCTA.tsx create mode 100644 src/components/Home/QortPrice.tsx create mode 100644 src/components/RegisterName.tsx create mode 100644 src/components/UserLookup.tsx/UserLookup.tsx diff --git a/src/App.tsx b/src/App.tsx index 7cc0d8f..3d7c1e1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,8 @@ import { Typography, } from "@mui/material"; import { decryptStoredWallet } from "./utils/decryptWallet"; +import PersonSearchIcon from '@mui/icons-material/PersonSearch'; + import { CountdownCircleTimer } from "react-countdown-circle-timer"; import Logo1 from "./assets/svgs/Logo1.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 EngineeringIcon from '@mui/icons-material/Engineering'; import WarningIcon from '@mui/icons-material/Warning'; - +import { DrawerUserLookup } from "./components/Drawer/DrawerUserLookup"; +import { UserLookup } from "./components/UserLookup.tsx/UserLookup"; import { createAccount, @@ -111,7 +114,7 @@ import { MainAvatar } from "./components/MainAvatar"; import { useRetrieveDataLocalStorage } from "./useRetrieveDataLocalStorage"; import { useQortalGetSaveSettings } from "./useQortalGetSaveSettings"; 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 { NotAuthenticated } from "./ExtStates/NotAuthenticated"; import { useFetchResources } from "./common/useFetchResources"; @@ -127,6 +130,10 @@ import { Minting } from "./components/Minting/Minting"; import { isRunningGateway } from "./qortalRequests"; import { QMailStatus } from "./components/QMailStatus"; 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 = | "not-authenticated" @@ -310,6 +317,10 @@ function App() { const [walletToBeDownloaded, setWalletToBeDownloaded] = useState(null); const [walletToBeDownloadedPassword, setWalletToBeDownloadedPassword] = useState(""); + const {isUserBlocked, + addToBlockList, + removeBlockFromList, getAllBlockedUsers} = useBlockedAddresses() + const [isOpenDrawerLookup, setIsOpenDrawerLookup] = useState(false) const [isMain, setIsMain] = useState( window?.location?.href?.includes("?main=true") ); @@ -333,6 +344,11 @@ function App() { const isFocusedRef = useRef(true); const { isShow, onCancel, onOk, show, message } = 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 [showSeed, setShowSeed] = useState(false) const [creationStep, setCreationStep] = useState(1) @@ -362,10 +378,6 @@ function App() { message: messageQortalRequestExtension, } = 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 [openSnack, setOpenSnack] = useState(false); const [hasLocalNode, setHasLocalNode] = useState(false); @@ -418,6 +430,7 @@ function App() { const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom); const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom) const resetAtomMailsAtom = useResetRecoilState(mailsAtom) + const resetGroupPropertiesAtom = useResetRecoilState(groupsPropertiesAtom) const resetAllRecoil = () => { resetAtomSortablePinnedAppsAtom(); resetAtomCanSaveSettingToQdnAtom(); @@ -427,6 +440,7 @@ function App() { resetAtomIsUsingImportExportSettingsAtom() resetAtomQMailLastEnteredTimestampAtom() resetAtomMailsAtom() + resetGroupPropertiesAtom() }; useEffect(() => { if (!isMobile) return; @@ -562,7 +576,7 @@ function App() { } catch (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 = () => { setQortBalanceLoading(true); chrome?.runtime?.sendMessage({ action: "balance" }, (response) => { @@ -616,6 +654,8 @@ function App() { setBalance(response); } setQortBalanceLoading(false); + balanceSetInterval() + }); }; const getLtcBalanceFunc = () => { @@ -1176,6 +1216,9 @@ function App() { resetAllRecoil() setShowSeed(false) setCreationStep(1) + if(balanceSetIntervalRef?.current){ + clearInterval(balanceSetIntervalRef?.current); + } }; function roundUpToDecimals(number, decimals = 8) { @@ -1332,68 +1375,244 @@ 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, + + + const renderProfileLeft = ()=> { + + return + + + {authenticatedMode === "qort" && ( + LITECOIN WALLET} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + { + + setAuthenticatedMode("ltc"); + }} + src={ltcLogo} + style={{ + cursor: "pointer", + width: "20px", + height: "auto", + }} + /> + + )} + {authenticatedMode === "ltc" && ( + QORTAL WALLET} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + { + setAuthenticatedMode("qort"); + }} + src={qortLogo} + style={{ + cursor: "pointer", + width: "20px", + height: "auto", + }} + /> + + )} + + + + {authenticatedMode === "ltc" ? ( + <> + + + + + {rawWallet?.ltcAddress?.slice(0, 6)}... + {rawWallet?.ltcAddress?.slice(-4)} + + + + {ltcBalanceLoading && ( + + )} + {!isNaN(+ltcBalance) && !ltcBalanceLoading && ( + + + {ltcBalance} LTC + + + + )} + + + ) : ( + <> + + + + {userInfo?.name} + + + + + {rawWallet?.address0?.slice(0, 6)}... + {rawWallet?.address0?.slice(-4)} + + + + {qortBalanceLoading && ( + + )} + {!qortBalanceLoading && balance >= 0 && ( + + + {balance?.toFixed(2)} QORT + + + + )} + + + {userInfo && !userInfo?.name && ( + { + executeEvent('openRegisterName', {}) + }} + > + REGISTER NAME + + )} + + { + setIsOpenSendQort(true); + // setExtstate("send-qort"); + setIsOpenDrawerProfile(false); + }} + > + Transfer QORT + + + + )} + { + executeEvent("addTab", { + data: { service: "APP", name: "q-trade" }, }); - } - } finally { - setIsLoadingRegisterName(false); - } - }; + executeEvent("open-apps-mode", {}); + }} + > + Get QORT at Q-Trade + + + } const renderProfile = () => { return ( @@ -1402,7 +1621,7 @@ function App() { width: isMobile ? "100vw" : "auto", display: "flex", backgroundColor: "var(--bg-2)", - justifyContent: 'flex-end' + justifyContent: "flex-end", }} > {isMobile && ( @@ -1424,171 +1643,21 @@ function App() { /> )} - -{desktopViewMode !== "apps" && desktopViewMode !== "dev" && desktopViewMode !== "chat" && ( - - - - {authenticatedMode === "ltc" ? ( + {desktopViewMode !== "apps" && + desktopViewMode !== "dev" && + desktopViewMode !== "chat" && ( <> - - - - - {rawWallet?.ltcAddress?.slice(0, 6)}... - {rawWallet?.ltcAddress?.slice(-4)} - - - - {ltcBalanceLoading && ( - - )} - {!isNaN(+ltcBalance) && !ltcBalanceLoading && ( - - - {ltcBalance} LTC - - - - )} - - - ) : ( - <> - - - - {userInfo?.name} - - - - - {rawWallet?.address0?.slice(0, 6)}... - {rawWallet?.address0?.slice(-4)} - - - - {qortBalanceLoading && ( - - )} - {!qortBalanceLoading && balance >= 0 && ( - - - {balance?.toFixed(2)} QORT - - - - )} - - - {userInfo && !userInfo?.name && ( - { - setOpenRegisterName(true); - }} - > - REGISTER NAME - - )} - - { - setIsOpenSendQort(true); - // setExtstate("send-qort"); - setIsOpenDrawerProfile(false); - }} - > - Transfer QORT - - + {renderProfileLeft()} )} - { - executeEvent("addTab", { data: { service: 'APP', name: 'q-trade' } }); - executeEvent("open-apps-mode", { }); - }} - > - Get QORT at Q-Trade - - - )} - - + - - {!isMobile && ( - <> - - { - logoutFunc(); - setIsOpenDrawerProfile(false); - }} - style={{ - cursor: "pointer", - }} - /> - - )} - + + {!isMobile && ( + <> + + LOG OUT} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + { + logoutFunc(); + setIsOpenDrawerProfile(false); + }} + style={{ + cursor: "pointer", + width: '20px', + height: 'auto' + }} + /> + + + )} + - { - setIsSettingsOpen(true); - }} - > - - - - {authenticatedMode === "qort" && ( - { - setAuthenticatedMode("ltc"); + setIsSettingsOpen(true); }} - src={ltcLogo} - style={{ - cursor: "pointer", - width: "20px", - height: "auto", - }} - /> - )} - {authenticatedMode === "ltc" && ( - + SETTINGS} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + + + + { - setAuthenticatedMode("qort"); + setIsOpenDrawerLookup(true); }} - src={qortLogo} - style={{ - cursor: "pointer", - width: "20px", - height: "auto", - }} - /> - )} + > + USER LOOKUP} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + + + {desktopViewMode !== 'home' && ( + <> + + + WALLET} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + { + setIsOpenDrawerProfile(true); + }}> + + + + + + + )} + + {/* {authenticatedMode === "qort" && ( + LITECOIN WALLET} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + { + if(desktopViewMode !== 'home'){ + setIsOpenDrawerProfile((prev)=> !prev) + } + setAuthenticatedMode("ltc"); + }} + src={ltcLogo} + style={{ + cursor: "pointer", + width: "20px", + height: "auto", + }} + /> + + )} + {authenticatedMode === "ltc" && ( + QORTAL WALLET} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + { + setAuthenticatedMode("qort"); + }} + src={qortLogo} + style={{ + cursor: "pointer", + width: "20px", + height: "auto", + }} + /> + + )} */} - - + - {extState === "authenticated" && isMainWindow && ( + {extState === "authenticated" && isMainWindow && ( + )} @@ -1708,42 +1932,96 @@ function App() { }) } }}> - + MINTING STATUS} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + - - {(desktopViewMode === "apps" || desktopViewMode === "home") && ( + {(desktopViewMode === "apps" || desktopViewMode === "home") && ( { if(desktopViewMode === "apps"){ showTutorial('qapps', true) - } else { showTutorial('getting-started', true) - } }} > - + TUTORIAL} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + )} + BACKUP WALLET} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > { - setExtstate("download-wallet"); - setIsOpenDrawerProfile(false); - }} - src={Download} - style={{ - cursor: "pointer", - }} - /> - - + onClick={() => { + setExtstate("download-wallet"); + setIsOpenDrawerProfile(false); + }} + src={Download} + style={{ + cursor: "pointer", + width: '20px' + }} + /> + + + ); @@ -1769,7 +2047,10 @@ function App() { {extState === "not-authenticated" && ( - + )} {/* {extState !== "not-authenticated" && ( @@ -1793,7 +2074,11 @@ function App() { setOpenSnackGlobal: setOpenSnack, infoSnackCustom: infoSnack, setInfoSnackCustom: setInfoSnack, - getIndividualUserInfo + getIndividualUserInfo, + isUserBlocked, + addToBlockList, + removeBlockFromList, + getAllBlockedUsers }} > @@ -2520,6 +2805,29 @@ function App() { }} ref={passwordRef} /> + {useLocalNode ? ( + <> + + + {"Using node: "} {currentNode?.url} + + + ) : ( + <> + + + {"Using public node"} + + + )} Authenticate @@ -2855,7 +3163,7 @@ function App() { display: "flex", flexDirection: "column", alignItems: "center", - zIndex: 6, + zIndex: 10000, }} > @@ -2955,6 +3263,9 @@ function App() { open={isShow} aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" + sx={{ + zIndex: 10001 + }} > {message.paymentFee ? "Payment" : "Publish"} @@ -3027,7 +3338,7 @@ function App() { aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" > - {"Warning"} + {"LOGOUT"} {messageUnsavedChanges.message} @@ -3068,7 +3379,6 @@ function App() { flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-start', - minHeight: '400px', maxHeight: '90vh', overflow: 'auto' }}> @@ -3310,52 +3620,7 @@ function App() { )} - { - setOpenRegisterName(false); - setRegisterNameValue(""); - }} - anchorOrigin={{ - vertical: "bottom", - horizontal: "center", - }} - transformOrigin={{ - vertical: "top", - horizontal: "center", - }} - style={{ marginTop: "8px" }} - > - - - setRegisterNameValue(e.target.value)} - value={registerNameValue} - placeholder="Choose a name" - /> - - - Register Name - - - + {isSettingsOpen && ( )} @@ -3369,8 +3634,11 @@ function App() { open={isOpenDrawerProfile} setOpen={setIsOpenDrawerProfile} > - {renderProfile()} + {renderProfileLeft()} + + + {extState === "create-wallet" && walletToBeDownloaded && ( { diff --git a/src/ExtStates/NotAuthenticated.tsx b/src/ExtStates/NotAuthenticated.tsx index e32fe64..2f5fa50 100644 --- a/src/ExtStates/NotAuthenticated.tsx +++ b/src/ExtStates/NotAuthenticated.tsx @@ -4,6 +4,7 @@ import { CustomButton, TextItalic, TextP, TextSpan } from "../App-styles"; import { Box, Button, + ButtonBase, Checkbox, Dialog, DialogActions, @@ -11,24 +12,24 @@ import { DialogTitle, FormControlLabel, Input, - Switch, - Tooltip, - Typography, - ButtonBase, styled, - tooltipClasses, - TooltipProps + Switch, + Typography, } from "@mui/material"; import Logo1 from "../assets/svgs/Logo1.svg"; import Logo1Dark from "../assets/svgs/Logo1Dark.svg"; import Info from "../assets/svgs/Info.svg"; +import HelpIcon from '@mui/icons-material/Help'; import { CustomizedSnackbars } from "../components/Snackbar/Snackbar"; import { set } from "lodash"; -import { cleanUrl, isUsingLocal } from "../background"; -import HelpIcon from '@mui/icons-material/Help'; +import { cleanUrl, gateways, isUsingLocal } from "../background"; import { GlobalContext } from "../App"; +import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip'; + +const manifestData = { + version: "0.5.2", +}; -const manifestData = chrome?.runtime?.getManifest(); export const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => ( @@ -41,40 +42,47 @@ export const HtmlTooltip = styled(({ className, ...props }: TooltipProps) => ( fontSize: theme.typography.pxToRem(12), }, })); +function removeTrailingSlash(url) { + return url.replace(/\/+$/, ''); +} + export const NotAuthenticated = ({ getRootProps, getInputProps, setExtstate, - apiKey, setApiKey, globalApiKey, handleSetGlobalApikey, + currentNode, + setCurrentNode, + useLocalNode, + setUseLocalNode }) => { const [isValidApiKey, setIsValidApiKey] = useState(null); const [hasLocalNode, setHasLocalNode] = useState(null); - const [useLocalNode, setUseLocalNode] = useState(false); + // const [useLocalNode, setUseLocalNode] = useState(false); const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); const [show, setShow] = React.useState(false); const [mode, setMode] = React.useState("list"); const [customNodes, setCustomNodes] = React.useState(null); - const [currentNode, setCurrentNode] = React.useState({ - url: "http://127.0.0.1:12391", - }); + // const [currentNode, setCurrentNode] = React.useState({ + // url: "http://127.0.0.1:12391", + // }); const [importedApiKey, setImportedApiKey] = React.useState(null); //add and edit states - const [url, setUrl] = React.useState("http://"); + const [url, setUrl] = React.useState("https://"); const [customApikey, setCustomApiKey] = React.useState(""); const [customNodeToSaveIndex, setCustomNodeToSaveIndex] = React.useState(null); - const importedApiKeyRef = useRef(null) - const currentNodeRef = useRef(null) - const hasLocalNodeRef = useRef(null) - const { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext); + const { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext); + const importedApiKeyRef = useRef(null); + const currentNodeRef = useRef(null); + const hasLocalNodeRef = useRef(null); const isLocal = cleanUrl(currentNode?.url) === "127.0.0.1:12391"; const handleFileChangeApiKey = (event) => { const file = event.target.files[0]; // Get the selected file @@ -84,13 +92,34 @@ export const NotAuthenticated = ({ const text = e.target.result; // Get the file content setImportedApiKey(text); // Store the file content in the state + if(customNodes){ + setCustomNodes((prev)=> { + const copyPrev = [...prev] + const findLocalIndex = copyPrev?.findIndex((item)=> item?.url === 'http://127.0.0.1:12391') + if(findLocalIndex === -1){ + copyPrev.unshift({ + url: "http://127.0.0.1:12391", + apikey: text + }) + } else { + copyPrev[findLocalIndex] = { + url: "http://127.0.0.1:12391", + apikey: text + } + } + chrome?.runtime?.sendMessage( + { action: "setCustomNodes", copyPrev } + ); + return copyPrev + }) + + } + }; reader.readAsText(file); // Read the file as text } }; - - const checkIfUserHasLocalNode = useCallback(async () => { try { const url = `http://127.0.0.1:12391/admin/status`; @@ -103,50 +132,105 @@ export const NotAuthenticated = ({ const data = await response.json(); if (data?.height) { setHasLocalNode(true); + return true } - } catch (error) {} + return false + + } catch (error) { + return false + + } }, []); useEffect(() => { checkIfUserHasLocalNode(); }, []); + + useEffect(() => { chrome?.runtime?.sendMessage( { action: "getCustomNodesFromStorage" }, (response) => { if (response) { setCustomNodes(response || []); + if(Array.isArray(response)){ + const findLocal = response?.find((item)=> item?.url === 'http://127.0.0.1:12391') + if(findLocal && findLocal?.apikey){ + setImportedApiKey(findLocal?.apikey) + } + } } } ); }, []); - useEffect(()=> { - importedApiKeyRef.current = importedApiKey - }, [importedApiKey]) - useEffect(()=> { - currentNodeRef.current = currentNode - }, [currentNode]) + useEffect(() => { + importedApiKeyRef.current = importedApiKey; + }, [importedApiKey]); + useEffect(() => { + currentNodeRef.current = currentNode; + }, [currentNode]); + + useEffect(() => { + hasLocalNodeRef.current = hasLocalNode; + }, [hasLocalNode]); + - useEffect(()=> { - hasLocalNodeRef.current = hasLocalNode - }, [hasLocalNode]) const validateApiKey = useCallback(async (key, fromStartUp) => { try { - if(!currentNodeRef.current) return - const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391"; - if(isLocalKey && !hasLocalNodeRef.current && !fromStartUp){ - throw new Error('Please turn on your local node') - + if(key === "isGateway") return + const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391"; + if (fromStartUp && key?.url && key?.apikey && !isLocalKey && !gateways.some(gateway => key?.url?.includes(gateway))) { + setCurrentNode({ + url: key?.url, + apikey: key?.apikey, + }); + + let isValid = false + + + const url = `${key?.url}/admin/settings/localAuthBypassEnabled`; + const response = await fetch(url); + + // Assuming the response is in plain text and will be 'true' or 'false' + const data = await response.text(); + if(data && data === 'true'){ + isValid = true + } else { + const url2 = `${key?.url}/admin/apikey/test?apiKey=${key?.apikey}`; + const response2 = await fetch(url2); + + // Assuming the response is in plain text and will be 'true' or 'false' + const data2 = await response2.text(); + if (data2 === "true") { + isValid = true + } } - const isCurrentNodeLocal = cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391"; - if(isLocalKey && !isCurrentNodeLocal) { - setIsValidApiKey(false); - setUseLocalNode(false); - return + + if (isValid) { + setIsValidApiKey(true); + setUseLocalNode(true); + return } + + } + if (!currentNodeRef.current) return; + const stillHasLocal = await checkIfUserHasLocalNode() + + if (isLocalKey && !stillHasLocal && !fromStartUp) { + throw new Error("Please turn on your local node"); + } + //check custom nodes + // !gateways.some(gateway => apiKey?.url?.includes(gateway)) + const isCurrentNodeLocal = + cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391"; + if (isLocalKey && !isCurrentNodeLocal) { + setIsValidApiKey(false); + setUseLocalNode(false); + return; + } let payload = {}; if (currentNodeRef.current?.url === "http://127.0.0.1:12391") { @@ -154,21 +238,32 @@ export const NotAuthenticated = ({ apikey: importedApiKeyRef.current || key?.apikey, url: currentNodeRef.current?.url, }; - } else if(currentNodeRef.current) { + } else if (currentNodeRef.current) { payload = currentNodeRef.current; } - const url = `${payload?.url}/admin/apikey/test`; - const response = await fetch(url, { - method: "GET", - headers: { - accept: "text/plain", - "X-API-KEY": payload?.apikey, // Include the API key here - }, - }); + let isValid = false + + + const url = `${payload?.url}/admin/settings/localAuthBypassEnabled`; + const response = await fetch(url); // Assuming the response is in plain text and will be 'true' or 'false' const data = await response.text(); - if (data === "true") { + if(data && data === 'true'){ + isValid = true + } else { + const url2 = `${payload?.url}/admin/apikey/test?apiKey=${payload?.apikey}`; + const response2 = await fetch(url2); + + // Assuming the response is in plain text and will be 'true' or 'false' + const data2 = await response2.text(); + if (data2 === "true") { + isValid = true + } + } + + + if (isValid) { chrome?.runtime?.sendMessage( { action: "setApiKey", payload }, (response) => { @@ -176,29 +271,49 @@ export const NotAuthenticated = ({ handleSetGlobalApikey(payload); setIsValidApiKey(true); setUseLocalNode(true); - if(!fromStartUp){ - setApiKey(payload) + if (!fromStartUp) { + setApiKey(payload); } } } - ); + ) } else { setIsValidApiKey(false); setUseLocalNode(false); - setInfoSnack({ - type: "error", - message: "Select a valid apikey", - }); - setOpenSnack(true); + if(!fromStartUp){ + setInfoSnack({ + type: "error", + message: "Select a valid apikey", + }); + setOpenSnack(true); + } + } } catch (error) { setIsValidApiKey(false); setUseLocalNode(false); + if (fromStartUp) { + setCurrentNode({ + url: "http://127.0.0.1:12391", + }); + chrome?.runtime?.sendMessage( + { action: "setApiKey", payload: "isGateway" }, + (response) => { + if (response) { + setApiKey(null); + handleSetGlobalApikey(null); + } + } + ) + return + } + if(!fromStartUp){ setInfoSnack({ type: "error", message: error?.message || "Select a valid apikey", }); setOpenSnack(true); + } console.error("Error validating API key:", error); } }, []); @@ -212,22 +327,22 @@ export const NotAuthenticated = ({ const addCustomNode = () => { setMode("add-node"); }; - - const saveCustomNodes = (myNodes) => { + const saveCustomNodes = (myNodes, isFullListOfNodes) => { let nodes = [...(myNodes || [])]; - if (customNodeToSaveIndex !== null) { + if (!isFullListOfNodes && customNodeToSaveIndex !== null) { nodes.splice(customNodeToSaveIndex, 1, { - url, + url: removeTrailingSlash(url), apikey: customApikey, }); - } else if (url && customApikey) { + } else if (!isFullListOfNodes && url) { nodes.push({ - url, + url: removeTrailingSlash(url), apikey: customApikey, }); } setCustomNodes(nodes); + setCustomNodeToSaveIndex(null); if (!nodes) return; chrome?.runtime?.sendMessage( @@ -235,14 +350,14 @@ export const NotAuthenticated = ({ (response) => { if (response) { setMode("list"); - setUrl("http://"); + setUrl("https://"); setCustomApiKey(""); // add alert } } ); - }; + }; return ( <> @@ -254,7 +369,7 @@ export const NotAuthenticated = ({ height: "154px", }} > - + - WELCOME TO YOUR

+ WELCOME TO QORTAL WALLET + }}> QORTAL
+ setExtstate('wallets')}> - Wallets + {/* */} + Accounts + {/* + + */} @@ -302,9 +420,10 @@ export const NotAuthenticated = ({ display: "flex", gap: "10px", alignItems: "center", + }} > - - Create wallet + Create account - + - - - - {"Using node: "} {currentNode?.url} - + + + + {"Using node: "} {currentNode?.url} + <> { - if (response) { - setApiKey(null); - handleSetGlobalApikey(null); - - } + url: "http://127.0.0.1:12391", + }); + setUseLocalNode(false); + chrome?.runtime?.sendMessage( + { action: "setApiKey", payload: null }, + (response) => { + if (response) { + setApiKey(null); + handleSetGlobalApikey(null); } - ); + } + ) } - }} disabled={false} defaultChecked /> } - label={`Use ${isLocal ? 'Local' : 'Custom'} Node`} + label={`Use ${isLocal ? "Local" : "Custom"} Node`} /> {currentNode?.url === "http://127.0.0.1:12391" && ( @@ -432,31 +549,33 @@ export const NotAuthenticated = ({ onChange={handleFileChangeApiKey} // File input handler /> - {`api key : ${importedApiKey}`} - - - - + {`api key : ${importedApiKey}`} )} - + - Build version: {manifestData?.version} + + Build version: {manifestData?.version} + - {mode === "list" && ( { - if (response) { - setApiKey(null); - handleSetGlobalApikey(null); - - } + chrome?.runtime?.sendMessage( + { action: "setApiKey", payload: null }, + (response) => { + if (response) { + setApiKey(null); + handleSetGlobalApikey(null); } - ); + } + ) }} variant="contained" > @@ -579,17 +696,16 @@ export const NotAuthenticated = ({ setMode("list"); setShow(false); setIsValidApiKey(false); - setUseLocalNode(false); - chrome?.runtime?.sendMessage( - { action: "setApiKey", payload:null }, - (response) => { - if (response) { - setApiKey(null); - handleSetGlobalApikey(null); - + setUseLocalNode(false); + chrome?.runtime?.sendMessage( + { action: "setApiKey", payload: null }, + (response) => { + if (response) { + setApiKey(null); + handleSetGlobalApikey(null); + } } - } - ); + ) }} variant="contained" > @@ -613,9 +729,8 @@ export const NotAuthenticated = ({ const nodesToSave = [ ...(customNodes || []), ].filter((item) => item?.url !== node?.url); - - saveCustomNodes(nodesToSave); + saveCustomNodes(nodesToSave, true); }} variant="contained" > @@ -652,9 +767,7 @@ export const NotAuthenticated = ({ /> )} - - {mode === "list" && ( @@ -690,7 +803,7 @@ export const NotAuthenticated = ({ + + + + )} + {valueTabPrivateApp === 1 && ( + <> + + + Select .zip file containing static content:{" "} + + + {` + 50mb MB maximum`} + {file && ( + <> + + {`Selected: (${file?.name})`} + + )} + + + + {" "} + + {file ? "Change" : "Choose"} File + + + + + + + + + + + + + setNewPrivateAppValues((prev) => { + return { + ...prev, + identifier: e.target.value, + }; + }) + } + /> + + + + + + setNewPrivateAppValues((prev) => { + return { + ...prev, + name: e.target.value, + }; + }) + } + /> + + + + setLogo(file)}> + + + {logo?.name} + + + + + + + + )} + + )} + + ); +}; diff --git a/src/components/Apps/SortablePinnedApps.tsx b/src/components/Apps/SortablePinnedApps.tsx index 15f54b9..98c2287 100644 --- a/src/components/Apps/SortablePinnedApps.tsx +++ b/src/components/Apps/SortablePinnedApps.tsx @@ -1,18 +1,21 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { DndContext, closestCenter } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; import { CSS } from '@dnd-kit/utilities'; import { Avatar, ButtonBase } from '@mui/material'; import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles'; -import { getBaseApiReact } from '../../App'; +import { getBaseApiReact, MyContext } from '../../App'; import { executeEvent } from '../../utils/events'; import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { saveToLocalStorage } from './AppsNavBar'; import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps'; - +import LockIcon from "@mui/icons-material/Lock"; +import { useHandlePrivateApps } from './useHandlePrivateApps'; const SortableItem = ({ id, name, app, isDesktop }) => { + const {openApp} = useHandlePrivateApps() + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); const style = { transform: CSS.Transform.toString(transform), @@ -28,17 +31,27 @@ const SortableItem = ({ id, name, app, isDesktop }) => { return ( - { - executeEvent("addTab", { - data: app - }) + onClick={async ()=> { + if(app?.isPrivate){ + try { + await openApp(app?.privateAppProperties) + } catch (error) { + console.error(error) + } + + } else { + executeEvent("addTab", { + data: app + }) + } + }} > { border: "none", }} > - + ) : ( + { } }} alt={app?.metadata?.title || app?.name} - src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${ + src={ app?.privateAppProperties?.logo ? app?.privateAppProperties?.logo :`${getBaseApiReact()}/arbitrary/THUMBNAIL/${ app?.name }/qortal_avatar?async=true`} > @@ -72,10 +93,19 @@ const SortableItem = ({ id, name, app, isDesktop }) => { alt="center-icon" /> + )} + - + {app?.isPrivate ? ( + + {`${app?.privateAppProperties?.appName || "Private"}`} + + ) : ( + {app?.metadata?.title || app?.name} + )} + diff --git a/src/components/Apps/TabComponent.tsx b/src/components/Apps/TabComponent.tsx index aca6b55..ecf17a7 100644 --- a/src/components/Apps/TabComponent.tsx +++ b/src/components/Apps/TabComponent.tsx @@ -5,6 +5,7 @@ import { getBaseApiReact } from '../../App'; import { Avatar, ButtonBase } from '@mui/material'; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { executeEvent } from '../../utils/events'; +import LockIcon from "@mui/icons-material/Lock"; const TabComponent = ({isSelected, app}) => { return ( @@ -34,25 +35,34 @@ const TabComponent = ({isSelected, app}) => { } src={NavCloseTab}/> ) } - - center-icon - + {app?.isPrivate && !app?.privateAppProperties?.logo ? ( + + ) : ( + + center-icon + + )}
) diff --git a/src/components/Apps/useHandlePrivateApps.tsx b/src/components/Apps/useHandlePrivateApps.tsx new file mode 100644 index 0000000..d21448c --- /dev/null +++ b/src/components/Apps/useHandlePrivateApps.tsx @@ -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, + }; +}; diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index 175dbbe..d688469 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -243,7 +243,7 @@ const UIQortalRequests = [ 'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE', 'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER', 'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER', - 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_PUBLIC_NODE', 'ADMIN_ACTION', 'SIGN_TRANSACTION', 'DECRYPT_QORTAL_GROUP_DATA', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'SHOW_ACTIONS', 'REGISTER_NAME', 'UPDATE_NAME', 'LEAVE_GROUP', 'INVITE_TO_GROUP', 'KICK_FROM_GROUP', 'BAN_FROM_GROUP', 'CANCEL_GROUP_BAN', 'ADD_GROUP_ADMIN','REMOVE_GROUP_ADMIN','DECRYPT_AESGCM', 'CANCEL_GROUP_INVITE', 'CREATE_GROUP' + 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_PUBLIC_NODE', 'ADMIN_ACTION', 'SIGN_TRANSACTION', 'DECRYPT_QORTAL_GROUP_DATA', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'SHOW_ACTIONS', 'REGISTER_NAME', 'UPDATE_NAME', 'LEAVE_GROUP', 'INVITE_TO_GROUP', 'KICK_FROM_GROUP', 'BAN_FROM_GROUP', 'CANCEL_GROUP_BAN', 'ADD_GROUP_ADMIN','REMOVE_GROUP_ADMIN','DECRYPT_AESGCM', 'CANCEL_GROUP_INVITE', 'CREATE_GROUP', 'GET_USER_WALLET_TRANSACTIONS' ]; diff --git a/src/components/BuyQortInformation.tsx b/src/components/BuyQortInformation.tsx new file mode 100644 index 0000000..b6aed75 --- /dev/null +++ b/src/components/BuyQortInformation.tsx @@ -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 ( + + + {"Get QORT"} + + + + Get QORT using Qortal's crosschain trade portal + { + executeEvent("addTab", { + data: { service: "APP", name: "q-trade" }, + }); + executeEvent("open-apps-mode", {}); + setIsOpen(false) + }} + > + + + Trade QORT + + + + Benefits of having QORT + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/Chat/ChatGroup.tsx b/src/components/Chat/ChatGroup.tsx index 7f55e3e..d85455b 100644 --- a/src/components/Chat/ChatGroup.tsx +++ b/src/components/Chat/ChatGroup.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' +import React, { useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from 'react' import { CreateCommonSecret } from './CreateCommonSecret' import { reusableGet } from '../../qdn/publish/pubish' import { uint8ArrayToObject } from '../../backgroundFunctions/encryption' @@ -10,11 +10,11 @@ import Tiptap from './TipTap' import { CustomButton } from '../../App-styles' import CircularProgress from '@mui/material/CircularProgress'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar' -import { getBaseApiReact, getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App' +import { getBaseApiReact, getBaseApiReactSocket, isMobile, MyContext, pauseAllQueues, resumeAllQueues } from '../../App' import { CustomizedSnackbars } from '../Snackbar/Snackbar' import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes' import { useMessageQueue } from '../../MessageQueueContext' -import { executeEvent } from '../../utils/events' +import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events' import { Box, ButtonBase, Divider, Typography } from '@mui/material' import ShortUniqueId from "short-unique-id"; import { ReplyPreview } from './MessageItem' @@ -47,6 +47,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, const [isOpenQManager, setIsOpenQManager] = useState(null) const [onEditMessage, setOnEditMessage] = useState(null) const [messageSize, setMessageSize] = useState(0) + const {isUserBlocked} = useContext(MyContext) const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const [, forceUpdate] = useReducer((x) => x + 1, 0); @@ -167,10 +168,28 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, }) } - const middletierFunc = async (data: any, groupId: string) => { + const updateChatMessagesWithBlocksFunc = (e) => { + if(e.detail){ + setMessages((prev)=> prev?.filter((item)=> { + return !isUserBlocked(item?.sender, item?.senderName) + })) + } + }; + + useEffect(() => { + subscribeToEvent("updateChatMessagesWithBlocks", updateChatMessagesWithBlocksFunc); + + return () => { + unsubscribeFromEvent("updateChatMessagesWithBlocks", updateChatMessagesWithBlocksFunc); + }; + }, []); + + const middletierFunc = async (data: any, groupId: string) => { try { if (hasInitialized.current) { - decryptMessages(data, true); + const dataRemovedBlock = data?.filter((item)=> !isUserBlocked(item?.sender, item?.senderName)) + + decryptMessages(dataRemovedBlock, true); return; } hasInitialized.current = true; @@ -182,7 +201,11 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, }, }); const responseData = await response.json(); - decryptMessages(responseData, false); + const dataRemovedBlock = responseData?.filter((item)=> { + return !isUserBlocked(item?.sender, item?.senderName) + }) + + decryptMessages(dataRemovedBlock, false); } catch (error) { console.error(error); } diff --git a/src/components/Chat/ChatOptions.tsx b/src/components/Chat/ChatOptions.tsx index de90452..dac2950 100644 --- a/src/components/Chat/ChatOptions.tsx +++ b/src/components/Chat/ChatOptions.tsx @@ -5,6 +5,7 @@ import { InputBase, MenuItem, Select, + Tooltip, Typography, } from "@mui/material"; import React, { useEffect, useMemo, useRef, useState } from "react"; @@ -584,49 +585,89 @@ export const ChatOptions = ({ minHeight: "200px", }} > - { - setMode("search"); - }} - > - - - { - setMode("default"); - setSearchValue(""); - setSelectedMember(0); - openQManager(); - }} - > - - - - { - setMode("mentions"); - setSearchValue(""); - setSelectedMember(0); + { + setMode("search") + }}> + SEARCH} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, }} > - 0 && - (!lastMentionTimestamp || - lastMentionTimestamp < mentionList[0]?.timestamp) - ? "var(--unread)" - : "white", - }} - /> - + + + + { + setMode("default") + setSearchValue('') + setSelectedMember(0) + openQManager() + }}> + Q-MANAGER} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + + + + { + setMode("mentions") + setSearchValue('') + setSelectedMember(0) + }}> + MENTIONED} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + 0 && (!lastMentionTimestamp || lastMentionTimestamp < mentionList[0]?.timestamp) ? 'var(--unread)' : 'white' + }} /> + + diff --git a/src/components/Chat/MessageDisplay.tsx b/src/components/Chat/MessageDisplay.tsx index b78cbc9..c99299a 100644 --- a/src/components/Chat/MessageDisplay.tsx +++ b/src/components/Chat/MessageDisplay.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import DOMPurify from 'dompurify'; import './styles.css'; import { executeEvent } from '../../utils/events'; @@ -63,30 +63,34 @@ function processText(input) { return wrapper.innerHTML; } +const linkify = (text) => { + if (!text) return ""; // Return an empty string if text is null or undefined + + let textFormatted = text; + const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g; + textFormatted = text.replace(urlPattern, (url) => { + const href = url.startsWith('http') ? url : `https://${url}`; + return `${DOMPurify.sanitize(url)}`; + }); + return processText(textFormatted); +}; + export const MessageDisplay = ({ htmlContent, isReply }) => { - const linkify = (text) => { - if (!text) return ""; // Return an empty string if text is null or undefined - - let textFormatted = text; - const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g; - textFormatted = text.replace(urlPattern, (url) => { - const href = url.startsWith('http') ? url : `https://${url}`; - return `${DOMPurify.sanitize(url)}`; - }); - return processText(textFormatted); - }; + - const sanitizedContent = DOMPurify.sanitize(linkify(htmlContent), { - ALLOWED_TAGS: [ - 'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'div', 'span', 'img', - 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td','s', 'hr' - ], - ALLOWED_ATTR: [ - 'href', 'target', 'rel', 'class', 'src', 'alt', 'title', - 'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url' - ], - }).replace(/]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, '');; + const sanitizedContent = useMemo(()=> { + return DOMPurify.sanitize(linkify(htmlContent), { + ALLOWED_TAGS: [ + 'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'div', 'span', 'img', + 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 's', 'hr' + ], + ALLOWED_ATTR: [ + 'href', 'target', 'rel', 'class', 'src', 'alt', 'title', + 'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url' + ], + }).replace(/]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, ''); + }, [htmlContent]) const handleClick = async (e) => { e.preventDefault(); @@ -94,7 +98,15 @@ export const MessageDisplay = ({ htmlContent, isReply }) => { const target = e.target; if (target.tagName === 'A') { const href = target.getAttribute('href'); - window.electronAPI.openExternal(href); + if (chrome && chrome.tabs) { + chrome.tabs.create({ url: href }, (tab) => { + if (chrome.runtime.lastError) { + console.error("Error opening tab:", chrome.runtime.lastError); + } else { + console.log("Tab opened successfully:", tab); + } + }); + } } else if (target.getAttribute('data-url')) { const url = target.getAttribute('data-url'); diff --git a/src/components/Chat/MessageItem.tsx b/src/components/Chat/MessageItem.tsx index 8746db1..1fd7203 100644 --- a/src/components/Chat/MessageItem.tsx +++ b/src/components/Chat/MessageItem.tsx @@ -1,5 +1,5 @@ import { Message } from "@chatscope/chat-ui-kit-react"; -import React, { useContext, useEffect, useState } from "react"; +import React, { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { useInView } from "react-intersection-observer"; import { MessageDisplay } from "./MessageDisplay"; import { Avatar, Box, Button, ButtonBase, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material"; @@ -8,6 +8,7 @@ import { getBaseApi } from "../../background"; import { MyContext, getBaseApiReact } from "../../App"; import { generateHTML } from "@tiptap/react"; import Highlight from "@tiptap/extension-highlight"; +import Mention from "@tiptap/extension-mention"; import StarterKit from "@tiptap/starter-kit"; import Underline from "@tiptap/extension-underline"; import { executeEvent } from "../../utils/events"; @@ -17,7 +18,6 @@ import { Spacer } from "../../common/Spacer"; import { ReactionPicker } from "../ReactionPicker"; import KeyOffIcon from '@mui/icons-material/KeyOff'; import EditIcon from '@mui/icons-material/Edit'; -import Mention from "@tiptap/extension-mention"; import TextStyle from '@tiptap/extension-text-style'; import { addressInfoKeySelector } from "../../atoms/global"; import { useRecoilValue } from "recoil"; @@ -50,8 +50,7 @@ const getBadgeImg = (level)=> { default: return level0Img } } - -export const MessageItem = ({ +export const MessageItem = React.memo(({ message, onSeen, isLast, @@ -67,40 +66,80 @@ export const MessageItem = ({ isUpdating, lastSignature, onEdit, - isPrivate, - setMobileViewModeKeepOpen + isPrivate }) => { - const {getIndividualUserInfo} = useContext(MyContext) - const userInfo = useRecoilValue(addressInfoKeySelector(message?.sender)); +const {getIndividualUserInfo} = useContext(MyContext) const [anchorEl, setAnchorEl] = useState(null); const [selectedReaction, setSelectedReaction] = useState(null); - const { ref, inView } = useInView({ - threshold: 0.7, // Fully visible - triggerOnce: false, // Only trigger once when it becomes visible - }); + const [userInfo, setUserInfo] = useState(null) - useEffect(() => { - if (inView && isLast && onSeen) { - onSeen(message.id); - } - }, [inView, message.id, isLast]); - useEffect(()=> { - if(message?.sender){ - getIndividualUserInfo(message?.sender) +useEffect(()=> { + const getInfo = async ()=> { + if(!message?.sender) return + try { + const res = await getIndividualUserInfo(message?.sender) + if(!res) return null + setUserInfo(res) + } catch (error) { + // } - }, [message?.sender]) + } + + getInfo() +}, [message?.sender, getIndividualUserInfo]) + +const htmlText = useMemo(()=> { + + if(message?.messageText){ + return generateHTML(message?.messageText, [ + StarterKit, + Underline, + Highlight, + Mention, + TextStyle + ]) + } + +}, []) + + + +const htmlReply = useMemo(()=> { + + if(reply?.messageText){ + return generateHTML(reply?.messageText, [ + StarterKit, + Underline, + Highlight, + Mention, + TextStyle + ]) + } + +}, []) + +const userAvatarUrl = useMemo(()=> { + return message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${ + message?.senderName + }/qortal_avatar?async=true` : '' +}, []) + +const onSeenFunc = useCallback(()=> { + onSeen(message.id); +}, [message?.id]) + return ( - <> + + {message?.divide && (
Unread messages below
)}
{message?.senderName?.charAt(0)} - + + }} src={getBadgeImg(userInfo)} /> )} @@ -195,7 +234,7 @@ export const MessageItem = ({ gap: '10px', alignItems: 'center' }}> - {message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && ( + {message?.sender === myAddress && (!message?.isNotEncrypted || isPrivate === false) && ( { onEdit(message); @@ -260,41 +299,27 @@ export const MessageItem = ({ }}>Replied to {reply?.senderName || reply?.senderAddress} {reply?.messageText && ( )} {reply?.decryptedData?.type === "notification" ? ( ) : ( - + )} )} - {message?.messageText && ( + - )} + {message?.decryptedData?.type === "notification" ? ( ) : ( - + )} { - event.stopPropagation(); // Prevent event bubbling - setAnchorEl(event.currentTarget); - setSelectedReaction(reaction); - }}> -
{reaction}
{numberOfReactions > 1 && ( + event.stopPropagation(); // Prevent event bubbling + setAnchorEl(event.currentTarget); + setSelectedReaction(reaction); + }}> +
{reaction}
{numberOfReactions > 1 && ( {' '} {numberOfReactions} @@ -361,7 +388,7 @@ export const MessageItem = ({ {reactions[selectedReaction]?.map((reactionItem) => ( @@ -404,14 +431,14 @@ export const MessageItem = ({ alignItems: 'center', gap: '15px' }}> - {message?.isNotEncrypted && isPrivate && ( + {message?.isNotEncrypted && isPrivate && ( )} - {isUpdating ? ( + {isUpdating ? (
- {/* */} - {/* {!message.unread && Seen} */} +
- +
); -}; +}); export const ReplyPreview = ({message, isEdit})=> { @@ -501,7 +518,7 @@ export const ReplyPreview = ({message, isEdit})=> { - {isEdit ? ( + {isEdit ? ( { )} + ) +} + +const MessageWragger = ({lastMessage, onSeen, isLast, children})=> { + + if(lastMessage){ + return ( + {children} + ) + } + 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
+ {children} +
+ } \ No newline at end of file diff --git a/src/components/Chat/useBlockUsers.tsx b/src/components/Chat/useBlockUsers.tsx new file mode 100644 index 0000000..13615d5 --- /dev/null +++ b/src/components/Chat/useBlockUsers.tsx @@ -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 + }; +}; diff --git a/src/components/ContextMenuPinnedApps.tsx b/src/components/ContextMenuPinnedApps.tsx index be0ae46..bb64a4c 100644 --- a/src/components/ContextMenuPinnedApps.tsx +++ b/src/components/ContextMenuPinnedApps.tsx @@ -124,11 +124,19 @@ export const ContextMenuPinnedApps = ({ children, app, isMine }) => { { handleClose(e); setSortablePinnedApps((prev) => { - const updatedApps = prev.filter( - (item) => !(item?.name === app?.name && item?.service === app?.service) - ); - saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps); - return updatedApps; + if(app?.isPrivate){ + const updatedApps = prev.filter( + (item) => !(item?.privateAppProperties?.name === app?.privateAppProperties?.name && item?.privateAppProperties?.service === app?.privateAppProperties?.service && item?.privateAppProperties?.identifier === app?.privateAppProperties?.identifier) + ); + saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps); + return updatedApps; + } else { + const updatedApps = prev.filter( + (item) => !(item?.name === app?.name && item?.service === app?.service) + ); + saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps); + return updatedApps; + } }); }}> diff --git a/src/components/CoreSyncStatus.tsx b/src/components/CoreSyncStatus.tsx index 641996a..2334bef 100644 --- a/src/components/CoreSyncStatus.tsx +++ b/src/components/CoreSyncStatus.tsx @@ -97,7 +97,7 @@ export const CoreSyncStatus = ({imageSize, position}) => {

{message}

Block Height: {height || ''}

Connected Peers: {numberOfConnections || ''}

-

Using gateway: {isUsingGateway?.toString()}

+

Using public node: {isUsingGateway?.toString()}

diff --git a/src/components/Desktop/DesktopHeader.tsx b/src/components/Desktop/DesktopHeader.tsx index 5939b15..4820054 100644 --- a/src/components/Desktop/DesktopHeader.tsx +++ b/src/components/Desktop/DesktopHeader.tsx @@ -18,9 +18,9 @@ import { NotificationIcon2 } from "../../assets/Icons/NotificationIcon2"; import { ChatIcon } from "../../assets/Icons/ChatIcon"; import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon"; import { MembersIcon } from "../../assets/Icons/MembersIcon"; +import { AdminsIcon } from "../../assets/Icons/AdminsIcon"; import LockIcon from '@mui/icons-material/Lock'; import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; -import { AdminsIcon } from "../../assets/Icons/AdminsIcon"; const IconWrapper = ({ children, label, color, selected, selectColor, customHeight }) => { return ( @@ -98,7 +98,7 @@ export const DesktopHeader = ({ padding: "10px", }} > - @@ -118,7 +118,7 @@ export const DesktopHeader = ({ fontWeight: 600, }} > - {selectedGroup?.groupName} + {selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName} - + { goToAnnouncements() @@ -139,6 +140,7 @@ export const DesktopHeader = ({ label="ANN" selected={isAnnouncement} selectColor="#09b6e8" + customHeight="55px" > { + + const toggleDrawer = (newOpen: boolean) => () => { + setOpen(newOpen); + }; + + + return ( +
+ + + + {children} + + +
+ ); +} diff --git a/src/components/Explore/Explore.tsx b/src/components/Explore/Explore.tsx new file mode 100644 index 0000000..7cc96a0 --- /dev/null +++ b/src/components/Explore/Explore.tsx @@ -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 ( + + { + executeEvent("addTab", { + data: { service: "APP", name: "q-trade" }, + }); + executeEvent("open-apps-mode", {}); + }} + > + + + Trade QORT + + + { + setDesktopViewMode('apps') + + }} + > + + + See Apps + + + { + executeEvent("openGroupMessage", { + from: "0" , + }); + }} + > + + + General Chat + + + + ); +}; diff --git a/src/components/Group/BlockedUsersModal.tsx b/src/components/Group/BlockedUsersModal.tsx new file mode 100644 index 0000000..84fa3fa --- /dev/null +++ b/src/components/Group/BlockedUsersModal.tsx @@ -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 ( + + Blocked Users + + + { + setValue(e.target.value); + }} + /> + + + + {Object.entries(blockedUsers?.addresses).length > 0 && ( + <> + + + Blocked Users for Chat ( addresses ) + + + + )} + + + {Object.entries(blockedUsers?.addresses || {})?.map( + ([key, value]) => { + return ( + + {key} + + + ); + } + )} + + {Object.entries(blockedUsers?.names).length > 0 && ( + <> + + + Blocked Users for QDN and Chat (names) + + + + )} + + + {Object.entries(blockedUsers?.names || {})?.map(([key, value]) => { + return ( + + {key} + + + ); + })} + + + + + + + ); +}; diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 003133a..e441e16 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -94,13 +94,16 @@ import { AppsDesktop } from "../Apps/AppsDesktop"; import { formatEmailDate } from "./QMailMessages"; import LockIcon from '@mui/icons-material/Lock'; import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; -import { useSetRecoilState } from "recoil"; -import { addressInfoControllerAtom, selectedGroupIdAtom } from "../../atoms/global"; +import { useRecoilState, useSetRecoilState } from "recoil"; +import { addressInfoControllerAtom, groupsPropertiesAtom, selectedGroupIdAtom } from "../../atoms/global"; import { sortArrayByTimestampAndGroupName } from "../../utils/time"; import { AdminSpace } from "../Chat/AdminSpace"; import { HubsIcon } from "../../assets/Icons/HubsIcon"; import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; import { DesktopSideBar } from "../DesktopSideBar"; +import BlockIcon from '@mui/icons-material/Block'; +import { BlockedUsersModal } from "./BlockedUsersModal"; + // let touchStartY = 0; // let disablePullToRefresh = false; @@ -480,6 +483,7 @@ export const Group = ({ const [mobileViewMode, setMobileViewMode] = useState("home"); const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState(""); const isFocusedRef = useRef(true); + const [isOpenBlockedUserModal, setIsOpenBlockedUserModal] = React.useState(false); const timestampEnterDataRef = useRef({}); const selectedGroupRef = useRef(null); const selectedDirectRef = useRef(null); @@ -497,9 +501,11 @@ export const Group = ({ const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false) const setSelectedGroupId = useSetRecoilState(selectedGroupIdAtom) - const [groupsProperties, setGroupsProperties] = useState({}) + const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom) const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom); + const isPrivate = useMemo(()=> { + if(selectedGroup?.groupId === '0') return false if(!selectedGroup?.groupId || !groupsProperties[selectedGroup?.groupId]) return null if(groupsProperties[selectedGroup?.groupId]?.isOpen === true) return false if(groupsProperties[selectedGroup?.groupId]?.isOpen === false) return true @@ -899,7 +905,10 @@ export const Group = ({ } if(isPrivate === false){ setTriedToFetchSecretKey(true); - getAdminsForPublic(selectedGroup) + if(selectedGroup?.groupId !== '0'){ + getAdminsForPublic(selectedGroup) + } + } }, [selectedGroup, isPrivate]); @@ -988,7 +997,7 @@ export const Group = ({ // Update the component state with the received 'sendqort' state setGroups(sortArrayByTimestampAndGroupName(message.payload)); getLatestRegularChat(message.payload) - setMemberGroups(message.payload); + setMemberGroups(message.payload?.filter((item)=> item?.groupId !== '0')); if (selectedGroupRef.current && groupSectionRef.current === "chat") { chrome?.runtime?.sendMessage({ @@ -1081,7 +1090,7 @@ export const Group = ({ !initiatedGetMembers.current && selectedGroup?.groupId && secretKey && - admins.includes(myAddress) + admins.includes(myAddress) && selectedGroup?.groupId !== '0' ) { // getAdmins(selectedGroup?.groupId); getMembers(selectedGroup?.groupId); @@ -1432,11 +1441,11 @@ export const Group = ({ if (isLoadingOpenSectionFromNotification.current) return; const groupId = e.detail?.from; - const findGroup = groups?.find((group) => +group?.groupId === +groupId); if (findGroup?.groupId === selectedGroup?.groupId) { isLoadingOpenSectionFromNotification.current = false; - + setChatMode("groups"); + setDesktopViewMode('chat') return; } if (findGroup) { @@ -2159,7 +2168,7 @@ export const Group = ({ - {chatMode === "groups" && ( + {chatMode === "groups" && ( + <> { setOpenAddGroup(true); @@ -2231,8 +2242,24 @@ export const Group = ({ color: "white", }} /> - Group Mgmt + Group Mgmt + { + setIsOpenBlockedUserModal(true); + }} + sx={{ + minWidth: 'unset', + padding: '10px' + }} + > + + + )} {chatMode === "directs" && ( )} - + {isOpenBlockedUserModal && ( + { + setIsOpenBlockedUserModal(false) + }} /> + )} {selectedDirect && !newChat && ( <> { const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState( [] ); + const [isExpanded, setIsExpanded] = React.useState(false); + const [loading, setLoading] = React.useState(true); const getJoinRequests = async () => { @@ -53,120 +57,129 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => { alignItems: "center", }} > - setIsExpanded((prev)=> !prev)} > - Group Invites: + Group Invites {groupsWithJoinRequests?.length > 0 && ` (${groupsWithJoinRequests?.length})`} - - + {isExpanded ? : ( + + )} +
+ + - {loading && groupsWithJoinRequests.length === 0 && ( - + {loading && groupsWithJoinRequests.length === 0 && ( + + + + )} + {!loading && groupsWithJoinRequests.length === 0 && ( + + + Nothing to display + + + )} + - - - )} - {!loading && groupsWithJoinRequests.length === 0 && ( - - - Nothing to display - - - )} - - {groupsWithJoinRequests?.map((group) => { - return ( - { - setOpenAddGroup(true); - setTimeout(() => { - executeEvent("openGroupInvitesRequest", {}); - }, 300); - }} - disablePadding - secondaryAction={ - - { + return ( + { + setOpenAddGroup(true); + setTimeout(() => { + executeEvent("openGroupInvitesRequest", {}); + }, 300); + }} + disablePadding + secondaryAction={ + + + + } + > + + - - } - > - - - - - ); - })} - - + + + ); + })} + +
+ ); }; diff --git a/src/components/Group/GroupJoinRequests.tsx b/src/components/Group/GroupJoinRequests.tsx index da9db07..76f958a 100644 --- a/src/components/Group/GroupJoinRequests.tsx +++ b/src/components/Group/GroupJoinRequests.tsx @@ -11,16 +11,20 @@ import InfoIcon from "@mui/icons-material/Info"; import { RequestQueueWithPromise } from "../../utils/queue/queue"; import GroupAddIcon from '@mui/icons-material/GroupAdd'; import { executeEvent } from "../../utils/events"; -import { Box, Typography } from "@mui/material"; +import { Box, ButtonBase, Collapse, Typography } from "@mui/material"; import { Spacer } from "../../common/Spacer"; import { CustomLoader } from "../../common/CustomLoader"; import { getBaseApi } from "../../background"; import { MyContext, getBaseApiReact, isMobile } from "../../App"; import { myGroupsWhereIAmAdminAtom } from "../../atoms/global"; import { useSetRecoilState } from "recoil"; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2) export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection, setMobileViewMode, setDesktopViewMode }) => { + const [isExpanded, setIsExpanded] = React.useState(false) + const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([]) const [loading, setLoading] = React.useState(true) const {txList, setTxList} = React.useContext(MyContext) @@ -34,7 +38,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get setLoading(true) let groupsAsAdmin = [] - const getAllGroupsAsAdmin = groups.map(async (group)=> { + const getAllGroupsAsAdmin = groups.filter((item)=> item.groupId !== '0').map(async (group)=> { const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(()=> { return fetch( @@ -55,7 +59,6 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get await Promise.all(getAllGroupsAsAdmin) setMyGroupsWhereIAmAdmin(groupsAsAdmin) - const res = await Promise.all(groupsAsAdmin.map(async (group)=> { const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> { @@ -110,26 +113,33 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get flexDirection: "column", alignItems: 'center' }}> - setIsExpanded((prev)=> !prev)} > - Join Requests: + Join Requests {filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length > 0 && ` (${filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length})`} - - - + {isExpanded ? : ( + + )} + + )} - + {filteredJoinRequests?.map((group)=> { if(group?.data?.length === 0) return null return ( @@ -228,6 +238,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get + ); }; diff --git a/src/components/Group/HomeDesktop.tsx b/src/components/Group/HomeDesktop.tsx index 70819df..7532eb9 100644 --- a/src/components/Group/HomeDesktop.tsx +++ b/src/components/Group/HomeDesktop.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Typography } from "@mui/material"; +import { Box, Button, Divider, Typography } from "@mui/material"; import React from "react"; import { Spacer } from "../../common/Spacer"; import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched"; @@ -7,10 +7,14 @@ import { GroupJoinRequests } from "./GroupJoinRequests"; import { GroupInvites } from "./GroupInvites"; import RefreshIcon from "@mui/icons-material/Refresh"; import { ListOfGroupPromotions } from "./ListOfGroupPromotions"; - +import { QortPrice } from "../Home/QortPrice"; +import ExploreIcon from "@mui/icons-material/Explore"; +import { Explore } from "../Explore/Explore"; +import { NewUsersCTA } from "../Home/NewUsersCTA"; export const HomeDesktop = ({ refreshHomeDataFunc, myAddress, + name, isLoadingGroups, balance, userInfo, @@ -22,140 +26,217 @@ export const HomeDesktop = ({ setOpenAddGroup, setMobileViewMode, setDesktopViewMode, - desktopViewMode + desktopViewMode, }) => { + const [checked1, setChecked1] = React.useState(false); + const [checked2, setChecked2] = React.useState(false); + React.useEffect(() => { + if (balance && +balance >= 6) { + setChecked1(true); + } + }, [balance]); + + + React.useEffect(() => { + if (name) setChecked2(true); + }, [name]); + + + const isLoaded = React.useMemo(()=> { + if(userInfo !== null) return true + return false + }, [ userInfo]) + + const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> { + if(isLoaded && checked1 && checked2) return true + return false + }, [checked1, isLoaded, checked2]) + + return ( - - 15 ? "16px" : "20px", - padding: '10px' + display: "flex", + width: "100%", + flexDirection: "column", + height: "100%", + alignItems: "flex-start", + maxWidth: "1036px", }} > - Welcome - {userInfo?.name ? ( - {`, ${userInfo?.name}`} - ) : null} - - - {!isLoadingGroups && ( - 15 ? "16px" : "20px", + padding: "10px", }} > - - - - {desktopViewMode === 'home' && ( - <> - - - - - - - - - - - )} - - )} - {!isLoadingGroups && ( - - )} - - - - - {/* {`, ${userInfo?.name}`} + ) : null} + + + {!isLoadingGroups && ( + + + - - */} + item?.groupId !== "0").length !== 0 + } + /> + + + {desktopViewMode === "home" && ( + <> + + {hasDoneNameAndBalanceAndIsLoaded && ( + <> + + + + + + + + )} + + + )} + + + + )} + {!isLoadingGroups && ( + <> + + + + {" "} + + Explore + {" "} + + + {!hasDoneNameAndBalanceAndIsLoaded && ( + + )} + + {hasDoneNameAndBalanceAndIsLoaded && ( + + + )} + + + + + + + + )} + + + + ); diff --git a/src/components/Group/ListOfGroupPromotions.tsx b/src/components/Group/ListOfGroupPromotions.tsx index 6c777dd..bbcd010 100644 --- a/src/components/Group/ListOfGroupPromotions.tsx +++ b/src/components/Group/ListOfGroupPromotions.tsx @@ -9,6 +9,8 @@ import { Avatar, Box, Button, + ButtonBase, + Collapse, Dialog, DialogActions, DialogContent, @@ -28,8 +30,8 @@ import { import { getNameInfo } from "./Group"; import { getBaseApi, getFee } from "../../background"; import { LoadingButton } from "@mui/lab"; -import LockIcon from '@mui/icons-material/Lock'; -import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; +import LockIcon from "@mui/icons-material/Lock"; +import NoEncryptionGmailerrorredIcon from "@mui/icons-material/NoEncryptionGmailerrorred"; import { MyContext, getArbitraryEndpointReact, @@ -40,7 +42,11 @@ import { Spacer } from "../../common/Spacer"; import { CustomLoader } from "../../common/CustomLoader"; import { RequestQueueWithPromise } from "../../utils/queue/queue"; import { useRecoilState } from "recoil"; -import { myGroupsWhereIAmAdminAtom, promotionTimeIntervalAtom, promotionsAtom } from "../../atoms/global"; +import { + myGroupsWhereIAmAdminAtom, + promotionTimeIntervalAtom, + promotionsAtom, +} from "../../atoms/global"; import { Label } from "./AddGroup"; import ShortUniqueId from "short-unique-id"; import { CustomizedSnackbars } from "../Snackbar/Snackbar"; @@ -48,7 +54,8 @@ import { getGroupNames } from "./UserListOfInvites"; import { WrapperUserAction } from "../WrapperUserAction"; import { useVirtualizer } from "@tanstack/react-virtual"; import ErrorBoundary from "../../common/ErrorBoundary"; - +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import ExpandLessIcon from "@mui/icons-material/ExpandLess"; export const requestQueuePromos = new RequestQueueWithPromise(20); export function utf8ToBase64(inputString: string): string { @@ -65,7 +72,6 @@ export function utf8ToBase64(inputString: string): string { const uid = new ShortUniqueId({ length: 8 }); - export function getGroupId(str) { const match = str.match(/group-(\d+)-/); return match ? match[1] : null; @@ -81,12 +87,12 @@ export const ListOfGroupPromotions = () => { const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState( myGroupsWhereIAmAdminAtom ); - const [promotions, setPromotions] = useRecoilState( - promotionsAtom - ); + const [promotions, setPromotions] = useRecoilState(promotionsAtom); const [promotionTimeInterval, setPromotionTimeInterval] = useRecoilState( promotionTimeIntervalAtom ); + const [isExpanded, setIsExpanded] = React.useState(false); + const [openSnack, setOpenSnack] = useState(false); const [infoSnack, setInfoSnack] = useState(null); const [fee, setFee] = useState(null); @@ -95,7 +101,6 @@ export const ListOfGroupPromotions = () => { const { show, setTxList } = useContext(MyContext); const listRef = useRef(); - const rowVirtualizer = useVirtualizer({ count: promotions.length, getItemKey: React.useCallback( @@ -107,7 +112,6 @@ export const ListOfGroupPromotions = () => { overscan: 10, // Number of items to render outside the visible area to improve smoothness }); - useEffect(() => { try { (async () => { @@ -118,7 +122,7 @@ export const ListOfGroupPromotions = () => { }, []); const getPromotions = useCallback(async () => { try { - setPromotionTimeInterval(Date.now()) + setPromotionTimeInterval(Date.now()); const identifier = `group-promotions-ui24-`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`; const response = await fetch(url, { @@ -169,7 +173,9 @@ export const ListOfGroupPromotions = () => { }); await Promise.all(getPromos); - const groupWithInfo = await getGroupNames(data.sort((a, b) => b.created - a.created)); + const groupWithInfo = await getGroupNames( + data.sort((a, b) => b.created - a.created) + ); setPromotions(groupWithInfo); } catch (error) { console.error(error); @@ -178,22 +184,23 @@ export const ListOfGroupPromotions = () => { useEffect(() => { const now = Date.now(); - + const timeSinceLastFetch = now - promotionTimeInterval; - const initialDelay = timeSinceLastFetch >= THIRTY_MINUTES - ? 0 - : THIRTY_MINUTES - timeSinceLastFetch; + const initialDelay = + timeSinceLastFetch >= THIRTY_MINUTES + ? 0 + : THIRTY_MINUTES - timeSinceLastFetch; const initialTimeout = setTimeout(() => { getPromotions(); - + // Start a 30-minute interval const interval = setInterval(() => { getPromotions(); }, THIRTY_MINUTES); - + return () => clearInterval(interval); }, initialDelay); - + return () => clearTimeout(initialTimeout); }, [getPromotions, promotionTimeInterval]); @@ -321,103 +328,144 @@ export const ListOfGroupPromotions = () => { }; - - - - return ( - - + setIsExpanded((prev) => !prev)} > - Group Promotions + Group promotions {promotions.length > 0 && ` (${promotions.length})`} - - - + {isExpanded ? ( + + ) : ( + + )} + + - - {loading && promotions.length === 0 && ( + + <> - - - )} - {!loading && promotions.length === 0 && ( - - - Nothing to display - + + + + - )} -
+ {loading && promotions.length === 0 && ( + + + + )} + {!loading && promotions.length === 0 && ( + + + Nothing to display + + + )} +
{ const index = virtualRow.index; const promotion = promotions[index]; return ( -
{ gap: "5px", }} > - - Error loading content: Invalid Data - - } - > - - { - if (reason === "backdropClick") { - // Prevent closing on backdrop click - return; - } - handlePopoverClose(); // Close only on other events like Esc key press - }} - anchorOrigin={{ - vertical: "top", - horizontal: "center", - }} - transformOrigin={{ - vertical: "bottom", - horizontal: "center", - }} - style={{ marginTop: "8px" }} - > - - - Group name: {` ${promotion?.groupName}`} - - - Number of members: {` ${promotion?.memberCount}`} - - {promotion?.description && ( - - {promotion?.description} - - )} - {promotion?.isOpen === false && ( - - *This is a closed/private group, so you will need to wait - until an admin accepts your request - - )} - - - - Close - - - handleJoinGroup(promotion, promotion?.isOpen) - } - > - Join - - - - + + Error loading content: Invalid Data + + } + > + + { + if (reason === "backdropClick") { + // Prevent closing on backdrop click + return; + } + handlePopoverClose(); // Close only on other events like Esc key press + }} + anchorOrigin={{ + vertical: "top", + horizontal: "center", + }} + transformOrigin={{ + vertical: "bottom", + horizontal: "center", + }} + style={{ marginTop: "8px" }} + > + + + Group name: {` ${promotion?.groupName}`} + + + Number of members:{" "} + {` ${promotion?.memberCount}`} + + {promotion?.description && ( + + {promotion?.description} + + )} + {promotion?.isOpen === false && ( + + *This is a closed/private group, so you + will need to wait until an admin accepts + your request + + )} + + + + Close + + + handleJoinGroup( + promotion, + promotion?.isOpen + ) + } + > + Join + + + + - - - - {promotion?.name?.charAt(0)} - - - {promotion?.name} - - - - {promotion?.groupName} - - - - - {promotion?.isOpen === false && ( - - )} - {promotion?.isOpen === true && ( - - )} - - {promotion?.isOpen ? 'Public group' : 'Private group' } - - - - - {promotion?.data} - - - - - - - + + + + {promotion?.name?.charAt(0)} + + + {promotion?.name} + + + + {promotion?.groupName} + + + + + {promotion?.isOpen === false && ( + + )} + {promotion?.isOpen === true && ( + + )} + + {promotion?.isOpen + ? "Public group" + : "Private group"} + + + + + {promotion?.data} + + + + + + +
- ); })}
-
+
+ + {isShowModal && ( @@ -712,7 +775,7 @@ export const ListOfGroupPromotions = () => { aria-describedby="alert-dialog-description" > - {"Promote your group to non-members"} + {"Promote your group to non-members"} @@ -738,6 +801,7 @@ export const ListOfGroupPromotions = () => { value={selectedGroup} label="Groups where you are an admin" onChange={(e) => setSelectedGroup(e.target.value)} + variant="outlined" > {myGroupsWhereIAmAdmin?.map((group) => { return ( diff --git a/src/components/Group/QMailMessages.tsx b/src/components/Group/QMailMessages.tsx index 5ebbc17..9dbdef0 100644 --- a/src/components/Group/QMailMessages.tsx +++ b/src/components/Group/QMailMessages.tsx @@ -1,11 +1,11 @@ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import List from "@mui/material/List"; import ListItem from "@mui/material/ListItem"; import ListItemButton from "@mui/material/ListItemButton"; import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; import moment from 'moment' -import { Box, Typography } from "@mui/material"; +import { Box, ButtonBase, Collapse, Typography } from "@mui/material"; import { Spacer } from "../../common/Spacer"; import { getBaseApiReact, isMobile } from "../../App"; import { MessagingIcon } from '../../assets/Icons/MessagingIcon'; @@ -15,6 +15,9 @@ import { executeEvent } from '../../utils/events'; import { CustomLoader } from '../../common/CustomLoader'; import { useRecoilState } from 'recoil'; import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import MarkEmailUnreadIcon from '@mui/icons-material/MarkEmailUnread'; export const isLessThanOneWeekOld = (timestamp) => { // Current time in milliseconds const now = Date.now(); @@ -41,8 +44,9 @@ export function formatEmailDate(timestamp: number) { } } export const QMailMessages = ({userName, userAddress}) => { - const [mails, setMails] = useRecoilState(mailsAtom) - const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom) + const [isExpanded, setIsExpanded] = useState(false) + const [mails, setMails] = useRecoilState(mailsAtom) + const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom) const [loading, setLoading] = useState(true) const getMails = useCallback(async () => { @@ -99,7 +103,16 @@ export const QMailMessages = ({userName, userAddress}) => { }, [getMails, userName, userAddress]); - + const anyUnread = useMemo(()=> { + let unread = false + + mails.forEach((mail)=> { + if(lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created)){ + unread = true + } + }) + return unread + }, [mails, lastEnteredTimestamp]) return ( { }} > - setIsExpanded((prev)=> !prev)} > Latest Q-Mails - - - + + {isExpanded ? : ( + + )} + + { + ) } diff --git a/src/components/Group/ThingsToDoInitial.tsx b/src/components/Group/ThingsToDoInitial.tsx index f7d05c4..367b3b1 100644 --- a/src/components/Group/ThingsToDoInitial.tsx +++ b/src/components/Group/ThingsToDoInitial.tsx @@ -12,27 +12,17 @@ import { Box, Typography } from "@mui/material"; import { Spacer } from "../../common/Spacer"; import { isMobile } from "../../App"; import { QMailMessages } from "./QMailMessages"; +import { executeEvent } from "../../utils/events"; export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInfo }) => { const [checked1, setChecked1] = React.useState(false); const [checked2, setChecked2] = React.useState(false); - const [checked3, setChecked3] = React.useState(false); + // const [checked3, setChecked3] = React.useState(false); - // const getAddressInfo = async (address) => { - // const response = await fetch(getBaseApiReact() + "/addresses/" + address); - // const data = await response.json(); - // if (data.error && data.error === 124) { - // setChecked1(false); - // } else if (data.address) { - // setChecked1(true); - // } - // }; + // React.useEffect(() => { + // if (hasGroups) setChecked3(true); + // }, [hasGroups]); - // const checkInfo = async () => { - // try { - // getAddressInfo(myAddress); - // } catch (error) {} - // }; React.useEffect(() => { if (balance && +balance >= 6) { @@ -40,9 +30,6 @@ export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInf } }, [balance]); - React.useEffect(() => { - if (hasGroups) setChecked3(true); - }, [hasGroups]); React.useEffect(() => { if (name) setChecked2(true); @@ -50,20 +37,21 @@ export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInf const isLoaded = React.useMemo(()=> { - if(userInfo !== null) return true - return false -}, [userInfo]) + if(userInfo !== null) return true + return false + }, [ userInfo]) -const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> { - if(isLoaded && checked1 && checked2) return true -return false + const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> { + if(isLoaded && checked1 && checked2) return true + return false }, [checked1, isLoaded, checked2]) if(hasDoneNameAndBalanceAndIsLoaded){ -return ( - -); + return ( + + ); } +if(!isLoaded) return null return ( - {!isLoaded ? 'Loading...' : 'Getting Started' } - + {!isLoaded ? 'Loading...' : 'Getting Started' } @@ -97,7 +84,6 @@ return ( - - - // - // - // } - disablePadding - sx={{ - marginBottom: '20px' - }} - > - - - - - {/* */} - - - - - // - // - // } - disablePadding - > - - - - - - - - - - // - // - // } - disablePadding - > - - - - - - - - - + {isLoaded && ( + + + { + executeEvent("openBuyQortInfo", {}) + }} + > + + + + {/* */} + + + + + // + // + // } + disablePadding + > + + + { + executeEvent('openRegisterName', {}) + }} sx={{ + "& .MuiTypography-root": { + fontSize: "1rem", + fontWeight: 400, + }, + }} primary={`Register a name`} /> + + + + + + {/* + + + + + + + + */} + + )} + ); diff --git a/src/components/Group/WebsocketActive.tsx b/src/components/Group/WebsocketActive.tsx index 06d5d4b..5785288 100644 --- a/src/components/Group/WebsocketActive.tsx +++ b/src/components/Group/WebsocketActive.tsx @@ -80,7 +80,15 @@ export const WebSocketActive = ({ myAddress, setIsLoadingGroups }) => { } const data = JSON.parse(e.data); - const filteredGroups = data.groups?.filter(item => item?.groupId !== 0) || []; + const copyGroups = [...(data?.groups || [])] + const findIndex = copyGroups?.findIndex(item => item?.groupId === 0) + if(findIndex !== -1){ + copyGroups[findIndex] = { + ...(copyGroups[findIndex] || {}), + groupId: "0" + } + } + const filteredGroups = copyGroups const sortedGroups = filteredGroups.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0)); const sortedDirects = (data?.direct || []).filter(item => item?.name !== 'extension-proxy' && item?.address !== 'QSMMGSgysEuqDCuLw3S4cHrQkBrh3vP3VH' diff --git a/src/components/Group/useHandleUserInfo.tsx b/src/components/Group/useHandleUserInfo.tsx index db6993a..a497259 100644 --- a/src/components/Group/useHandleUserInfo.tsx +++ b/src/components/Group/useHandleUserInfo.tsx @@ -1,34 +1,32 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useRef } from "react"; import { getBaseApiReact } from "../../App"; -import { useRecoilState, useSetRecoilState } from "recoil"; -import { addressInfoControllerAtom } from "../../atoms/global"; export const useHandleUserInfo = () => { - const [userInfo, setUserInfo] = useRecoilState(addressInfoControllerAtom); - + const userInfoRef = useRef({}) const getIndividualUserInfo = useCallback(async (address)=> { try { - if(!address || userInfo[address]) return + if(!address) return null + if(userInfoRef.current[address] !== undefined) return userInfoRef.current[address] + const url = `${getBaseApiReact()}/addresses/${address}`; const response = await fetch(url); if (!response.ok) { throw new Error("network error"); } const data = await response.json(); - setUserInfo((prev)=> { - return { - ...prev, - [address]: data - } - }) + userInfoRef.current = { + ...userInfoRef.current, + [address]: data?.level + } + return data?.level } catch (error) { //error } - }, [userInfo]) + }, []) return { getIndividualUserInfo, diff --git a/src/components/Home/NewUsersCTA.tsx b/src/components/Home/NewUsersCTA.tsx new file mode 100644 index 0000000..486c4da --- /dev/null +++ b/src/components/Home/NewUsersCTA.tsx @@ -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 ( + + + + + + Are you a new user? + + + + Please message us on Telegram or Discord if you need 4 QORT to start + chatting without any limitations + + + + { + 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 + + { + 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 + + + + + ); +}; diff --git a/src/components/Home/QortPrice.tsx b/src/components/Home/QortPrice.tsx new file mode 100644 index 0000000..f4586fb --- /dev/null +++ b/src/components/Home/QortPrice.tsx @@ -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 ( + + + Based on the latest 20 trades + + } + placement="bottom" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + + Price + + {!ltcPerQort ? ( + + ) : ( + + {ltcPerQort} LTC/QORT + + )} + + + + + Supply + + {!supply ? ( + + ) : ( + + {supply} QORT + + )} + + + {lastBlock?.timestamp && formatDate(lastBlock?.timestamp)} + + } + placement="bottom" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + + + Last height + + {!lastBlock?.height ? ( + + ) : ( + + {lastBlock?.height} + + )} + + + + + ); +}; diff --git a/src/components/MainAvatar.tsx b/src/components/MainAvatar.tsx index 71aed3b..c10c4ca 100644 --- a/src/components/MainAvatar.tsx +++ b/src/components/MainAvatar.tsx @@ -7,8 +7,9 @@ import ImageUploader from "../common/ImageUploader"; import { getFee } from "../background"; import { fileToBase64 } from "../utils/fileReading"; import { LoadingButton } from "@mui/lab"; +import ErrorIcon from '@mui/icons-material/Error'; -export const MainAvatar = ({ myName }) => { +export const MainAvatar = ({ myName, balance, setOpenSnack, setInfoSnack }) => { const [hasAvatar, setHasAvatar] = useState(false); const [avatarFile, setAvatarFile] = useState(null); const [tempAvatar, setTempAvatar] = useState(null) @@ -52,10 +53,11 @@ const [isLoading, setIsLoading] = useState(false) checkIfAvatarExists(); }, [myName]); + const publishAvatar = async ()=> { try { const fee = await getFee('ARBITRARY') - + if(+balance < +fee.fee) throw new Error(`Publishing an Avatar requires ${fee.fee}`) await show({ message: "Would you like to publish an avatar?" , publishFee: fee.fee + ' QORT' @@ -63,30 +65,36 @@ const [isLoading, setIsLoading] = useState(false) setIsLoading(true); const avatarBase64 = await fileToBase64(avatarFile) await new Promise((res, rej) => { - chrome?.runtime?.sendMessage( - { - action: "publishOnQDN", - payload: { - data: avatarBase64, - identifier: "qortal_avatar", - service: 'THUMBNAIL' - }, + chrome?.runtime?.sendMessage( + { + action: "publishOnQDN", + payload: { + data: avatarBase64, + identifier: "qortal_avatar", + service: 'THUMBNAIL' }, - (response) => { - - if (!response?.error) { - res(response); - return - } - rej(response.error); + }, + (response) => { + + if (!response?.error) { + res(response); + return } - ); - }); + rej(response.error); + } + ); + }); setAvatarFile(null); setTempAvatar(`data:image/webp;base64,${avatarBase64}`) handleClose() } catch (error) { - + if (error?.message) { + setOpenSnack(true) + setInfoSnack({ + type: "error", + message: error?.message, + }); + } } finally { setIsLoading(false); } @@ -115,7 +123,7 @@ const [isLoading, setIsLoading] = useState(false) change avatar - + ); } @@ -143,7 +151,7 @@ const [isLoading, setIsLoading] = useState(false) change avatar - + ); } @@ -161,13 +169,13 @@ const [isLoading, setIsLoading] = useState(false) set avatar - + ); }; -const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose, publishAvatar, isLoading}) => { +const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose, publishAvatar, isLoading, myName}) => { return ( {avatarFile?.name} - - + {!myName && ( + + + A registered name is required to set an avatar + + )} + + + Publish avatar diff --git a/src/components/QMailStatus.tsx b/src/components/QMailStatus.tsx index 2787336..d2c1738 100644 --- a/src/components/QMailStatus.tsx +++ b/src/components/QMailStatus.tsx @@ -3,7 +3,7 @@ import QMailLogo from '../assets/QMailLogo.png' import { useRecoilState } from 'recoil' import { mailsAtom, qMailLastEnteredTimestampAtom } from '../atoms/global' import { isLessThanOneWeekOld } from './Group/QMailMessages' -import { ButtonBase } from '@mui/material' +import { ButtonBase, Tooltip } from '@mui/material' import { executeEvent } from '../utils/events' export const QMailStatus = () => { const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom) @@ -35,9 +35,28 @@ export const QMailStatus = () => { borderRadius: '50%', outline: '1px solid white' }} /> - )} + )} + Q-MAIL} + placement="left" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + + + ) } diff --git a/src/components/RegisterName.tsx b/src/components/RegisterName.tsx new file mode 100644 index 0000000..94d80d2 --- /dev/null +++ b/src/components/RegisterName.tsx @@ -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.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 ( + + + {"Register name"} + + + + + setRegisterNameValue(e.target.value)} + value={registerNameValue} + placeholder="Choose a name" + /> + {(!balance || (nameFee && balance && balance < nameFee))&& ( + <> + + + + Your balance is {balance ?? 0} QORT. A name registration requires a {nameFee} QORT fee + + + + + )} + + {isNameAvailable === Availability.AVAILABLE && ( + + + {registerNameValue} is available + + )} + {isNameAvailable === Availability.NOT_AVAILABLE && ( + + + {registerNameValue} is unavailable + + )} + {isNameAvailable === Availability.LOADING && ( + + + Checking if name already existis + + )} + + Benefits of a name + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/Snackbar/Snackbar.tsx b/src/components/Snackbar/Snackbar.tsx index 3e5fccb..59fa295 100644 --- a/src/components/Snackbar/Snackbar.tsx +++ b/src/components/Snackbar/Snackbar.tsx @@ -22,7 +22,7 @@ export const CustomizedSnackbars = ({open, setOpen, info, setInfo, duration}) = if(!open) return null return (
- + { + 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 ( + + + + + + setNameOrAddress(e.target.value)} + size="small" + placeholder="Address or Name" + autoComplete="off" + onKeyDown={(e) => { + if (e.key === "Enter" && nameOrAddress) { + lookupFunc(); + } + }} + /> + { + lookupFunc(); + }} > + + + { + onClose() + }}> + + + + + {!isLoadingUser && errorMessage && ( + + {errorMessage} + + )} + {isLoadingUser && ( + + + + )} + {!isLoadingUser && addressInfo && ( + <> + + + + + + {addressInfo?.name ?? "Name not registered"} + + + + {addressInfo?.name ? ( + + + + ) : ( + + )} + + + + Level {addressInfo?.level} + + + + + + Address + + + copy address + + } + placement="bottom" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + { + navigator.clipboard.writeText(addressInfo?.address); + }} + > + + {addressInfo?.address} + + + + + + Balance + {addressInfo?.balance} + + + + + + + + + )} + + {isLoadingPayments && ( + + + + )} + {!isLoadingPayments && addressInfo && ( + + 20 most recent payments + + {!isLoadingPayments && payments?.length === 0 && ( + + No payments + + )} + + + + Sender + Reciver + Amount + Time + + + + {payments.map((payment, index) => ( + + + + copy address + + } + placement="bottom" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + { + navigator.clipboard.writeText( + payment?.creatorAddress + ); + }} + > + {formatAddress(payment?.creatorAddress)} + + + + + + copy address + + } + placement="bottom" + arrow + sx={{ fontSize: "24" }} + slotProps={{ + tooltip: { + sx: { + color: "#ffffff", + backgroundColor: "#444444", + }, + }, + arrow: { + sx: { + color: "#444444", + }, + }, + }} + > + { + navigator.clipboard.writeText(payment?.recipient); + }} + > + {formatAddress(payment?.recipient)} + + + + + + {payment?.amount} + + {formatTimestamp(payment?.timestamp)} + + ))} + +
+
+ )} + +
+
+
+ ); +}; diff --git a/src/components/WrapperUserAction.tsx b/src/components/WrapperUserAction.tsx index 8337943..8bb9824 100644 --- a/src/components/WrapperUserAction.tsx +++ b/src/components/WrapperUserAction.tsx @@ -1,6 +1,8 @@ -import React, { useState } from 'react'; -import { Popover, Button, Box } from '@mui/material'; +import React, { useContext, useEffect, useState } from 'react'; +import { Popover, Button, Box, CircularProgress } from '@mui/material'; import { executeEvent } from '../utils/events'; +import { BlockedUsersModal } from './Group/BlockedUsersModal'; +import { MyContext } from '../App'; export const WrapperUserAction = ({ children, address, name, disabled }) => { const [anchorEl, setAnchorEl] = useState(null); @@ -46,6 +48,7 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => { {/* Popover */} + {open && ( { > Copy address + + + + + + )} ); }; + +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 ( + + ) +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 2bdf4b3..01b99b0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -40,6 +40,24 @@ const theme = createTheme({ color: '#b0b0b0', // Lighter text for body2, often used for secondary text }, }, + components: { + MuiOutlinedInput: { + styleOverrides: { + root: { + ".MuiOutlinedInput-notchedOutline": { + borderColor: "white", // ⚪ Default outline color + }, + }, + }, + }, + MuiSelect: { + styleOverrides: { + icon: { + color: "white", // ✅ Caret (dropdown arrow) color + }, + }, + }, + }, }); export default theme; diff --git a/src/qortalRequests.ts b/src/qortalRequests.ts index cbc417c..f29699f 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,5 +1,5 @@ import { banFromGroup, gateways, getApiKeyFromStorage } from "./background"; -import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createBuyOrder, createGroupRequest, createPoll, createSellOrder, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll } from "./qortalRequests/get"; +import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createBuyOrder, createGroupRequest, createPoll, createSellOrder, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getUserWalletTransactions, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll } from "./qortalRequests/get"; const listOfAllQortalRequests = [ 'GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS', @@ -756,6 +756,20 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => { }); break; } + + case "GET_USER_WALLET_TRANSACTIONS" : { + const data = request.payload; + + getUserWalletTransactions(data, isFromExtension, appInfo) + .then((res) => { + sendResponse(res); + }) + .catch((error) => { + sendResponse({ error: error.message }); + }); + break; + } + } } return true; diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 0059666..17667bb 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -657,7 +657,7 @@ export const decryptData = async (data) => { export const getListItems = async (data, isFromExtension) => { const isGateway = await isRunningGateway() if(isGateway){ - throw new Error('This action cannot be done through a gateway') + throw new Error("This action cannot be done through a public node"); } const requiredFields = ["list_name"]; const missingFields: string[] = []; @@ -711,7 +711,7 @@ export const getListItems = async (data, isFromExtension) => { export const addListItems = async (data, isFromExtension) => { const isGateway = await isRunningGateway() if(isGateway){ - throw new Error('This action cannot be done through a gateway') + throw new Error("This action cannot be done through a public node"); } const requiredFields = ["list_name", "items"]; const missingFields: string[] = []; @@ -766,7 +766,7 @@ export const addListItems = async (data, isFromExtension) => { export const deleteListItems = async (data, isFromExtension) => { const isGateway = await isRunningGateway() if(isGateway){ - throw new Error('This action cannot be done through a gateway') + throw new Error("This action cannot be done through a public node"); } const requiredFields = ["list_name"]; const missingFields: string[] = []; @@ -2280,7 +2280,7 @@ export const getTxActivitySummary = async (data) => { export const updateForeignFee = async (data) => { const isGateway = await isRunningGateway(); if (isGateway) { - throw new Error("This action cannot be done through a gateway"); + throw new Error("This action cannot be done through a public node"); } const requiredFields = ['coin', 'type', 'value']; const missingFields: string[] = []; @@ -2379,7 +2379,7 @@ export const getTxActivitySummary = async (data) => { export const setCurrentForeignServer = async (data) => { const isGateway = await isRunningGateway(); if (isGateway) { - throw new Error("This action cannot be done through a gateway"); + throw new Error("This action cannot be done through a public node"); } const requiredFields = ['coin']; const missingFields: string[] = []; @@ -2440,7 +2440,7 @@ export const getTxActivitySummary = async (data) => { export const addForeignServer = async (data) => { const isGateway = await isRunningGateway(); if (isGateway) { - throw new Error("This action cannot be done through a gateway"); + throw new Error("This action cannot be done through a public node"); } const requiredFields = ['coin']; const missingFields: string[] = []; @@ -2500,7 +2500,7 @@ export const getTxActivitySummary = async (data) => { export const removeForeignServer = async (data) => { const isGateway = await isRunningGateway(); if (isGateway) { - throw new Error("This action cannot be done through a gateway"); + throw new Error("This action cannot be done through a public node"); } const requiredFields = ['coin']; const missingFields: string[] = []; @@ -3053,7 +3053,7 @@ const crosschainAtInfo = await Promise.all(atPromises); }, 0) )} ${` ${crosschainAtInfo?.[0]?.foreignBlockchain}`}`, - highlightedText: `Is using gateway: ${isGateway}`, + highlightedText: `Is using public node: ${isGateway}`, fee: '', foreignFee: `${sellerForeignFee[foreignBlockchain].value} ${sellerForeignFee[foreignBlockchain].ticker}` }, isFromExtension); @@ -3224,13 +3224,15 @@ export const createSellOrder = async (data, isFromExtension) => { throw new Error(errorMsg); } + const parsedForeignAmount = Number(data.foreignAmount)?.toFixed(8) + const receivingAddress = await getUserWalletFunc(data.foreignBlockchain) try { const resPermission = await getUserPermission({ text1: "Do you give this application permission to perform a sell order?", text2: `${data.qortAmount}${" "} ${`QORT`}`, - text3: `FOR ${data.foreignAmount} ${data.foreignBlockchain}`, + text3: `FOR ${parsedForeignAmount} ${data.foreignBlockchain}`, fee: '0.02' }, isFromExtension); const { accepted } = resPermission; @@ -3247,12 +3249,12 @@ const receivingAddress = await getUserWalletFunc(data.foreignBlockchain) }; const response = await tradeBotCreateRequest({ creatorPublicKey: userPublicKey, - qortAmount: parseFloat(data.qortAmount), - fundingQortAmount: parseFloat(data.qortAmount) + 0.001, - foreignBlockchain: data.foreignBlockchain, - foreignAmount: parseFloat(data.foreignAmount), - tradeTimeout: 120, - receivingAddress: receivingAddress.address + qortAmount: parseFloat(data.qortAmount), + fundingQortAmount: parseFloat(data.qortAmount) + 0.01, + foreignBlockchain: data.foreignBlockchain, + foreignAmount: parseFloat(parsedForeignAmount), + tradeTimeout: 120, + receivingAddress: receivingAddress.address }, keyPair) return response @@ -3353,7 +3355,7 @@ export const adminAction = async (data, isFromExtension) => { } const isGateway = await isRunningGateway(); if (isGateway) { - throw new Error("This action cannot be done through a gateway"); + throw new Error("This action cannot be done through a public node"); } let apiEndpoint = ""; @@ -3769,7 +3771,7 @@ url export const getHostedData = async (data, isFromExtension) => { const isGateway = await isRunningGateway(); if (isGateway) { - throw new Error("This action cannot be done through a gateway"); + throw new Error("This action cannot be done through a public node"); } const resPermission = await getUserPermission( { @@ -3805,7 +3807,7 @@ export const getHostedData = async (data, isFromExtension) => { export const deleteHostedData = async (data, isFromExtension) => { const isGateway = await isRunningGateway(); if (isGateway) { - throw new Error("This action cannot be done through a gateway"); + throw new Error("This action cannot be done through a public node"); } const requiredFields = ["hostedData"]; const missingFields: string[] = []; @@ -4378,4 +4380,97 @@ export const createGroupRequest = async (data, isFromExtension) => { } else { throw new Error("User declined request"); } +}; + +export const getUserWalletTransactions = async (data, isFromExtension, appInfo) => { + const requiredFields = ["coin"]; + const missingFields: string[] = []; + requiredFields.forEach((field) => { + if (!data[field]) { + missingFields.push(field); + } + }); + if (missingFields.length > 0) { + const missingFieldsString = missingFields.join(", "); + const errorMsg = `Missing fields: ${missingFieldsString}`; + throw new Error(errorMsg); + } + + const value = + (await getPermission( + `getUserWalletTransactions-${appInfo?.name}-${data.coin}` + )) || false; +let skip = false; +if (value) { + skip = true; +} + let resPermission; + + if (!skip) { + + resPermission = await getUserPermission( + { + text1: + "Do you give this application permission to retrieve your wallet transactions", + highlightedText: `coin: ${data.coin}`, + checkbox1: { + value: true, + label: "Always allow wallet txs to be retrieved automatically", + }, + }, + isFromExtension + ); +} +const { accepted = false, checkbox1 = false } = resPermission || {}; + +if (resPermission) { + setPermission( + `getUserWalletTransactions-${appInfo?.name}-${data.coin}`, + checkbox1 + ); +} + + if (accepted || skip) { + const coin = data.coin; + const walletKeys = await getUserWalletFunc(coin); + let publicKey + if(data?.coin === 'ARRR'){ + const resKeyPair = await getKeyPair(); + const parsedData = resKeyPair; + publicKey = parsedData.arrrSeed58; + } else { + publicKey = walletKeys["publickey"] + } + + const _url = await createEndpoint( + `/crosschain/` + data.coin.toLowerCase() + `/wallettransactions` + ); + const _body = publicKey; + try { + const response = await fetch(_url, { + method: "POST", + headers: { + Accept: "*/*", + "Content-Type": "application/json", + }, + body: _body, + }); + if (!response?.ok) throw new Error("Unable to fetch wallet transactions"); + let res; + try { + res = await response.clone().json(); + } catch (e) { + res = await response.text(); + } + if (res?.error && res?.message) { + throw new Error(res.message); + } + + return res; + } catch (error) { + throw new Error(error?.message || "Fetch Wallet Transactions Failed"); + } + } else { + throw new Error("User declined request"); + } }; \ No newline at end of file diff --git a/src/utils/time.ts b/src/utils/time.ts index b0a27cf..c89c1cd 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -12,7 +12,7 @@ export function formatTimestamp(timestamp: number): string { } else if (elapsedTime < 1440) { return `${Math.floor(elapsedTime / 60)}h ago` } else { - return timestampMoment.format('MMM D') + return timestampMoment.format('MMM D, YYYY') } } export function formatTimestampForum(timestamp: number): string {