homepage, block, registername, userlookup, tripple tap

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

View File

@ -117,9 +117,13 @@ import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil";
import {
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<any>(null);
const [authenticatedMode, setAuthenticatedMode] = useState("qort");
const [requestAuthentication, setRequestAuthentication] = useState<any>(null);
const [isOpenDrawerLookup, setIsOpenDrawerLookup] = useState(false)
const [userInfo, setUserInfo] = useState<any>(null);
const [balance, setBalance] = useState<any>(null);
const [ltcBalance, setLtcBalance] = useState<any>(null);
@ -418,6 +428,9 @@ function App() {
const holdRefExtState = useRef<extStates>("not-authenticated");
const isFocusedRef = useRef<boolean>(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
}}
>
<Box
@ -1835,7 +1902,7 @@ function App() {
display: "flex",
flexDirection: "column",
alignItems: "center",
zIndex: 6,
zIndex: 10000,
}}
>
<Spacer height="22px" />
@ -2885,7 +2952,7 @@ await showInfo({
display: "flex",
flexDirection: "column",
alignItems: "center",
zIndex: 6,
zIndex: 10000,
}}
>
<Spacer height="48px" />
@ -2985,6 +3052,9 @@ await showInfo({
open={isShow}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
sx={{
zIndex: 10001
}}
>
<DialogTitle id="alert-dialog-title">{message.paymentFee ? "Payment" : "Publish"}</DialogTitle>
<DialogContent>
@ -3059,7 +3129,7 @@ await showInfo({
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{"Warning"}</DialogTitle>
<DialogTitle id="alert-dialog-title">{"LOGOUT"}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{messageUnsavedChanges.message}
@ -3409,6 +3479,9 @@ await showInfo({
>
{renderProfile()}
</DrawerComponent>
<UserLookup isOpenDrawerLookup={isOpenDrawerLookup} setIsOpenDrawerLookup={setIsOpenDrawerLookup} />
<RegisterName balance={balance} show={show} setTxList={setTxList} userInfo={userInfo} setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack}/>
<BuyQortInformation balance={balance} />
</GlobalContext.Provider>
{extState === "create-wallet" && walletToBeDownloaded && (
<ButtonBase onClick={()=> {

View File

@ -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<boolean | null>(null);
const [hasLocalNode, setHasLocalNode] = useState<boolean | null>(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 <TextItalic sx={{
fontSize: '18px'
}}>YOUR</TextItalic> <br></br>
WELCOME TO
<TextSpan sx={{
fontSize: '18px'
}}> QORTAL WALLET</TextSpan>
fontSize: '16px'
}}> QORTAL</TextSpan>
</TextP>
<Spacer height="30px" />
<Box
sx={{
@ -366,9 +413,15 @@ export const NotAuthenticated = ({
alignItems: "center",
}}
>
<CustomButton onClick={() => setExtstate("wallets")}>
Wallets
<CustomButton onClick={()=> setExtstate('wallets')}>
{/* <input {...getInputProps()} /> */}
Accounts
</CustomButton>
{/* <Tooltip title="Authenticate by importing your Qortal JSON file" arrow>
<img src={Info} />
</Tooltip> */}
</Box>
<Spacer height="6px" />
@ -377,8 +430,10 @@ export const NotAuthenticated = ({
display: "flex",
gap: "10px",
alignItems: "center",
}}
>
<CustomButton
onClick={() => {
setExtstate("create-wallet");
@ -392,8 +447,10 @@ export const NotAuthenticated = ({
}
}}
>
Create wallet
Create account
</CustomButton>
</Box>
<Spacer height="15px" />
@ -432,6 +489,12 @@ export const NotAuthenticated = ({
}}
>
<FormControlLabel
sx={{
"& .MuiFormControlLabel-label": {
fontSize: '14px'
}
}}
control={
<Switch
sx={{
@ -677,7 +740,7 @@ export const NotAuthenticated = ({
...(customNodes || []),
].filter((item) => item?.url !== node?.url);
saveCustomNodes(nodesToSave);
saveCustomNodes(nodesToSave, true);
}}
variant="contained"
>
@ -750,7 +813,7 @@ export const NotAuthenticated = ({
<Button
variant="contained"
disabled={!customApikey || !url}
disabled={!url}
onClick={() => saveCustomNodes(customNodes)}
autoFocus
>
@ -761,8 +824,8 @@ export const NotAuthenticated = ({
</DialogActions>
</Dialog>
)}
<ButtonBase onClick={()=> {
showTutorial('create-account', true)
<ButtonBase onClick={()=> {
showTutorial('create-account', true)
}} sx={{
position: 'fixed',
bottom: '25px',

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -102,6 +102,7 @@ import {
createRewardShareCase,
getRewardSharePrivateKeyCase,
removeRewardShareCase,
listActionsCase,
} from "./background-cases";
import { getData, removeKeysAndLogout, storeData } from "./utils/chromeStorage";
import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch';
@ -778,7 +779,7 @@ export async function getNameInfoForOthers(address) {
}
}
async function getAddressInfo(address) {
export async function getAddressInfo(address) {
const validApi = await getBaseApi();
const response = await fetch(validApi + "/addresses/" + address);
const data = await response.json();
@ -3008,6 +3009,9 @@ function setupMessageListener() {
case "getEnteredQmailTimestamp":
getEnteredQmailTimestampCase(request, event);
break;
case "listActions":
listActionsCase(request, event);
break;
case "logout":
{
try {

View File

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

View File

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

View File

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

View File

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

View File

@ -297,7 +297,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
>
{mode !== "viewer" && !selectedTab && <Spacer height="30px" />}
{mode === "home" && (
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
<AppsHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
)}
<AppsLibrary
@ -326,6 +326,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
isSelected={tab?.tabId === selectedTab?.tabId}
app={tab}
ref={iframeRefs.current[tab.tabId]}
isDevMode={tab?.service ? false : true}
/>
);
})}
@ -333,7 +334,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
{isNewTabWindow && mode === "viewer" && (
<>
<Spacer height="30px" />
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
<AppsHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
</>
)}
{mode !== "viewer" && !selectedTab && <Spacer height="180px" />}

View File

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

View File

@ -132,10 +132,20 @@ export const AppsNavBar = ({appsMode}) => {
};
}, []);
const isSelectedAppPinned = !!sortablePinnedApps?.find(
(item) =>
item?.name === selectedTab?.name && item?.service === selectedTab?.service
);
const isSelectedAppPinned = useMemo(()=> {
if(selectedTab?.isPrivate){
return !!sortablePinnedApps?.find(
(item) =>
item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name && item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service && item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier
);
} else {
return !!sortablePinnedApps?.find(
(item) =>
item?.name === selectedTab?.name && item?.service === selectedTab?.service
);
}
}, [selectedTab,sortablePinnedApps])
return (
<AppsNavBarParent>
<AppsNavBarLeft>
@ -259,27 +269,54 @@ export const AppsNavBar = ({appsMode}) => {
onClick={() => {
if (!selectedTab) return;
setSortablePinnedApps((prev) => {
setSortablePinnedApps((prev) => {
let updatedApps;
if (isSelectedAppPinned) {
// Remove the selected app if it is pinned
updatedApps = prev.filter(
(item) =>
!(
item?.name === selectedTab?.name &&
item?.service === selectedTab?.service
)
);
if(selectedTab?.isPrivate){
updatedApps = prev.filter(
(item) =>
!(
item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name &&
item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service &&
item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier
)
);
} else {
updatedApps = prev.filter(
(item) =>
!(
item?.name === selectedTab?.name &&
item?.service === selectedTab?.service
)
);
}
} else {
// Add the selected app if it is not pinned
updatedApps = [
if(selectedTab?.isPrivate){
updatedApps = [
...prev,
{
name: selectedTab?.name,
service: selectedTab?.service,
isPreview: true,
isPrivate: true,
privateAppProperties: {
...(selectedTab?.privateAppProperties || {})
}
},
];
} else {
updatedApps = [
...prev,
{
name: selectedTab?.name,
service: selectedTab?.service,
},
];
}
}
saveToLocalStorage(
@ -320,9 +357,15 @@ export const AppsNavBar = ({appsMode}) => {
</MenuItem>
<MenuItem
onClick={() => {
executeEvent("refreshApp", {
tabId: selectedTab?.tabId,
});
if (selectedTab?.refreshFunc) {
selectedTab.refreshFunc(selectedTab?.tabId);
} else {
executeEvent("refreshApp", {
tabId: selectedTab?.tabId,
});
}
handleClose();
}}
>
@ -350,6 +393,7 @@ export const AppsNavBar = ({appsMode}) => {
primary="Refresh"
/>
</MenuItem>
{!selectedTab?.isPrivate && (
<MenuItem
onClick={() => {
executeEvent("copyLink", {
@ -382,6 +426,7 @@ export const AppsNavBar = ({appsMode}) => {
primary="Copy link"
/>
</MenuItem>
)}
</Menu>
</AppsNavBarParent>
);

View File

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

View File

@ -1,115 +1,173 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { 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 (
<ContextMenuPinnedApps app={app} isMine={!!app?.isMine}>
<ButtonBase
ref={setNodeRef} {...attributes} {...listeners}
sx={{
height: "80px",
width: "60px",
transform: CSS.Transform.toString(transform),
transition,
}}
onClick={()=> {
executeEvent("addTab", {
data: app
})
}}
>
<AppCircleContainer sx={{
border: "none",
gap: isDesktop ? '10px': '5px'
}}>
<AppCircle
sx={{
border: "none",
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",
};
return (
<ContextMenuPinnedApps app={app} isMine={!!app?.isMine}>
<ButtonBase
ref={setNodeRef}
{...attributes}
{...listeners}
sx={{
height: "80px",
width: "60px",
transform: CSS.Transform.toString(transform),
transition,
}}
onClick={async () => {
if (app?.isPrivate) {
try {
await openApp(app?.privateAppProperties);
} catch (error) {
console.error(error);
}
} else {
executeEvent("addTab", {
data: app,
});
}
}}
>
<AppCircleContainer
sx={{
border: "none",
gap: isDesktop ? "10px" : "5px",
}}
>
<AppCircle
sx={{
border: "none",
}}
>
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
<LockIcon
sx={{
height: "31px",
width: "31px",
}}
/>
) : (
<Avatar
sx={{
height: "31px",
width: "31px",
"& img": {
objectFit: "fill",
},
}}
alt={app?.metadata?.title || app?.name}
src={
app?.privateAppProperties?.logo
? app?.privateAppProperties?.logo
: `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`
}
>
<img
style={{
width: "31px",
height: "auto",
}}
>
<Avatar
sx={{
height: "31px",
width: "31px",
'& img': {
objectFit: 'fill',
}
}}
alt={app?.metadata?.title || app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: "31px",
height: "auto",
}}
// src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
<AppCircleLabel>
{app?.metadata?.title || app?.name}
</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
</ContextMenuPinnedApps>
);
// src={LogoSelected}
alt="center-icon"
/>
</Avatar>
)}
</AppCircle>
{app?.isPrivate ? (
<AppCircleLabel>
{`${app?.privateAppProperties?.appName || "Private"}`}
</AppCircleLabel>
) : (
<AppCircleLabel>{app?.metadata?.title || app?.name}</AppCircleLabel>
)}
</AppCircleContainer>
</ButtonBase>
</ContextMenuPinnedApps>
);
};
export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps = [] }) => {
export const SortablePinnedApps = ({
isDesktop,
myWebsite,
myApp,
availableQapps = [],
}) => {
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom);
const 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 (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={transformPinnedApps.map((app) => `${app?.service}-${app?.name}`)}
>
<SortableContext items={transformPinnedApps.map((app) => `${app?.service}-${app?.name}`)}>
{transformPinnedApps.map((app) => (
<SortableItem
isDesktop={isDesktop}
key={`${app?.service}-${app?.name}`}
id={`${app?.service}-${app?.name}`}
name={app?.name}
app={app}
/>
))}
</SortableContext>
</DndContext>
{transformPinnedApps.map((app) => (
<SortableItem
isDesktop={isDesktop}
key={`${app?.service}-${app?.name}`}
id={`${app?.service}-${app?.name}`}
name={app?.name}
app={app}
/>
))}
</SortableContext>
</DndContext>
);
};

View File

@ -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}/>
) }
<Avatar
sx={{
height: "28px",
width: "28px",
}}
alt={app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: "28px",
height: "auto",
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
<LockIcon
sx={{
height: "28px",
width: "28px",
}}
/>
) : (
<Avatar
sx={{
height: "28px",
width: "28px",
}}
alt={app?.name}
src={app?.privateAppProperties?.logo ? app?.privateAppProperties?.logo :`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: "28px",
height: "auto",
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
)}
</TabParent>
</ButtonBase>
)

View File

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

View File

@ -221,7 +221,7 @@ const UIQortalRequests = [
'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE',
'GET_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'
];

View File

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

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import React, { useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { CreateCommonSecret } from './CreateCommonSecret'
import { 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);
}

View File

@ -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 `<a href="${DOMPurify.sanitize(href)}" class="auto-link">${DOMPurify.sanitize(url)}</a>`;
});
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 `<a href="${DOMPurify.sanitize(href)}" class="auto-link">${DOMPurify.sanitize(url)}</a>`;
});
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(/<span[^>]*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(/<span[^>]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, '');
}, [htmlContent])
const handleClick = async (e) => {
e.preventDefault();

View File

@ -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 (
<>
<MessageWragger lastMessage={lastSignature === message?.signature} isLast={isLast} onSeen={onSeenFunc}>
{message?.divide && (
<div className="unread-divider" id="unread-divider-id">
Unread messages below
</div>
)}
<div
ref={lastSignature === message?.signature ? ref : null}
style={{
padding: "10px",
backgroundColor: "#232428",
@ -147,9 +186,7 @@ export const MessageItem = ({
color: "white",
}}
alt={message?.senderName}
src={message?.senderName ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
message?.senderName
}/qortal_avatar?async=true` : ''}
src={userAvatarUrl}
>
{message?.senderName?.charAt(0)}
</Avatar>
@ -176,7 +213,7 @@ export const MessageItem = ({
visibility: userInfo?.level !== undefined ? 'visible' : 'hidden',
width: '30px',
height: 'auto'
}} src={getBadgeImg(userInfo?.level)} />
}} src={getBadgeImg(userInfo)} />
</Tooltip>
</div>
</ClickAwayListener>
@ -285,13 +322,7 @@ export const MessageItem = ({
}}>Replied to {reply?.senderName || reply?.senderAddress}</Typography>
{reply?.messageText && (
<MessageDisplay
htmlContent={generateHTML(reply?.messageText, [
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])}
htmlContent={htmlReply}
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
/>
)}
@ -306,13 +337,7 @@ export const MessageItem = ({
)}
{message?.messageText && (
<MessageDisplay
htmlContent={generateHTML(message?.messageText, [
StarterKit,
Underline,
Highlight,
Mention,
TextStyle
])}
htmlContent={htmlText}
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
/>
)}
@ -485,21 +510,11 @@ export const MessageItem = ({
</Box>
</Box>
{/* <Message
model={{
direction: 'incoming',
message: message.text,
position: 'single',
sender: message.senderName,
sentTime: message.timestamp
}}
></Message> */}
{/* {!message.unread && <span style={{ color: 'green' }}> Seen</span>} */}
</div>
</>
</MessageWragger>
);
};
});
export const ReplyPreview = ({message, isEdit})=> {
@ -557,4 +572,37 @@ export const ReplyPreview = ({message, isEdit})=> {
</Box>
</Box>
)
}
const MessageWragger = ({lastMessage, onSeen, isLast, children})=> {
if(lastMessage){
return (
<WatchComponent onSeen={onSeen} isLast={isLast}>{children}</WatchComponent>
)
}
return children
}
const WatchComponent = ({onSeen, isLast, children})=> {
const { ref, inView } = useInView({
threshold: 0.7, // Fully visible
triggerOnce: true, // Only trigger once when it becomes visible
});
useEffect(() => {
if (inView && isLast && onSeen) {
onSeen();
}
}, [inView, isLast, onSeen]);
return <div ref={ref} style={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}>
{children}
</div>
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,8 @@ import React, {
useRef,
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 && (
<Box
sx={{
@ -1978,6 +1994,8 @@ export const Group = ({
setIsOpenDrawer(false);
setTimeout(() => {
setSelectedGroup(group);
setLastEnteredGroupIdAtom(group?.groupId)
// getTimestampEnterChat();
}, 200);
@ -2054,7 +2072,7 @@ export const Group = ({
</ListItemAvatar>
<ListItemText
primary={group.groupName}
primary={group.groupId === '0' ? 'General' : group.groupName}
secondary={!group?.timestamp ? 'no messages' :`last message: ${formatEmailDate(group?.timestamp)}`}
primaryTypographyProps={{
style: {
@ -2113,21 +2131,39 @@ export const Group = ({
width: "100%",
justifyContent: "center",
padding: "10px",
gap: '10px'
}}
>
{chatMode === "groups" && (
<CustomButton
onClick={() => {
setOpenAddGroup(true);
}}
>
<AddCircleOutlineIcon
sx={{
color: "white",
<>
<CustomButton
onClick={() => {
setOpenAddGroup(true);
}}
/>
Group Mgmt
</CustomButton>
>
<AddCircleOutlineIcon
sx={{
color: "white",
}}
/>
Group Mgmt
</CustomButton>
<CustomButton
onClick={() => {
setIsOpenBlockedUserModal(true);
}}
sx={{
minWidth: 'unset',
padding: '10px'
}}
>
<BlockIcon
sx={{
color: "white",
}}
/>
</CustomButton>
</>
)}
{chatMode === "directs" && (
<CustomButton
@ -2157,6 +2193,7 @@ export const Group = ({
myAddress={myAddress}
setIsLoadingGroups={setIsLoadingGroups}
/>
<GlobalTouchMenu />
<CustomizedSnackbars
open={openSnack}
setOpen={setOpenSnack}
@ -2383,7 +2420,7 @@ export const Group = ({
fontWeight: 600,
}}
>
{selectedGroup?.groupName}
{selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName}
</Typography>
<Box
sx={{
@ -2410,7 +2447,9 @@ export const Group = ({
)}
{isMobile && mobileViewMode === "group" && (
<>
<div style={{
visibility: selectedGroup?.groupId === '0' ? 'hidden' : 'visibile'
}}>
<GroupMenu
setGroupSection={setGroupSection}
groupSection={groupSection}
@ -2420,7 +2459,7 @@ export const Group = ({
hasUnreadAnnouncements={isUnread}
hasUnreadChat={isUnreadChat}
/>
</>
</div>
)}
<Box
sx={{
@ -2610,7 +2649,11 @@ export const Group = ({
)}
</>
)}
{isOpenBlockedUserModal && (
<BlockedUsersModal close={()=> {
setIsOpenBlockedUserModal(false)
}} />
)}
{selectedDirect && !newChat && (
<>
<Box
@ -2690,6 +2733,7 @@ export const Group = ({
)}
{isMobile && mobileViewMode === "home" && (
<Home
name={userInfo?.name}
refreshHomeDataFunc={refreshHomeDataFunc}
myAddress={myAddress}
isLoadingGroups={isLoadingGroups}

View File

@ -10,16 +10,20 @@ import CommentIcon from "@mui/icons-material/Comment";
import InfoIcon from "@mui/icons-material/Info";
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 { getGroupNames } from "./UserListOfInvites";
import { CustomLoader } from "../../common/CustomLoader";
import { getBaseApiReact, isMobile } from "../../App";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
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",
}}
>
<Box
<ButtonBase
sx={{
width: "322px",
width: "100%",
display: "flex",
flexDirection: "column",
flexDirection: "row",
padding: "0px 20px",
gap: '10px',
justifyContent: 'flex-start',
marginBottom: '5px'
}}
onClick={()=> setIsExpanded((prev)=> !prev)}
>
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
fontSize: "1rem",
}}
>
Group Invites:
Group Invites {groupsWithJoinRequests?.length > 0 && ` (${groupsWithJoinRequests?.length})`}
</Typography>
<Spacer height="10px" />
</Box>
<Box
sx={{
width: "322px",
height: isMobile ? "165px" : "250px",
display: "flex",
flexDirection: "column",
bgcolor: "background.paper",
padding: "20px",
borderRadius: "19px",
}}
>
{loading && groupsWithJoinRequests.length === 0 && (
<Box
{isExpanded ? <ExpandLessIcon sx={{
marginLeft: 'auto'
}} /> : (
<ExpandMoreIcon sx={{
marginLeft: 'auto'
}}/>
)}
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box
sx={{
width: "322px",
maxWidth: '100%',
display: "flex",
flexDirection: "column",
bgcolor: "background.paper",
padding: "20px",
borderRadius: "19px",
}}
>
{loading && groupsWithJoinRequests.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
<CustomLoader />
</Box>
)}
{!loading && groupsWithJoinRequests.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
>
<Typography
sx={{
fontSize: "11px",
fontWeight: 400,
color: "rgba(255, 255, 255, 0.2)",
}}
>
Nothing to display
</Typography>
</Box>
)}
<List
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
maxWidth: 360,
bgcolor: "background.paper",
maxHeight: "300px",
overflow: "auto",
}}
className="scrollable-container"
>
<CustomLoader />
</Box>
)}
{!loading && groupsWithJoinRequests.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: 'center',
height: '100%',
}}
>
<Typography
sx={{
fontSize: "11px",
fontWeight: 400,
color: 'rgba(255, 255, 255, 0.2)'
}}
>
Nothing to display
</Typography>
</Box>
)}
<List
sx={{
width: "100%",
maxWidth: 360,
bgcolor: "background.paper",
maxHeight: "300px",
overflow: "auto",
}}
>
{groupsWithJoinRequests?.map((group) => {
return (
<ListItem
sx={{
marginBottom: "20px",
}}
key={group?.groupId}
onClick={() => {
setOpenAddGroup(true);
setTimeout(() => {
executeEvent("openGroupInvitesRequest", {});
}, 300);
}}
disablePadding
secondaryAction={
<IconButton edge="end" aria-label="comments">
<GroupAddIcon
{groupsWithJoinRequests?.map((group) => {
return (
<ListItem
sx={{
marginBottom: "20px",
}}
key={group?.groupId}
onClick={() => {
setOpenAddGroup(true);
setTimeout(() => {
executeEvent("openGroupInvitesRequest", {});
}, 300);
}}
disablePadding
secondaryAction={
<IconButton edge="end" aria-label="comments">
<GroupAddIcon
sx={{
color: "white",
fontSize: "18px",
}}
/>
</IconButton>
}
>
<ListItemButton disableRipple role={undefined} dense>
<ListItemText
sx={{
color: "white",
fontSize: "18px",
"& .MuiTypography-root": {
fontSize: "13px",
fontWeight: 400,
},
}}
primary={`${group?.groupName} has invited you`}
/>
</IconButton>
}
>
<ListItemButton disableRipple role={undefined} dense>
<ListItemText
sx={{
"& .MuiTypography-root": {
fontSize: "13px",
fontWeight: 400,
},
}}
primary={`${group?.groupName} has invited you`}
/>
</ListItemButton>
</ListItem>
);
})}
</List>
</Box>
</ListItemButton>
</ListItem>
);
})}
</List>
</Box>
</Collapse>
</Box>
);
};

View File

@ -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'
}}>
<Box
<ButtonBase
sx={{
width: "322px",
width: "100%",
display: "flex",
flexDirection: "column",
flexDirection: "row",
padding: '0px 20px',
gap: '10px',
justifyContent: 'flex-start',
marginBottom: '5px'
}}
onClick={()=> setIsExpanded((prev)=> !prev)}
>
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
fontSize: "1rem",
}}
>
Join Requests:
Join Requests {filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length > 0 && ` (${filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length})`}
</Typography>
<Spacer height="10px" />
</Box>
{isExpanded ? <ExpandLessIcon sx={{
marginLeft: 'auto'
}} /> : (
<ExpandMoreIcon sx={{
marginLeft: 'auto'
}}/>
)}
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box
sx={{
width: "322px",
height: isMobile ? "165px" : "250px",
maxWidth: '100%',
display: "flex",
flexDirection: "column",
@ -173,7 +184,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
</Typography>
</Box>
)}
<List sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper", maxHeight: '300px', overflow: 'auto' }}>
<List className="scrollable-container" sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper", maxHeight: '300px', overflow: 'auto' }}>
{filteredJoinRequests?.map((group)=> {
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
</List>
</Box>
</Collapse>
</Box>
);
};

View File

@ -1,4 +1,4 @@
import { Box, Button, ButtonBase, Typography } from "@mui/material";
import { Box, Button, ButtonBase, Divider, Typography } from "@mui/material";
import React, { useContext } from "react";
import { 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 (
<Box
sx={{
@ -101,19 +130,27 @@ export const Home = ({
display: "flex",
gap: "15px",
flexWrap: "wrap",
justifyContent: "center",
alignItems: "center",
flexDirection: 'column',
width: '100%'
}}
>
<ThingsToDoInitial
balance={balance}
myAddress={myAddress}
name={userInfo?.name}
hasGroups={groups?.length !== 0}
hasGroups={
groups?.filter((item) => item?.groupId !== "0").length !== 0
}
userInfo={userInfo}
/>
<ListOfThreadPostsWatched />
{/* <ListOfThreadPostsWatched /> */}
<QortPrice />
{hasDoneNameAndBalanceAndIsLoaded && (
<>
<Spacer height="20px" />
<QMailMessages userAddress={userInfo?.address} userName={userInfo?.name} />
<GroupJoinRequests
setGroupSection={setGroupSection}
setSelectedGroup={setSelectedGroup}
@ -129,11 +166,43 @@ export const Home = ({
groups={groups}
setMobileViewMode={setMobileViewMode}
/>
<ListOfGroupPromotions />
<Divider
color="secondary"
sx={{
width: "100%",
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
}}
>
<ExploreIcon
sx={{
color: "white",
}}
/>{" "}
<Typography
sx={{
fontSize: "1rem",
}}
>
Explore
</Typography>{" "}
</Box>
</Divider>
<Explore setMobileViewMode={setMobileViewMode} />
</>
)}
</Box>
)}
{!isLoadingGroups && (
<ListOfGroupPromotions />
)}
<Spacer height="180px" />
</Box>
);

View File

@ -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 (
<Box
sx={{
@ -339,91 +343,124 @@ export const ListOfGroupPromotions = () => {
display: "flex",
flexDirection: "column",
alignItems: "center",
marginTop: "25px",
}}
>
<Box
<ButtonBase
sx={{
width: isMobile ? "320px" : "750px",
maxWidth: "90%",
width: "100%",
display: "flex",
flexDirection: "column",
flexDirection: "row",
padding: "0px 20px",
gap: '10px',
justifyContent: 'flex-start',
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
onClick={() => setIsExpanded((prev) => !prev)}
>
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
fontSize: "1rem",
}}
>
Group Promotions
Group promotions {promotions.length > 0 && ` (${promotions.length})`}
</Typography>
<Button
variant="contained"
onClick={() => setIsShowModal(true)}
sx={{
fontSize: "12px",
}}
>
Add Promotion
</Button>
</Box>
<Spacer height="10px" />
</Box>
<Box
sx={{
width: isMobile ? "320px" : "750px",
maxWidth: "90%",
maxHeight: "700px",
display: "flex",
flexDirection: "column",
bgcolor: "background.paper",
padding: "20px 0px",
borderRadius: "19px",
}}
>
{loading && promotions.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
<CustomLoader />
</Box>
)}
{!loading && promotions.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
>
<Typography
{isExpanded ? (
<ExpandLessIcon
sx={{
fontSize: "11px",
fontWeight: 400,
color: "rgba(255, 255, 255, 0.2)",
marginLeft: "auto",
}}
/>
) : (
<ExpandMoreIcon
sx={{
marginLeft: "auto",
}}
/>
)}
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<>
<Box
sx={{
width: '100%',
display: "flex",
flexDirection: "column",
padding: "0px 20px",
marginTop: '15px',
width: "322px",
maxWidth: '100%'
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
Nothing to display
</Typography>
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
}}
></Typography>
<Button
variant="contained"
onClick={() => setIsShowModal(true)}
sx={{
fontSize: "12px",
}}
>
Add Promotion
</Button>
</Box>
<Spacer height="10px" />
</Box>
)}
<Box
sx={{
width: isMobile ? "320px" : "750px",
maxHeight: "700px",
display: "flex",
flexDirection: "column",
bgcolor: "background.paper",
padding: "20px 0px",
borderRadius: "19px",
}}
>
{loading && promotions.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
<CustomLoader />
</Box>
)}
{!loading && promotions.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
}}
>
<Typography
sx={{
fontSize: "11px",
fontWeight: 400,
color: "rgba(255, 255, 255, 0.2)",
}}
>
Nothing to display
</Typography>
</Box>
)}
<div
style={{
height: "600px",
@ -462,7 +499,6 @@ export const ListOfGroupPromotions = () => {
const index = virtualRow.index;
const promotion = promotions[index];
return (
<div
data-index={virtualRow.index} //needed for dynamic row height measurement
ref={rowVirtualizer.measureElement} //measure dynamic row height
@ -481,237 +517,251 @@ export const ListOfGroupPromotions = () => {
gap: "5px",
}}
>
<ErrorBoundary
fallback={
<Typography>
Error loading content: Invalid Data
</Typography>
}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
padding: "0px 20px",
}}
>
<Popover
open={openPopoverIndex === promotion?.groupId}
anchorEl={popoverAnchor}
onClose={(event, reason) => {
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" }}
>
<Box
sx={{
width: "325px",
height: "auto",
maxHeight: "400px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
}}
>
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
}}
>
Group name: {` ${promotion?.groupName}`}
</Typography>
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
}}
>
Number of members: {` ${promotion?.memberCount}`}
</Typography>
{promotion?.description && (
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
}}
>
{promotion?.description}
</Typography>
)}
{promotion?.isOpen === false && (
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
}}
>
*This is a closed/private group, so you will need to wait
until an admin accepts your request
</Typography>
)}
<Spacer height="5px" />
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
width: "100%",
justifyContent: "center",
}}
>
<LoadingButton
loading={isLoadingJoinGroup}
loadingPosition="start"
variant="contained"
onClick={handlePopoverClose}
>
Close
</LoadingButton>
<LoadingButton
loading={isLoadingJoinGroup}
loadingPosition="start"
variant="contained"
onClick={() =>
handleJoinGroup(promotion, promotion?.isOpen)
}
>
Join
</LoadingButton>
</Box>
</Box>
</Popover>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "15px",
}}
>
<Avatar
sx={{
backgroundColor: "#27282c",
color: "white",
}}
alt={promotion?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
promotion?.name
}/qortal_avatar?async=true`}
>
{promotion?.name?.charAt(0)}
</Avatar>
<Typography
sx={{
fontWight: 600,
fontFamily: "Inter",
color: "cadetBlue",
}}
>
{promotion?.name}
</Typography>
</Box>
</Box>
<Spacer height="20px"/>
<Typography
sx={{
fontWight: 600,
fontFamily: "Inter",
color: "cadetBlue",
}}
>
{promotion?.groupName}
</Typography>
<Spacer height="20px" />
<Box sx={{
display: 'flex',
gap: '20px',
alignItems: 'center'
}}>
{promotion?.isOpen === false && (
<LockIcon sx={{
color: 'var(--green)'
}} />
)}
{promotion?.isOpen === true && (
<NoEncryptionGmailerrorredIcon sx={{
color: 'var(--danger)'
}} />
)}
<Typography
sx={{
fontSize: "15px",
fontWeight: 600,
}}
>
{promotion?.isOpen ? 'Public group' : 'Private group' }
</Typography>
</Box>
<Spacer height="20px" />
<Typography
sx={{
fontWight: 600,
fontFamily: "Inter",
color: "cadetBlue",
}}
>
{promotion?.data}
</Typography>
<Spacer height="20px" />
<Box
sx={{
display: "flex",
justifyContent: "center",
width: "100%",
}}
>
<Button
onClick={(event) => handlePopoverOpen(event, promotion?.groupId)}
sx={{
fontSize: "12px",
color: 'white'
}}
>
Join Group: {` ${promotion?.groupName}`}
</Button>
</Box>
</Box>
<Spacer height="50px" />
<ErrorBoundary
fallback={
<Typography>
Error loading content: Invalid Data
</Typography>
}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
padding: "0px 20px",
}}
>
<Popover
open={openPopoverIndex === promotion?.groupId}
anchorEl={popoverAnchor}
onClose={(event, reason) => {
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" }}
>
<Box
sx={{
width: "325px",
height: "auto",
maxHeight: "400px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
}}
>
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
}}
>
Group name: {` ${promotion?.groupName}`}
</Typography>
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
}}
>
Number of members:{" "}
{` ${promotion?.memberCount}`}
</Typography>
{promotion?.description && (
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
}}
>
{promotion?.description}
</Typography>
)}
{promotion?.isOpen === false && (
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
}}
>
*This is a closed/private group, so you
will need to wait until an admin accepts
your request
</Typography>
)}
<Spacer height="5px" />
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
width: "100%",
justifyContent: "center",
}}
>
<LoadingButton
loading={isLoadingJoinGroup}
loadingPosition="start"
variant="contained"
onClick={handlePopoverClose}
>
Close
</LoadingButton>
<LoadingButton
loading={isLoadingJoinGroup}
loadingPosition="start"
variant="contained"
onClick={() =>
handleJoinGroup(
promotion,
promotion?.isOpen
)
}
>
Join
</LoadingButton>
</Box>
</Box>
</Popover>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "15px",
}}
>
<Avatar
sx={{
backgroundColor: "#27282c",
color: "white",
}}
alt={promotion?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
promotion?.name
}/qortal_avatar?async=true`}
>
{promotion?.name?.charAt(0)}
</Avatar>
<Typography
sx={{
fontWight: 600,
fontFamily: "Inter",
color: "cadetBlue",
}}
>
{promotion?.name}
</Typography>
</Box>
<Typography
sx={{
fontWight: 600,
fontFamily: "Inter",
color: "cadetBlue",
}}
>
{promotion?.groupName}
</Typography>
</Box>
<Spacer height="20px" />
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
}}
>
{promotion?.isOpen === false && (
<LockIcon
sx={{
color: "var(--green)",
}}
/>
)}
{promotion?.isOpen === true && (
<NoEncryptionGmailerrorredIcon
sx={{
color: "var(--danger)",
}}
/>
)}
<Typography
sx={{
fontSize: "15px",
fontWeight: 600,
}}
>
{promotion?.isOpen
? "Public group"
: "Private group"}
</Typography>
</Box>
<Spacer height="20px" />
<Typography
sx={{
fontWight: 600,
fontFamily: "Inter",
color: "cadetBlue",
}}
>
{promotion?.data}
</Typography>
<Spacer height="20px" />
<Box
sx={{
display: "flex",
justifyContent: "center",
width: "100%",
}}
>
<Button
// variant="contained"
onClick={(event) =>
handlePopoverOpen(event, promotion?.groupId)
}
sx={{
fontSize: "12px",
color: "white",
}}
>
Join Group: {` ${promotion?.groupName}`}
</Button>
</Box>
</Box>
<Spacer height="50px" />
</ErrorBoundary>
</div>
);
})}
</div>
</div>
</div>
</div>
</Box>
</Box>
</>
</Collapse>
<Spacer height="20px" />
{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 (

View File

@ -1,11 +1,11 @@
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import List from "@mui/material/List";
import 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 (
<Box
@ -109,28 +124,43 @@ export const QMailMessages = ({userName, userAddress}) => {
}}
>
<Box
<ButtonBase
sx={{
width: "322px",
width: "100%",
display: "flex",
flexDirection: "column",
flexDirection: "row",
gap: '10px',
padding: "0px 20px",
justifyContent: 'flex-start',
marginBottom: '5px'
}}
onClick={()=> setIsExpanded((prev)=> !prev)}
>
<Typography
sx={{
fontSize: "13px",
fontWeight: 600,
fontSize: "1rem",
}}
>
Latest Q-Mails
</Typography>
<Spacer height="10px" />
</Box>
<MarkEmailUnreadIcon sx={{
color: anyUnread ? '--unread' : 'white'
}}/>
{isExpanded ? <ExpandLessIcon sx={{
marginLeft: 'auto'
}} /> : (
<ExpandMoreIcon sx={{
color: anyUnread ? '--unread' : 'white',
marginLeft: 'auto'
}} />
)}
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box
className="scrollable-container"
sx={{
width: "322px",
maxWidth: '100%',
height: isMobile ? "165px" : "250px",
display: "flex",
flexDirection: "column",
@ -186,6 +216,8 @@ export const QMailMessages = ({userName, userAddress}) => {
onClick={()=> {
executeEvent("addTab", { data: { service: 'APP', name: 'q-mail' } });
executeEvent("open-apps-mode", { });
setLastEnteredTimestamp(Date.now())
}}
>
<ListItemButton
@ -219,7 +251,7 @@ export const QMailMessages = ({userName, userAddress}) => {
<MailOutlineIcon sx={{
color: 'white'
}} />
): lastEnteredTimestamp < mail?.created ? (
): (lastEnteredTimestamp < mail?.created) && isLessThanOneWeekOld(mail?.created) ? (
<MailIcon sx={{
color: 'var(--unread)'
}} />
@ -243,6 +275,7 @@ export const QMailMessages = ({userName, userAddress}) => {
</Box>
</Collapse>
</Box>
)
}

View File

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

View File

@ -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'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
import React, { useState } from 'react';
import { 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
</Button>
<Button
variant="text"
onClick={() => {
executeEvent('openUserLookupDrawer', {
addressOrName: name || address
})
handleClose();
}}
sx={{
color: 'white',
justifyContent: 'flex-start'
}}
>
User lookup
</Button>
<BlockUser handleClose={handleClose} address={address} name={name} />
</Box>
</Popover>
</>
);
};
const BlockUser = ({address, name, handleClose})=> {
const [isAlreadyBlocked, setIsAlreadyBlocked] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const {isUserBlocked,
addToBlockList,
removeBlockFromList} = useContext(MyContext)
useEffect(()=> {
if(!address) return
setIsAlreadyBlocked(isUserBlocked(address, name))
}, [address, setIsAlreadyBlocked, isUserBlocked, name])
return (
<Button
variant="text"
onClick={async () => {
try {
setIsLoading(true)
if(isAlreadyBlocked === true){
await removeBlockFromList(address, name)
} else if(isAlreadyBlocked === false) {
await addToBlockList(address, name)
}
executeEvent('updateChatMessagesWithBlocks', true)
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
handleClose();
}
}}
sx={{
color: 'white',
justifyContent: 'flex-start',
gap: '10px'
}}
>
{(isAlreadyBlocked === null || isLoading) && (
<CircularProgress color="secondary" size={24} />
)}
{isAlreadyBlocked && (
'Unblock name'
)}
{isAlreadyBlocked === false && (
'Block name'
)}
</Button>
)
}

View File

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

View File

@ -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;
}

View File

@ -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");
}
};

View File

@ -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 {