mirror of
https://github.com/Qortal/qortal-mobile.git
synced 2025-03-14 11:52:33 +00:00
homepage, block, registername, userlookup, tripple tap
This commit is contained in:
parent
3a39bd5e22
commit
74cdd3e34d
85
src/App.tsx
85
src/App.tsx
@ -117,9 +117,13 @@ import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil";
|
||||
import {
|
||||
canSaveSettingToQdnAtom,
|
||||
fullScreenAtom,
|
||||
groupsPropertiesAtom,
|
||||
hasSettingsChangedAtom,
|
||||
isUsingImportExportSettingsAtom,
|
||||
lastEnteredGroupIdAtom,
|
||||
mailsAtom,
|
||||
oldPinnedAppsAtom,
|
||||
qMailLastEnteredTimestampAtom,
|
||||
settingsLocalLastUpdatedAtom,
|
||||
settingsQDNLastUpdatedAtom,
|
||||
sortablePinnedAppsAtom,
|
||||
@ -137,6 +141,10 @@ import { useHandleUserInfo } from "./components/Group/useHandleUserInfo";
|
||||
import { Minting } from "./components/Minting/Minting";
|
||||
import { isRunningGateway } from "./qortalRequests";
|
||||
import { GlobalActions } from "./components/GlobalActions/GlobalActions";
|
||||
import { useBlockedAddresses } from "./components/Chat/useBlockUsers";
|
||||
import { UserLookup } from "./components/UserLookup.tsx/UserLookup";
|
||||
import { RegisterName } from "./components/RegisterName";
|
||||
import { BuyQortInformation } from "./components/BuyQortInformation";
|
||||
|
||||
|
||||
type extStates =
|
||||
@ -381,6 +389,8 @@ function App() {
|
||||
const [requestBuyOrder, setRequestBuyOrder] = useState<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={()=> {
|
||||
|
@ -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',
|
||||
|
BIN
src/assets/Icons/q-trade-logo.webp
Normal file
BIN
src/assets/Icons/q-trade-logo.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
@ -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
@ -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 {
|
||||
|
10
src/common/Spinners/BarSpinner/BarSpinner.tsx
Normal file
10
src/common/Spinners/BarSpinner/BarSpinner.tsx
Normal 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>
|
||||
)
|
||||
}
|
19
src/common/Spinners/BarSpinner/barSpinner.css
Normal file
19
src/common/Spinners/BarSpinner/barSpinner.css
Normal 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% }
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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" />}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
550
src/components/Apps/AppsPrivate.tsx
Normal file
550
src/components/Apps/AppsPrivate.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
|
237
src/components/Apps/useHandlePrivateApps.tsx
Normal file
237
src/components/Apps/useHandlePrivateApps.tsx
Normal 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,
|
||||
};
|
||||
};
|
@ -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'
|
||||
];
|
||||
|
||||
|
||||
|
154
src/components/BuyQortInformation.tsx
Normal file
154
src/components/BuyQortInformation.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
||||
}
|
192
src/components/Chat/useBlockUsers.tsx
Normal file
192
src/components/Chat/useBlockUsers.tsx
Normal 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
|
||||
};
|
||||
};
|
22
src/components/Drawer/DrawerUserLookup.tsx
Normal file
22
src/components/Drawer/DrawerUserLookup.tsx
Normal 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>
|
||||
);
|
||||
}
|
102
src/components/Explore/Explore.tsx
Normal file
102
src/components/Explore/Explore.tsx
Normal 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>
|
||||
);
|
||||
};
|
117
src/components/GlobalTouchMenu.tsx
Normal file
117
src/components/GlobalTouchMenu.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
190
src/components/Group/BlockedUsersModal.tsx
Normal file
190
src/components/Group/BlockedUsersModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 (
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -59,9 +59,7 @@ return false
|
||||
}, [checked1, isLoaded, checked2])
|
||||
|
||||
if(hasDoneNameAndBalanceAndIsLoaded){
|
||||
return (
|
||||
<QMailMessages userAddress={userInfo?.address} userName={userInfo?.name} />
|
||||
);
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
93
src/components/Home/NewUsersCTA.tsx
Normal file
93
src/components/Home/NewUsersCTA.tsx
Normal 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>
|
||||
);
|
||||
};
|
257
src/components/Home/QortPrice.tsx
Normal file
257
src/components/Home/QortPrice.tsx
Normal 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>
|
||||
);
|
||||
};
|
312
src/components/RegisterName.tsx
Normal file
312
src/components/RegisterName.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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
|
||||
|
||||
|
||||
|
507
src/components/UserLookup.tsx/UserLookup.tsx
Normal file
507
src/components/UserLookup.tsx/UserLookup.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
)
|
||||
}
|
18
src/main.tsx
18
src/main.tsx
@ -41,6 +41,24 @@ const theme = createTheme({
|
||||
color: '#b0b0b0', // Lighter text for body2, often used for secondary text
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
".MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "white", // ⚪ Default outline color
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiSelect: {
|
||||
styleOverrides: {
|
||||
icon: {
|
||||
color: "white", // ✅ Caret (dropdown arrow) color
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
};
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user