diff --git a/src/App.tsx b/src/App.tsx index 626d2fc..3ab4e40 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -117,9 +117,13 @@ import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil"; import { canSaveSettingToQdnAtom, fullScreenAtom, + groupsPropertiesAtom, hasSettingsChangedAtom, isUsingImportExportSettingsAtom, + lastEnteredGroupIdAtom, + mailsAtom, oldPinnedAppsAtom, + qMailLastEnteredTimestampAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom, @@ -137,6 +141,10 @@ import { useHandleUserInfo } from "./components/Group/useHandleUserInfo"; import { Minting } from "./components/Minting/Minting"; import { isRunningGateway } from "./qortalRequests"; import { GlobalActions } from "./components/GlobalActions/GlobalActions"; +import { useBlockedAddresses } from "./components/Chat/useBlockUsers"; +import { UserLookup } from "./components/UserLookup.tsx/UserLookup"; +import { RegisterName } from "./components/RegisterName"; +import { BuyQortInformation } from "./components/BuyQortInformation"; type extStates = @@ -381,6 +389,8 @@ function App() { const [requestBuyOrder, setRequestBuyOrder] = useState(null); const [authenticatedMode, setAuthenticatedMode] = useState("qort"); const [requestAuthentication, setRequestAuthentication] = useState(null); + const [isOpenDrawerLookup, setIsOpenDrawerLookup] = useState(false) + const [userInfo, setUserInfo] = useState(null); const [balance, setBalance] = useState(null); const [ltcBalance, setLtcBalance] = useState(null); @@ -418,6 +428,9 @@ function App() { const holdRefExtState = useRef("not-authenticated"); const isFocusedRef = useRef(true); const { isShow, onCancel, onOk, show, message } = useModal(); + const {isUserBlocked, + addToBlockList, + removeBlockFromList, getAllBlockedUsers} = useBlockedAddresses() const { isShow: isShowUnsavedChanges, onCancel: onCancelUnsavedChanges, @@ -473,6 +486,9 @@ function App() { const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom); const {getIndividualUserInfo} = useHandleUserInfo() + const balanceSetIntervalRef = useRef(null) + + const { toggleFullScreen } = useAppFullScreen(setFullScreen); const generatorRef = useRef(null) const exportSeedphrase = async ()=> { @@ -529,7 +545,10 @@ function App() { ); const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom); const resetAtomIsUsingImportExportSettingsAtom = useResetRecoilState(isUsingImportExportSettingsAtom) - + const resetGroupPropertiesAtom = useResetRecoilState(groupsPropertiesAtom) + const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom) + const resetAtomMailsAtom = useResetRecoilState(mailsAtom) + const resetLastEnteredGroupIdAtom = useResetRecoilState(lastEnteredGroupIdAtom) const resetAllRecoil = () => { resetAtomSortablePinnedAppsAtom(); resetAtomCanSaveSettingToQdnAtom(); @@ -537,6 +556,10 @@ function App() { resetAtomSettingsLocalLastUpdatedAtom(); resetAtomOldPinnedAppsAtom(); resetAtomIsUsingImportExportSettingsAtom(); + resetAtomQMailLastEnteredTimestampAtom() + resetAtomMailsAtom() + resetGroupPropertiesAtom() + resetLastEnteredGroupIdAtom() }; useEffect(() => { if (!isMobile) return; @@ -763,6 +786,30 @@ function App() { }; }; + const balanceSetInterval = ()=> { + try { + if(balanceSetIntervalRef?.current){ + clearInterval(balanceSetIntervalRef?.current); + } + + let isCalling = false; + balanceSetIntervalRef.current = setInterval(async () => { + if (isCalling) return; + isCalling = true; + chrome?.runtime?.sendMessage({ action: "balance" }, (response) => { + if (!response?.error && !isNaN(+response)) { + setBalance(response); + } + + isCalling = false + }); + + }, 40000); + } catch (error) { + console.error(error) + } + } + const getBalanceFunc = () => { setQortBalanceLoading(true); window @@ -772,6 +819,7 @@ function App() { setBalance(response); } setQortBalanceLoading(false); + balanceSetInterval() }) .catch((error) => { console.error("Failed to get balance:", error); @@ -1188,6 +1236,9 @@ function App() { resetAllRecoil(); setShowSeed(false) setCreationStep(1) + if(balanceSetIntervalRef?.current){ + clearInterval(balanceSetIntervalRef?.current); + } }; function roundUpToDecimals(number, decimals = 8) { @@ -1358,6 +1409,18 @@ function App() { }; }, []); + const openUserProfile = (e) => { + setIsOpenDrawerProfile(true); + }; + + useEffect(() => { + subscribeToEvent("openUserProfile", openUserProfile); + + return () => { + unsubscribeFromEvent("openUserProfile", openUserProfile); + }; + }, []); + const openGlobalSnackBarFunc = (e) => { const message = e.detail?.message; const type = e.detail?.type; @@ -1595,7 +1658,7 @@ function App() { textDecoration: "underline", }} onClick={() => { - setOpenRegisterName(true); + executeEvent('openRegisterName', {}) }} > REGISTER NAME @@ -1784,7 +1847,11 @@ function App() { setInfoSnackCustom: setInfoSnack, userInfo: userInfo, downloadResource, - getIndividualUserInfo + getIndividualUserInfo, + isUserBlocked, + addToBlockList, + removeBlockFromList, + getAllBlockedUsers }} > @@ -2885,7 +2952,7 @@ await showInfo({ display: "flex", flexDirection: "column", alignItems: "center", - zIndex: 6, + zIndex: 10000, }} > @@ -2985,6 +3052,9 @@ await showInfo({ open={isShow} aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" + sx={{ + zIndex: 10001 + }} > {message.paymentFee ? "Payment" : "Publish"} @@ -3059,7 +3129,7 @@ await showInfo({ aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" > - {"Warning"} + {"LOGOUT"} {messageUnsavedChanges.message} @@ -3409,6 +3479,9 @@ await showInfo({ > {renderProfile()} + + + {extState === "create-wallet" && walletToBeDownloaded && ( { diff --git a/src/ExtStates/NotAuthenticated.tsx b/src/ExtStates/NotAuthenticated.tsx index 496fde0..a0f160d 100644 --- a/src/ExtStates/NotAuthenticated.tsx +++ b/src/ExtStates/NotAuthenticated.tsx @@ -12,41 +12,46 @@ import { DialogTitle, FormControlLabel, Input, + styled, Switch, - Tooltip, 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, gateways, isUsingLocal } from "../background"; -import HelpIcon from '@mui/icons-material/Help'; import { GlobalContext } from "../App"; +import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip'; export const manifestData = { version: "0.5.2", }; + + function removeTrailingSlash(url) { return url.replace(/\/+$/, ''); } + + export const NotAuthenticated = ({ getRootProps, getInputProps, setExtstate, - currentNode, - setCurrentNode, - useLocalNode, - setUseLocalNode, + apiKey, setApiKey, globalApiKey, handleSetGlobalApikey, - handleFilePick, + currentNode, + setCurrentNode, + useLocalNode, + setUseLocalNode }) => { const [isValidApiKey, setIsValidApiKey] = useState(null); const [hasLocalNode, setHasLocalNode] = useState(null); @@ -59,14 +64,14 @@ export const NotAuthenticated = ({ // const [currentNode, setCurrentNode] = React.useState({ // url: "http://127.0.0.1:12391", // }); - const { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext); - 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 { showTutorial, hasSeenGettingStarted } = useContext(GlobalContext); + const importedApiKeyRef = useRef(null); const currentNodeRef = useRef(null); const hasLocalNodeRef = useRef(null); @@ -106,6 +111,7 @@ export const NotAuthenticated = ({ }) } + }; reader.readAsText(file); // Read the file as text } @@ -123,12 +129,14 @@ export const NotAuthenticated = ({ const data = await response.json(); if (data?.height) { setHasLocalNode(true); - return true; + return true } - return false; + return false + } catch (error) { - return false; - } + return false + + } }, []); useEffect(() => { @@ -141,12 +149,16 @@ export const NotAuthenticated = ({ .then((response) => { setCustomNodes(response || []); + if(window?.electronAPI?.setAllowedDomains){ + window.electronAPI.setAllowedDomains(response?.map((node)=> node.url)) + } if(Array.isArray(response)){ const findLocal = response?.find((item)=> item?.url === 'http://127.0.0.1:12391') if(findLocal && findLocal?.apikey){ setImportedApiKey(findLocal?.apikey) } } + }) .catch((error) => { console.error( @@ -167,37 +179,54 @@ export const NotAuthenticated = ({ hasLocalNodeRef.current = hasLocalNode; }, [hasLocalNode]); + + const validateApiKey = useCallback(async (key, fromStartUp) => { try { + if(key === "isGateway") return const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391"; - if(fromStartUp && key?.url && key?.apikey && !isLocalKey && !gateways.some(gateway => apiKey?.url?.includes(gateway))){ + if (fromStartUp && key?.url && key?.apikey && !isLocalKey && !gateways.some(gateway => key?.url?.includes(gateway))) { setCurrentNode({ url: key?.url, apikey: key?.apikey, }); - const url = `${key?.url}/admin/apikey/test`; - const response = await fetch(url, { - method: "GET", - headers: { - accept: "text/plain", - "X-API-KEY": key?.apikey, // Include the API key here - }, - }); - + + 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 === "true") { - setIsValidApiKey(true); - setUseLocalNode(true); - return + if(data && data === 'true'){ + isValid = true + } else { + const url2 = `${key?.url}/admin/apikey/test?apiKey=${key?.apikey}`; + const response2 = await fetch(url2); + + // Assuming the response is in plain text and will be 'true' or 'false' + const data2 = await response2.text(); + if (data2 === "true") { + isValid = true + } } - + + if (isValid) { + setIsValidApiKey(true); + setUseLocalNode(true); + return + } + } if (!currentNodeRef.current) return; - const stillHasLocal = await checkIfUserHasLocalNode(); + 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) { @@ -215,18 +244,29 @@ export const NotAuthenticated = ({ } 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) { window .sendMessage("setApiKey", payload) .then((response) => { @@ -248,21 +288,24 @@ export const NotAuthenticated = ({ } 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){ + if (fromStartUp) { setCurrentNode({ url: "http://127.0.0.1:12391", }); window - .sendMessage("setApiKey", null) + .sendMessage("setApiKey", "isGateway") .then((response) => { if (response) { setApiKey(null); @@ -277,11 +320,13 @@ export const NotAuthenticated = ({ }); return } + if(!fromStartUp){ setInfoSnack({ type: "error", message: error?.message || "Select a valid apikey", }); setOpenSnack(true); + } console.error("Error validating API key:", error); } }, []); @@ -295,15 +340,14 @@ 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: removeTrailingSlash(url), apikey: customApikey, }); - } else if (url && customApikey) { + } else if (!isFullListOfNodes && url) { nodes.push({ url: removeTrailingSlash(url), apikey: customApikey, @@ -311,6 +355,7 @@ export const NotAuthenticated = ({ } setCustomNodes(nodes); + setCustomNodeToSaveIndex(null); if (!nodes) return; window @@ -318,8 +363,11 @@ export const NotAuthenticated = ({ .then((response) => { if (response) { setMode("list"); - setUrl("http://"); + setUrl("https://"); setCustomApiKey(""); + if(window?.electronAPI?.setAllowedDomains){ + window.electronAPI.setAllowedDomains(nodes?.map((node) => node.url)) + } // add alert if needed } }) @@ -351,13 +399,12 @@ export const NotAuthenticated = ({ fontSize: '16px' }} > - WELCOME TO YOUR

+ WELCOME TO QORTAL WALLET + fontSize: '16px' + }}> QORTAL + - setExtstate("wallets")}> - Wallets + + setExtstate('wallets')}> + {/* */} + Accounts + + {/* + + */} @@ -377,8 +430,10 @@ export const NotAuthenticated = ({ display: "flex", gap: "10px", alignItems: "center", + }} > + { setExtstate("create-wallet"); @@ -392,8 +447,10 @@ export const NotAuthenticated = ({ } }} > - Create wallet + Create account + +
@@ -432,6 +489,12 @@ export const NotAuthenticated = ({ }} > item?.url !== node?.url); - saveCustomNodes(nodesToSave); + saveCustomNodes(nodesToSave, true); }} variant="contained" > @@ -750,7 +813,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 7a0e3bd..8a75638 100644 --- a/src/components/Apps/SortablePinnedApps.tsx +++ b/src/components/Apps/SortablePinnedApps.tsx @@ -1,115 +1,173 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { DndContext, MouseSensor, 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 { executeEvent } from '../../utils/events'; -import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; -import { useRecoilState, useSetRecoilState } from 'recoil'; -import { saveToLocalStorage } from './AppsNavBar'; -import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps'; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { DndContext, MouseSensor, 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 { 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 { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - padding: '10px', - border: '1px solid #ccc', - marginBottom: '5px', - borderRadius: '4px', - backgroundColor: '#f9f9f9', - cursor: 'grab', - color: 'black' - }; + const { openApp } = useHandlePrivateApps(); - return ( - - { - executeEvent("addTab", { - data: app - }) - }} - > - - + { + if (app?.isPrivate) { + try { + await openApp(app?.privateAppProperties); + } catch (error) { + console.error(error); + } + } else { + executeEvent("addTab", { + data: app, + }); + } + }} + > + + + {app?.isPrivate && !app?.privateAppProperties?.logo ? ( + + ) : ( + + - - center-icon - - - - {app?.metadata?.title || app?.name} - - - - - ); + // src={LogoSelected} + alt="center-icon" + /> + + )} + + {app?.isPrivate ? ( + + {`${app?.privateAppProperties?.appName || "Private"}`} + + ) : ( + {app?.metadata?.title || app?.name} + )} + + + + ); }; -export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps = [] }) => { +export const SortablePinnedApps = ({ + isDesktop, + myWebsite, + myApp, + availableQapps = [], +}) => { const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); - const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); + const setSettingsLocalLastUpdated = useSetRecoilState( + settingsLocalLastUpdatedAtom + ); const transformPinnedApps = useMemo(() => { - // Clone the existing pinned apps list let pinned = [...pinnedApps]; // Function to add or update `isMine` property const addOrUpdateIsMine = (pinnedList, appToCheck) => { - if (!appToCheck) return pinnedList; + if (!appToCheck) return pinnedList; - const existingIndex = pinnedList.findIndex( - (item) => item?.service === appToCheck?.service && item?.name === appToCheck?.name - ); - - if (existingIndex !== -1) { - // If the app is already in the list, update it with `isMine: true` - pinnedList[existingIndex] = { ...pinnedList[existingIndex], isMine: true }; - } else { - // If not in the list, add it with `isMine: true` at the beginning - pinnedList.unshift({ ...appToCheck, isMine: true }); - } + const existingIndex = pinnedList.findIndex( + (item) => + item?.service === appToCheck?.service && + item?.name === appToCheck?.name + ); - return pinnedList; + if (existingIndex !== -1) { + // If the app is already in the list, update it with `isMine: true` + pinnedList[existingIndex] = { + ...pinnedList[existingIndex], + isMine: true, + }; + } else { + // If not in the list, add it with `isMine: true` at the beginning + pinnedList.unshift({ ...appToCheck, isMine: true }); + } + + return pinnedList; }; // Update or add `myWebsite` and `myApp` while preserving their positions @@ -118,76 +176,77 @@ export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps // Update pinned list based on availableQapps pinned = pinned.map((pin) => { - const findIndex = availableQapps?.findIndex( - (item) => item?.service === pin?.service && item?.name === pin?.name - ); - if (findIndex !== -1) return { + const findIndex = availableQapps?.findIndex( + (item) => item?.service === pin?.service && item?.name === pin?.name + ); + if (findIndex !== -1) + return { ...availableQapps[findIndex], - ...pin - } + ...pin, + }; - return pin; + return pin; }); return pinned; -}, [myApp, myWebsite, pinnedApps, availableQapps]); + }, [myApp, myWebsite, pinnedApps, availableQapps]); const sensors = useSensors( - useSensor(MouseSensor, { - activationConstraint: { - distance: 10, - }, - }), - useSensor(TouchSensor, { - activationConstraint: { - delay: 500, // Delay in milliseconds before drag activates - tolerance: 5, // Movement tolerance in pixels during the delay - }, - }), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }) + useSensor(MouseSensor, { + activationConstraint: { + distance: 10, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 500, // Delay in milliseconds before drag activates + tolerance: 5, // Movement tolerance in pixels during the delay + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) ); const handleDragEnd = (event) => { - const { active, over } = event; + const { active, over } = event; - if (!over) return; + if (!over) return; - if (active.id !== over.id) { - const oldIndex = transformPinnedApps.findIndex( - (item) => `${item?.service}-${item?.name}` === active.id - ); - const newIndex = transformPinnedApps.findIndex( - (item) => `${item?.service}-${item?.name}` === over.id - ); + if (active.id !== over.id) { + const oldIndex = transformPinnedApps.findIndex( + (item) => `${item?.service}-${item?.name}` === active.id + ); + const newIndex = transformPinnedApps.findIndex( + (item) => `${item?.service}-${item?.name}` === over.id + ); - const newOrder = arrayMove(transformPinnedApps, oldIndex, newIndex); - setPinnedApps(newOrder); - saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', newOrder); - setSettingsLocalLastUpdated(Date.now()); - } + const newOrder = arrayMove(transformPinnedApps, oldIndex, newIndex); + setPinnedApps(newOrder); + saveToLocalStorage("ext_saved_settings", "sortablePinnedApps", newOrder); + setSettingsLocalLastUpdated(Date.now()); + } }; return ( - + `${app?.service}-${app?.name}`)} > - `${app?.service}-${app?.name}`)}> - {transformPinnedApps.map((app) => ( - - ))} - - + {transformPinnedApps.map((app) => ( + + ))} + + ); }; - - diff --git a/src/components/Apps/TabComponent.tsx b/src/components/Apps/TabComponent.tsx index aca6b55..fdf9c62 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..2eaa5f9 --- /dev/null +++ b/src/components/Apps/useHandlePrivateApps.tsx @@ -0,0 +1,237 @@ +import React, { useContext, useState } from "react"; +import { executeEvent } from "../../utils/events"; +import { getBaseApiReact, MyContext } from "../../App"; +import { createEndpoint } from "../../background"; +import { useRecoilState, useSetRecoilState } from "recoil"; +import { + settingsLocalLastUpdatedAtom, + sortablePinnedAppsAtom, +} from "../../atoms/global"; +import { saveToLocalStorage } from "./AppsNavBarDesktop"; +import { base64ToBlobUrl } from "../../utils/fileReading"; +import { base64ToUint8Array } from "../../qdn/encryption/group-encryption"; +import { uint8ArrayToObject } from "../../backgroundFunctions/encryption"; + +export const useHandlePrivateApps = () => { + const [status, setStatus] = useState(""); + const { + openSnackGlobal, + setOpenSnackGlobal, + infoSnackCustom, + setInfoSnackCustom, + } = useContext(MyContext); + const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState( + sortablePinnedAppsAtom + ); + const setSettingsLocalLastUpdated = useSetRecoilState( + settingsLocalLastUpdatedAtom + ); + const openApp = async ( + privateAppProperties, + addToPinnedApps, + setLoadingStatePrivateApp + ) => { + try { + + + if(setLoadingStatePrivateApp){ + setLoadingStatePrivateApp(`Downloading and decrypting private app.`); + + } + setOpenSnackGlobal(true); + + setInfoSnackCustom({ + type: "info", + message: "Fetching app data", + duration: null + }); + const urlData = `${getBaseApiReact()}/arbitrary/${ + privateAppProperties?.service + }/${privateAppProperties?.name}/${ + privateAppProperties?.identifier + }?encoding=base64`; + let data; + try { + const responseData = await fetch(urlData, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if(!responseData?.ok){ + if(setLoadingStatePrivateApp){ + setLoadingStatePrivateApp("Error! Unable to download private app."); + } + + throw new Error("Unable to fetch app"); + } + + data = await responseData.text(); + if (data?.error) { + if(setLoadingStatePrivateApp){ + + setLoadingStatePrivateApp("Error! Unable to download private app."); + } + throw new Error("Unable to fetch app"); + } + } catch (error) { + if(setLoadingStatePrivateApp){ + + setLoadingStatePrivateApp("Error! Unable to download private app."); + } + throw error; + } + + let decryptedData; + // eslint-disable-next-line no-useless-catch + try { + decryptedData = await window.sendMessage( + "DECRYPT_QORTAL_GROUP_DATA", + + { + base64: data, + groupId: privateAppProperties?.groupId, + } + ); + if (decryptedData?.error) { + if(setLoadingStatePrivateApp){ + + setLoadingStatePrivateApp("Error! Unable to decrypt private app."); + } + throw new Error(decryptedData?.error); + } + } catch (error) { + if(setLoadingStatePrivateApp){ + + setLoadingStatePrivateApp("Error! Unable to decrypt private app."); + } + throw error; + } + + try { + const convertToUint = base64ToUint8Array(decryptedData); + const UintToObject = uint8ArrayToObject(convertToUint); + + if (decryptedData) { + setInfoSnackCustom({ + type: "info", + message: "Building app", + }); + const endpoint = await createEndpoint( + `/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true` + ); + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: UintToObject?.app, + }); + const previewPath = await response.text(); + const refreshfunc = async (tabId, privateAppProperties) => { + const checkIfPreviewLinkStillWorksUrl = await createEndpoint( + `/render/hash/HmtnZpcRPwisMfprUXuBp27N2xtv5cDiQjqGZo8tbZS?secret=E39WTiG4qBq3MFcMPeRZabtQuzyfHg9ZuR5SgY7nW1YH` + ); + const res = await fetch(checkIfPreviewLinkStillWorksUrl); + if (res.ok) { + executeEvent("refreshApp", { + tabId: tabId, + }); + } else { + const endpoint = await createEndpoint( + `/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true` + ); + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: UintToObject?.app, + }); + const previewPath = await response.text(); + executeEvent("updateAppUrl", { + tabId: tabId, + url: await createEndpoint(previewPath), + }); + + setTimeout(() => { + executeEvent("refreshApp", { + tabId: tabId, + }); + }, 300); + } + }; + + const appName = UintToObject?.name; + const logo = UintToObject?.logo + ? `data:image/png;base64,${UintToObject?.logo}` + : null; + + const dataBody = { + url: await createEndpoint(previewPath), + isPreview: true, + isPrivate: true, + privateAppProperties: { ...privateAppProperties, logo, appName }, + filePath: "", + refreshFunc: (tabId) => { + refreshfunc(tabId, privateAppProperties); + }, + }; + executeEvent("addTab", { + data: dataBody, + }); + setInfoSnackCustom({ + type: "success", + message: "Opened", + }); + if(setLoadingStatePrivateApp){ + + setLoadingStatePrivateApp(``); + } + if (addToPinnedApps) { + setSortablePinnedApps((prev) => { + const updatedApps = [ + ...prev, + { + isPrivate: true, + isPreview: true, + privateAppProperties: { + ...privateAppProperties, + logo, + appName, + }, + }, + ]; + + saveToLocalStorage( + "ext_saved_settings", + "sortablePinnedApps", + updatedApps + ); + return updatedApps; + }); + setSettingsLocalLastUpdated(Date.now()); + } + } + } catch (error) { + if(setLoadingStatePrivateApp){ + + setLoadingStatePrivateApp(`Error! ${error?.message || 'Unable to build private app.'}`); + } + throw error + } + } + catch (error) { + setInfoSnackCustom({ + type: "error", + message: error?.message || "Unable to fetch app", + }); + } + + }; + return { + openApp, + status, + }; +}; diff --git a/src/components/Apps/useQortalMessageListener.tsx b/src/components/Apps/useQortalMessageListener.tsx index 6c32532..2e2888b 100644 --- a/src/components/Apps/useQortalMessageListener.tsx +++ b/src/components/Apps/useQortalMessageListener.tsx @@ -221,7 +221,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', 'SIGN_TRANSACTION', 'ADMIN_ACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK', 'DECRYPT_QORTAL_GROUP_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'SHOW_ACTIONS', 'REGISTER_NAME', 'UPDATE_NAME', 'LEAVE_GROUP', 'INVITE_TO_GROUP', 'KICK_FROM_GROUP', 'BAN_FROM_GROUP', 'CANCEL_GROUP_BAN', 'ADD_GROUP_ADMIN', 'REMOVE_GROUP_ADMIN','DECRYPT_AESGCM', 'CANCEL_GROUP_INVITE', 'CREATE_GROUP' + 'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_PUBLIC_NODE', 'SIGN_TRANSACTION', 'ADMIN_ACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK', 'DECRYPT_QORTAL_GROUP_DATA', 'DECRYPT_DATA_WITH_SHARING_KEY', 'DELETE_HOSTED_DATA', 'GET_HOSTED_DATA', 'SHOW_ACTIONS', 'REGISTER_NAME', 'UPDATE_NAME', 'LEAVE_GROUP', 'INVITE_TO_GROUP', 'KICK_FROM_GROUP', 'BAN_FROM_GROUP', 'CANCEL_GROUP_BAN', 'ADD_GROUP_ADMIN', 'REMOVE_GROUP_ADMIN','DECRYPT_AESGCM', 'CANCEL_GROUP_INVITE', 'CREATE_GROUP', 'GET_USER_WALLET_TRANSACTIONS' ]; 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 cc32fc1..3248587 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' @@ -55,6 +55,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, const editorRef = useRef(null); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const handleUpdateRef = useRef(null); + const {isUserBlocked} = useContext(MyContext) const lastReadTimestamp = useRef(null) @@ -166,10 +167,28 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey, }) } + const updateChatMessagesWithBlocksFunc = (e) => { + if(e.detail){ + setMessages((prev)=> prev?.filter((item)=> { + return !isUserBlocked(item?.sender, item?.senderName) + })) + } + }; + + useEffect(() => { + subscribeToEvent("updateChatMessagesWithBlocks", updateChatMessagesWithBlocksFunc); + + return () => { + unsubscribeFromEvent("updateChatMessagesWithBlocks", updateChatMessagesWithBlocksFunc); + }; + }, []); + const middletierFunc = async (data: any, groupId: string) => { try { if (hasInitialized.current) { - decryptMessages(data, true); + const dataRemovedBlock = data?.filter((item)=> !isUserBlocked(item?.sender, item?.senderName)) + + decryptMessages(dataRemovedBlock, true); return; } hasInitialized.current = true; @@ -181,7 +200,12 @@ 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/MessageDisplay.tsx b/src/components/Chat/MessageDisplay.tsx index 62529c7..0804e82 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, setMobileViewModeKeepOpen }) => { - const linkify = (text) => { - if (!text) return ""; // Return an empty string if text is null or undefined - - let textFormatted = text; - const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g; - textFormatted = text.replace(urlPattern, (url) => { - const href = url.startsWith('http') ? url : `https://${url}`; - return `${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(); diff --git a/src/components/Chat/MessageItem.tsx b/src/components/Chat/MessageItem.tsx index f07762b..e7644b6 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, ClickAwayListener, List, ListItem, ListItemText, Popover, Tooltip, Typography } from "@mui/material"; @@ -52,7 +52,7 @@ const getBadgeImg = (level)=> { } } -export const MessageItem = ({ +export const MessageItem = React.memo(({ message, onSeen, isLast, @@ -72,7 +72,6 @@ export const MessageItem = ({ setMobileViewModeKeepOpen }) => { const {getIndividualUserInfo} = useContext(MyContext) - const userInfo = useRecoilValue(addressInfoKeySelector(message?.sender)); const [open, setOpen] = useState(false); const handleTooltipClose = () => { @@ -85,32 +84,72 @@ export const MessageItem = ({ 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)} @@ -176,7 +213,7 @@ export const MessageItem = ({ visibility: userInfo?.level !== undefined ? 'visible' : 'hidden', width: '30px', height: 'auto' - }} src={getBadgeImg(userInfo?.level)} /> + }} src={getBadgeImg(userInfo)} />
@@ -285,13 +322,7 @@ export const MessageItem = ({ }}>Replied to {reply?.senderName || reply?.senderAddress} {reply?.messageText && ( )} @@ -306,13 +337,7 @@ export const MessageItem = ({ )} {message?.messageText && ( )} @@ -485,21 +510,11 @@ export const MessageItem = ({ - {/* */} - {/* {!message.unread && Seen} */} + - +
); -}; +}); export const ReplyPreview = ({message, isEdit})=> { @@ -557,4 +572,37 @@ export const ReplyPreview = ({message, 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..05cbe90 --- /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) => { + window.sendMessage("listActions", { + + type: 'get', + listName: `blockedAddresses`, + + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + res(response); + } + }) + .catch((error) => { + console.error("Failed qortalRequest", error); + }); + }) + const blockedUsers = {} + response?.forEach((item)=> { + blockedUsers[item] = true + }) + userBlockedRef.current = blockedUsers + + const response2 = await new Promise((res, rej) => { + window.sendMessage("listActions", { + + type: 'get', + listName: `blockedNames`, + + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + res(response); + } + }) + .catch((error) => { + console.error("Failed qortalRequest", error); + }); + }) + const blockedUsers2 = {} + response2?.forEach((item)=> { + blockedUsers2[item] = true + }) + userNamesBlockedRef.current = blockedUsers2 + + + } catch (error) { + console.error(error) + } + } + fetchBlockedList() + }, []) + + const removeBlockFromList = useCallback(async (address, name)=> { + await new Promise((res, rej) => { + window.sendMessage("listActions", { + + type: 'remove', + items: name ? [name] : [address], + listName: name ? 'blockedNames' : 'blockedAddresses' + + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + if(!name){ + const copyObject = {...userBlockedRef.current} + delete copyObject[address] + userBlockedRef.current = copyObject + } else { + const copyObject = {...userNamesBlockedRef.current} + delete copyObject[name] + userNamesBlockedRef.current = copyObject + } + + res(response); + } + }) + .catch((error) => { + console.error("Failed qortalRequest", error); + }); + }) + if(name && userBlockedRef.current[address]){ + await new Promise((res, rej) => { + window.sendMessage("listActions", { + + type: 'remove', + items: !name ? [name] : [address], + listName: !name ? 'blockedNames' : 'blockedAddresses' + + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + const copyObject = {...userBlockedRef.current} + delete copyObject[address] + userBlockedRef.current = copyObject + res(response); + } + }) + .catch((error) => { + console.error("Failed qortalRequest", error); + }); + }) + } + + }, []) + + const addToBlockList = useCallback(async (address, name)=> { + await new Promise((res, rej) => { + window.sendMessage("listActions", { + + type: 'add', + items: name ? [name] : [address], + listName: name ? 'blockedNames' : 'blockedAddresses' + + }) + .then((response) => { + if (response.error) { + rej(response?.message); + return; + } else { + if(name){ + + const copyObject = {...userNamesBlockedRef.current} + copyObject[name] = true + userNamesBlockedRef.current = copyObject + }else { + const copyObject = {...userBlockedRef.current} + copyObject[address] = true + userBlockedRef.current = copyObject + + } + + res(response); + } + }) + .catch((error) => { + console.error("Failed qortalRequest", error); + }); + }) + }, []) + + return { + isUserBlocked, + addToBlockList, + removeBlockFromList, + getAllBlockedUsers + }; +}; diff --git a/src/components/Drawer/DrawerUserLookup.tsx b/src/components/Drawer/DrawerUserLookup.tsx new file mode 100644 index 0000000..5de0d33 --- /dev/null +++ b/src/components/Drawer/DrawerUserLookup.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Drawer from '@mui/material/Drawer'; + +export const DrawerUserLookup = ({open, setOpen, children}) => { + + const toggleDrawer = (newOpen: boolean) => () => { + setOpen(newOpen); + }; + + + return ( +
+ + + + {children} + + +
+ ); +} diff --git a/src/components/Explore/Explore.tsx b/src/components/Explore/Explore.tsx new file mode 100644 index 0000000..c8d7789 --- /dev/null +++ b/src/components/Explore/Explore.tsx @@ -0,0 +1,102 @@ +import { Box, ButtonBase, Typography } from "@mui/material"; +import React from "react"; +import ChatIcon from "@mui/icons-material/Chat"; +import qTradeLogo from "../../assets/Icons/q-trade-logo.webp"; +import AppsIcon from "@mui/icons-material/Apps"; +import { executeEvent } from "../../utils/events"; +export const Explore = ({setMobileViewMode}) => { + return ( + + { + executeEvent("addTab", { + data: { service: "APP", name: "q-trade" }, + }); + executeEvent("open-apps-mode", {}); + }} + > + + + Trade QORT + + + { + setMobileViewMode('apps') + + }} + > + + + See Apps + + + { + executeEvent("openGroupMessage", { + from: "0" , + }); + }} + > + + + General Chat + + + + ); +}; diff --git a/src/components/GlobalTouchMenu.tsx b/src/components/GlobalTouchMenu.tsx new file mode 100644 index 0000000..6de2ae9 --- /dev/null +++ b/src/components/GlobalTouchMenu.tsx @@ -0,0 +1,117 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Box, Divider, Menu, MenuItem, Typography, styled } from '@mui/material'; +import { executeEvent } from '../utils/events'; +import { useRecoilState } from 'recoil'; +import { lastEnteredGroupIdAtom } from '../atoms/global'; +import CloseIcon from '@mui/icons-material/Close'; + +const CustomStyledMenu = styled(Menu)(({ theme }) => ({ + '& .MuiPaper-root': { + backgroundColor: '#f9f9f9', + borderRadius: '12px', + padding: theme.spacing(1), + boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)', + }, + '& .MuiMenuItem-root': { + fontSize: '14px', + color: '#444', + transition: '0.3s background-color', + '&:hover': { + backgroundColor: '#f0f0f0', + }, + }, +})); + +export const GlobalTouchMenu = () => { + const [menuOpen, setMenuOpen] = useState(false); + const tapCount = useRef(0); + const lastTapTime = useRef(0); + const [menuPosition, setMenuPosition] = useState(null); + const [lastEnteredGroupId] = useRecoilState(lastEnteredGroupIdAtom) + + + useEffect(() => { + const handleTouchStart = (event) => { + const currentTime = new Date().getTime(); + const tapGap = currentTime - lastTapTime.current; + const { clientX, clientY } = event.touches[0]; + + if (tapGap < 400) { + tapCount.current += 1; + } else { + tapCount.current = 1; // Reset if too much time has passed + } + + lastTapTime.current = currentTime; + + if (tapCount.current === 3) { + setMenuPosition({ + top: clientY, + left: clientX, + }); + setMenuOpen(true); + tapCount.current = 0; // Reset after activation + } + }; + + document.addEventListener('touchstart', handleTouchStart); + + return () => { + document.removeEventListener('touchstart', handleTouchStart); + }; + }, []); + + const handleClose = () => { + setMenuOpen(false); + }; + + return ( + + + + + Close Menu + + + + + { + executeEvent('open-apps-mode', {}) + handleClose() + }}> + Apps + + { + executeEvent("openGroupMessage", { + from: lastEnteredGroupId , + }); + handleClose() + }}> + Group Chat + + { + executeEvent('openUserLookupDrawer', { + addressOrName: "" + }) + handleClose() + }}> + User Lookup + + { + executeEvent('openUserProfile',{}) + handleClose() + }}> + Wallet + + + ); +}; + 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 28fcb3a..8ce4459 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -19,6 +19,8 @@ import React, { useRef, useState, } from "react"; +import BlockIcon from '@mui/icons-material/Block'; + import SettingsIcon from "@mui/icons-material/Settings"; import { ChatGroup } from "../Chat/ChatGroup"; import { CreateCommonSecret } from "../Chat/CreateCommonSecret"; @@ -95,9 +97,11 @@ import { AppsDesktop } from "../Apps/AppsDesktop"; import { formatEmailDate } from "./QMailMessages"; import { useHandleMobileNativeBack } from "../../hooks/useHandleMobileNativeBack"; import { AdminSpace } from "../Chat/AdminSpace"; -import { useSetRecoilState } from "recoil"; -import { addressInfoControllerAtom, selectedGroupIdAtom } from "../../atoms/global"; +import { useRecoilState, useSetRecoilState } from "recoil"; +import { addressInfoControllerAtom, groupsPropertiesAtom, lastEnteredGroupIdAtom, selectedGroupIdAtom } from "../../atoms/global"; import { sortArrayByTimestampAndGroupName } from "../../utils/time"; +import { BlockedUsersModal } from "./BlockedUsersModal"; +import { GlobalTouchMenu } from "../GlobalTouchMenu"; // let touchStartY = 0; // let disablePullToRefresh = false; @@ -500,10 +504,12 @@ export const Group = ({ const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false) const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false) const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false) - const [groupsProperties, setGroupsProperties] = useState({}) + const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom) const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom); - + const [isOpenBlockedUserModal, setIsOpenBlockedUserModal] = React.useState(false); + const setLastEnteredGroupIdAtom = useSetRecoilState(lastEnteredGroupIdAtom) 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 @@ -906,7 +912,10 @@ export const Group = ({ } if(isPrivate === false){ setTriedToFetchSecretKey(true); - getAdminsForPublic(selectedGroup) + if(selectedGroup?.groupId !== '0'){ + getAdminsForPublic(selectedGroup) + } + } }, [selectedGroup, isPrivate]); @@ -997,7 +1006,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") { window.sendMessage("addTimestampEnterChat", { @@ -1091,7 +1100,7 @@ export const Group = ({ !initiatedGetMembers.current && selectedGroup?.groupId && secretKey && - admins.includes(myAddress) + admins.includes(myAddress) && selectedGroup?.groupId !== '0' ) { // getAdmins(selectedGroup?.groupId); getMembers(selectedGroup?.groupId); @@ -1441,7 +1450,8 @@ export const Group = ({ const findGroup = groups?.find((group) => +group?.groupId === +groupId); if (findGroup?.groupId === selectedGroup?.groupId) { isLoadingOpenSectionFromNotification.current = false; - + setChatMode("groups"); + setMobileViewMode('group') return; } if (findGroup) { @@ -1475,6 +1485,7 @@ export const Group = ({ setTimeout(() => { setSelectedGroup(findGroup); + setLastEnteredGroupIdAtom(findGroup?.groupId) setMobileViewMode("group"); setDesktopSideView('groups') setDesktopViewMode('home') @@ -1525,6 +1536,8 @@ export const Group = ({ setTimeout(() => { setSelectedGroup(findGroup); + setLastEnteredGroupIdAtom(findGroup?.groupId) + setMobileViewMode("group"); setDesktopSideView('groups') setDesktopViewMode('home') @@ -1582,6 +1595,8 @@ export const Group = ({ setTimeout(() => { setSelectedGroup(findGroup); + setLastEnteredGroupIdAtom(findGroup?.groupId) + setMobileViewMode("group"); setDesktopSideView('groups') setDesktopViewMode('home') @@ -1713,6 +1728,7 @@ export const Group = ({ borderRadius: !isMobile && '0px 15px 15px 0px' }} > + {isMobile && ( { setSelectedGroup(group); + setLastEnteredGroupIdAtom(group?.groupId) + // getTimestampEnterChat(); }, 200); @@ -2054,7 +2072,7 @@ export const Group = ({ {chatMode === "groups" && ( - { - setOpenAddGroup(true); - }} - > - + { + setOpenAddGroup(true); }} - /> - Group Mgmt - + > + + Group Mgmt + + { + setIsOpenBlockedUserModal(true); + }} + sx={{ + minWidth: 'unset', + padding: '10px' + }} + > + + + )} {chatMode === "directs" && ( + - {selectedGroup?.groupName} + {selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName} +
- +
)} )} - + {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})`} - - - - - {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 fef5862..02a26ec 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 }) => { +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,30 +113,38 @@ 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 ( @@ -185,6 +196,9 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get getTimestampEnterChat() setGroupSection("announcement") setOpenManageMembers(true) + if(!isMobile){ + setDesktopViewMode('chat') + } setTimeout(() => { executeEvent("openGroupJoinRequest", {}); @@ -225,6 +239,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get +
); }; diff --git a/src/components/Group/Home.tsx b/src/components/Group/Home.tsx index 4154fc6..c5bb66b 100644 --- a/src/components/Group/Home.tsx +++ b/src/components/Group/Home.tsx @@ -1,4 +1,4 @@ -import { Box, Button, ButtonBase, Typography } from "@mui/material"; +import { Box, Button, ButtonBase, Divider, Typography } from "@mui/material"; import React, { useContext } from "react"; import { Spacer } from "../../common/Spacer"; import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched"; @@ -10,8 +10,13 @@ import { ListOfGroupPromotions } from "./ListOfGroupPromotions"; import HelpIcon from '@mui/icons-material/Help'; import { useHandleTutorials } from "../Tutorials/useHandleTutorials"; import { GlobalContext } from "../../App"; +import { QortPrice } from "../Home/QortPrice"; +import { QMailMessages } from "./QMailMessages"; +import { Explore } from "../Explore/Explore"; +import ExploreIcon from "@mui/icons-material/Explore"; export const Home = ({ + name, refreshHomeDataFunc, myAddress, isLoadingGroups, @@ -27,6 +32,30 @@ export const Home = ({ }) => { const { showTutorial } = useContext(GlobalContext); + const [checked1, setChecked1] = React.useState(false); + const [checked2, setChecked2] = React.useState(false); + React.useEffect(() => { + if (balance && +balance >= 6) { + setChecked1(true); + } + }, [balance]); + + + React.useEffect(() => { + if (name) setChecked2(true); + }, [name]); + + + const isLoaded = React.useMemo(()=> { + if(userInfo !== null) return true + return false + }, [ userInfo]) + + const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> { + if(isLoaded && checked1 && checked2) return true + return false + }, [checked1, isLoaded, checked2]) + return ( item?.groupId !== "0").length !== 0 + } userInfo={userInfo} /> - - + {/* */} + + {hasDoneNameAndBalanceAndIsLoaded && ( + <> + + + + + + + + {" "} + + Explore + {" "} + + + + + + )} )} - {!isLoadingGroups && ( - - )} +
); diff --git a/src/components/Group/ListOfGroupPromotions.tsx b/src/components/Group/ListOfGroupPromotions.tsx index d5750b3..8389785 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,8 +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; @@ -82,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); @@ -96,18 +101,16 @@ export const ListOfGroupPromotions = () => { const { show, setTxList } = useContext(MyContext); const listRef = useRef(); - const rowVirtualizer = useVirtualizer({ - count: promotions.length, - getItemKey: React.useCallback( - (index) => promotions[index]?.identifier, - [promotions] - ), - getScrollElement: () => listRef.current, - estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed - overscan: 10, // Number of items to render outside the visible area to improve smoothness - }); - - + const rowVirtualizer = useVirtualizer({ + count: promotions.length, + getItemKey: React.useCallback( + (index) => promotions[index]?.identifier, + [promotions] + ), + getScrollElement: () => listRef.current, + estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed + overscan: 10, // Number of items to render outside the visible area to improve smoothness + }); useEffect(() => { try { @@ -119,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, { @@ -170,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); @@ -179,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]); @@ -330,8 +336,6 @@ export const ListOfGroupPromotions = () => { } }; - - return ( { display: "flex", flexDirection: "column", alignItems: "center", - marginTop: "25px", }} > - - setIsExpanded((prev) => !prev)} > - Group Promotions + Group promotions {promotions.length > 0 && ` (${promotions.length})`} - - - - - - - {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 - - - - - - - - - {promotion?.name?.charAt(0)} - - - {promotion?.name} - - - - - - - {promotion?.groupName} - - - - {promotion?.isOpen === false && ( - - )} - {promotion?.isOpen === true && ( - - )} - - {promotion?.isOpen ? 'Public group' : 'Private group' } - - - - - {promotion?.data} - - - - - - - + + 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} + + + + + + +
- ); })}
- -
+
+ + {isShowModal && ( @@ -747,6 +797,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 8e067a7..6fbf270 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'; @@ -13,7 +13,12 @@ import MailIcon from '@mui/icons-material/Mail'; import MailOutlineIcon from '@mui/icons-material/MailOutline'; import { executeEvent } from '../../utils/events'; import { CustomLoader } from '../../common/CustomLoader'; -const isLessThanOneWeekOld = (timestamp) => { +import { useRecoilState } from 'recoil'; +import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import MarkEmailUnreadIcon from '@mui/icons-material/MarkEmailUnread'; +export const isLessThanOneWeekOld = (timestamp) => { // Current time in milliseconds const now = Date.now(); @@ -39,8 +44,9 @@ export function formatEmailDate(timestamp: number) { } } export const QMailMessages = ({userName, userAddress}) => { - const [mails, setMails] = useState([]) - const [lastEnteredTimestamp, setLastEnteredTimestamp] = useState(null) + const [isExpanded, setIsExpanded] = useState(false) + const [mails, setMails] = useRecoilState(mailsAtom) + const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom) const [loading, setLoading] = useState(true) const getMails = useCallback(async () => { @@ -97,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 ? : ( + + )} + + { onClick={()=> { executeEvent("addTab", { data: { service: 'APP', name: 'q-mail' } }); executeEvent("open-apps-mode", { }); + setLastEnteredTimestamp(Date.now()) + }} > { - ): lastEnteredTimestamp < mail?.created ? ( + ): (lastEnteredTimestamp < mail?.created) && isLessThanOneWeekOld(mail?.created) ? ( @@ -243,6 +275,7 @@ export const QMailMessages = ({userName, userAddress}) => { + ) } diff --git a/src/components/Group/ThingsToDoInitial.tsx b/src/components/Group/ThingsToDoInitial.tsx index 9ec493d..c810ebc 100644 --- a/src/components/Group/ThingsToDoInitial.tsx +++ b/src/components/Group/ThingsToDoInitial.tsx @@ -59,9 +59,7 @@ return false }, [checked1, isLoaded, checked2]) if(hasDoneNameAndBalanceAndIsLoaded){ -return ( - -); +return null } return ( diff --git a/src/components/Group/WebsocketActive.tsx b/src/components/Group/WebsocketActive.tsx index 18941a4..ec113c4 100644 --- a/src/components/Group/WebsocketActive.tsx +++ b/src/components/Group/WebsocketActive.tsx @@ -79,7 +79,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/RegisterName.tsx b/src/components/RegisterName.tsx new file mode 100644 index 0000000..35af458 --- /dev/null +++ b/src/components/RegisterName.tsx @@ -0,0 +1,312 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react' +import { + Avatar, + Box, + Button, + ButtonBase, + Collapse, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Input, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemIcon, + ListItemText, + List, + MenuItem, + Popover, + Select, + TextField, + Typography, + } from "@mui/material"; +import { Label } from './Group/AddGroup'; +import { Spacer } from '../common/Spacer'; +import { LoadingButton } from '@mui/lab'; +import { getBaseApiReact, MyContext } from '../App'; +import { getFee } from '../background'; +import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked'; +import { subscribeToEvent, unsubscribeFromEvent } from '../utils/events'; +import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner'; +import CheckIcon from '@mui/icons-material/Check'; +import ErrorIcon from '@mui/icons-material/Error'; + +enum Availability { + NULL = 'null', + LOADING = 'loading', + AVAILABLE = 'available', + NOT_AVAILABLE = 'not-available' +} +export const RegisterName = ({setOpenSnack, setInfoSnack, userInfo, show, setTxList, balance}) => { + const [isOpen, setIsOpen] = useState(false) + const [registerNameValue, setRegisterNameValue] = useState('') + const [isLoadingRegisterName, setIsLoadingRegisterName] = useState(false) + const [isNameAvailable, setIsNameAvailable] = useState(Availability.NULL) + const [nameFee, setNameFee] = useState(null) + + const checkIfNameExisits = async (name)=> { + if(!name?.trim()){ + setIsNameAvailable(Availability.NULL) + + return + } + setIsNameAvailable(Availability.LOADING) + try { + const res = await fetch(`${getBaseApiReact()}/names/` + name); + const data = await res.json() + if(data?.message === 'name unknown'){ + setIsNameAvailable(Availability.AVAILABLE) + } else { + setIsNameAvailable(Availability.NOT_AVAILABLE) + } + } catch (error) { + console.error(error) + } finally { + } + } + // Debounce logic + useEffect(() => { + const handler = setTimeout(() => { + checkIfNameExisits(registerNameValue); + }, 500); + + // Cleanup timeout if searchValue changes before the timeout completes + return () => { + clearTimeout(handler); + }; + }, [registerNameValue]); + + const openRegisterNameFunc = useCallback((e) => { + setIsOpen(true) + + }, [ setIsOpen]); + + useEffect(() => { + subscribeToEvent("openRegisterName", openRegisterNameFunc); + + return () => { + unsubscribeFromEvent("openRegisterName", openRegisterNameFunc); + }; + }, [openRegisterNameFunc]); + + useEffect(()=> { + const nameRegistrationFee = async ()=> { + try { + const fee = await getFee("REGISTER_NAME"); + setNameFee(fee?.fee) + } catch (error) { + console.error(error) + } + } + nameRegistrationFee() + }, []) + + const registerName = async () => { + try { + if (!userInfo?.address) throw new Error("Your address was not found"); + if(!registerNameValue) throw new Error('Enter a name') + + const fee = await getFee("REGISTER_NAME"); + await show({ + message: "Would you like to register this name?", + publishFee: fee.fee + " QORT", + }); + setIsLoadingRegisterName(true); + new Promise((res, rej) => { + window + .sendMessage("registerName", { + name: registerNameValue, + }) + .then((response) => { + if (!response?.error) { + res(response); + setIsLoadingRegisterName(false); + setInfoSnack({ + type: "success", + message: + "Successfully registered. It may take a couple of minutes for the changes to propagate", + }); + setIsOpen(false); + setRegisterNameValue(""); + setOpenSnack(true); + setTxList((prev) => [ + { + ...response, + type: "register-name", + label: `Registered name: awaiting confirmation. This may take a couple minutes.`, + labelDone: `Registered name: success!`, + done: false, + }, + ...prev.filter((item) => !item.done), + ]); + return; + } + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error); + }) + .catch((error) => { + setInfoSnack({ + type: "error", + message: error.message || "An error occurred", + }); + setOpenSnack(true); + rej(error); + }); + }); + } catch (error) { + if (error?.message) { + setOpenSnack(true) + setInfoSnack({ + type: "error", + message: error?.message, + }); + } + } finally { + setIsLoadingRegisterName(false); + } + }; + + return ( + + + {"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 dc1d61e..7c623dc 100644 --- a/src/components/Snackbar/Snackbar.tsx +++ b/src/components/Snackbar/Snackbar.tsx @@ -24,7 +24,7 @@ export const CustomizedSnackbars = ({open, setOpen, info, setInfo, duration}) =
+ }} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={info?.duration === null ? null : (duration || 6000)} onClose={handleClose}> { + 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 8758c99..8dde696 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); @@ -119,8 +121,78 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => { > 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 2b89ed3..990b128 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -41,6 +41,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 1dd59a4..556f2de 100644 --- a/src/qortalRequests.ts +++ b/src/qortalRequests.ts @@ -1,6 +1,6 @@ import { gateways, getApiKeyFromStorage } from "./background"; import { listOfAllQortalRequests } from "./components/Apps/useQortalMessageListener"; -import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createGroupRequest, createPoll, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, openNewTab, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll } from "./qortalRequests/get"; +import { addForeignServer, addGroupAdminRequest, addListItems, adminAction, banFromGroupRequest, cancelGroupBanRequest, cancelGroupInviteRequest, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createGroupRequest, createPoll, decryptAESGCMRequest, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteHostedData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getHostedData, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getUserWalletTransactions, getWalletBalance, inviteToGroupRequest, joinGroup, kickFromGroupRequest, leaveGroupRequest, openNewTab, publishMultipleQDNResources, publishQDNResource, registerNameRequest, removeForeignServer, removeGroupAdminRequest, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, signTransaction, updateForeignFee, updateNameRequest, voteOnPoll } from "./qortalRequests/get"; import { getData, storeData } from "./utils/chromeStorage"; @@ -1103,6 +1103,25 @@ export const isRunningGateway = async ()=> { } break; } + case "GET_USER_WALLET_TRANSACTIONS": { + try { + const res = await getUserWalletTransactions(request.payload, isFromExtension, appInfo); + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + payload: res, + type: "backgroundMessageResponse", + }, event.origin); + } catch (error) { + event.source.postMessage({ + requestId: request.requestId, + action: request.action, + error: error.message, + type: "backgroundMessageResponse", + }, event.origin); + } + break; + } default: break; } diff --git a/src/qortalRequests/get.ts b/src/qortalRequests/get.ts index 049453c..886b450 100644 --- a/src/qortalRequests/get.ts +++ b/src/qortalRequests/get.ts @@ -4323,4 +4323,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 {