Merge pull request #17 from nbenaglia/feature/css-qmail-page

Apply dark/light theme in chat pages
This commit is contained in:
Phillip 2025-04-20 12:58:35 +03:00 committed by GitHub
commit b965f0b317
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
115 changed files with 12368 additions and 11034 deletions

View File

@ -34,9 +34,9 @@ import ltcLogo from './assets/ltc.png';
import PersonSearchIcon from '@mui/icons-material/PersonSearch'; import PersonSearchIcon from '@mui/icons-material/PersonSearch';
import qortLogo from './assets/qort.png'; import qortLogo from './assets/qort.png';
import { CopyToClipboard } from 'react-copy-to-clipboard'; import { CopyToClipboard } from 'react-copy-to-clipboard';
import { Download } from './assets/svgs/Download.tsx'; import { Download } from './assets/Icons/Download.tsx';
import { Logout } from './assets/svgs/Logout.tsx'; import { Logout } from './assets/Icons/Logout.tsx';
import { Return } from './assets/svgs/Return.tsx'; import { Return } from './assets/Icons/Return.tsx';
import WarningIcon from '@mui/icons-material/Warning'; import WarningIcon from '@mui/icons-material/Warning';
import Success from './assets/svgs/Success.svg'; import Success from './assets/svgs/Success.svg';
import './utils/seedPhrase/RandomSentenceGenerator'; import './utils/seedPhrase/RandomSentenceGenerator';
@ -69,13 +69,11 @@ import { Spacer } from './common/Spacer';
import { Loader } from './components/Loader'; import { Loader } from './components/Loader';
import { PasswordField, ErrorText } from './components'; import { PasswordField, ErrorText } from './components';
import { Group, requestQueueMemberNames } from './components/Group/Group'; import { Group, requestQueueMemberNames } from './components/Group/Group';
import { TaskManager } from './components/TaskManager/TaskManger'; import { TaskManager } from './components/TaskManager/TaskManager.tsx';
import { useModal } from './common/useModal'; import { useModal } from './common/useModal';
import { Label } from './components/Group/AddGroup';
import { CustomizedSnackbars } from './components/Snackbar/Snackbar'; import { CustomizedSnackbars } from './components/Snackbar/Snackbar';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import HelpIcon from '@mui/icons-material/Help'; import HelpIcon from '@mui/icons-material/Help';
import { import {
cleanUrl, cleanUrl,
getProtocol, getProtocol,
@ -119,11 +117,6 @@ import {
} from './atoms/global'; } from './atoms/global';
import { useAppFullScreen } from './useAppFullscreen'; import { useAppFullScreen } from './useAppFullscreen';
import { NotAuthenticated } from './ExtStates/NotAuthenticated'; import { NotAuthenticated } from './ExtStates/NotAuthenticated';
import {
openIndexedDB,
showSaveFilePicker,
} from './components/Apps/useQortalMessageListener';
import { fileToBase64 } from './utils/fileReading';
import { handleGetFileFromIndexedDB } from './utils/indexedDB'; import { handleGetFileFromIndexedDB } from './utils/indexedDB';
import { CoreSyncStatus } from './components/CoreSyncStatus'; import { CoreSyncStatus } from './components/CoreSyncStatus';
import { Wallets } from './Wallets'; import { Wallets } from './Wallets';
@ -131,7 +124,6 @@ import { RandomSentenceGenerator } from './utils/seedPhrase/RandomSentenceGenera
import { useFetchResources } from './common/useFetchResources'; import { useFetchResources } from './common/useFetchResources';
import { Tutorials } from './components/Tutorials/Tutorials'; import { Tutorials } from './components/Tutorials/Tutorials';
import { useHandleTutorials } from './components/Tutorials/useHandleTutorials'; import { useHandleTutorials } from './components/Tutorials/useHandleTutorials';
import BoundedNumericTextField from './common/BoundedNumericTextField';
import { useHandleUserInfo } from './components/Group/useHandleUserInfo'; import { useHandleUserInfo } from './components/Group/useHandleUserInfo';
import { Minting } from './components/Minting/Minting'; import { Minting } from './components/Minting/Minting';
import { isRunningGateway } from './qortalRequests'; import { isRunningGateway } from './qortalRequests';
@ -139,7 +131,6 @@ import { QMailStatus } from './components/QMailStatus';
import { GlobalActions } from './components/GlobalActions/GlobalActions'; import { GlobalActions } from './components/GlobalActions/GlobalActions';
import { useBlockedAddresses } from './components/Group/useBlockUsers'; import { useBlockedAddresses } from './components/Group/useBlockUsers';
import { WalletIcon } from './assets/Icons/WalletIcon'; import { WalletIcon } from './assets/Icons/WalletIcon';
import { DrawerUserLookup } from './components/Drawer/DrawerUserLookup';
import { UserLookup } from './components/UserLookup.tsx/UserLookup'; import { UserLookup } from './components/UserLookup.tsx/UserLookup';
import { RegisterName } from './components/RegisterName'; import { RegisterName } from './components/RegisterName';
import { BuyQortInformation } from './components/BuyQortInformation'; import { BuyQortInformation } from './components/BuyQortInformation';
@ -626,7 +617,9 @@ function App() {
setIsDisabledEditorEnter(parsedVal); setIsDisabledEditorEnter(parsedVal);
} }
} }
} catch (error) {} } catch (error) {
console.log(error);
}
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -684,7 +677,9 @@ function App() {
try { try {
if (typeof fileContents !== 'string') return; if (typeof fileContents !== 'string') return;
pf = JSON.parse(fileContents); pf = JSON.parse(fileContents);
} catch (e) {} } catch (e) {
console.log(error);
}
try { try {
const requiredFields = [ const requiredFields = [
@ -932,7 +927,9 @@ function App() {
}); });
getBalanceFunc(); getBalanceFunc();
} catch (error) {} } catch (error) {
console.log(error);
}
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -985,7 +982,6 @@ function App() {
); );
} catch (error: any) { } catch (error: any) {
setWalletToBeDownloadedError(error?.message); setWalletToBeDownloadedError(error?.message);
} finally {
} }
}; };
@ -1107,7 +1103,9 @@ function App() {
error.message || 'An error occurred' error.message || 'An error occurred'
); );
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const returnToMain = () => { const returnToMain = () => {
@ -1333,9 +1331,7 @@ function App() {
{authenticatedMode === 'qort' && ( {authenticatedMode === 'qort' && (
<Tooltip <Tooltip
title={ title={
<span <span style={{ fontSize: '14px', fontWeight: 700 }}>
style={{ color: 'white', fontSize: '14px', fontWeight: 700 }}
>
LITECOIN WALLET LITECOIN WALLET
</span> </span>
} }
@ -1345,13 +1341,13 @@ function App() {
slotProps={{ slotProps={{
tooltip: { tooltip: {
sx: { sx: {
color: '#ffffff', color: theme.palette.text.primary,
backgroundColor: '#444444', backgroundColor: theme.palette.background.default,
}, },
}, },
arrow: { arrow: {
sx: { sx: {
color: '#444444', color: theme.palette.text.primary,
}, },
}, },
}} }}
@ -1372,9 +1368,7 @@ function App() {
{authenticatedMode === 'ltc' && ( {authenticatedMode === 'ltc' && (
<Tooltip <Tooltip
title={ title={
<span <span style={{ fontSize: '14px', fontWeight: 700 }}>
style={{ color: 'white', fontSize: '14px', fontWeight: 700 }}
>
QORTAL WALLET QORTAL WALLET
</span> </span>
} }
@ -1384,13 +1378,13 @@ function App() {
slotProps={{ slotProps={{
tooltip: { tooltip: {
sx: { sx: {
color: '#ffffff', color: theme.palette.text.primary,
backgroundColor: '#444444', backgroundColor: theme.palette.background.default,
}, },
}, },
arrow: { arrow: {
sx: { sx: {
color: '#444444', color: theme.palette.text.primary,
}, },
}, },
}} }}
@ -1435,10 +1429,10 @@ function App() {
> >
<TextP <TextP
sx={{ sx={{
textAlign: 'center',
lineHeight: '24px',
fontSize: '20px', fontSize: '20px',
fontWeight: 700, fontWeight: 700,
lineHeight: '24px',
textAlign: 'center',
}} }}
> >
{ltcBalance} LTC {ltcBalance} LTC
@ -1447,7 +1441,6 @@ function App() {
onClick={getLtcBalanceFunc} onClick={getLtcBalanceFunc}
sx={{ sx={{
fontSize: '16px', fontSize: '16px',
color: 'white',
cursor: 'pointer', cursor: 'pointer',
}} }}
/> />
@ -1494,10 +1487,10 @@ function App() {
> >
<TextP <TextP
sx={{ sx={{
textAlign: 'center',
lineHeight: '24px',
fontSize: '20px', fontSize: '20px',
fontWeight: 700, fontWeight: 700,
lineHeight: '24px',
textAlign: 'center',
}} }}
> >
{balance?.toFixed(2)} QORT {balance?.toFixed(2)} QORT
@ -1506,7 +1499,6 @@ function App() {
onClick={getBalanceFunc} onClick={getBalanceFunc}
sx={{ sx={{
fontSize: '16px', fontSize: '16px',
color: 'white',
cursor: 'pointer', cursor: 'pointer',
}} }}
/> />
@ -1517,13 +1509,13 @@ function App() {
{userInfo && !userInfo?.name && ( {userInfo && !userInfo?.name && (
<TextP <TextP
sx={{ sx={{
textAlign: 'center', color: 'red',
lineHeight: 1.2, cursor: 'pointer',
fontSize: '16px', fontSize: '16px',
fontWeight: 500, fontWeight: 500,
cursor: 'pointer', lineHeight: 1.2,
marginTop: '10px', marginTop: '10px',
color: 'red', textAlign: 'center',
textDecoration: 'underline', textDecoration: 'underline',
}} }}
onClick={() => { onClick={() => {
@ -1548,12 +1540,12 @@ function App() {
)} )}
<TextP <TextP
sx={{ sx={{
textAlign: 'center', cursor: 'pointer',
lineHeight: '24px',
fontSize: '12px', fontSize: '12px',
fontWeight: 500, fontWeight: 500,
cursor: 'pointer', lineHeight: '24px',
marginTop: '10px', marginTop: '10px',
textAlign: 'center',
textDecoration: 'underline', textDecoration: 'underline',
}} }}
onClick={async () => { onClick={async () => {
@ -1573,18 +1565,18 @@ function App() {
return ( return (
<AuthenticatedContainer <AuthenticatedContainer
sx={{ sx={{
width: isMobile ? '100vw' : 'auto', backgroundColor: theme.palette.background.default,
display: 'flex', display: 'flex',
backgroundColor: 'var(--bg-2)',
justifyContent: 'flex-end', justifyContent: 'flex-end',
width: isMobile ? '100vw' : 'auto',
}} }}
> >
{isMobile && ( {isMobile && (
<Box <Box
sx={{ sx={{
padding: '10px',
display: 'flex', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'flex-end',
padding: '10px',
}} }}
> >
<CloseIcon <CloseIcon
@ -1593,7 +1585,6 @@ function App() {
}} }}
sx={{ sx={{
cursor: 'pointer', cursor: 'pointer',
color: 'white',
}} }}
/> />
</Box> </Box>
@ -1610,10 +1601,10 @@ function App() {
> >
<Box <Box
sx={{ sx={{
width: '100%', alignItems: 'center',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', width: '100%',
}} }}
> >
<Spacer height="20px" /> <Spacer height="20px" />
@ -1625,7 +1616,6 @@ function App() {
title={ title={
<span <span
style={{ style={{
color: 'white',
fontSize: '14px', fontSize: '14px',
fontWeight: 700, fontWeight: 700,
}} }}
@ -1639,13 +1629,13 @@ function App() {
slotProps={{ slotProps={{
tooltip: { tooltip: {
sx: { sx: {
color: '#ffffff', color: theme.palette.text.primary,
backgroundColor: '#444444', backgroundColor: theme.palette.background.default,
}, },
}, },
arrow: { arrow: {
sx: { sx: {
color: '#444444', color: theme.palette.text.primary,
}, },
}, },
}} }}
@ -1664,6 +1654,7 @@ function App() {
</Tooltip> </Tooltip>
</> </>
)} )}
<Spacer height="20px" /> <Spacer height="20px" />
<ButtonBase <ButtonBase
@ -1675,7 +1666,6 @@ function App() {
title={ title={
<span <span
style={{ style={{
color: 'white',
fontSize: '14px', fontSize: '14px',
fontWeight: 700, fontWeight: 700,
}} }}
@ -1689,13 +1679,13 @@ function App() {
slotProps={{ slotProps={{
tooltip: { tooltip: {
sx: { sx: {
color: '#ffffff', color: theme.palette.text.primary,
backgroundColor: '#444444', backgroundColor: theme.palette.background.default,
}, },
}, },
arrow: { arrow: {
sx: { sx: {
color: '#444444', color: theme.palette.text.primary,
}, },
}, },
}} }}
@ -1703,7 +1693,9 @@ function App() {
<SettingsIcon /> <SettingsIcon />
</Tooltip> </Tooltip>
</ButtonBase> </ButtonBase>
<Spacer height="20px" /> <Spacer height="20px" />
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
setIsOpenDrawerLookup(true); setIsOpenDrawerLookup(true);
@ -1713,7 +1705,6 @@ function App() {
title={ title={
<span <span
style={{ style={{
color: 'white',
fontSize: '14px', fontSize: '14px',
fontWeight: 700, fontWeight: 700,
}} }}
@ -1727,13 +1718,13 @@ function App() {
slotProps={{ slotProps={{
tooltip: { tooltip: {
sx: { sx: {
color: '#ffffff', color: theme.palette.text.primary,
backgroundColor: '#444444', backgroundColor: theme.palette.background.default,
}, },
}, },
arrow: { arrow: {
sx: { sx: {
color: '#444444', color: theme.palette.text.primary,
}, },
}, },
}} }}
@ -1741,7 +1732,9 @@ function App() {
<PersonSearchIcon /> <PersonSearchIcon />
</Tooltip> </Tooltip>
</ButtonBase> </ButtonBase>
<Spacer height="20px" /> <Spacer height="20px" />
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
executeEvent('openWalletsApp', {}); executeEvent('openWalletsApp', {});
@ -1751,7 +1744,6 @@ function App() {
title={ title={
<span <span
style={{ style={{
color: 'white',
fontSize: '14px', fontSize: '14px',
fontWeight: 700, fontWeight: 700,
}} }}
@ -1765,13 +1757,13 @@ function App() {
slotProps={{ slotProps={{
tooltip: { tooltip: {
sx: { sx: {
color: '#ffffff', color: theme.palette.text.primary,
backgroundColor: '#444444', backgroundColor: theme.palette.background.default,
}, },
}, },
arrow: { arrow: {
sx: { sx: {
color: '#444444', color: theme.palette.text.primary,
}, },
}, },
}} }}
@ -1788,7 +1780,6 @@ function App() {
title={ title={
<span <span
style={{ style={{
color: 'white',
fontSize: '14px', fontSize: '14px',
fontWeight: 700, fontWeight: 700,
}} }}
@ -1802,13 +1793,13 @@ function App() {
slotProps={{ slotProps={{
tooltip: { tooltip: {
sx: { sx: {
color: '#ffffff', color: theme.palette.text.primary,
backgroundColor: '#444444', backgroundColor: theme.palette.background.default,
}, },
}, },
arrow: { arrow: {
sx: { sx: {
color: '#444444', color: theme.palette.text.primary,
}, },
}, },
}} }}
@ -1825,20 +1816,21 @@ function App() {
)} )}
<Spacer height="20px" /> <Spacer height="20px" />
<CoreSyncStatus />
<Spacer height="20px" />
<QMailStatus /> <QMailStatus />
<Spacer height="20px" /> <Spacer height="20px" />
{extState === 'authenticated' && ( {extState === 'authenticated' && (
<GeneralNotifications address={userInfo?.address} /> <GeneralNotifications address={userInfo?.address} />
)} )}
</Box> </Box>
<Box <Box
sx={{ sx={{
width: '100%', alignItems: 'center',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', width: '100%',
}} }}
> >
{extState === 'authenticated' && isMainWindow && ( {extState === 'authenticated' && isMainWindow && (
@ -1873,7 +1865,9 @@ function App() {
<GlobalActions memberGroups={memberGroups} /> <GlobalActions memberGroups={memberGroups} />
</MyContext.Provider> </MyContext.Provider>
)} )}
<Spacer height="20px" /> <Spacer height="20px" />
<ButtonBase <ButtonBase
onClick={async () => { onClick={async () => {
try { try {
@ -1896,7 +1890,6 @@ function App() {
title={ title={
<span <span
style={{ style={{
color: 'white',
fontSize: '14px', fontSize: '14px',
fontWeight: 700, fontWeight: 700,
}} }}
@ -1910,13 +1903,13 @@ function App() {
slotProps={{ slotProps={{
tooltip: { tooltip: {
sx: { sx: {
color: '#ffffff', color: theme.palette.text.primary,
backgroundColor: '#444444', backgroundColor: theme.palette.background.default,
}, },
}, },
arrow: { arrow: {
sx: { sx: {
color: '#444444', color: theme.palette.text.primary,
}, },
}, },
}} }}
@ -1926,6 +1919,7 @@ function App() {
</ButtonBase> </ButtonBase>
<Spacer height="20px" /> <Spacer height="20px" />
{(desktopViewMode === 'apps' || desktopViewMode === 'home') && ( {(desktopViewMode === 'apps' || desktopViewMode === 'home') && (
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
@ -1940,7 +1934,6 @@ function App() {
title={ title={
<span <span
style={{ style={{
color: 'white',
fontSize: '14px', fontSize: '14px',
fontWeight: 700, fontWeight: 700,
}} }}
@ -1954,13 +1947,13 @@ function App() {
slotProps={{ slotProps={{
tooltip: { tooltip: {
sx: { sx: {
color: '#ffffff', color: theme.palette.text.primary,
backgroundColor: '#444444', backgroundColor: theme.palette.background.default,
}, },
}, },
arrow: { arrow: {
sx: { sx: {
color: '#444444', color: theme.palette.text.primary,
}, },
}, },
}} }}
@ -1973,9 +1966,7 @@ function App() {
<Spacer height="20px" /> <Spacer height="20px" />
<Tooltip <Tooltip
title={ title={
<span <span style={{ fontSize: '14px', fontWeight: 700 }}>
style={{ color: 'white', fontSize: '14px', fontWeight: 700 }}
>
BACKUP WALLET BACKUP WALLET
</span> </span>
} }
@ -1985,13 +1976,13 @@ function App() {
slotProps={{ slotProps={{
tooltip: { tooltip: {
sx: { sx: {
color: '#ffffff', color: theme.palette.text.primary,
backgroundColor: '#444444', backgroundColor: theme.palette.background.default,
}, },
}, },
arrow: { arrow: {
sx: { sx: {
color: '#444444', color: theme.palette.text.primary,
}, },
}, },
}} }}
@ -2113,7 +2104,7 @@ function App() {
width: '100%', width: '100%',
height: '100%', height: '100%',
position: 'fixed', position: 'fixed',
background: '#27282c', background: theme.palette.background.default,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
@ -2403,17 +2394,7 @@ function App() {
)} )}
{` ${requestBuyOrder?.crosschainAtInfo?.[0]?.foreignBlockchain}`} {` ${requestBuyOrder?.crosschainAtInfo?.[0]?.foreignBlockchain}`}
</TextP> </TextP>
{/* <Spacer height="29px" />
<CustomLabel htmlFor="standard-adornment-password">
Confirm Wallet Password
</CustomLabel>
<Spacer height="5px" />
<PasswordField
id="standard-adornment-password"
value={paymentPassword}
onChange={(e) => setPaymentPassword(e.target.value)}
/> */}
<Spacer height="29px" /> <Spacer height="29px" />
<Box <Box
sx={{ sx={{
@ -2592,6 +2573,7 @@ function App() {
<img src={Logo1Dark} className="base-image" /> <img src={Logo1Dark} className="base-image" />
</div> </div>
<Spacer height="38px" /> <Spacer height="38px" />
<TextP <TextP
sx={{ sx={{
textAlign: 'center', textAlign: 'center',
@ -2602,7 +2584,9 @@ function App() {
<TextItalic>{requestConnection?.hostname}</TextItalic> <br></br> <TextItalic>{requestConnection?.hostname}</TextItalic> <br></br>
<TextSpan>requests authentication</TextSpan> <TextSpan>requests authentication</TextSpan>
</TextP> </TextP>
<Spacer height="38px" /> <Spacer height="38px" />
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@ -2610,12 +2594,16 @@ function App() {
gap: '14px', gap: '14px',
}} }}
></Box> ></Box>
<Spacer height="38px" /> <Spacer height="38px" />
<CustomButton {...getRootProps()}> <CustomButton {...getRootProps()}>
<input {...getInputProps()} /> <input {...getInputProps()} />
Authenticate Authenticate
</CustomButton> </CustomButton>
<Spacer height="6px" /> <Spacer height="6px" />
<CustomButton <CustomButton
onClick={() => { onClick={() => {
setExtstate('create-wallet'); setExtstate('create-wallet');
@ -2628,14 +2616,15 @@ function App() {
{extState === 'wallets' && ( {extState === 'wallets' && (
<> <>
<Spacer height="22px" /> <Spacer height="22px" />
<Box <Box
sx={{ sx={{
display: 'flex',
width: '100%',
justifyContent: 'flex-start',
paddingLeft: '22px',
boxSizing: 'border-box', boxSizing: 'border-box',
display: 'flex',
justifyContent: 'flex-start',
maxWidth: '700px', maxWidth: '700px',
paddingLeft: '22px',
width: '100%',
}} }}
> >
<Return <Return
@ -2650,6 +2639,7 @@ function App() {
}} }}
/> />
</Box> </Box>
<Wallets <Wallets
setRawWallet={setRawWallet} setRawWallet={setRawWallet}
setExtState={setExtstate} setExtState={setExtstate}
@ -2662,12 +2652,12 @@ function App() {
<Spacer height="22px" /> <Spacer height="22px" />
<Box <Box
sx={{ sx={{
display: 'flex',
width: '100%',
justifyContent: 'flex-start',
paddingLeft: '22px',
boxSizing: 'border-box', boxSizing: 'border-box',
display: 'flex',
justifyContent: 'flex-start',
maxWidth: '700px', maxWidth: '700px',
paddingLeft: '22px',
width: '100%',
}} }}
> >
<Return <Return
@ -2703,7 +2693,9 @@ function App() {
<Typography> <Typography>
{rawWallet?.name ? rawWallet?.name : rawWallet?.address0} {rawWallet?.name ? rawWallet?.name : rawWallet?.address0}
</Typography> </Typography>
<Spacer height="10px" /> <Spacer height="10px" />
<TextP <TextP
sx={{ sx={{
textAlign: 'start', textAlign: 'start',
@ -2715,13 +2707,16 @@ function App() {
Authenticate Authenticate
</TextP> </TextP>
</Box> </Box>
<Spacer height="35px" /> <Spacer height="35px" />
<> <>
<CustomLabel htmlFor="standard-adornment-password"> <CustomLabel htmlFor="standard-adornment-password">
Wallet Password Wallet Password
</CustomLabel> </CustomLabel>
<Spacer height="5px" /> <Spacer height="5px" />
<PasswordField <PasswordField
id="standard-adornment-password" id="standard-adornment-password"
value={authenticatePassword} value={authenticatePassword}
@ -2758,9 +2753,11 @@ function App() {
)} )}
<Spacer height="20px" /> <Spacer height="20px" />
<CustomButton onClick={authenticateWallet}> <CustomButton onClick={authenticateWallet}>
Authenticate Authenticate
</CustomButton> </CustomButton>
<ErrorText>{walletToBeDecryptedError}</ErrorText> <ErrorText>{walletToBeDecryptedError}</ErrorText>
</> </>
</> </>
@ -2770,12 +2767,12 @@ function App() {
<Spacer height="22px" /> <Spacer height="22px" />
<Box <Box
sx={{ sx={{
display: 'flex',
width: '100%',
justifyContent: 'flex-start',
paddingLeft: '22px',
boxSizing: 'border-box', boxSizing: 'border-box',
display: 'flex',
justifyContent: 'flex-start',
maxWidth: '700px', maxWidth: '700px',
paddingLeft: '22px',
width: '100%',
}} }}
> >
<Return <Return
@ -2796,7 +2793,9 @@ function App() {
> >
<img src={Logo1Dark} className="base-image" /> <img src={Logo1Dark} className="base-image" />
</div> </div>
<Spacer height="35px" /> <Spacer height="35px" />
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@ -2815,7 +2814,9 @@ function App() {
Download Account Download Account
</TextP> </TextP>
</Box> </Box>
<Spacer height="35px" /> <Spacer height="35px" />
{!walletToBeDownloaded && ( {!walletToBeDownloaded && (
<> <>
<CustomLabel htmlFor="standard-adornment-password"> <CustomLabel htmlFor="standard-adornment-password">
@ -2858,14 +2859,15 @@ function App() {
{!walletToBeDownloaded && ( {!walletToBeDownloaded && (
<> <>
<Spacer height="22px" /> <Spacer height="22px" />
<Box <Box
sx={{ sx={{
display: 'flex',
width: '100%',
justifyContent: 'flex-start',
paddingLeft: '22px',
boxSizing: 'border-box', boxSizing: 'border-box',
display: 'flex',
justifyContent: 'flex-start',
maxWidth: '700px', maxWidth: '700px',
paddingLeft: '22px',
width: '100%',
}} }}
> >
<Return <Return
@ -2966,7 +2968,6 @@ function App() {
sx={{ sx={{
fontSize: '18px', fontSize: '18px',
marginTop: '15px', marginTop: '15px',
textAlign: 'center', textAlign: 'center',
}} }}
> >
@ -3007,11 +3008,11 @@ function App() {
<DialogContent> <DialogContent>
<Box <Box
sx={{ sx={{
flexDirection: 'column',
maxWidth: '400px',
alignItems: 'center', alignItems: 'center',
gap: '10px',
display: showSeed ? 'flex' : 'none', display: showSeed ? 'flex' : 'none',
flexDirection: 'column',
gap: '10px',
maxWidth: '400px',
}} }}
> >
<Typography <Typography
@ -3024,11 +3025,11 @@ function App() {
<Box <Box
sx={{ sx={{
textAlign: 'center', background: theme.palette.background.paper,
width: '100%',
backgroundColor: '#1f2023',
borderRadius: '5px', borderRadius: '5px',
padding: '10px', padding: '10px',
textAlign: 'center',
width: '100%',
}} }}
> >
{generatorRef.current?.parsedString} {generatorRef.current?.parsedString}
@ -3055,6 +3056,7 @@ function App() {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</Box> </Box>
<Box <Box
sx={{ sx={{
display: creationStep === 2 ? 'flex' : 'none', display: creationStep === 2 ? 'flex' : 'none',
@ -3063,10 +3065,13 @@ function App() {
}} }}
> >
<Spacer height="14px" /> <Spacer height="14px" />
<CustomLabel htmlFor="standard-adornment-password"> <CustomLabel htmlFor="standard-adornment-password">
Wallet Password Wallet Password
</CustomLabel> </CustomLabel>
<Spacer height="5px" /> <Spacer height="5px" />
<PasswordField <PasswordField
id="standard-adornment-password" id="standard-adornment-password"
value={walletToBeDownloadedPassword} value={walletToBeDownloadedPassword}
@ -3074,11 +3079,15 @@ function App() {
setWalletToBeDownloadedPassword(e.target.value) setWalletToBeDownloadedPassword(e.target.value)
} }
/> />
<Spacer height="6px" /> <Spacer height="6px" />
<CustomLabel htmlFor="standard-adornment-password"> <CustomLabel htmlFor="standard-adornment-password">
Confirm Wallet Password Confirm Wallet Password
</CustomLabel> </CustomLabel>
<Spacer height="5px" /> <Spacer height="5px" />
<PasswordField <PasswordField
id="standard-adornment-password" id="standard-adornment-password"
value={walletToBeDownloadedPasswordConfirm} value={walletToBeDownloadedPasswordConfirm}
@ -3087,9 +3096,11 @@ function App() {
} }
/> />
<Spacer height="5px" /> <Spacer height="5px" />
<Typography variant="body2"> <Typography variant="body2">
There is no minimum length requirement There is no minimum length requirement
</Typography> </Typography>
<Spacer height="17px" /> <Spacer height="17px" />
<CustomButton onClick={createAccountFunc}> <CustomButton onClick={createAccountFunc}>
@ -3146,13 +3157,13 @@ function App() {
{isOpenSendQortSuccess && ( {isOpenSendQortSuccess && (
<Box <Box
sx={{ sx={{
width: '100%', alignItems: 'center',
height: '100%', background: theme.palette.background.default,
position: 'fixed',
background: '#27282c',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', height: '100%',
position: 'fixed',
width: '100%',
zIndex: 10000, zIndex: 10000,
}} }}
> >
@ -3279,7 +3290,7 @@ function App() {
<Button <Button
sx={{ sx={{
backgroundColor: 'var(--green)', backgroundColor: 'var(--green)',
color: 'black', color: theme.palette.text.primary,
fontWeight: 'bold', fontWeight: 'bold',
opacity: 0.7, opacity: 0.7,
'&:hover': { '&:hover': {

View File

@ -657,7 +657,6 @@ export const NotAuthenticated = ({
} }
}} }}
disabled={false} disabled={false}
defaultChecked
/> />
} }
label={`Use ${isLocal ? 'Local' : 'Custom'} Node`} label={`Use ${isLocal ? 'Local' : 'Custom'} Node`}

View File

@ -16,6 +16,7 @@ import {
DialogTitle, DialogTitle,
IconButton, IconButton,
Input, Input,
useTheme,
} from '@mui/material'; } from '@mui/material';
import { CustomButton } from './styles/App-styles'; import { CustomButton } from './styles/App-styles';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
@ -266,6 +267,7 @@ export const Wallets = ({ setExtState, setRawWallet, rawWallet }) => {
onClick={handleSetSeedValue} onClick={handleSetSeedValue}
sx={{ sx={{
padding: '10px', padding: '10px',
display: 'inline',
}} }}
> >
Add seed-phrase Add seed-phrase
@ -401,6 +403,7 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => {
const [name, setName] = useState(''); const [name, setName] = useState('');
const [note, setNote] = useState(''); const [note, setNote] = useState('');
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const theme = useTheme();
useEffect(() => { useEffect(() => {
if (wallet?.name) { if (wallet?.name) {
@ -423,10 +426,10 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => {
> >
<ListItem <ListItem
sx={{ sx={{
bgcolor: 'background.paper', // TODO: set background color bgcolor: theme.palette.background.default,
flexGrow: 1, flexGrow: 1,
'&:hover': { '&:hover': {
backgroundColor: 'secondary.main', backgroundColor: theme.palette.background.paper,
transform: 'scale(1.01)', transform: 'scale(1.01)',
}, },
transition: 'all 0.1s ease-in-out', transition: 'all 0.1s ease-in-out',
@ -454,7 +457,7 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => {
<Typography <Typography
component="span" component="span"
variant="body2" variant="body2"
sx={{ color: 'text.primary', display: 'inline' }} sx={{ color: theme.palette.text.primary, display: 'inline' }}
> >
{wallet?.address0} {wallet?.address0}
</Typography> </Typography>
@ -471,6 +474,7 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => {
} }
/> />
</ListItem> </ListItem>
<IconButton <IconButton
sx={{ sx={{
alignSelf: 'flex-start', alignSelf: 'flex-start',
@ -482,11 +486,7 @@ const WalletItem = ({ wallet, updateWalletItem, idx, setSelectedWallet }) => {
edge="end" edge="end"
aria-label="edit" aria-label="edit"
> >
<EditIcon <EditIcon />
sx={{
color: 'white',
}}
/>
</IconButton> </IconButton>
</ButtonBase> </ButtonBase>
{isEdit && ( {isEdit && (

View File

@ -1,4 +1,4 @@
import { useTheme } from "@mui/material"; import { useTheme } from '@mui/material';
export const AppsIcon = ({ height = 31, width = 31 }) => { export const AppsIcon = ({ height = 31, width = 31 }) => {
const theme = useTheme(); const theme = useTheme();

View File

@ -21,8 +21,8 @@ export const Download: React.FC<SVGProps> = ({
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M12.8047 0.393196V7.21185H16.3036L10.0003 13.5139L3.69697 7.21185H7.19584V0H12.8045L12.8047 0.393196ZM2.7047 16.8587V13.9861H0V18.6179C0 19.3774 0.622589 20 1.38213 20H18.6179C19.3774 20 20 19.3774 20 18.6179V13.9861H17.2962V17.2963L2.70461 17.2954L2.7047 16.8587Z" d="M12.8047 0.393196V7.21185H16.3036L10.0003 13.5139L3.69697 7.21185H7.19584V0H12.8045L12.8047 0.393196ZM2.7047 16.8587V13.9861H0V18.6179C0 19.3774 0.622589 20 1.38213 20H18.6179C19.3774 20 20 19.3774 20 18.6179V13.9861H17.2962V17.2963L2.70461 17.2954L2.7047 16.8587Z"
fill={setColor} fill={setColor}
fill-opacity={setOpacity} fill-opacity={setOpacity}

View File

@ -1,13 +1,20 @@
import React from 'react'; import { useTheme } from '@mui/material';
export const ExitIcon= ({ color = 'white', height, width }) => { export const ExitIcon = () => {
return ( const theme = useTheme();
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 0L0 2L4 6L0 10L2 12L6 8L10 12L12 10L8 6L12 2L10 0L6 4L2 0Z" fill={color}/>
</svg>
return (
); <svg
}; width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 0L0 2L4 6L0 10L2 12L6 8L10 12L12 10L8 6L12 2L10 0L6 4L2 0Z"
fill={theme.palette.text.primary}
/>
</svg>
);
};

View File

@ -1,4 +1,4 @@
import { useTheme } from "@mui/material"; import { useTheme } from '@mui/material';
export const HomeIcon = ({ height = 20, width = 23 }) => { export const HomeIcon = ({ height = 20, width = 23 }) => {
const theme = useTheme(); const theme = useTheme();

View File

@ -17,8 +17,8 @@ export const Logout: React.FC<SVGProps> = ({ color, opacity, ...children }) => {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M7.56485 0H16.3611C17.2662 0 18 0.727797 18 1.62558V18.3744C18 19.2722 17.2662 20 16.3611 20H7.56485C6.65969 20 5.92593 19.2722 5.92593 18.3744V12.6013H10.6168C11.4569 12.6013 12.1404 11.9039 12.1404 11.0467V8.87329C12.1404 8.01613 11.4569 7.31875 10.6168 7.31875H5.92593V1.62558C5.92593 0.727797 6.65969 0 7.56485 0ZM11.1667 11.0467C11.1667 11.3719 10.9205 11.6354 10.6168 11.6354H4.8144C4.74521 11.6354 4.68911 11.6955 4.68911 11.7696V12.8632C4.68911 13.3492 4.17007 13.6259 3.8078 13.3329L0.218431 10.4298C-0.0728102 10.1942 -0.0728102 9.72579 0.218431 9.49024L3.8078 6.58709C4.17005 6.29409 4.68911 6.57077 4.68911 7.05684V8.1504C4.68911 8.2245 4.74521 8.28454 4.8144 8.28454H10.6168C10.9205 8.28454 11.1667 8.54813 11.1667 8.87329V11.0467Z" d="M7.56485 0H16.3611C17.2662 0 18 0.727797 18 1.62558V18.3744C18 19.2722 17.2662 20 16.3611 20H7.56485C6.65969 20 5.92593 19.2722 5.92593 18.3744V12.6013H10.6168C11.4569 12.6013 12.1404 11.9039 12.1404 11.0467V8.87329C12.1404 8.01613 11.4569 7.31875 10.6168 7.31875H5.92593V1.62558C5.92593 0.727797 6.65969 0 7.56485 0ZM11.1667 11.0467C11.1667 11.3719 10.9205 11.6354 10.6168 11.6354H4.8144C4.74521 11.6354 4.68911 11.6955 4.68911 11.7696V12.8632C4.68911 13.3492 4.17007 13.6259 3.8078 13.3329L0.218431 10.4298C-0.0728102 10.1942 -0.0728102 9.72579 0.218431 9.49024L3.8078 6.58709C4.17005 6.29409 4.68911 6.57077 4.68911 7.05684V8.1504C4.68911 8.2245 4.74521 8.28454 4.8144 8.28454H10.6168C10.9205 8.28454 11.1667 8.54813 11.1667 8.87329V11.0467Z"
fill={setColor} fill={setColor}
fill-opacity={setOpacity} fill-opacity={setOpacity}

View File

@ -1,12 +1,18 @@
import React from 'react'; export const LogoutIcon = ({ color, height = 20, width = 18 }) => {
return (
export const LogoutIcon= ({ color, height = 20, width = 18}) => { <svg
return ( width={width}
<svg width={width} height={height} viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg"> height={height}
<path fillRule="evenodd" clipRule="evenodd" d="M10.4351 0H1.63891C0.733765 0 0 0.727797 0 1.62558V18.3744C0 19.2722 0.733765 20 1.63891 20H10.4351C11.3403 20 12.0741 19.2722 12.0741 18.3744V12.6013H7.38321C6.54312 12.6013 5.85964 11.9039 5.85964 11.0467V8.87329C5.85964 8.01613 6.54312 7.31875 7.38323 7.31875H12.0741V1.62558C12.0741 0.727797 11.3403 0 10.4351 0ZM6.83334 11.0467C6.83334 11.3719 7.07952 11.6354 7.38321 11.6354H13.1856C13.2548 11.6354 13.3109 11.6955 13.3109 11.7696V12.8632C13.3109 13.3492 13.8299 13.6259 14.1922 13.3329L17.7816 10.4298C18.0728 10.1942 18.0728 9.72579 17.7816 9.49024L14.1922 6.58709C13.8299 6.29409 13.3109 6.57077 13.3109 7.05684V8.1504C13.3109 8.2245 13.2548 8.28454 13.1856 8.28454H7.38322C7.07952 8.28454 6.83334 8.54813 6.83334 8.87329V11.0467Z" fill={color} /> viewBox="0 0 18 20"
</svg> fill="none"
xmlns="http://www.w3.org/2000/svg"
>
); <path
}; fillRule="evenodd"
clipRule="evenodd"
d="M10.4351 0H1.63891C0.733765 0 0 0.727797 0 1.62558V18.3744C0 19.2722 0.733765 20 1.63891 20H10.4351C11.3403 20 12.0741 19.2722 12.0741 18.3744V12.6013H7.38321C6.54312 12.6013 5.85964 11.9039 5.85964 11.0467V8.87329C5.85964 8.01613 6.54312 7.31875 7.38323 7.31875H12.0741V1.62558C12.0741 0.727797 11.3403 0 10.4351 0ZM6.83334 11.0467C6.83334 11.3719 7.07952 11.6354 7.38321 11.6354H13.1856C13.2548 11.6354 13.3109 11.6955 13.3109 11.7696V12.8632C13.3109 13.3492 13.8299 13.6259 14.1922 13.3329L17.7816 10.4298C18.0728 10.1942 18.0728 9.72579 17.7816 9.49024L14.1922 6.58709C13.8299 6.29409 13.3109 6.57077 13.3109 7.05684V8.1504C13.3109 8.2245 13.2548 8.28454 13.1856 8.28454H7.38322C7.07952 8.28454 6.83334 8.54813 6.83334 8.87329V11.0467Z"
fill={color}
/>
</svg>
);
};

View File

@ -0,0 +1,32 @@
import { useTheme } from '@mui/material';
import { SVGProps } from './interfaces';
export const NavAdd: React.FC<SVGProps> = ({ color, opacity, ...children }) => {
const theme = useTheme();
const setColor = color ? color : theme.palette.text.primary;
const setOpacity = opacity ? opacity : 1;
return (
<svg
{...children}
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="20"
cy="19.9999"
r="18"
fill={theme.palette.background.paper}
/>
<path
d="M30 21.6666H21.6666V30C21.6666 30.9166 20.9166 31.6666 20 31.6666C19.0833 31.6666 18.3333 30.9166 18.3333 30V21.6666H9.99998C9.08331 21.6666 8.33331 20.9166 8.33331 20C8.33331 19.0833 9.08331 18.3333 9.99998 18.3333H18.3333V9.99995C18.3333 9.08328 19.0833 8.33328 20 8.33328C20.9166 8.33328 21.6666 9.08328 21.6666 9.99995V18.3333H30C30.9166 18.3333 31.6666 19.0833 31.6666 20C31.6666 20.9166 30.9166 21.6666 30 21.6666Z"
fill={setColor}
fill-opacity={setOpacity}
/>
</svg>
);
};

View File

@ -0,0 +1,30 @@
import { useTheme } from '@mui/material';
import { SVGProps } from './interfaces';
export const NavBack: React.FC<SVGProps> = ({
color,
opacity,
...children
}) => {
const theme = useTheme();
const setColor = color ? color : theme.palette.text.primary;
const setOpacity = opacity ? opacity : 1;
return (
<svg
{...children}
width="34"
height="34"
viewBox="0 0 34 34"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M28.3334 15.5833H11.0925L19.0117 7.6641L17 5.6666L5.66669 16.9999L17 28.3333L18.9975 26.3358L11.0925 18.4166H28.3334V15.5833Z"
fill={setColor}
opacity={setOpacity}
/>
</svg>
);
};

View File

@ -0,0 +1,46 @@
import { useTheme } from '@mui/material';
import { SVGProps } from './interfaces';
export const NavCloseTab: React.FC<SVGProps> = ({
color,
opacity,
...children
}) => {
const theme = useTheme();
const setColor = color ? color : theme.palette.text.primary;
const setOpacity = opacity ? opacity : 1;
return (
<svg
{...children}
width="17"
height="17"
viewBox="0 0 17 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="8.5" cy="8.5" r="8.5" fill={theme.palette.text.primary} />
<circle
cx="8.5"
cy="8.50003"
r="6.61111"
fill={theme.palette.background.paper}
/>
<path
d="M5.66675 5.66669L11.3334 11.3334"
stroke={theme.palette.text.primary}
stroke-width="2"
fill={setColor}
fill-opacity={setOpacity}
/>
<path
d="M11.3333 5.66675L5.66658 11.3334"
stroke={theme.palette.text.primary}
stroke-width="2"
fill={setColor}
fill-opacity={setOpacity}
/>
</svg>
);
};

View File

@ -0,0 +1,30 @@
import { useTheme } from '@mui/material';
import { SVGProps } from './interfaces';
export const NavMoreMenu: React.FC<SVGProps> = ({
color,
opacity,
...children
}) => {
const theme = useTheme();
const setColor = color ? color : theme.palette.text.primary;
const setOpacity = opacity ? opacity : 1;
return (
<svg
{...children}
width="34"
height="34"
viewBox="0 0 34 34"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.49996 14.1666C6.94163 14.1666 5.66663 15.4416 5.66663 16.9999C5.66663 18.5583 6.94163 19.8333 8.49996 19.8333C10.0583 19.8333 11.3333 18.5583 11.3333 16.9999C11.3333 15.4416 10.0583 14.1666 8.49996 14.1666ZM25.5 14.1666C23.9416 14.1666 22.6666 15.4416 22.6666 16.9999C22.6666 18.5583 23.9416 19.8333 25.5 19.8333C27.0583 19.8333 28.3333 18.5583 28.3333 16.9999C28.3333 15.4416 27.0583 14.1666 25.5 14.1666ZM17 14.1666C15.4416 14.1666 14.1666 15.4416 14.1666 16.9999C14.1666 18.5583 15.4416 19.8333 17 19.8333C18.5583 19.8333 19.8333 18.5583 19.8333 16.9999C19.8333 15.4416 18.5583 14.1666 17 14.1666Z"
fill={setColor}
fill-opacity={setOpacity}
/>
</svg>
);
};

View File

@ -16,8 +16,8 @@ export const Return: React.FC<SVGProps> = ({ color, opacity, ...children }) => {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
fill-rule="evenodd" fillRule="evenodd"
clip-rule="evenodd" clipRule="evenodd"
d="M2.645 5.81803H15C15.9471 5.81803 16.8557 6.20131 17.5257 6.88278C18.195 7.56497 18.5714 8.49007 18.5714 9.45445V10.909C18.5714 11.8734 18.195 12.7985 17.5257 13.4807C16.8557 14.1622 15.9471 14.5454 15 14.5454C12.0164 14.5454 8.57143 14.5454 8.57143 14.5454C8.17714 14.5454 7.85714 14.8713 7.85714 15.2727C7.85714 15.6742 8.17714 16 8.57143 16H15C16.3264 16 17.5979 15.464 18.5357 14.5091C19.4736 13.5541 20 12.2596 20 10.909C20 10.4268 20 9.93664 20 9.45445C20 8.10461 19.4736 6.80932 18.5357 5.8544C17.5979 4.9002 16.3264 4.36347 15 4.36347H2.645L6.17929 1.27906C6.47857 1.01797 6.51286 0.55832 6.25643 0.253588C6 -0.0511433 5.54857 -0.0860541 5.24929 0.175041L0.249285 4.53874C0.0914279 4.67692 0 4.87838 0 5.09075C0 5.30312 0.0914279 5.50458 0.249285 5.64276L5.24929 10.0065C5.54857 10.2676 6 10.2326 6.25643 9.92791C6.51286 9.62318 6.47857 9.16353 6.17929 8.90244L2.645 5.81803Z" d="M2.645 5.81803H15C15.9471 5.81803 16.8557 6.20131 17.5257 6.88278C18.195 7.56497 18.5714 8.49007 18.5714 9.45445V10.909C18.5714 11.8734 18.195 12.7985 17.5257 13.4807C16.8557 14.1622 15.9471 14.5454 15 14.5454C12.0164 14.5454 8.57143 14.5454 8.57143 14.5454C8.17714 14.5454 7.85714 14.8713 7.85714 15.2727C7.85714 15.6742 8.17714 16 8.57143 16H15C16.3264 16 17.5979 15.464 18.5357 14.5091C19.4736 13.5541 20 12.2596 20 10.909C20 10.4268 20 9.93664 20 9.45445C20 8.10461 19.4736 6.80932 18.5357 5.8544C17.5979 4.9002 16.3264 4.36347 15 4.36347H2.645L6.17929 1.27906C6.47857 1.01797 6.51286 0.55832 6.25643 0.253588C6 -0.0511433 5.54857 -0.0860541 5.24929 0.175041L0.249285 4.53874C0.0914279 4.67692 0 4.87838 0 5.09075C0 5.30312 0.0914279 5.50458 0.249285 5.64276L5.24929 10.0065C5.54857 10.2676 6 10.2326 6.25643 9.92791C6.51286 9.62318 6.47857 9.16353 6.17929 8.90244L2.645 5.81803Z"
fill={setColor} fill={setColor}
fill-opacity={opacity} fill-opacity={opacity}

View File

@ -1,14 +1,20 @@
import React from 'react'; import { useTheme } from '@mui/material';
export const ReturnIcon= ({ color = 'white', height, width }) => { export const ReturnIcon = () => {
return ( const theme = useTheme();
<svg width="20" height="13" viewBox="0 0 20 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.0937 3.73451H3.6243L5.84808 1.67047C6.04153 1.48456 6.14857 1.23558 6.14615 0.97713C6.14373 0.718684 6.03205 0.471459 5.83515 0.288703C5.63825 0.105948 5.37189 0.00228309 5.09344 3.72619e-05C4.815 -0.00220856 4.54674 0.0971438 4.34645 0.276696L0.310933 4.02233C0.111843 4.20718 0 4.45785 0 4.71922C0 4.98059 0.111843 5.23126 0.310933 5.41611L4.34645 9.16175C4.54674 9.3413 4.815 9.44065 5.09344 9.43841C5.37189 9.43616 5.63825 9.33249 5.83515 9.14974C6.03205 8.96698 6.14373 8.71976 6.14615 8.46131C6.14857 8.20287 6.04153 7.95388 5.84808 7.76797L3.6243 5.7059H15.0937C15.8316 5.7059 16.5393 5.97799 17.0611 6.4623C17.5829 6.94662 17.876 7.60349 17.876 8.28842C17.876 8.97335 17.5829 9.63022 17.0611 10.1145C16.5393 10.5989 15.8316 10.8709 15.0937 10.8709V12.8423C16.3949 12.8423 17.6429 12.3625 18.563 11.5085C19.4831 10.6545 20 9.49619 20 8.28842C20 7.08065 19.4831 5.92234 18.563 5.06832C17.6429 4.2143 16.3949 3.73451 15.0937 3.73451Z" fill={color}/>
</svg>
return (
); <svg
}; fill="none"
height="13"
viewBox="0 0 20 13"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.0937 3.73451H3.6243L5.84808 1.67047C6.04153 1.48456 6.14857 1.23558 6.14615 0.97713C6.14373 0.718684 6.03205 0.471459 5.83515 0.288703C5.63825 0.105948 5.37189 0.00228309 5.09344 3.72619e-05C4.815 -0.00220856 4.54674 0.0971438 4.34645 0.276696L0.310933 4.02233C0.111843 4.20718 0 4.45785 0 4.71922C0 4.98059 0.111843 5.23126 0.310933 5.41611L4.34645 9.16175C4.54674 9.3413 4.815 9.44065 5.09344 9.43841C5.37189 9.43616 5.63825 9.33249 5.83515 9.14974C6.03205 8.96698 6.14373 8.71976 6.14615 8.46131C6.14857 8.20287 6.04153 7.95388 5.84808 7.76797L3.6243 5.7059H15.0937C15.8316 5.7059 16.5393 5.97799 17.0611 6.4623C17.5829 6.94662 17.876 7.60349 17.876 8.28842C17.876 8.97335 17.5829 9.63022 17.0611 10.1145C16.5393 10.5989 15.8316 10.8709 15.0937 10.8709V12.8423C16.3949 12.8423 17.6429 12.3625 18.563 11.5085C19.4831 10.6545 20 9.49619 20 8.28842C20 7.08065 19.4831 5.92234 18.563 5.06832C17.6429 4.2143 16.3949 3.73451 15.0937 3.73451Z"
fill={theme.palette.text.primary}
/>
</svg>
);
};

View File

@ -1,13 +1,18 @@
import React from 'react'; export const ThreadsIcon = ({ color = 'white', height = 11, width = 15 }) => {
return (
export const ThreadsIcon= ({ color = 'white', height = 11, width = 15 }) => { <svg
return ( width={width}
<svg width={width} height={height} viewBox="0 0 15 11" fill="none" xmlns="http://www.w3.org/2000/svg"> height={height}
<path fillRule="evenodd" clipRule="evenodd" d="M7.76526 1.80943H6.51017C5.91236 1.80943 5.42723 2.37147 5.42723 3.06406V6.55419V6.82615H4L1.90297 9L2.00782 6.82615H0.8482C0.380282 6.82615 0 6.38558 0 5.84347V0.982675C0 0.440572 0.380282 0 0.8482 0H7.1518C7.62128 0 8 0.440572 8 0.982675V1.80943H7.76526ZM8.89437 2H14.0458C14.5722 2 15 2.44057 15 2.98268V7.84166C15 8.38558 14.5722 8.82434 14.0458 8.82434H12.7412L12.8592 11L10.5 8.82434H6.95423C6.42606 8.82434 6 8.38558 6 7.84166V7.01672V6.74476V6.47281V2.98268C6 2.44057 6.42606 2 6.95423 2H8.3662H8.63028H8.89437Z" fill={color}/> viewBox="0 0 15 11"
</svg> fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
); fillRule="evenodd"
}; clipRule="evenodd"
d="M7.76526 1.80943H6.51017C5.91236 1.80943 5.42723 2.37147 5.42723 3.06406V6.55419V6.82615H4L1.90297 9L2.00782 6.82615H0.8482C0.380282 6.82615 0 6.38558 0 5.84347V0.982675C0 0.440572 0.380282 0 0.8482 0H7.1518C7.62128 0 8 0.440572 8 0.982675V1.80943H7.76526ZM8.89437 2H14.0458C14.5722 2 15 2.44057 15 2.98268V7.84166C15 8.38558 14.5722 8.82434 14.0458 8.82434H12.7412L12.8592 11L10.5 8.82434H6.95423C6.42606 8.82434 6 8.38558 6 7.84166V7.01672V6.74476V6.47281V2.98268C6 2.44057 6.42606 2 6.95423 2H8.3662H8.63028H8.89437Z"
fill={color}
/>
</svg>
);
};

View File

@ -1,11 +1,16 @@
import React from 'react'; export const TradingIcon = ({ color, height, width }) => {
return (
export const TradingIcon= ({ color, height, width }) => { <svg
return ( width="31"
<svg width="31" height="31" viewBox="0 0 31 31" fill="none" xmlns="http://www.w3.org/2000/svg"> height="31"
<path d="M29.6627 28.5686H5.6079C5.06735 28.5647 4.53939 28.4034 4.08736 28.104L8.41371 24.3903C8.68787 24.1495 9.01146 23.9731 9.36122 23.8737C9.71097 23.7744 10.0782 23.7546 10.4364 23.8157L18.8398 25.2345C19.5576 25.3564 20.294 25.3032 20.9873 25.0792C21.6806 24.8552 22.3109 24.467 22.8255 23.947L29.617 17.1085C29.8031 16.9068 29.9044 16.64 29.8996 16.3643C29.8948 16.0886 29.7842 15.8256 29.5912 15.6307C29.3982 15.4357 29.1378 15.324 28.8649 15.3192C28.5919 15.3143 28.3278 15.4166 28.1281 15.6046L21.3225 22.4537C21.047 22.7365 20.7084 22.9485 20.3351 23.0719C19.9618 23.1953 19.5646 23.2266 19.1769 23.1631L10.7876 21.7195C10.1245 21.6058 9.44477 21.6422 8.79738 21.8263C8.15 22.0103 7.55117 22.3373 7.04417 22.7836L2.87233 26.3624C2.82441 26.1554 2.79968 25.9437 2.79859 25.7311V24.3123L7.18463 16.8389C7.44944 16.3923 7.84654 16.041 8.3198 15.8348C8.79305 15.6286 9.31851 15.5778 9.82188 15.6897L17.5299 17.3745C18.1946 17.5208 18.8833 17.5152 19.5455 17.358C20.2078 17.2009 20.8269 16.8963 21.3576 16.4665L29.5327 9.8408C29.6456 9.75519 29.7402 9.64756 29.8111 9.52429C29.8819 9.40103 29.9275 9.26464 29.9452 9.12323C29.9629 8.98181 29.9522 8.83826 29.9139 8.70108C29.8756 8.5639 29.8104 8.4359 29.7221 8.32467C29.6339 8.21344 29.5244 8.12125 29.4002 8.05357C29.276 7.98589 29.1396 7.9441 28.9991 7.93069C28.8587 7.91727 28.7169 7.9325 28.5824 7.97547C28.4478 8.01844 28.3232 8.08828 28.2159 8.18084L20.0408 14.8065C19.755 15.0381 19.4216 15.2023 19.065 15.2869C18.7083 15.3716 18.3374 15.3747 17.9794 15.296L10.2714 13.597C9.33145 13.387 8.34986 13.4825 7.46689 13.8698C6.58392 14.2571 5.84477 14.9164 5.35506 15.7535L2.79859 20.1411V1.74669C2.79859 1.46448 2.68759 1.19383 2.49003 0.994272C2.29246 0.794718 2.0245 0.68261 1.74509 0.68261C1.46569 0.68261 1.19773 0.794718 1.00016 0.994272C0.802591 1.19383 0.691598 1.46448 0.691598 1.74669V25.7311C0.689568 26.7954 1.02832 27.8318 1.6573 28.6857C1.6573 28.707 1.68539 28.7318 1.70295 28.7531C2.1613 29.3522 2.74923 29.8376 3.42178 30.172C4.09433 30.5064 4.83369 30.6811 5.58332 30.6826H29.6381C29.9175 30.6826 30.1855 30.5705 30.383 30.3709C30.5806 30.1714 30.6916 29.9007 30.6916 29.6185C30.6916 29.3363 30.5806 29.0657 30.383 28.8661C30.1855 28.6666 29.9175 28.5544 29.6381 28.5544L29.6627 28.5686Z" fill={color}/> viewBox="0 0 31 31"
</svg> fill="none"
xmlns="http://www.w3.org/2000/svg"
); >
}; <path
d="M29.6627 28.5686H5.6079C5.06735 28.5647 4.53939 28.4034 4.08736 28.104L8.41371 24.3903C8.68787 24.1495 9.01146 23.9731 9.36122 23.8737C9.71097 23.7744 10.0782 23.7546 10.4364 23.8157L18.8398 25.2345C19.5576 25.3564 20.294 25.3032 20.9873 25.0792C21.6806 24.8552 22.3109 24.467 22.8255 23.947L29.617 17.1085C29.8031 16.9068 29.9044 16.64 29.8996 16.3643C29.8948 16.0886 29.7842 15.8256 29.5912 15.6307C29.3982 15.4357 29.1378 15.324 28.8649 15.3192C28.5919 15.3143 28.3278 15.4166 28.1281 15.6046L21.3225 22.4537C21.047 22.7365 20.7084 22.9485 20.3351 23.0719C19.9618 23.1953 19.5646 23.2266 19.1769 23.1631L10.7876 21.7195C10.1245 21.6058 9.44477 21.6422 8.79738 21.8263C8.15 22.0103 7.55117 22.3373 7.04417 22.7836L2.87233 26.3624C2.82441 26.1554 2.79968 25.9437 2.79859 25.7311V24.3123L7.18463 16.8389C7.44944 16.3923 7.84654 16.041 8.3198 15.8348C8.79305 15.6286 9.31851 15.5778 9.82188 15.6897L17.5299 17.3745C18.1946 17.5208 18.8833 17.5152 19.5455 17.358C20.2078 17.2009 20.8269 16.8963 21.3576 16.4665L29.5327 9.8408C29.6456 9.75519 29.7402 9.64756 29.8111 9.52429C29.8819 9.40103 29.9275 9.26464 29.9452 9.12323C29.9629 8.98181 29.9522 8.83826 29.9139 8.70108C29.8756 8.5639 29.8104 8.4359 29.7221 8.32467C29.6339 8.21344 29.5244 8.12125 29.4002 8.05357C29.276 7.98589 29.1396 7.9441 28.9991 7.93069C28.8587 7.91727 28.7169 7.9325 28.5824 7.97547C28.4478 8.01844 28.3232 8.08828 28.2159 8.18084L20.0408 14.8065C19.755 15.0381 19.4216 15.2023 19.065 15.2869C18.7083 15.3716 18.3374 15.3747 17.9794 15.296L10.2714 13.597C9.33145 13.387 8.34986 13.4825 7.46689 13.8698C6.58392 14.2571 5.84477 14.9164 5.35506 15.7535L2.79859 20.1411V1.74669C2.79859 1.46448 2.68759 1.19383 2.49003 0.994272C2.29246 0.794718 2.0245 0.68261 1.74509 0.68261C1.46569 0.68261 1.19773 0.794718 1.00016 0.994272C0.802591 1.19383 0.691598 1.46448 0.691598 1.74669V25.7311C0.689568 26.7954 1.02832 27.8318 1.6573 28.6857C1.6573 28.707 1.68539 28.7318 1.70295 28.7531C2.1613 29.3522 2.74923 29.8376 3.42178 30.172C4.09433 30.5064 4.83369 30.6811 5.58332 30.6826H29.6381C29.9175 30.6826 30.1855 30.5705 30.383 30.3709C30.5806 30.1714 30.6916 29.9007 30.6916 29.6185C30.6916 29.3363 30.5806 29.0657 30.383 28.8661C30.1855 28.6666 29.9175 28.5544 29.6381 28.5544L29.6627 28.5686Z"
fill={color}
/>
</svg>
);
};

View File

@ -1,5 +1,5 @@
import { useTheme } from '@mui/material'; import { useTheme } from '@mui/material';
import { SVGProps } from '../svgs/interfaces'; import { SVGProps } from './interfaces';
export const WalletIcon: React.FC<SVGProps> = ({ export const WalletIcon: React.FC<SVGProps> = ({
color, color,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.8047 0.393196V7.21185H16.3036L10.0003 13.5139L3.69697 7.21185H7.19584V0H12.8045L12.8047 0.393196ZM2.7047 16.8587V13.9861H0V18.6179C0 19.3774 0.622589 20 1.38213 20H18.6179C19.3774 20 20 19.3774 20 18.6179V13.9861H17.2962V17.2963L2.70461 17.2954L2.7047 16.8587Z" fill="white" fill-opacity="0.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 452 B

View File

@ -1,3 +0,0 @@
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.56485 0H16.3611C17.2662 0 18 0.727797 18 1.62558V18.3744C18 19.2722 17.2662 20 16.3611 20H7.56485C6.65969 20 5.92593 19.2722 5.92593 18.3744V12.6013H10.6168C11.4569 12.6013 12.1404 11.9039 12.1404 11.0467V8.87329C12.1404 8.01613 11.4569 7.31875 10.6168 7.31875H5.92593V1.62558C5.92593 0.727797 6.65969 0 7.56485 0ZM11.1667 11.0467C11.1667 11.3719 10.9205 11.6354 10.6168 11.6354H4.8144C4.74521 11.6354 4.68911 11.6955 4.68911 11.7696V12.8632C4.68911 13.3492 4.17007 13.6259 3.8078 13.3329L0.218431 10.4298C-0.0728102 10.1942 -0.0728102 9.72579 0.218431 9.49024L3.8078 6.58709C4.17005 6.29409 4.68911 6.57077 4.68911 7.05684V8.1504C4.68911 8.2245 4.74521 8.28454 4.8144 8.28454H10.6168C10.9205 8.28454 11.1667 8.54813 11.1667 8.87329V11.0467Z" fill="white" fill-opacity="0.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 932 B

View File

@ -1,4 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="20" cy="19.9999" r="18" fill="#434343"/>
<path d="M30 21.6666H21.6666V30C21.6666 30.9166 20.9166 31.6666 20 31.6666C19.0833 31.6666 18.3333 30.9166 18.3333 30V21.6666H9.99998C9.08331 21.6666 8.33331 20.9166 8.33331 20C8.33331 19.0833 9.08331 18.3333 9.99998 18.3333H18.3333V9.99995C18.3333 9.08328 19.0833 8.33328 20 8.33328C20.9166 8.33328 21.6666 9.08328 21.6666 9.99995V18.3333H30C30.9166 18.3333 31.6666 19.0833 31.6666 20C31.6666 20.9166 30.9166 21.6666 30 21.6666Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 602 B

View File

@ -1,10 +0,0 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_400_537)">
<path d="M28.3334 15.5833H11.0925L19.0117 7.6641L17 5.6666L5.66669 16.9999L17 28.3333L18.9975 26.3358L11.0925 18.4166H28.3334V15.5833Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_400_537">
<rect width="34" height="34" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 396 B

View File

@ -1,6 +0,0 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8.5" cy="8.5" r="8.5" fill="white"/>
<circle cx="8.5" cy="8.50003" r="6.61111" fill="#434343"/>
<path d="M5.66675 5.66669L11.3334 11.3334" stroke="white" stroke-width="2"/>
<path d="M11.3333 5.66675L5.66658 11.3334" stroke="white" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 365 B

View File

@ -1,10 +0,0 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_400_556)">
<path d="M8.49996 14.1666C6.94163 14.1666 5.66663 15.4416 5.66663 16.9999C5.66663 18.5583 6.94163 19.8333 8.49996 19.8333C10.0583 19.8333 11.3333 18.5583 11.3333 16.9999C11.3333 15.4416 10.0583 14.1666 8.49996 14.1666ZM25.5 14.1666C23.9416 14.1666 22.6666 15.4416 22.6666 16.9999C22.6666 18.5583 23.9416 19.8333 25.5 19.8333C27.0583 19.8333 28.3333 18.5583 28.3333 16.9999C28.3333 15.4416 27.0583 14.1666 25.5 14.1666ZM17 14.1666C15.4416 14.1666 14.1666 15.4416 14.1666 16.9999C14.1666 18.5583 15.4416 19.8333 17 19.8333C18.5583 19.8333 19.8333 18.5583 19.8333 16.9999C19.8333 15.4416 18.5583 14.1666 17 14.1666Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_400_556">
<rect width="34" height="34" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 874 B

View File

@ -51,6 +51,10 @@ export const sortablePinnedAppsAtom = atom({
name: 'Q-Search', name: 'Q-Search',
service: 'APP', service: 'APP',
}, },
{
name: 'Q-Nodecontrol',
service: 'APP',
},
], ],
}); });

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,15 @@
import { Box } from "@mui/material"; import { Box } from '@mui/material';
export const Spacer = ({ height, width, ...props }: any) => { export const Spacer = ({ height, width, ...props }: any) => {
return ( return (
<Box <Box
sx={{ sx={{
height: height ? height : '0px', height: height ? height : '0px',
display: 'flex', display: 'flex',
flexShrink: 0, flexShrink: 0,
width: width ? width : '0px', width: width ? width : '0px',
...(props || {}) ...(props || {}),
}} }}
/> />
); );
}; };

View File

@ -1,20 +1,14 @@
import { Box, Rating, Typography } from "@mui/material"; import { Box, Rating } from '@mui/material';
import React, { import { useCallback, useContext, useEffect, useRef, useState } from 'react';
useCallback, import { getFee } from '../../background';
useContext, import { MyContext, getBaseApiReact } from '../../App';
useEffect, import { CustomizedSnackbars } from '../Snackbar/Snackbar';
useRef, import { StarFilledIcon } from '../../assets/Icons/StarFilled';
useState, import { StarEmptyIcon } from '../../assets/Icons/StarEmpty';
} from "react"; import { AppInfoUserName } from './Apps-styles';
import { getFee } from "../../background"; import { Spacer } from '../../common/Spacer';
import { MyContext, getBaseApiReact } from "../../App";
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
import { StarFilledIcon } from "../../assets/svgs/StarFilled";
import { StarEmptyIcon } from "../../assets/svgs/StarEmpty";
import { AppInfoUserName } from "./Apps-styles";
import { Spacer } from "../../common/Spacer";
export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => { export const AppRating = ({ app, myName, ratingCountPosition = 'right' }) => {
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
const { show } = useContext(MyContext); const { show } = useContext(MyContext);
const [hasPublishedRating, setHasPublishedRating] = useState<null | boolean>( const [hasPublishedRating, setHasPublishedRating] = useState<null | boolean>(
@ -33,14 +27,14 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => {
const url = `${getBaseApiReact()}/polls/${pollName}`; const url = `${getBaseApiReact()}/polls/${pollName}`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
const responseData = await response.json(); const responseData = await response.json();
if (responseData?.message?.includes("POLL_NO_EXISTS")) { if (responseData?.message?.includes('POLL_NO_EXISTS')) {
setHasPublishedRating(false); setHasPublishedRating(false);
} else if (responseData?.pollName) { } else if (responseData?.pollName) {
setPollInfo(responseData); setPollInfo(responseData);
@ -48,9 +42,9 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => {
const urlVotes = `${getBaseApiReact()}/polls/votes/${pollName}`; const urlVotes = `${getBaseApiReact()}/polls/votes/${pollName}`;
const responseVotes = await fetch(urlVotes, { const responseVotes = await fetch(urlVotes, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
@ -59,15 +53,15 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => {
const voteCount = responseDataVotes.voteCounts; const voteCount = responseDataVotes.voteCounts;
// Include initial value vote in the calculation // Include initial value vote in the calculation
const ratingVotes = voteCount.filter( const ratingVotes = voteCount.filter(
(vote) => !vote.optionName.startsWith("initialValue-") (vote) => !vote.optionName.startsWith('initialValue-')
); );
const initialValueVote = voteCount.find((vote) => const initialValueVote = voteCount.find((vote) =>
vote.optionName.startsWith("initialValue-") vote.optionName.startsWith('initialValue-')
); );
if (initialValueVote) { if (initialValueVote) {
// Convert "initialValue-X" to just "X" and add it to the ratingVotes array // Convert "initialValue-X" to just "X" and add it to the ratingVotes array
const initialRating = parseInt( const initialRating = parseInt(
initialValueVote.optionName.split("-")[1], initialValueVote.optionName.split('-')[1],
10 10
); );
ratingVotes.push({ ratingVotes.push({
@ -92,7 +86,7 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => {
setValue(averageRating); setValue(averageRating);
} }
} catch (error) { } catch (error) {
if (error?.message?.includes("POLL_NO_EXISTS")) { if (error?.message?.includes('POLL_NO_EXISTS')) {
setHasPublishedRating(false); setHasPublishedRating(false);
} }
} }
@ -105,45 +99,47 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => {
const rateFunc = async (event, chosenValue, currentValue) => { const rateFunc = async (event, chosenValue, currentValue) => {
try { try {
const newValue = chosenValue || currentValue const newValue = chosenValue || currentValue;
if (!myName) throw new Error("You need a name to rate."); if (!myName) throw new Error('You need a name to rate.');
if (!app?.name) return; if (!app?.name) return;
const fee = await getFee("CREATE_POLL"); const fee = await getFee('CREATE_POLL');
await show({ await show({
message: `Would you like to rate this app a rating of ${newValue}?. It will create a POLL tx.`, message: `Would you like to rate this app a rating of ${newValue}?. It will create a POLL tx.`,
publishFee: fee.fee + " QORT", publishFee: fee.fee + ' QORT',
}); });
if (hasPublishedRating === false) { if (hasPublishedRating === false) {
const pollName = `app-library-${app.service}-rating-${app.name}`; const pollName = `app-library-${app.service}-rating-${app.name}`;
const pollOptions = [`1, 2, 3, 4, 5, initialValue-${newValue}`]; const pollOptions = [`1, 2, 3, 4, 5, initialValue-${newValue}`];
await new Promise((res, rej) => { await new Promise((res, rej) => {
window.sendMessage("createPoll", { window
.sendMessage(
pollName: pollName, 'createPoll',
pollDescription: `Rating for ${app.service} ${app.name}`, {
pollOptions: pollOptions, pollName: pollName,
pollOwnerAddress: myName, pollDescription: `Rating for ${app.service} ${app.name}`,
pollOptions: pollOptions,
}, 60000) pollOwnerAddress: myName,
.then((response) => { },
if (response.error) { 60000
rej(response?.message); )
return; .then((response) => {
} else { if (response.error) {
res(response); rej(response?.message);
setInfoSnack({ return;
type: "success", } else {
message: res(response);
"Successfully rated. Please wait a couple minutes for the network to propogate the changes.", setInfoSnack({
}); type: 'success',
setOpenSnack(true); message:
} 'Successfully rated. Please wait a couple minutes for the network to propogate the changes.',
}) });
.catch((error) => { setOpenSnack(true);
console.error("Failed qortalRequest", error); }
}); })
.catch((error) => {
console.error('Failed qortalRequest', error);
});
}); });
} else { } else {
const pollName = `app-library-${app.service}-rating-${app.name}`; const pollName = `app-library-${app.service}-rating-${app.name}`;
@ -152,39 +148,41 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => {
(option) => +option.optionName === +newValue (option) => +option.optionName === +newValue
); );
if (isNaN(optionIndex) || optionIndex === -1) if (isNaN(optionIndex) || optionIndex === -1)
throw new Error("Cannot find rating option"); throw new Error('Cannot find rating option');
await new Promise((res, rej) => { await new Promise((res, rej) => {
window.sendMessage("voteOnPoll", { window
.sendMessage(
pollName: pollName, 'voteOnPoll',
optionIndex, {
pollName: pollName,
}, 60000) optionIndex,
.then((response) => { },
if (response.error) { 60000
rej(response?.message); )
return; .then((response) => {
} else { if (response.error) {
res(response); rej(response?.message);
setInfoSnack({ return;
type: "success", } else {
message: res(response);
"Successfully rated. Please wait a couple minutes for the network to propogate the changes.", setInfoSnack({
}); type: 'success',
setOpenSnack(true); message:
} 'Successfully rated. Please wait a couple minutes for the network to propogate the changes.',
}) });
.catch((error) => { setOpenSnack(true);
console.error("Failed qortalRequest", error); }
}); })
.catch((error) => {
console.error('Failed qortalRequest', error);
});
}); });
} }
} catch (error) { } catch (error) {
console.log('error', error) console.log('error', error);
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: error?.message || "Unable to rate", message: error?.message || 'Unable to rate',
}); });
setOpenSnack(true); setOpenSnack(true);
} }
@ -194,17 +192,17 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => {
<div> <div>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
flexDirection: ratingCountPosition === "top" ? "column" : "row", flexDirection: ratingCountPosition === 'top' ? 'column' : 'row',
}} }}
> >
{ratingCountPosition === "top" && ( {ratingCountPosition === 'top' && (
<> <>
<AppInfoUserName> <AppInfoUserName>
{(votesInfo?.totalVotes ?? 0) + {(votesInfo?.totalVotes ?? 0) +
(votesInfo?.voteCounts?.length === 6 ? 1 : 0)}{" "} (votesInfo?.voteCounts?.length === 6 ? 1 : 0)}{' '}
{" RATINGS"} {' RATINGS'}
</AppInfoUserName> </AppInfoUserName>
<Spacer height="6px" /> <Spacer height="6px" />
<AppInfoUserName>{value?.toFixed(1)}</AppInfoUserName> <AppInfoUserName>{value?.toFixed(1)}</AppInfoUserName>
@ -214,17 +212,17 @@ export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => {
<Rating <Rating
value={value} value={value}
onChange={(event, rating)=> rateFunc(event, rating, value)} onChange={(event, rating) => rateFunc(event, rating, value)}
precision={1} precision={1}
size="small" size="small"
icon={<StarFilledIcon />} icon={<StarFilledIcon />}
emptyIcon={<StarEmptyIcon />} emptyIcon={<StarEmptyIcon />}
sx={{ sx={{
display: "flex", display: 'flex',
gap: "2px", gap: '2px',
}} }}
/> />
{ratingCountPosition === "right" && ( {ratingCountPosition === 'right' && (
<AppInfoUserName> <AppInfoUserName>
{(votesInfo?.totalVotes ?? 0) + {(votesInfo?.totalVotes ?? 0) +
(votesInfo?.voteCounts?.length === 6 ? 1 : 0)} (votesInfo?.voteCounts?.length === 6 ? 1 : 0)}

View File

@ -18,7 +18,7 @@ export const AppsParent = styled(Box)(({ theme }) => ({
scrollbarWidth: 'none', // Hides the scrollbar in Firefox scrollbarWidth: 'none', // Hides the scrollbar in Firefox
// Optional for better cross-browser consistency // Optional for better cross-browser consistency
'-msOverflowStyle': 'none', // Hides scrollbar in IE and Edge msOverflowStyle: 'none', // Hides scrollbar in IE and Edge
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary, color: theme.palette.text.primary,
@ -56,61 +56,59 @@ export const AppsWidthLimiter = styled(Box)(({ theme }) => ({
})); }));
export const AppsSearchContainer = styled(Box)(({ theme }) => ({ export const AppsSearchContainer = styled(Box)(({ theme }) => ({
display: 'flex',
width: '90%',
justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
borderRadius: '8px',
padding: '0px 10px',
height: '36px',
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
borderRadius: '8px',
color: theme.palette.text.primary, color: theme.palette.text.primary,
display: 'flex',
height: '36px',
justifyContent: 'space-between',
padding: '0px 10px',
width: '90%',
})); }));
export const AppsSearchLeft = styled(Box)(({ theme }) => ({ export const AppsSearchLeft = styled(Box)(({ theme }) => ({
display: 'flex',
width: '90%',
justifyContent: 'flex-start',
alignItems: 'center', alignItems: 'center',
gap: '10px',
flexGrow: 1,
flexShrink: 0,
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary, color: theme.palette.text.primary,
display: 'flex',
flexGrow: 1,
flexShrink: 0,
gap: '10px',
justifyContent: 'flex-start',
width: '90%',
})); }));
export const AppsSearchRight = styled(Box)(({ theme }) => ({ export const AppsSearchRight = styled(Box)(({ theme }) => ({
display: 'flex',
width: '90%',
justifyContent: 'flex-end',
alignItems: 'center', alignItems: 'center',
flexShrink: 1,
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary, color: theme.palette.text.primary,
display: 'flex',
flexShrink: 1,
justifyContent: 'flex-end',
width: '90%',
})); }));
export const AppCircleContainer = styled(Box)(({ theme }) => ({ export const AppCircleContainer = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '5px', gap: '5px',
alignItems: 'center',
width: '100%', width: '100%',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
})); }));
export const Add = styled(Typography)(({ theme }) => ({ export const Add = styled(Typography)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
fontSize: '36px', fontSize: '36px',
fontWeight: 500, fontWeight: 500,
lineHeight: '43.57px', lineHeight: '43.57px',
textAlign: 'left', textAlign: 'left',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
})); }));
export const AppCircleLabel = styled(Typography)(({ theme }) => ({ export const AppCircleLabel = styled(Typography)(({ theme }) => ({
'-webkit-box-orient': 'vertical',
'-webkit-line-clamp': '2',
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary, color: theme.palette.text.primary,
display: '-webkit-box', display: '-webkit-box',
@ -119,15 +117,17 @@ export const AppCircleLabel = styled(Typography)(({ theme }) => ({
lineHeight: 1.2, lineHeight: 1.2,
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: '2',
width: '120%', width: '120%',
})); }));
export const AppLibrarySubTitle = styled(Typography)(({ theme }) => ({ export const AppLibrarySubTitle = styled(Typography)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
fontSize: '16px', fontSize: '16px',
fontWeight: 500, fontWeight: 500,
lineHeight: 1.2, lineHeight: 1.2,
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
})); }));
export const AppCircle = styled(Box)(({ theme }) => ({ export const AppCircle = styled(Box)(({ theme }) => ({
@ -312,58 +312,58 @@ export const PublishQAppCTAButton = styled(ButtonBase)(({ theme }) => ({
})); }));
export const PublishQAppDotsBG = styled(Box)(({ theme }) => ({ export const PublishQAppDotsBG = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
width: '60px',
height: '60px',
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary, color: theme.palette.text.primary,
display: 'flex',
height: '60px',
justifyContent: 'center',
width: '60px',
})); }));
export const PublishQAppInfo = styled(Typography)(({ theme }) => ({ export const PublishQAppInfo = styled(Typography)(({ theme }) => ({
fontSize: '10px',
fontWeight: 400,
lineHeight: 1.2,
fontStyle: 'italic',
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary, color: theme.palette.text.primary,
fontSize: '10px',
fontStyle: 'italic',
fontWeight: 400,
lineHeight: 1.2,
})); }));
export const PublishQAppChoseFile = styled(ButtonBase)(({ theme }) => ({ export const PublishQAppChoseFile = styled(ButtonBase)(({ theme }) => ({
width: '101px',
height: '30px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
borderRadius: '5px',
fontWeight: 600,
fontSize: '10px',
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
borderRadius: '5px',
color: theme.palette.text.primary, color: theme.palette.text.primary,
display: 'flex',
fontSize: '10px',
fontWeight: 600,
height: '30px',
justifyContent: 'center',
width: '101px',
})); }));
export const AppsCategoryInfo = styled(Box)(({ theme }) => ({ export const AppsCategoryInfo = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center', alignItems: 'center',
width: '100%',
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary, color: theme.palette.text.primary,
display: 'flex',
width: '100%',
})); }));
export const AppsCategoryInfoSub = styled(Box)(({ theme }) => ({ export const AppsCategoryInfoSub = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary, color: theme.palette.text.primary,
display: 'flex',
flexDirection: 'column',
})); }));
export const AppsCategoryInfoLabel = styled(Typography)(({ theme }) => ({ export const AppsCategoryInfoLabel = styled(Typography)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
fontSize: '12px', fontSize: '12px',
fontWeight: 700, fontWeight: 700,
lineHeight: 1.2, lineHeight: 1.2,
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
})); }));
export const AppsCategoryInfoValue = styled(Typography)(({ theme }) => ({ export const AppsCategoryInfoValue = styled(Typography)(({ theme }) => ({

View File

@ -1,45 +1,47 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { AppsHome } from "./AppsHome"; import { AppsHome } from './AppsHome';
import { Spacer } from "../../common/Spacer"; import { Spacer } from '../../common/Spacer';
import { getBaseApiReact } from "../../App"; import { getBaseApiReact } from '../../App';
import { AppInfo } from "./AppInfo"; import { AppInfo } from './AppInfo';
import { import {
executeEvent, executeEvent,
subscribeToEvent, subscribeToEvent,
unsubscribeFromEvent, unsubscribeFromEvent,
} from "../../utils/events"; } from '../../utils/events';
import { AppsParent } from "./Apps-styles"; import { AppsParent } from './Apps-styles';
import AppViewerContainer from "./AppViewerContainer"; import AppViewerContainer from './AppViewerContainer';
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from 'short-unique-id';
import { AppPublish } from "./AppPublish"; import { AppPublish } from './AppPublish';
import { AppsCategory } from "./AppsCategory"; import { AppsCategory } from './AppsCategory';
import { AppsLibrary } from "./AppsLibrary"; import { AppsLibrary } from './AppsLibrary';
const uid = new ShortUniqueId({ length: 8 }); const uid = new ShortUniqueId({ length: 8 });
export const Apps = ({ mode, setMode, show , myName}) => { export const Apps = ({ mode, setMode, show, myName }) => {
const [availableQapps, setAvailableQapps] = useState([]); const [availableQapps, setAvailableQapps] = useState([]);
const [selectedAppInfo, setSelectedAppInfo] = useState(null); const [selectedAppInfo, setSelectedAppInfo] = useState(null);
const [selectedCategory, setSelectedCategory] = useState(null) const [selectedCategory, setSelectedCategory] = useState(null);
const [tabs, setTabs] = useState([]); const [tabs, setTabs] = useState([]);
const [selectedTab, setSelectedTab] = useState(null); const [selectedTab, setSelectedTab] = useState(null);
const [isNewTabWindow, setIsNewTabWindow] = useState(false); const [isNewTabWindow, setIsNewTabWindow] = useState(false);
const [categories, setCategories] = useState([]) const [categories, setCategories] = useState([]);
const iframeRefs = useRef({}); const iframeRefs = useRef({});
const myApp = useMemo(() => {
return availableQapps.find(
(app) => app.name === myName && app.service === 'APP'
);
}, [myName, availableQapps]);
const myApp = useMemo(()=> { const myWebsite = useMemo(() => {
return availableQapps.find(
return availableQapps.find((app)=> app.name === myName && app.service === 'APP') (app) => app.name === myName && app.service === 'WEBSITE'
}, [myName, availableQapps]) );
const myWebsite = useMemo(()=> { }, [myName, availableQapps]);
return availableQapps.find((app)=> app.name === myName && app.service === 'WEBSITE')
}, [myName, availableQapps])
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
executeEvent("setTabsToNav", { executeEvent('setTabsToNav', {
data: { data: {
tabs: tabs, tabs: tabs,
selectedTab: selectedTab, selectedTab: selectedTab,
@ -54,17 +56,17 @@ export const Apps = ({ mode, setMode, show , myName}) => {
const url = `${getBaseApiReact()}/arbitrary/categories`; const url = `${getBaseApiReact()}/arbitrary/categories`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
if (!response?.ok) return; if (!response?.ok) return;
const responseData = await response.json(); const responseData = await response.json();
setCategories(responseData); setCategories(responseData);
} catch (error) { } catch (error) {
console.log(error);
} finally { } finally {
// dispatch(setIsLoadingGlobal(false)) // dispatch(setIsLoadingGlobal(false))
} }
@ -78,9 +80,9 @@ export const Apps = ({ mode, setMode, show , myName}) => {
const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&limit=0&includestatus=true&includemetadata=true`; const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&limit=0&includestatus=true&includemetadata=true`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
if (!response?.ok) return; if (!response?.ok) return;
@ -88,75 +90,84 @@ export const Apps = ({ mode, setMode, show , myName}) => {
const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&limit=0&includestatus=true&includemetadata=true`; const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&limit=0&includestatus=true&includemetadata=true`;
const responseWebsites = await fetch(urlWebsites, { const responseWebsites = await fetch(urlWebsites, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
if (!responseWebsites?.ok) return; if (!responseWebsites?.ok) return;
const responseDataWebsites = await responseWebsites.json(); const responseDataWebsites = await responseWebsites.json();
apps = responseData; apps = responseData;
websites = responseDataWebsites; websites = responseDataWebsites;
const combine = [...apps, ...websites]; const combine = [...apps, ...websites];
setAvailableQapps(combine); setAvailableQapps(combine);
} catch (error) { } catch (error) {
console.log(error);
} finally { } finally {
// dispatch(setIsLoadingGlobal(false)) // dispatch(setIsLoadingGlobal(false))
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
getQapps(); getQapps();
getCategories() getCategories();
}, [getQapps, getCategories]); }, [getQapps, getCategories]);
const selectedAppInfoFunc = (e) => { const selectedAppInfoFunc = (e) => {
const data = e.detail?.data; const data = e.detail?.data;
setSelectedAppInfo(data); setSelectedAppInfo(data);
setMode("appInfo"); setMode('appInfo');
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("selectedAppInfo", selectedAppInfoFunc); subscribeToEvent('selectedAppInfo', selectedAppInfoFunc);
return () => { return () => {
unsubscribeFromEvent("selectedAppInfo", selectedAppInfoFunc); unsubscribeFromEvent('selectedAppInfo', selectedAppInfoFunc);
}; };
}, []); }, []);
const selectedAppInfoCategoryFunc = (e) => { const selectedAppInfoCategoryFunc = (e) => {
const data = e.detail?.data; const data = e.detail?.data;
setSelectedAppInfo(data); setSelectedAppInfo(data);
setMode("appInfo-from-category"); setMode('appInfo-from-category');
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc); subscribeToEvent('selectedAppInfoCategory', selectedAppInfoCategoryFunc);
return () => { return () => {
unsubscribeFromEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc); unsubscribeFromEvent(
'selectedAppInfoCategory',
selectedAppInfoCategoryFunc
);
}; };
}, []); }, []);
const selectedCategoryFunc = (e) => { const selectedCategoryFunc = (e) => {
const data = e.detail?.data; const data = e.detail?.data;
setSelectedCategory(data); setSelectedCategory(data);
setMode("category"); setMode('category');
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("selectedCategory", selectedCategoryFunc); subscribeToEvent('selectedCategory', selectedCategoryFunc);
return () => { return () => {
unsubscribeFromEvent("selectedCategory", selectedCategoryFunc); unsubscribeFromEvent('selectedCategory', selectedCategoryFunc);
}; };
}, []); }, []);
const navigateBackFunc = (e) => { const navigateBackFunc = (e) => {
if (['category', 'appInfo-from-category', 'appInfo', 'library', 'publish'].includes(mode)) { if (
[
'category',
'appInfo-from-category',
'appInfo',
'library',
'publish',
].includes(mode)
) {
// Handle the various modes as needed // Handle the various modes as needed
if (mode === 'category') { if (mode === 'category') {
setMode('library'); setMode('library');
@ -174,16 +185,16 @@ export const Apps = ({ mode, setMode, show , myName}) => {
} else if (mode === 'publish') { } else if (mode === 'publish') {
setMode('library'); setMode('library');
} }
} else if(selectedTab?.tabId) { } else if (selectedTab?.tabId) {
executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {}) executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {});
} }
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("navigateBack", navigateBackFunc); subscribeToEvent('navigateBack', navigateBackFunc);
return () => { return () => {
unsubscribeFromEvent("navigateBack", navigateBackFunc); unsubscribeFromEvent('navigateBack', navigateBackFunc);
}; };
}, [mode, selectedTab]); }, [mode, selectedTab]);
@ -195,16 +206,16 @@ export const Apps = ({ mode, setMode, show , myName}) => {
}; };
setTabs((prev) => [...prev, newTab]); setTabs((prev) => [...prev, newTab]);
setSelectedTab(newTab); setSelectedTab(newTab);
setMode("viewer"); setMode('viewer');
setIsNewTabWindow(false); setIsNewTabWindow(false);
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("addTab", addTabFunc); subscribeToEvent('addTab', addTabFunc);
return () => { return () => {
unsubscribeFromEvent("addTab", addTabFunc); unsubscribeFromEvent('addTab', addTabFunc);
}; };
}, [tabs]); }, [tabs]);
@ -213,7 +224,7 @@ export const Apps = ({ mode, setMode, show , myName}) => {
setSelectedTab(data); setSelectedTab(data);
setTimeout(() => { setTimeout(() => {
executeEvent("setTabsToNav", { executeEvent('setTabsToNav', {
data: { data: {
tabs: tabs, tabs: tabs,
selectedTab: data, selectedTab: data,
@ -225,10 +236,10 @@ export const Apps = ({ mode, setMode, show , myName}) => {
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("setSelectedTab", setSelectedTabFunc); subscribeToEvent('setSelectedTab', setSelectedTabFunc);
return () => { return () => {
unsubscribeFromEvent("setSelectedTab", setSelectedTabFunc); unsubscribeFromEvent('setSelectedTab', setSelectedTabFunc);
}; };
}, [tabs, isNewTabWindow]); }, [tabs, isNewTabWindow]);
@ -236,14 +247,14 @@ export const Apps = ({ mode, setMode, show , myName}) => {
const data = e.detail?.data; const data = e.detail?.data;
const copyTabs = [...tabs].filter((tab) => tab?.tabId !== data?.tabId); const copyTabs = [...tabs].filter((tab) => tab?.tabId !== data?.tabId);
if (copyTabs?.length === 0) { if (copyTabs?.length === 0) {
setMode("home"); setMode('home');
} else { } else {
setSelectedTab(copyTabs[0]); setSelectedTab(copyTabs[0]);
} }
setTabs(copyTabs); setTabs(copyTabs);
setSelectedTab(copyTabs[0]); setSelectedTab(copyTabs[0]);
setTimeout(() => { setTimeout(() => {
executeEvent("setTabsToNav", { executeEvent('setTabsToNav', {
data: { data: {
tabs: copyTabs, tabs: copyTabs,
selectedTab: copyTabs[0], selectedTab: copyTabs[0],
@ -253,59 +264,74 @@ export const Apps = ({ mode, setMode, show , myName}) => {
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("removeTab", removeTabFunc); subscribeToEvent('removeTab', removeTabFunc);
return () => { return () => {
unsubscribeFromEvent("removeTab", removeTabFunc); unsubscribeFromEvent('removeTab', removeTabFunc);
}; };
}, [tabs]); }, [tabs]);
const setNewTabWindowFunc = (e) => { const setNewTabWindowFunc = (e) => {
setIsNewTabWindow(true); setIsNewTabWindow(true);
setSelectedTab(null) setSelectedTab(null);
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("newTabWindow", setNewTabWindowFunc); subscribeToEvent('newTabWindow', setNewTabWindowFunc);
return () => { return () => {
unsubscribeFromEvent("newTabWindow", setNewTabWindowFunc); unsubscribeFromEvent('newTabWindow', setNewTabWindowFunc);
}; };
}, [tabs]); }, [tabs]);
return ( return (
<AppsParent <AppsParent
sx={{ sx={{
display: !show && "none", display: !show && 'none',
}} }}
> >
{mode !== "viewer" && !selectedTab && <Spacer height="30px" />} {mode !== 'viewer' && !selectedTab && <Spacer height="30px" />}
{mode === "home" && ( {mode === 'home' && (
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHome
)}
<AppsLibrary
isShow={mode === "library" && !selectedTab}
availableQapps={availableQapps} availableQapps={availableQapps}
setMode={setMode} setMode={setMode}
myName={myName} myApp={myApp}
hasPublishApp={!!(myApp || myWebsite)} myWebsite={myWebsite}
categories={categories}
/> />
)}
{mode === "appInfo" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />}
{mode === "appInfo-from-category" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />} <AppsLibrary
<AppsCategory availableQapps={availableQapps} isShow={mode === 'category' && !selectedTab} category={selectedCategory} myName={myName} /> isShow={mode === 'library' && !selectedTab}
{mode === "publish" && !selectedTab && <AppPublish names={myName ? [myName] : []} categories={categories} />} availableQapps={availableQapps}
setMode={setMode}
myName={myName}
hasPublishApp={!!(myApp || myWebsite)}
categories={categories}
/>
{mode === 'appInfo' && !selectedTab && (
<AppInfo app={selectedAppInfo} myName={myName} />
)}
{mode === 'appInfo-from-category' && !selectedTab && (
<AppInfo app={selectedAppInfo} myName={myName} />
)}
<AppsCategory
availableQapps={availableQapps}
isShow={mode === 'category' && !selectedTab}
category={selectedCategory}
myName={myName}
/>
{mode === 'publish' && !selectedTab && (
<AppPublish names={myName ? [myName] : []} categories={categories} />
)}
{tabs.map((tab) => { {tabs.map((tab) => {
if (!iframeRefs.current[tab.tabId]) { if (!iframeRefs.current[tab.tabId]) {
iframeRefs.current[tab.tabId] = React.createRef(); iframeRefs.current[tab.tabId] = React.createRef();
} }
return ( return (
<AppViewerContainer <AppViewerContainer
key={tab?.tabId} key={tab?.tabId}
hide={isNewTabWindow} hide={isNewTabWindow}
isSelected={tab?.tabId === selectedTab?.tabId} isSelected={tab?.tabId === selectedTab?.tabId}
app={tab} app={tab}
@ -314,13 +340,18 @@ export const Apps = ({ mode, setMode, show , myName}) => {
); );
})} })}
{isNewTabWindow && mode === "viewer" && ( {isNewTabWindow && mode === 'viewer' && (
<> <>
<Spacer height="30px" /> <Spacer height="30px" />
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} /> <AppsHome
availableQapps={availableQapps}
setMode={setMode}
myApp={myApp}
myWebsite={myWebsite}
/>
</> </>
)} )}
{mode !== "viewer" && !selectedTab && <Spacer height="180px" />} {mode !== 'viewer' && !selectedTab && <Spacer height="180px" />}
</AppsParent> </AppsParent>
); );
}; };

View File

@ -42,7 +42,8 @@ const officialAppList = [
"q-support", "q-support",
"q-manager", "q-manager",
"q-wallets", "q-wallets",
"q-search" "q-search",
"q-nodecontrol"
]; ];
const ScrollerStyled = styled('div')({ const ScrollerStyled = styled('div')({

View File

@ -5,7 +5,7 @@ import React, {
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from "react"; } from 'react';
import { import {
AppCircle, AppCircle,
AppCircleContainer, AppCircleContainer,
@ -23,67 +23,71 @@ import {
PublishQAppCTAParent, PublishQAppCTAParent,
PublishQAppCTARight, PublishQAppCTARight,
PublishQAppDotsBG, PublishQAppDotsBG,
} from "./Apps-styles"; } from './Apps-styles';
import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material"; import { Avatar, Box, ButtonBase, InputBase, styled } from '@mui/material';
import { Add } from "@mui/icons-material"; import { Add } from '@mui/icons-material';
import { MyContext, getBaseApiReact } from "../../App"; import { MyContext, getBaseApiReact } from '../../App';
import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import LogoSelected from '../../assets/svgs/LogoSelected.svg';
import IconSearch from "../../assets/svgs/Search.svg"; import IconSearch from '../../assets/svgs/Search.svg';
import IconClearInput from "../../assets/svgs/ClearInput.svg"; import IconClearInput from '../../assets/svgs/ClearInput.svg';
import qappDevelopText from "../../assets/svgs/qappDevelopText.svg"; import qappDevelopText from '../../assets/svgs/qappDevelopText.svg';
import qappDots from "../../assets/svgs/qappDots.svg"; import qappDots from '../../assets/svgs/qappDots.svg';
import { Spacer } from "../../common/Spacer"; import { Spacer } from '../../common/Spacer';
import { AppInfoSnippet } from "./AppInfoSnippet"; import { AppInfoSnippet } from './AppInfoSnippet';
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from 'react-virtuoso';
import { executeEvent } from "../../utils/events"; import { executeEvent } from '../../utils/events';
import { AppsDesktopLibraryBody, AppsDesktopLibraryHeader } from "./AppsDesktop-styles"; import {
AppsDesktopLibraryBody,
AppsDesktopLibraryHeader,
} from './AppsDesktop-styles';
const officialAppList = [ const officialAppList = [
"q-tube", 'q-tube',
"q-blog", 'q-blog',
"q-share", 'q-share',
"q-support", 'q-support',
"q-mail", 'q-mail',
"q-fund", 'q-fund',
"q-shop", 'q-shop',
"q-trade", 'q-trade',
"q-support", 'q-support',
"q-manager", 'q-manager',
"q-wallets", 'q-wallets',
"q-search" 'q-search',
'q-nodecontrol',
]; ];
const ScrollerStyled = styled("div")({ const ScrollerStyled = styled('div')({
// Hide scrollbar for WebKit browsers (Chrome, Safari) // Hide scrollbar for WebKit browsers (Chrome, Safari)
"::-webkit-scrollbar": { '::-webkit-scrollbar': {
width: "0px", width: '0px',
height: "0px", height: '0px',
}, },
// Hide scrollbar for Firefox // Hide scrollbar for Firefox
scrollbarWidth: "none", scrollbarWidth: 'none',
// Hide scrollbar for IE and older Edge // Hide scrollbar for IE and older Edge
"-msOverflowStyle": "none", '-msOverflowStyle': 'none',
}); });
const StyledVirtuosoContainer = styled("div")({ const StyledVirtuosoContainer = styled('div')({
position: "relative", position: 'relative',
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
// Hide scrollbar for WebKit browsers (Chrome, Safari) // Hide scrollbar for WebKit browsers (Chrome, Safari)
"::-webkit-scrollbar": { '::-webkit-scrollbar': {
width: "0px", width: '0px',
height: "0px", height: '0px',
}, },
// Hide scrollbar for Firefox // Hide scrollbar for Firefox
scrollbarWidth: "none", scrollbarWidth: 'none',
// Hide scrollbar for IE and older Edge // Hide scrollbar for IE and older Edge
"-msOverflowStyle": "none", '-msOverflowStyle': 'none',
}); });
export const AppsCategoryDesktop = ({ export const AppsCategoryDesktop = ({
@ -92,29 +96,28 @@ export const AppsCategoryDesktop = ({
category, category,
isShow, isShow,
}) => { }) => {
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState('');
const virtuosoRef = useRef(); const virtuosoRef = useRef();
const { rootHeight } = useContext(MyContext); const { rootHeight } = useContext(MyContext);
const categoryList = useMemo(() => { const categoryList = useMemo(() => {
if(category?.id === 'all') return availableQapps if (category?.id === 'all') return availableQapps;
return availableQapps.filter( return availableQapps.filter(
(app) => app?.metadata?.category === category?.id (app) => app?.metadata?.category === category?.id
); );
}, [availableQapps, category]); }, [availableQapps, category]);
const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value const [debouncedValue, setDebouncedValue] = useState(''); // Debounced value
// Debounce logic // Debounce logic
useEffect(() => { useEffect(() => {
const handler = setTimeout(() => { const handler = setTimeout(() => {
setDebouncedValue(searchValue); setDebouncedValue(searchValue);
}, 350); }, 350);
setTimeout(() => { setTimeout(() => {
virtuosoRef.current.scrollToIndex({ if (virtuosoRef.current) {
index: 0 virtuosoRef.current.scrollToIndex({ index: 0 });
}); }
}, 500); }, 500);
// Cleanup timeout if searchValue changes before the timeout completes // Cleanup timeout if searchValue changes before the timeout completes
return () => { return () => {
@ -126,8 +129,13 @@ export const AppsCategoryDesktop = ({
const searchedList = useMemo(() => { const searchedList = useMemo(() => {
if (!debouncedValue) return categoryList; if (!debouncedValue) return categoryList;
return categoryList.filter((app) => return categoryList.filter(
app.name.toLowerCase().includes(debouncedValue.toLowerCase()) || (app?.metadata?.title && app?.metadata?.title?.toLowerCase().includes(debouncedValue.toLowerCase())) (app) =>
app.name.toLowerCase().includes(debouncedValue.toLowerCase()) ||
(app?.metadata?.title &&
app?.metadata?.title
?.toLowerCase()
.includes(debouncedValue.toLowerCase()))
); );
}, [debouncedValue, categoryList]); }, [debouncedValue, categoryList]);
@ -140,7 +148,7 @@ export const AppsCategoryDesktop = ({
myName={myName} myName={myName}
isFromCategory={true} isFromCategory={true}
parentStyles={{ parentStyles={{
padding: '0px 10px' padding: '0px 10px',
}} }}
/> />
); );
@ -149,27 +157,29 @@ export const AppsCategoryDesktop = ({
return ( return (
<AppsLibraryContainer <AppsLibraryContainer
sx={{ sx={{
display: !isShow && "none", display: !isShow && 'none',
padding: "0px", padding: '0px',
height: "100vh", height: '100vh',
overflow: "hidden", overflow: 'hidden',
paddingTop: "30px", paddingTop: '30px',
}} }}
> >
<AppsDesktopLibraryHeader <AppsDesktopLibraryHeader
sx={{ sx={{
maxWidth: "1500px", maxWidth: '1500px',
width: "90%", width: '90%',
}} }}
> >
<AppsWidthLimiter <AppsWidthLimiter
sx={{ sx={{
alignItems: "flex-end", alignItems: 'flex-end',
}} }}
> >
<AppsSearchContainer sx={{ <AppsSearchContainer
width: "412px", sx={{
}}> width: '412px',
}}
>
<AppsSearchLeft> <AppsSearchLeft>
<img src={IconSearch} /> <img src={IconSearch} />
<InputBase <InputBase
@ -178,8 +188,8 @@ export const AppsCategoryDesktop = ({
sx={{ ml: 1, flex: 1 }} sx={{ ml: 1, flex: 1 }}
placeholder="Search for apps" placeholder="Search for apps"
inputProps={{ inputProps={{
"aria-label": "Search for apps", 'aria-label': 'Search for apps',
fontSize: "16px", fontSize: '16px',
fontWeight: 400, fontWeight: 400,
}} }}
/> />
@ -188,7 +198,7 @@ export const AppsCategoryDesktop = ({
{searchValue && ( {searchValue && (
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
setSearchValue(""); setSearchValue('');
}} }}
> >
<img src={IconClearInput} /> <img src={IconClearInput} />
@ -201,9 +211,9 @@ export const AppsCategoryDesktop = ({
<AppsDesktopLibraryBody <AppsDesktopLibraryBody
sx={{ sx={{
height: `calc(100vh - 36px)`, height: `calc(100vh - 36px)`,
overflow: "auto", overflow: 'auto',
padding: "0px", padding: '0px',
alignItems: "center", alignItems: 'center',
}} }}
> >
<Spacer height="25px" /> <Spacer height="25px" />
@ -214,7 +224,7 @@ export const AppsCategoryDesktop = ({
</AppsWidthLimiter> </AppsWidthLimiter>
<AppsWidthLimiter> <AppsWidthLimiter>
<StyledVirtuosoContainer <StyledVirtuosoContainer
sx={{ sx={{
height: `calc(100vh - 36px - 90px - 25px)`, height: `calc(100vh - 36px - 90px - 25px)`,
}} }}
> >

View File

@ -1,34 +1,29 @@
import React, { import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
useContext, import { AppsHomeDesktop } from './AppsHomeDesktop';
useEffect, import { Spacer } from '../../common/Spacer';
useMemo, import { GlobalContext, getBaseApiReact } from '../../App';
useRef, import { AppInfo } from './AppInfo';
useState,
} from "react";
import { AppsHomeDesktop } from "./AppsHomeDesktop";
import { Spacer } from "../../common/Spacer";
import { GlobalContext, getBaseApiReact } from "../../App";
import { AppInfo } from "./AppInfo";
import { import {
executeEvent, executeEvent,
subscribeToEvent, subscribeToEvent,
unsubscribeFromEvent, unsubscribeFromEvent,
} from "../../utils/events"; } from '../../utils/events';
import { AppsParent } from "./Apps-styles"; import { AppsParent } from './Apps-styles';
import AppViewerContainer from "./AppViewerContainer"; import AppViewerContainer from './AppViewerContainer';
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from 'short-unique-id';
import { AppPublish } from "./AppPublish"; import { AppPublish } from './AppPublish';
import { AppsLibraryDesktop } from "./AppsLibraryDesktop"; import { AppsLibraryDesktop } from './AppsLibraryDesktop';
import { AppsCategoryDesktop } from "./AppsCategoryDesktop"; import { AppsCategoryDesktop } from './AppsCategoryDesktop';
import { AppsNavBarDesktop } from "./AppsNavBarDesktop"; import { AppsNavBarDesktop } from './AppsNavBarDesktop';
import { Box, ButtonBase, useTheme } from "@mui/material"; import { Box, ButtonBase, useTheme } from '@mui/material';
import { HomeIcon } from "../../assets/Icons/HomeIcon"; import { HomeIcon } from '../../assets/Icons/HomeIcon';
import { MessagingIcon } from "../../assets/Icons/MessagingIcon"; import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
import { Save } from "../Save/Save"; import { Save } from '../Save/Save';
import { IconWrapper } from "../Desktop/DesktopFooter"; import { IconWrapper } from '../Desktop/DesktopFooter';
import { useRecoilState } from "recoil"; import { useRecoilState } from 'recoil';
import { enabledDevModeAtom } from "../../atoms/global"; import { enabledDevModeAtom } from '../../atoms/global';
import { AppsIcon } from "../../assets/Icons/AppsIcon"; import { AppsIcon } from '../../assets/Icons/AppsIcon';
import { CoreSyncStatus } from '../CoreSyncStatus';
const uid = new ShortUniqueId({ length: 8 }); const uid = new ShortUniqueId({ length: 8 });
@ -58,25 +53,25 @@ export const AppsDesktop = ({
const myApp = useMemo(() => { const myApp = useMemo(() => {
return availableQapps.find( return availableQapps.find(
(app) => app.name === myName && app.service === "APP" (app) => app.name === myName && app.service === 'APP'
); );
}, [myName, availableQapps]); }, [myName, availableQapps]);
const myWebsite = useMemo(() => { const myWebsite = useMemo(() => {
return availableQapps.find( return availableQapps.find(
(app) => app.name === myName && app.service === "WEBSITE" (app) => app.name === myName && app.service === 'WEBSITE'
); );
}, [myName, availableQapps]); }, [myName, availableQapps]);
useEffect(() => { useEffect(() => {
if (show) { if (show) {
showTutorial("qapps"); showTutorial('qapps');
} }
}, [show]); }, [show]);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
executeEvent("setTabsToNav", { executeEvent('setTabsToNav', {
data: { data: {
tabs: tabs, tabs: tabs,
selectedTab: selectedTab, selectedTab: selectedTab,
@ -91,9 +86,9 @@ export const AppsDesktop = ({
const url = `${getBaseApiReact()}/arbitrary/categories`; const url = `${getBaseApiReact()}/arbitrary/categories`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
if (!response?.ok) return; if (!response?.ok) return;
@ -115,9 +110,9 @@ export const AppsDesktop = ({
const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&limit=0&includestatus=true&includemetadata=true`; const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&limit=0&includestatus=true&includemetadata=true`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
if (!response?.ok) return; if (!response?.ok) return;
@ -125,9 +120,9 @@ export const AppsDesktop = ({
const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&limit=0&includestatus=true&includemetadata=true`; const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&limit=0&includestatus=true&includemetadata=true`;
const responseWebsites = await fetch(urlWebsites, { const responseWebsites = await fetch(urlWebsites, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
if (!responseWebsites?.ok) return; if (!responseWebsites?.ok) return;
@ -150,9 +145,12 @@ export const AppsDesktop = ({
useEffect(() => { useEffect(() => {
getQapps(); getQapps();
const interval = setInterval(() => { const interval = setInterval(
getQapps(); () => {
}, 20 * 60 * 1000); // 20 minutes in milliseconds getQapps();
},
20 * 60 * 1000
); // 20 minutes in milliseconds
return () => clearInterval(interval); return () => clearInterval(interval);
}, [getQapps]); }, [getQapps]);
@ -160,29 +158,29 @@ export const AppsDesktop = ({
const selectedAppInfoFunc = (e) => { const selectedAppInfoFunc = (e) => {
const data = e.detail?.data; const data = e.detail?.data;
setSelectedAppInfo(data); setSelectedAppInfo(data);
setMode("appInfo"); setMode('appInfo');
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("selectedAppInfo", selectedAppInfoFunc); subscribeToEvent('selectedAppInfo', selectedAppInfoFunc);
return () => { return () => {
unsubscribeFromEvent("selectedAppInfo", selectedAppInfoFunc); unsubscribeFromEvent('selectedAppInfo', selectedAppInfoFunc);
}; };
}, []); }, []);
const selectedAppInfoCategoryFunc = (e) => { const selectedAppInfoCategoryFunc = (e) => {
const data = e.detail?.data; const data = e.detail?.data;
setSelectedAppInfo(data); setSelectedAppInfo(data);
setMode("appInfo-from-category"); setMode('appInfo-from-category');
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc); subscribeToEvent('selectedAppInfoCategory', selectedAppInfoCategoryFunc);
return () => { return () => {
unsubscribeFromEvent( unsubscribeFromEvent(
"selectedAppInfoCategory", 'selectedAppInfoCategory',
selectedAppInfoCategoryFunc selectedAppInfoCategoryFunc
); );
}; };
@ -191,43 +189,43 @@ export const AppsDesktop = ({
const selectedCategoryFunc = (e) => { const selectedCategoryFunc = (e) => {
const data = e.detail?.data; const data = e.detail?.data;
setSelectedCategory(data); setSelectedCategory(data);
setMode("category"); setMode('category');
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("selectedCategory", selectedCategoryFunc); subscribeToEvent('selectedCategory', selectedCategoryFunc);
return () => { return () => {
unsubscribeFromEvent("selectedCategory", selectedCategoryFunc); unsubscribeFromEvent('selectedCategory', selectedCategoryFunc);
}; };
}, []); }, []);
const navigateBackFunc = (e) => { const navigateBackFunc = (e) => {
if ( if (
[ [
"category", 'category',
"appInfo-from-category", 'appInfo-from-category',
"appInfo", 'appInfo',
"library", 'library',
"publish", 'publish',
].includes(mode) ].includes(mode)
) { ) {
// Handle the various modes as needed // Handle the various modes as needed
if (mode === "category") { if (mode === 'category') {
setMode("library"); setMode('library');
setSelectedCategory(null); setSelectedCategory(null);
} else if (mode === "appInfo-from-category") { } else if (mode === 'appInfo-from-category') {
setMode("category"); setMode('category');
} else if (mode === "appInfo") { } else if (mode === 'appInfo') {
setMode("library"); setMode('library');
} else if (mode === "library") { } else if (mode === 'library') {
if (isNewTabWindow) { if (isNewTabWindow) {
setMode("viewer"); setMode('viewer');
} else { } else {
setMode("home"); setMode('home');
} }
} else if (mode === "publish") { } else if (mode === 'publish') {
setMode("library"); setMode('library');
} }
} else if (selectedTab?.tabId) { } else if (selectedTab?.tabId) {
executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {}); executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {});
@ -235,10 +233,10 @@ export const AppsDesktop = ({
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("navigateBack", navigateBackFunc); subscribeToEvent('navigateBack', navigateBackFunc);
return () => { return () => {
unsubscribeFromEvent("navigateBack", navigateBackFunc); unsubscribeFromEvent('navigateBack', navigateBackFunc);
}; };
}, [mode, selectedTab]); }, [mode, selectedTab]);
@ -250,16 +248,16 @@ export const AppsDesktop = ({
}; };
setTabs((prev) => [...prev, newTab]); setTabs((prev) => [...prev, newTab]);
setSelectedTab(newTab); setSelectedTab(newTab);
setMode("viewer"); setMode('viewer');
setIsNewTabWindow(false); setIsNewTabWindow(false);
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("addTab", addTabFunc); subscribeToEvent('addTab', addTabFunc);
return () => { return () => {
unsubscribeFromEvent("addTab", addTabFunc); unsubscribeFromEvent('addTab', addTabFunc);
}; };
}, [tabs]); }, [tabs]);
const setSelectedTabFunc = (e) => { const setSelectedTabFunc = (e) => {
@ -268,7 +266,7 @@ export const AppsDesktop = ({
setSelectedTab(data); setSelectedTab(data);
setTimeout(() => { setTimeout(() => {
executeEvent("setTabsToNav", { executeEvent('setTabsToNav', {
data: { data: {
tabs: tabs, tabs: tabs,
selectedTab: data, selectedTab: data,
@ -280,10 +278,10 @@ export const AppsDesktop = ({
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("setSelectedTab", setSelectedTabFunc); subscribeToEvent('setSelectedTab', setSelectedTabFunc);
return () => { return () => {
unsubscribeFromEvent("setSelectedTab", setSelectedTabFunc); unsubscribeFromEvent('setSelectedTab', setSelectedTabFunc);
}; };
}, [tabs, isNewTabWindow]); }, [tabs, isNewTabWindow]);
@ -291,14 +289,14 @@ export const AppsDesktop = ({
const data = e.detail?.data; const data = e.detail?.data;
const copyTabs = [...tabs].filter((tab) => tab?.tabId !== data?.tabId); const copyTabs = [...tabs].filter((tab) => tab?.tabId !== data?.tabId);
if (copyTabs?.length === 0) { if (copyTabs?.length === 0) {
setMode("home"); setMode('home');
} else { } else {
setSelectedTab(copyTabs[0]); setSelectedTab(copyTabs[0]);
} }
setTabs(copyTabs); setTabs(copyTabs);
setSelectedTab(copyTabs[0]); setSelectedTab(copyTabs[0]);
setTimeout(() => { setTimeout(() => {
executeEvent("setTabsToNav", { executeEvent('setTabsToNav', {
data: { data: {
tabs: copyTabs, tabs: copyTabs,
selectedTab: copyTabs[0], selectedTab: copyTabs[0],
@ -308,10 +306,10 @@ export const AppsDesktop = ({
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("removeTab", removeTabFunc); subscribeToEvent('removeTab', removeTabFunc);
return () => { return () => {
unsubscribeFromEvent("removeTab", removeTabFunc); unsubscribeFromEvent('removeTab', removeTabFunc);
}; };
}, [tabs]); }, [tabs]);
@ -321,10 +319,10 @@ export const AppsDesktop = ({
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("newTabWindow", setNewTabWindowFunc); subscribeToEvent('newTabWindow', setNewTabWindowFunc);
return () => { return () => {
unsubscribeFromEvent("newTabWindow", setNewTabWindowFunc); unsubscribeFromEvent('newTabWindow', setNewTabWindowFunc);
}; };
}, [tabs]); }, [tabs]);
@ -333,24 +331,33 @@ export const AppsDesktop = ({
sx={{ sx={{
position: !show && 'fixed', position: !show && 'fixed',
left: !show && '-200vw', left: !show && '-200vw',
flexDirection: 'row' flexDirection: 'row',
}} }}
> >
<Box <Box
sx={{ sx={{
width: "60px", alignItems: 'center',
flexDirection: "column", display: 'flex',
height: "100vh", flexDirection: 'column',
alignItems: "center", gap: '25px',
display: "flex", height: '100vh',
gap: "25px", width: '60px',
}} }}
> >
<ButtonBase <ButtonBase
sx={{ sx={{
width: "60px", width: '70px',
height: "60px", height: '70px',
paddingTop: "23px", paddingTop: '23px',
}}
>
<CoreSyncStatus />
</ButtonBase>
<ButtonBase
sx={{
width: '60px',
height: '60px',
}} }}
onClick={() => { onClick={() => {
goToHome(); goToHome();
@ -361,7 +368,7 @@ export const AppsDesktop = ({
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
setDesktopViewMode("apps"); setDesktopViewMode('apps');
}} }}
> >
<IconWrapper label="Apps" disableWidth> <IconWrapper label="Apps" disableWidth>
@ -371,13 +378,13 @@ export const AppsDesktop = ({
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
setDesktopViewMode("chat"); setDesktopViewMode('chat');
}} }}
> >
<IconWrapper <IconWrapper
color={ color={
hasUnreadDirects || hasUnreadGroups hasUnreadDirects || hasUnreadGroups
? "var(--unread)" ? 'var(--unread)'
: theme.palette.text.primary : theme.palette.text.primary
} }
label="Chat" label="Chat"
@ -387,7 +394,7 @@ export const AppsDesktop = ({
height={30} height={30}
color={ color={
hasUnreadDirects || hasUnreadGroups hasUnreadDirects || hasUnreadGroups
? "var(--unread)" ? 'var(--unread)'
: theme.palette.text.primary : theme.palette.text.primary
} }
/> />
@ -434,7 +441,7 @@ export const AppsDesktop = ({
{isEnabledDevMode && ( {isEnabledDevMode && (
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
setDesktopViewMode("dev"); setDesktopViewMode('dev');
}} }}
> >
<IconWrapper label="Dev" disableWidth> <IconWrapper label="Dev" disableWidth>
@ -442,21 +449,21 @@ export const AppsDesktop = ({
</IconWrapper> </IconWrapper>
</ButtonBase> </ButtonBase>
)} )}
{mode !== "home" && ( {mode !== 'home' && (
<AppsNavBarDesktop <AppsNavBarDesktop
disableBack={isNewTabWindow && mode === "viewer"} disableBack={isNewTabWindow && mode === 'viewer'}
/> />
)} )}
</Box> </Box>
{mode === "home" && ( {mode === 'home' && (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
width: "100%", width: '100%',
flexDirection: "column", flexDirection: 'column',
height: "100vh", height: '100vh',
overflow: "auto", overflow: 'auto',
}} }}
> >
<Spacer height="30px" /> <Spacer height="30px" />
@ -471,7 +478,7 @@ export const AppsDesktop = ({
)} )}
<AppsLibraryDesktop <AppsLibraryDesktop
isShow={mode === "library" && !selectedTab} isShow={mode === 'library' && !selectedTab}
availableQapps={availableQapps} availableQapps={availableQapps}
setMode={setMode} setMode={setMode}
myName={myName} myName={myName}
@ -480,19 +487,19 @@ export const AppsDesktop = ({
getQapps={getQapps} getQapps={getQapps}
/> />
{mode === "appInfo" && !selectedTab && ( {mode === 'appInfo' && !selectedTab && (
<AppInfo app={selectedAppInfo} myName={myName} /> <AppInfo app={selectedAppInfo} myName={myName} />
)} )}
{mode === "appInfo-from-category" && !selectedTab && ( {mode === 'appInfo-from-category' && !selectedTab && (
<AppInfo app={selectedAppInfo} myName={myName} /> <AppInfo app={selectedAppInfo} myName={myName} />
)} )}
<AppsCategoryDesktop <AppsCategoryDesktop
availableQapps={availableQapps} availableQapps={availableQapps}
isShow={mode === "category" && !selectedTab} isShow={mode === 'category' && !selectedTab}
category={selectedCategory} category={selectedCategory}
myName={myName} myName={myName}
/> />
{mode === "publish" && !selectedTab && ( {mode === 'publish' && !selectedTab && (
<AppPublish names={myName ? [myName] : []} categories={categories} /> <AppPublish names={myName ? [myName] : []} categories={categories} />
)} )}
{tabs.map((tab) => { {tabs.map((tab) => {
@ -511,15 +518,15 @@ export const AppsDesktop = ({
); );
})} })}
{isNewTabWindow && mode === "viewer" && ( {isNewTabWindow && mode === 'viewer' && (
<> <>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
width: "100%", width: '100%',
flexDirection: "column", flexDirection: 'column',
height: "100vh", height: '100vh',
overflow: "auto", overflow: 'auto',
}} }}
> >
<Spacer height="30px" /> <Spacer height="30px" />

View File

@ -31,6 +31,7 @@ import { HubsIcon } from '../../assets/Icons/HubsIcon';
import { AppsDevModeNavBar } from './AppsDevModeNavBar'; import { AppsDevModeNavBar } from './AppsDevModeNavBar';
import { AppsIcon } from '../../assets/Icons/AppsIcon'; import { AppsIcon } from '../../assets/Icons/AppsIcon';
import { IconWrapper } from '../Desktop/DesktopFooter'; import { IconWrapper } from '../Desktop/DesktopFooter';
import { CoreSyncStatus } from '../CoreSyncStatus';
const uid = new ShortUniqueId({ length: 8 }); const uid = new ShortUniqueId({ length: 8 });
@ -243,11 +244,20 @@ export const AppsDevMode = ({
gap: '25px', gap: '25px',
}} }}
> >
<ButtonBase
sx={{
width: '70px',
height: '70px',
paddingTop: '23px',
}}
>
<CoreSyncStatus />
</ButtonBase>
<ButtonBase <ButtonBase
sx={{ sx={{
width: '60px', width: '60px',
height: '60px', height: '60px',
paddingTop: '23px',
}} }}
onClick={() => { onClick={() => {
goToHome(); goToHome();

View File

@ -1,4 +1,4 @@
import React, { useContext, useMemo, useState } from "react"; import React, { useContext, useMemo, useState } from 'react';
import { import {
AppCircle, AppCircle,
AppCircleContainer, AppCircleContainer,
@ -6,8 +6,8 @@ import {
AppLibrarySubTitle, AppLibrarySubTitle,
AppsContainer, AppsContainer,
AppsParent, AppsParent,
} from "./Apps-styles"; } from './Apps-styles';
import { Buffer } from "buffer"; import { Buffer } from 'buffer';
import { import {
Avatar, Avatar,
@ -20,17 +20,17 @@ import {
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
Input, Input,
} from "@mui/material"; } from '@mui/material';
import { Add } from "@mui/icons-material"; import { Add } from '@mui/icons-material';
import { MyContext, getBaseApiReact, isMobile } from "../../App"; import { MyContext, getBaseApiReact, isMobile } from '../../App';
import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import LogoSelected from '../../assets/svgs/LogoSelected.svg';
import { executeEvent } from "../../utils/events"; import { executeEvent } from '../../utils/events';
import { Spacer } from "../../common/Spacer"; import { Spacer } from '../../common/Spacer';
import { useModal } from "../../common/useModal"; import { useModal } from '../../common/useModal';
import { createEndpoint, isUsingLocal } from "../../background"; import { createEndpoint, isUsingLocal } from '../../background';
import { Label } from "../Group/AddGroup"; import { Label } from '../Group/AddGroup';
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from 'short-unique-id';
import swaggerSVG from '../../assets/svgs/swagger.svg' import swaggerSVG from '../../assets/svgs/swagger.svg';
const uid = new ShortUniqueId({ length: 8 }); const uid = new ShortUniqueId({ length: 8 });
export const AppsDevModeHome = ({ export const AppsDevModeHome = ({
@ -40,8 +40,8 @@ export const AppsDevModeHome = ({
availableQapps, availableQapps,
myName, myName,
}) => { }) => {
const [domain, setDomain] = useState("127.0.0.1"); const [domain, setDomain] = useState('127.0.0.1');
const [port, setPort] = useState(""); const [port, setPort] = useState('');
const [selectedPreviewFile, setSelectedPreviewFile] = useState(null); const [selectedPreviewFile, setSelectedPreviewFile] = useState(null);
const { isShow, onCancel, onOk, show, message } = useModal(); const { isShow, onCancel, onOk, show, message } = useModal();
@ -58,7 +58,7 @@ export const AppsDevModeHome = ({
const content = await window.electron.readFile(filePath); const content = await window.electron.readFile(filePath);
return { buffer: content, filePath }; return { buffer: content, filePath };
} else { } else {
console.log("No file selected."); console.log('No file selected.');
} }
}; };
const handleSelectDirectry = async (existingDirectoryPath) => { const handleSelectDirectry = async (existingDirectoryPath) => {
@ -67,7 +67,7 @@ export const AppsDevModeHome = ({
if (buffer) { if (buffer) {
return { buffer, directoryPath }; return { buffer, directoryPath };
} else { } else {
console.log("No file selected."); console.log('No file selected.');
} }
}; };
@ -78,34 +78,36 @@ export const AppsDevModeHome = ({
setOpenSnackGlobal(true); setOpenSnackGlobal(true);
setInfoSnackCustom({ setInfoSnackCustom({
type: "error", type: 'error',
message: message:
"Please use your local node for dev mode! Logout and use Local node.", 'Please use your local node for dev mode! Logout and use Local node.',
}); });
return; return;
} }
const { portVal, domainVal } = await show({ const { portVal, domainVal } = await show({
message: "", message: '',
publishFee: "", publishFee: '',
}); });
const framework = domainVal + ":" + portVal; const framework = domainVal + ':' + portVal;
const response = await fetch( const response = await fetch(
`${getBaseApiReact()}/developer/proxy/start`, `${getBaseApiReact()}/developer/proxy/start`,
{ {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "text/plain", 'Content-Type': 'text/plain',
}, },
body: framework, body: framework,
} }
); );
const responseData = await response.text(); const responseData = await response.text();
executeEvent("appsDevModeAddTab", { executeEvent('appsDevModeAddTab', {
data: { data: {
url: "http://127.0.0.1:" + responseData, url: 'http://127.0.0.1:' + responseData,
}, },
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const addPreviewApp = async (isRefresh, existingFilePath, tabId) => { const addPreviewApp = async (isRefresh, existingFilePath, tabId) => {
@ -115,9 +117,9 @@ export const AppsDevModeHome = ({
setOpenSnackGlobal(true); setOpenSnackGlobal(true);
setInfoSnackCustom({ setInfoSnackCustom({
type: "error", type: 'error',
message: message:
"Please use your local node for dev mode! Logout and use Local node.", 'Please use your local node for dev mode! Logout and use Local node.',
}); });
return; return;
} }
@ -125,8 +127,8 @@ export const AppsDevModeHome = ({
setOpenSnackGlobal(true); setOpenSnackGlobal(true);
setInfoSnackCustom({ setInfoSnackCustom({
type: "error", type: 'error',
message: "You need a name to use preview", message: 'You need a name to use preview',
}); });
return; return;
} }
@ -137,29 +139,29 @@ export const AppsDevModeHome = ({
setOpenSnackGlobal(true); setOpenSnackGlobal(true);
setInfoSnackCustom({ setInfoSnackCustom({
type: "error", type: 'error',
message: "Please select a file", message: 'Please select a file',
}); });
return; return;
} }
const postBody = Buffer.from(buffer).toString("base64"); const postBody = Buffer.from(buffer).toString('base64');
const endpoint = await createEndpoint( const endpoint = await createEndpoint(
`/arbitrary/APP/${myName}/zip?preview=true` `/arbitrary/APP/${myName}/zip?preview=true`
); );
const response = await fetch(endpoint, { const response = await fetch(endpoint, {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "text/plain", 'Content-Type': 'text/plain',
}, },
body: postBody, body: postBody,
}); });
if (!response?.ok) throw new Error("Invalid zip"); if (!response?.ok) throw new Error('Invalid zip');
const previewPath = await response.text(); const previewPath = await response.text();
if (tabId) { if (tabId) {
executeEvent("appsDevModeUpdateTab", { executeEvent('appsDevModeUpdateTab', {
data: { data: {
url: "http://127.0.0.1:12391" + previewPath, url: 'http://127.0.0.1:12391' + previewPath,
isPreview: true, isPreview: true,
filePath, filePath,
refreshFunc: (tabId) => { refreshFunc: (tabId) => {
@ -170,9 +172,9 @@ export const AppsDevModeHome = ({
}); });
return; return;
} }
executeEvent("appsDevModeAddTab", { executeEvent('appsDevModeAddTab', {
data: { data: {
url: "http://127.0.0.1:12391" + previewPath, url: 'http://127.0.0.1:12391' + previewPath,
isPreview: true, isPreview: true,
filePath, filePath,
refreshFunc: (tabId) => { refreshFunc: (tabId) => {
@ -192,9 +194,9 @@ export const AppsDevModeHome = ({
setOpenSnackGlobal(true); setOpenSnackGlobal(true);
setInfoSnackCustom({ setInfoSnackCustom({
type: "error", type: 'error',
message: message:
"Please use your local node for dev mode! Logout and use Local node.", 'Please use your local node for dev mode! Logout and use Local node.',
}); });
return; return;
} }
@ -202,8 +204,8 @@ export const AppsDevModeHome = ({
setOpenSnackGlobal(true); setOpenSnackGlobal(true);
setInfoSnackCustom({ setInfoSnackCustom({
type: "error", type: 'error',
message: "You need a name to use preview", message: 'You need a name to use preview',
}); });
return; return;
} }
@ -214,29 +216,29 @@ export const AppsDevModeHome = ({
setOpenSnackGlobal(true); setOpenSnackGlobal(true);
setInfoSnackCustom({ setInfoSnackCustom({
type: "error", type: 'error',
message: "Please select a file", message: 'Please select a file',
}); });
return; return;
} }
const postBody = Buffer.from(buffer).toString("base64"); const postBody = Buffer.from(buffer).toString('base64');
const endpoint = await createEndpoint( const endpoint = await createEndpoint(
`/arbitrary/APP/${myName}/zip?preview=true` `/arbitrary/APP/${myName}/zip?preview=true`
); );
const response = await fetch(endpoint, { const response = await fetch(endpoint, {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "text/plain", 'Content-Type': 'text/plain',
}, },
body: postBody, body: postBody,
}); });
if (!response?.ok) throw new Error("Invalid zip"); if (!response?.ok) throw new Error('Invalid zip');
const previewPath = await response.text(); const previewPath = await response.text();
if (tabId) { if (tabId) {
executeEvent("appsDevModeUpdateTab", { executeEvent('appsDevModeUpdateTab', {
data: { data: {
url: "http://127.0.0.1:12391" + previewPath, url: 'http://127.0.0.1:12391' + previewPath,
isPreview: true, isPreview: true,
directoryPath, directoryPath,
refreshFunc: (tabId) => { refreshFunc: (tabId) => {
@ -247,9 +249,9 @@ export const AppsDevModeHome = ({
}); });
return; return;
} }
executeEvent("appsDevModeAddTab", { executeEvent('appsDevModeAddTab', {
data: { data: {
url: "http://127.0.0.1:12391" + previewPath, url: 'http://127.0.0.1:12391' + previewPath,
isPreview: true, isPreview: true,
directoryPath, directoryPath,
refreshFunc: (tabId) => { refreshFunc: (tabId) => {
@ -266,12 +268,12 @@ export const AppsDevModeHome = ({
<> <>
<AppsContainer <AppsContainer
sx={{ sx={{
justifyContent: "flex-start", justifyContent: 'flex-start',
}} }}
> >
<AppLibrarySubTitle <AppLibrarySubTitle
sx={{ sx={{
fontSize: "30px", fontSize: '30px',
}} }}
> >
Dev Mode Apps Dev Mode Apps
@ -280,8 +282,8 @@ export const AppsDevModeHome = ({
<Spacer height="45px" /> <Spacer height="45px" />
<AppsContainer <AppsContainer
sx={{ sx={{
gap: "75px", gap: '75px',
justifyContent: "flex-start", justifyContent: 'flex-start',
}} }}
> >
<ButtonBase <ButtonBase
@ -291,7 +293,7 @@ export const AppsDevModeHome = ({
> >
<AppCircleContainer <AppCircleContainer
sx={{ sx={{
gap: !isMobile ? "10px" : "5px", gap: !isMobile ? '10px' : '5px',
}} }}
> >
<AppCircle> <AppCircle>
@ -307,7 +309,7 @@ export const AppsDevModeHome = ({
> >
<AppCircleContainer <AppCircleContainer
sx={{ sx={{
gap: !isMobile ? "10px" : "5px", gap: !isMobile ? '10px' : '5px',
}} }}
> >
<AppCircle> <AppCircle>
@ -323,7 +325,7 @@ export const AppsDevModeHome = ({
> >
<AppCircleContainer <AppCircleContainer
sx={{ sx={{
gap: !isMobile ? "10px" : "5px", gap: !isMobile ? '10px' : '5px',
}} }}
> >
<AppCircle> <AppCircle>
@ -334,10 +336,10 @@ export const AppsDevModeHome = ({
</ButtonBase> </ButtonBase>
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
executeEvent("appsDevModeAddTab", { executeEvent('appsDevModeAddTab', {
data: { data: {
service: "APP", service: 'APP',
name: "Q-Sandbox", name: 'Q-Sandbox',
tabId: uid.rnd(), tabId: uid.rnd(),
}, },
}); });
@ -345,16 +347,16 @@ export const AppsDevModeHome = ({
> >
<AppCircleContainer <AppCircleContainer
sx={{ sx={{
gap: !isMobile ? "10px" : "5px", gap: !isMobile ? '10px' : '5px',
}} }}
> >
<AppCircle> <AppCircle>
<Avatar <Avatar
sx={{ sx={{
height: "42px", height: '42px',
width: "42px", width: '42px',
"& img": { '& img': {
objectFit: "fill", objectFit: 'fill',
}, },
}} }}
alt="Q-Sandbox" alt="Q-Sandbox"
@ -362,8 +364,8 @@ export const AppsDevModeHome = ({
> >
<img <img
style={{ style={{
width: "31px", width: '31px',
height: "auto", height: 'auto',
}} }}
alt="center-icon" alt="center-icon"
/> />
@ -374,27 +376,27 @@ export const AppsDevModeHome = ({
</ButtonBase> </ButtonBase>
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
executeEvent("appsDevModeAddTab", { executeEvent('appsDevModeAddTab', {
data: { data: {
url: "http://127.0.0.1:12391", url: 'http://127.0.0.1:12391',
isPreview: false, isPreview: false,
customIcon: swaggerSVG customIcon: swaggerSVG,
}, },
}); });
}} }}
> >
<AppCircleContainer <AppCircleContainer
sx={{ sx={{
gap: !isMobile ? "10px" : "5px", gap: !isMobile ? '10px' : '5px',
}} }}
> >
<AppCircle> <AppCircle>
<Avatar <Avatar
sx={{ sx={{
height: "42px", height: '42px',
width: "42px", width: '42px',
"& img": { '& img': {
objectFit: "fill", objectFit: 'fill',
}, },
}} }}
alt="API" alt="API"
@ -402,8 +404,8 @@ export const AppsDevModeHome = ({
> >
<img <img
style={{ style={{
width: "31px", width: '31px',
height: "auto", height: 'auto',
}} }}
alt="center-icon" alt="center-icon"
/> />
@ -419,20 +421,20 @@ export const AppsDevModeHome = ({
aria-labelledby="alert-dialog-title" aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" && domain && port) { if (e.key === 'Enter' && domain && port) {
onOk({ portVal: port, domainVal: domain }); onOk({ portVal: port, domainVal: domain });
} }
}} }}
> >
<DialogTitle id="alert-dialog-title"> <DialogTitle id="alert-dialog-title">
{"Add custom framework"} {'Add custom framework'}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '5px',
}} }}
> >
<Label>Domain</Label> <Label>Domain</Label>
@ -444,10 +446,10 @@ export const AppsDevModeHome = ({
</Box> </Box>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '5px',
marginTop: "15px", marginTop: '15px',
}} }}
> >
<Label>Port</Label> <Label>Port</Label>

View File

@ -1,51 +1,34 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from 'react';
import { import {
AppsNavBarLeft, AppsNavBarLeft,
AppsNavBarParent, AppsNavBarParent,
AppsNavBarRight, AppsNavBarRight,
} from "./Apps-styles"; } from './Apps-styles';
import NavBack from "../../assets/svgs/NavBack.svg"; import { NavBack } from '../../assets/Icons/NavBack.tsx';
import NavAdd from "../../assets/svgs/NavAdd.svg"; import { NavAdd } from '../../assets/Icons/NavAdd.tsx';
import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg"; import { ButtonBase, Tab, Tabs } from '@mui/material';
import {
ButtonBase,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Tab,
Tabs,
} from "@mui/material";
import { import {
executeEvent, executeEvent,
subscribeToEvent, subscribeToEvent,
unsubscribeFromEvent, unsubscribeFromEvent,
} from "../../utils/events"; } from '../../utils/events';
import TabComponent from "./TabComponent"; import RefreshIcon from '@mui/icons-material/Refresh';
import PushPinIcon from "@mui/icons-material/PushPin"; import { useRecoilState } from 'recoil';
import RefreshIcon from "@mui/icons-material/Refresh"; import { navigationControllerAtom } from '../../atoms/global';
import { useRecoilState, useSetRecoilState } from "recoil"; import { AppsDevModeTabComponent } from './AppsDevModeTabComponent';
import {
navigationControllerAtom,
settingsLocalLastUpdatedAtom,
sortablePinnedAppsAtom,
} from "../../atoms/global";
import { AppsDevModeTabComponent } from "./AppsDevModeTabComponent";
export const AppsDevModeNavBar = () => { export const AppsDevModeNavBar = () => {
const [tabs, setTabs] = useState([]); const [tabs, setTabs] = useState([]);
const [selectedTab, setSelectedTab] = useState(null); const [selectedTab, setSelectedTab] = useState(null);
const [navigationController, setNavigationController] = useRecoilState(navigationControllerAtom) const [navigationController, setNavigationController] = useRecoilState(
navigationControllerAtom
);
const [isNewTabWindow, setIsNewTabWindow] = useState(false); const [isNewTabWindow, setIsNewTabWindow] = useState(false);
const tabsRef = useRef(null); const tabsRef = useRef(null);
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const handleClick = (event) => { const handleClick = (event) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
}; };
@ -57,27 +40,25 @@ export const AppsDevModeNavBar = () => {
useEffect(() => { useEffect(() => {
// Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added) // Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added)
if (tabsRef.current) { if (tabsRef.current) {
const tabElements = tabsRef.current.querySelectorAll(".MuiTab-root"); const tabElements = tabsRef.current.querySelectorAll('.MuiTab-root');
if (tabElements.length > 0) { if (tabElements.length > 0) {
const lastTab = tabElements[tabElements.length - 1]; const lastTab = tabElements[tabElements.length - 1];
lastTab.scrollIntoView({ lastTab.scrollIntoView({
behavior: "smooth", behavior: 'smooth',
block: "nearest", block: 'nearest',
inline: "end", inline: 'end',
}); });
} }
} }
}, [tabs.length]); // Dependency on the number of tabs }, [tabs.length]); // Dependency on the number of tabs
const isDisableBackButton = useMemo(() => {
const isDisableBackButton = useMemo(()=> { if (selectedTab && navigationController[selectedTab?.tabId]?.hasBack)
if(selectedTab && navigationController[selectedTab?.tabId]?.hasBack) return false return false;
if(selectedTab && !navigationController[selectedTab?.tabId]?.hasBack) return true if (selectedTab && !navigationController[selectedTab?.tabId]?.hasBack)
return false return true;
}, [navigationController, selectedTab]) return false;
}, [navigationController, selectedTab]);
const setTabsToNav = (e) => { const setTabsToNav = (e) => {
const { tabs, selectedTab, isNewTabWindow } = e.detail?.data; const { tabs, selectedTab, isNewTabWindow } = e.detail?.data;
@ -88,45 +69,43 @@ export const AppsDevModeNavBar = () => {
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("appsDevModeSetTabsToNav", setTabsToNav); subscribeToEvent('appsDevModeSetTabsToNav', setTabsToNav);
return () => { return () => {
unsubscribeFromEvent("appsDevModeSetTabsToNav", setTabsToNav); unsubscribeFromEvent('appsDevModeSetTabsToNav', setTabsToNav);
}; };
}, []); }, []);
return ( return (
<AppsNavBarParent <AppsNavBarParent
sx={{ sx={{
position: "relative", position: 'relative',
flexDirection: "column", flexDirection: 'column',
width: "60px", width: '60px',
height: "unset", height: 'unset',
maxHeight: "70vh", maxHeight: '70vh',
borderRadius: "0px 30px 30px 0px", borderRadius: '0px 30px 30px 0px',
padding: "10px", padding: '10px',
}} }}
> >
<AppsNavBarLeft <AppsNavBarLeft
sx={{ sx={{
flexDirection: "column", flexDirection: 'column',
}} }}
> >
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
executeEvent("devModeNavigateBack", selectedTab?.tabId); executeEvent('devModeNavigateBack', selectedTab?.tabId);
}} }}
disabled={isDisableBackButton} disabled={isDisableBackButton}
sx={{ sx={{
opacity: !isDisableBackButton ? 1 : 0.1, opacity: !isDisableBackButton ? 1 : 0.1,
cursor: !isDisableBackButton ? 'pointer': 'default' cursor: !isDisableBackButton ? 'pointer' : 'default',
}} }}
> >
<img src={NavBack} /> <NavBack />
</ButtonBase> </ButtonBase>
<Tabs <Tabs
orientation="vertical" orientation="vertical"
ref={tabsRef} ref={tabsRef}
@ -134,11 +113,11 @@ export const AppsDevModeNavBar = () => {
variant="scrollable" // Make tabs scrollable variant="scrollable" // Make tabs scrollable
scrollButtons={true} scrollButtons={true}
sx={{ sx={{
"& .MuiTabs-indicator": { '& .MuiTabs-indicator': {
backgroundColor: "white", backgroundColor: 'white',
}, },
maxHeight: `320px`, // Ensure the tabs container fits within the available space maxHeight: `320px`, // Ensure the tabs container fits within the available space
overflow: "hidden", // Prevents overflow on small screens overflow: 'hidden', // Prevents overflow on small screens
}} }}
> >
{tabs?.map((tab) => ( {tabs?.map((tab) => (
@ -153,65 +132,61 @@ export const AppsDevModeNavBar = () => {
/> />
} // Pass custom component } // Pass custom component
sx={{ sx={{
"&.Mui-selected": { '&.Mui-selected': {
color: "white", color: 'white',
}, },
padding: "0px", padding: '0px',
margin: "0px", margin: '0px',
minWidth: "0px", minWidth: '0px',
width: "50px", width: '50px',
}} }}
/> />
))} ))}
</Tabs> </Tabs>
</AppsNavBarLeft> </AppsNavBarLeft>
{selectedTab && ( {selectedTab && (
<AppsNavBarRight <AppsNavBarRight
sx={{ sx={{
gap: "10px", gap: '10px',
flexDirection: "column", flexDirection: 'column',
}}
>
<ButtonBase
onClick={() => {
setSelectedTab(null);
executeEvent("devModeNewTabWindow", {});
}} }}
> >
<img <ButtonBase
style={{ onClick={() => {
height: "40px", setSelectedTab(null);
width: "40px", executeEvent('devModeNewTabWindow', {});
}} }}
src={NavAdd} >
/> <NavAdd
</ButtonBase> style={{
height: '40px',
<ButtonBase
onClick={(e) => {
if(selectedTab?.refreshFunc){
selectedTab.refreshFunc(selectedTab?.tabId)
} else {
executeEvent("refreshApp", {
tabId: selectedTab?.tabId,
});
}
}}
>
<RefreshIcon
sx={{
color: "rgba(250, 250, 250, 0.5)",
width: '40px', width: '40px',
height: 'auto'
}} }}
/> />
</ButtonBase> </ButtonBase>
</AppsNavBarRight>
<ButtonBase
onClick={(e) => {
if (selectedTab?.refreshFunc) {
selectedTab.refreshFunc(selectedTab?.tabId);
} else {
executeEvent('refreshApp', {
tabId: selectedTab?.tabId,
});
}
}}
>
<RefreshIcon
sx={{
color: 'rgba(250, 250, 250, 0.5)',
width: '40px',
height: 'auto',
}}
/>
</ButtonBase>
</AppsNavBarRight>
)} )}
</AppsNavBarParent> </AppsNavBarParent>
); );
}; };

View File

@ -1,5 +1,5 @@
import { TabParent } from './Apps-styles'; import { TabParent } from './Apps-styles';
import NavCloseTab from '../../assets/svgs/NavCloseTab.svg'; import { NavCloseTab } from '../../assets/Icons/NavCloseTab.tsx';
import { getBaseApiReact } from '../../App'; import { getBaseApiReact } from '../../App';
import { Avatar, ButtonBase } from '@mui/material'; import { Avatar, ButtonBase } from '@mui/material';
import LogoSelected from '../../assets/svgs/LogoSelected.svg'; import LogoSelected from '../../assets/svgs/LogoSelected.svg';
@ -27,14 +27,13 @@ export const AppsDevModeTabComponent = ({ isSelected, app }) => {
}} }}
> >
{isSelected && ( {isSelected && (
<img <NavCloseTab
style={{ style={{
position: 'absolute', position: 'absolute',
top: '-5px', top: '-5px',
right: '-5px', right: '-5px',
zIndex: 1, zIndex: 1,
}} }}
src={NavCloseTab}
/> />
)} )}
<Avatar <Avatar

View File

@ -1,11 +1,4 @@
import React, { import { useContext, useEffect, useMemo, useRef, useState } from 'react';
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { import {
AppCircle, AppCircle,
AppCircleContainer, AppCircleContainer,
@ -13,7 +6,6 @@ import {
AppLibrarySubTitle, AppLibrarySubTitle,
AppsContainer, AppsContainer,
AppsLibraryContainer, AppsLibraryContainer,
AppsParent,
AppsSearchContainer, AppsSearchContainer,
AppsSearchLeft, AppsSearchLeft,
AppsSearchRight, AppsSearchRight,
@ -24,16 +16,22 @@ import {
PublishQAppCTARight, PublishQAppCTARight,
PublishQAppDotsBG, PublishQAppDotsBG,
} from './Apps-styles'; } from './Apps-styles';
import { Avatar, Box, ButtonBase, InputBase, styled } from '@mui/material'; import {
import { Add } from '@mui/icons-material'; Avatar,
Box,
ButtonBase,
InputBase,
styled,
useTheme,
} from '@mui/material';
import { MyContext, getBaseApiReact } from '../../App'; import { MyContext, getBaseApiReact } from '../../App';
import LogoSelected from '../../assets/svgs/LogoSelected.svg'; import LogoSelected from '../../assets/svgs/LogoSelected.svg';
import IconSearch from '../../assets/svgs/Search.svg'; import IconSearch from '../../assets/svgs/Search.svg';
import IconClearInput from '../../assets/svgs/ClearInput.svg'; import IconClearInput from '../../assets/svgs/ClearInput.svg';
import qappDevelopText from '../../assets/svgs/qappDevelopText.svg'; import qappDevelopText from '../../assets/svgs/qappDevelopText.svg';
import qappDots from '../../assets/svgs/qappDots.svg'; import qappDots from '../../assets/svgs/qappDots.svg';
// import { Return } from './assets/svgs/Return.tsx';
import ReturnSVG from '../../assets/svgs/Return.svg'; import ReturnSVG from '../../assets/svgs/Return.svg';
import { Spacer } from '../../common/Spacer'; import { Spacer } from '../../common/Spacer';
import { AppInfoSnippet } from './AppInfoSnippet'; import { AppInfoSnippet } from './AppInfoSnippet';
import { Virtuoso } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso';
@ -43,6 +41,7 @@ import {
MailIconImg, MailIconImg,
ShowMessageReturnButton, ShowMessageReturnButton,
} from '../Group/Forum/Mail-styles'; } from '../Group/Forum/Mail-styles';
const officialAppList = [ const officialAppList = [
'q-tube', 'q-tube',
'q-blog', 'q-blog',
@ -56,10 +55,9 @@ const officialAppList = [
'q-manager', 'q-manager',
'q-wallets', 'q-wallets',
'q-search', 'q-search',
'q-nodecontrol',
]; ];
// TODO: apply dark/light style
const ScrollerStyled = styled('div')({ const ScrollerStyled = styled('div')({
// Hide scrollbar for WebKit browsers (Chrome, Safari) // Hide scrollbar for WebKit browsers (Chrome, Safari)
'::-webkit-scrollbar': { '::-webkit-scrollbar': {
@ -75,10 +73,10 @@ const ScrollerStyled = styled('div')({
}); });
const StyledVirtuosoContainer = styled('div')({ const StyledVirtuosoContainer = styled('div')({
position: 'relative',
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
position: 'relative',
width: '100%',
// Hide scrollbar for WebKit browsers (Chrome, Safari) // Hide scrollbar for WebKit browsers (Chrome, Safari)
'::-webkit-scrollbar': { '::-webkit-scrollbar': {
@ -147,6 +145,8 @@ export const AppsLibrary = ({
); );
}; };
const theme = useTheme();
return ( return (
<AppsLibraryContainer <AppsLibraryContainer
sx={{ sx={{
@ -164,6 +164,7 @@ export const AppsLibrary = ({
<AppsSearchContainer> <AppsSearchContainer>
<AppsSearchLeft> <AppsSearchLeft>
<img src={IconSearch} /> <img src={IconSearch} />
<InputBase <InputBase
value={searchValue} value={searchValue}
onChange={(e) => setSearchValue(e.target.value)} onChange={(e) => setSearchValue(e.target.value)}
@ -190,7 +191,9 @@ export const AppsLibrary = ({
</AppsSearchContainer> </AppsSearchContainer>
</Box> </Box>
</AppsWidthLimiter> </AppsWidthLimiter>
<Spacer height="25px" /> <Spacer height="25px" />
<ShowMessageReturnButton <ShowMessageReturnButton
sx={{ sx={{
padding: '2px', padding: '2px',
@ -199,10 +202,12 @@ export const AppsLibrary = ({
executeEvent('navigateBack', {}); executeEvent('navigateBack', {});
}} }}
> >
<MailIconImg src={ReturnSVG} /> <MailIconImg src={ReturnSVG} /> // TODO return icon
<ComposeP>Return to Apps Dashboard</ComposeP> <ComposeP>Return to Apps Dashboard</ComposeP>
</ShowMessageReturnButton> </ShowMessageReturnButton>
<Spacer height="25px" /> <Spacer height="25px" />
{searchedList?.length > 0 ? ( {searchedList?.length > 0 ? (
<AppsWidthLimiter> <AppsWidthLimiter>
<StyledVirtuosoContainer <StyledVirtuosoContainer
@ -226,7 +231,9 @@ export const AppsLibrary = ({
<> <>
<AppsWidthLimiter> <AppsWidthLimiter>
<AppLibrarySubTitle>Official Apps</AppLibrarySubTitle> <AppLibrarySubTitle>Official Apps</AppLibrarySubTitle>
<Spacer height="18px" /> <Spacer height="18px" />
<AppsContainer> <AppsContainer>
{officialApps?.map((qapp) => { {officialApps?.map((qapp) => {
return ( return (
@ -270,6 +277,7 @@ export const AppsLibrary = ({
/> />
</Avatar> </Avatar>
</AppCircle> </AppCircle>
<AppCircleLabel> <AppCircleLabel>
{qapp?.metadata?.title || qapp?.name} {qapp?.metadata?.title || qapp?.name}
</AppCircleLabel> </AppCircleLabel>
@ -278,20 +286,27 @@ export const AppsLibrary = ({
); );
})} })}
</AppsContainer> </AppsContainer>
<Spacer height="30px" /> <Spacer height="30px" />
<AppLibrarySubTitle> <AppLibrarySubTitle>
{hasPublishApp ? 'Update Apps!' : 'Create Apps!'} {hasPublishApp ? 'Update Apps!' : 'Create Apps!'}
</AppLibrarySubTitle> </AppLibrarySubTitle>
<Spacer height="18px" /> <Spacer height="18px" />
</AppsWidthLimiter> </AppsWidthLimiter>
<PublishQAppCTAParent> <PublishQAppCTAParent>
<PublishQAppCTALeft> <PublishQAppCTALeft>
<PublishQAppDotsBG> <PublishQAppDotsBG>
<img src={qappDots} /> <img src={qappDots} />
</PublishQAppDotsBG> </PublishQAppDotsBG>
<Spacer width="29px" /> <Spacer width="29px" />
<img src={qappDevelopText} /> <img src={qappDevelopText} />
</PublishQAppCTALeft> </PublishQAppCTALeft>
<PublishQAppCTARight <PublishQAppCTARight
onClick={() => { onClick={() => {
setMode('publish'); setMode('publish');
@ -300,13 +315,18 @@ export const AppsLibrary = ({
<PublishQAppCTAButton> <PublishQAppCTAButton>
{hasPublishApp ? 'Update' : 'Publish'} {hasPublishApp ? 'Update' : 'Publish'}
</PublishQAppCTAButton> </PublishQAppCTAButton>
<Spacer width="20px" /> <Spacer width="20px" />
</PublishQAppCTARight> </PublishQAppCTARight>
</PublishQAppCTAParent> </PublishQAppCTAParent>
<AppsWidthLimiter> <AppsWidthLimiter>
<Spacer height="18px" /> <Spacer height="18px" />
<AppLibrarySubTitle>Categories</AppLibrarySubTitle> <AppLibrarySubTitle>Categories</AppLibrarySubTitle>
<Spacer height="18px" /> <Spacer height="18px" />
<AppsWidthLimiter <AppsWidthLimiter
sx={{ sx={{
flexDirection: 'row', flexDirection: 'row',
@ -337,18 +357,17 @@ export const AppsLibrary = ({
> >
<Box <Box
sx={{ sx={{
display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', background: theme.palette.background.default,
height: '110px',
width: '110px',
background:
'linear-gradient(163.47deg, #4BBCFE 27.55%, #1386C9 86.56%)',
color: '#1D1D1E',
fontWeight: 700,
fontSize: '16px',
flexShrink: 0,
borderRadius: '11px', borderRadius: '11px',
color: theme.palette.text.primary,
display: 'flex',
flexShrink: 0,
fontSize: '16px',
fontWeight: 700,
height: '110px',
justifyContent: 'center',
width: '110px',
}} }}
> >
{category?.name} {category?.name}

View File

@ -71,6 +71,7 @@ const officialAppList = [
'q-mintership', 'q-mintership',
'q-wallets', 'q-wallets',
'q-search', 'q-search',
'q-nodecontrol',
]; ];
const ScrollerStyled = styled('div')({ const ScrollerStyled = styled('div')({
@ -134,9 +135,9 @@ export const AppsLibraryDesktop = ({
setDebouncedValue(searchValue); setDebouncedValue(searchValue);
}, 350); }, 350);
setTimeout(() => { setTimeout(() => {
virtuosoRef.current.scrollToIndex({ if (virtuosoRef.current) {
index: 0, virtuosoRef.current.scrollToIndex({ index: 0 });
}); }
}, 500); }, 500);
// Cleanup timeout if searchValue changes before the timeout completes // Cleanup timeout if searchValue changes before the timeout completes
return () => { return () => {

View File

@ -1,12 +1,12 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from 'react';
import { import {
AppsNavBarLeft, AppsNavBarLeft,
AppsNavBarParent, AppsNavBarParent,
AppsNavBarRight, AppsNavBarRight,
} from "./Apps-styles"; } from './Apps-styles';
import NavBack from "../../assets/svgs/NavBack.svg"; import { NavBack } from '../../assets/Icons/NavBack.tsx';
import NavAdd from "../../assets/svgs/NavAdd.svg"; import { NavAdd } from '../../assets/Icons/NavAdd.tsx';
import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg"; import { NavMoreMenu } from '../../assets/Icons/NavMoreMenu.tsx';
import { import {
ButtonBase, ButtonBase,
ListItemIcon, ListItemIcon,
@ -15,27 +15,33 @@ import {
MenuItem, MenuItem,
Tab, Tab,
Tabs, Tabs,
} from "@mui/material"; } from '@mui/material';
import { import {
executeEvent, executeEvent,
subscribeToEvent, subscribeToEvent,
unsubscribeFromEvent, unsubscribeFromEvent,
} from "../../utils/events"; } from '../../utils/events';
import TabComponent from "./TabComponent"; import TabComponent from './TabComponent';
import PushPinIcon from "@mui/icons-material/PushPin"; import PushPinIcon from '@mui/icons-material/PushPin';
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from '@mui/icons-material/Refresh';
import { useRecoilState, useSetRecoilState } from "recoil"; import { useRecoilState, useSetRecoilState } from 'recoil';
import { import {
navigationControllerAtom, navigationControllerAtom,
settingsLocalLastUpdatedAtom, settingsLocalLastUpdatedAtom,
sortablePinnedAppsAtom, sortablePinnedAppsAtom,
} from "../../atoms/global"; } from '../../atoms/global';
export function saveToLocalStorage(key, subKey, newValue, otherRootData = {}, deleteWholeKey) { export function saveToLocalStorage(
key,
subKey,
newValue,
otherRootData = {},
deleteWholeKey
) {
try { try {
if(deleteWholeKey){ if (deleteWholeKey) {
localStorage.setItem(key, null); localStorage.setItem(key, null);
return return;
} }
// Fetch existing data // Fetch existing data
const existingData = localStorage.getItem(key); const existingData = localStorage.getItem(key);
@ -64,7 +70,7 @@ export function saveToLocalStorage(key, subKey, newValue, otherRootData = {}, de
const serializedValue = JSON.stringify(combinedData); const serializedValue = JSON.stringify(combinedData);
localStorage.setItem(key, serializedValue); localStorage.setItem(key, serializedValue);
} catch (error) { } catch (error) {
console.error("Error saving to localStorage:", error); console.error('Error saving to localStorage:', error);
} }
} }
@ -78,13 +84,17 @@ export const AppsNavBar = () => {
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState( const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(
sortablePinnedAppsAtom sortablePinnedAppsAtom
); );
const [navigationController, setNavigationController] = useRecoilState(navigationControllerAtom) const [navigationController, setNavigationController] = useRecoilState(
navigationControllerAtom
);
const isDisableBackButton = useMemo(()=> { const isDisableBackButton = useMemo(() => {
if(selectedTab && navigationController[selectedTab?.tabId]?.hasBack) return false if (selectedTab && navigationController[selectedTab?.tabId]?.hasBack)
if(selectedTab && !navigationController[selectedTab?.tabId]?.hasBack) return true return false;
return false if (selectedTab && !navigationController[selectedTab?.tabId]?.hasBack)
}, [navigationController, selectedTab]) return true;
return false;
}, [navigationController, selectedTab]);
const setSettingsLocalLastUpdated = useSetRecoilState( const setSettingsLocalLastUpdated = useSetRecoilState(
settingsLocalLastUpdatedAtom settingsLocalLastUpdatedAtom
@ -101,13 +111,13 @@ export const AppsNavBar = () => {
useEffect(() => { useEffect(() => {
// Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added) // Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added)
if (tabsRef.current) { if (tabsRef.current) {
const tabElements = tabsRef.current.querySelectorAll(".MuiTab-root"); const tabElements = tabsRef.current.querySelectorAll('.MuiTab-root');
if (tabElements.length > 0) { if (tabElements.length > 0) {
const lastTab = tabElements[tabElements.length - 1]; const lastTab = tabElements[tabElements.length - 1];
lastTab.scrollIntoView({ lastTab.scrollIntoView({
behavior: "smooth", behavior: 'smooth',
block: "nearest", block: 'nearest',
inline: "end", inline: 'end',
}); });
} }
} }
@ -122,10 +132,10 @@ export const AppsNavBar = () => {
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("setTabsToNav", setTabsToNav); subscribeToEvent('setTabsToNav', setTabsToNav);
return () => { return () => {
unsubscribeFromEvent("setTabsToNav", setTabsToNav); unsubscribeFromEvent('setTabsToNav', setTabsToNav);
}; };
}, []); }, []);
@ -137,28 +147,29 @@ export const AppsNavBar = () => {
<AppsNavBarParent> <AppsNavBarParent>
<AppsNavBarLeft> <AppsNavBarLeft>
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
executeEvent("navigateBack", selectedTab?.tabId); executeEvent('navigateBack', selectedTab?.tabId);
}} }}
disabled={isDisableBackButton} disabled={isDisableBackButton}
sx={{ sx={{
opacity: !isDisableBackButton ? 1 : 0.1, opacity: !isDisableBackButton ? 1 : 0.3,
cursor: !isDisableBackButton ? 'pointer': 'default' cursor: !isDisableBackButton ? 'pointer' : 'default',
}} }}
> >
<img src={NavBack} /> <NavBack />
</ButtonBase> </ButtonBase>
<Tabs <Tabs
ref={tabsRef} ref={tabsRef}
aria-label="basic tabs example" aria-label="basic tabs example"
variant="scrollable" // Make tabs scrollable variant="scrollable" // Make tabs scrollable
scrollButtons={false} scrollButtons={false}
sx={{ sx={{
"& .MuiTabs-indicator": { '& .MuiTabs-indicator': {
backgroundColor: "white", backgroundColor: 'white',
}, },
maxWidth: `calc(100vw - 150px)`, // Ensure the tabs container fits within the available space maxWidth: `calc(100vw - 150px)`, // Ensure the tabs container fits within the available space
overflow: "hidden", // Prevents overflow on small screens overflow: 'hidden', // Prevents overflow on small screens
}} }}
> >
{tabs?.map((tab) => ( {tabs?.map((tab) => (
@ -173,83 +184,83 @@ export const AppsNavBar = () => {
/> />
} // Pass custom component } // Pass custom component
sx={{ sx={{
"&.Mui-selected": { '&.Mui-selected': {
color: "white", color: 'white',
}, },
padding: "0px", padding: '0px',
margin: "0px", margin: '0px',
minWidth: "0px", minWidth: '0px',
width: "50px", width: '50px',
}} }}
/> />
))} ))}
</Tabs> </Tabs>
</AppsNavBarLeft> </AppsNavBarLeft>
{selectedTab && ( {selectedTab && (
<AppsNavBarRight <AppsNavBarRight
sx={{ sx={{
gap: "10px", gap: '10px',
}} }}
> >
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
setSelectedTab(null); setSelectedTab(null);
executeEvent("newTabWindow", {}); executeEvent('newTabWindow', {});
}} }}
> >
<img <NavAdd
style={{ style={{
height: "40px", height: '40px',
width: "40px", width: '40px',
}} }}
src={NavAdd} />
/> </ButtonBase>
</ButtonBase>
<ButtonBase <ButtonBase
onClick={(e) => { onClick={(e) => {
if (!selectedTab) return; if (!selectedTab) return;
handleClick(e); handleClick(e);
}} }}
> >
<img <NavMoreMenu
style={{ style={{
height: "34px", height: '34px',
width: "34px", width: '34px',
}} }}
src={NavMoreMenu} />
/> </ButtonBase>
</ButtonBase> </AppsNavBarRight>
</AppsNavBarRight>
)} )}
<Menu <Menu
id="navbar-more-mobile" id="navbar-more-mobile"
anchorEl={anchorEl} anchorEl={anchorEl}
open={open} open={open}
onClose={handleClose} onClose={handleClose}
MenuListProps={{ MenuListProps={{
"aria-labelledby": "basic-button", 'aria-labelledby': 'basic-button',
}} }}
anchorOrigin={{ anchorOrigin={{
vertical: "bottom", vertical: 'bottom',
horizontal: "center", horizontal: 'center',
}} }}
transformOrigin={{ transformOrigin={{
vertical: "top", vertical: 'top',
horizontal: "center", horizontal: 'center',
}} }}
slotProps={{ slotProps={{
paper: { paper: {
sx: { sx: {
backgroundColor: "var(--bg-primary)", backgroundColor: 'var(--bg-primary)',
color: "#fff", color: '#fff',
width: "148px", width: '148px',
borderRadius: "5px", borderRadius: '5px',
}, },
}, },
}} }}
sx={{ sx={{
marginTop: "10px", marginTop: '10px',
}} }}
> >
<MenuItem <MenuItem
@ -280,8 +291,8 @@ export const AppsNavBar = () => {
} }
saveToLocalStorage( saveToLocalStorage(
"ext_saved_settings", 'ext_saved_settings',
"sortablePinnedApps", 'sortablePinnedApps',
updatedApps updatedApps
); );
return updatedApps; return updatedApps;
@ -293,31 +304,33 @@ export const AppsNavBar = () => {
> >
<ListItemIcon <ListItemIcon
sx={{ sx={{
minWidth: "24px !important", minWidth: '24px !important',
marginRight: "5px", marginRight: '5px',
}} }}
> >
<PushPinIcon <PushPinIcon
height={20} height={20}
sx={{ sx={{
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)", color: isSelectedAppPinned ? 'red' : 'rgba(250, 250, 250, 0.5)',
}} }}
/> />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
sx={{ sx={{
"& .MuiTypography-root": { '& .MuiTypography-root': {
fontSize: "12px", fontSize: '12px',
fontWeight: 600, fontWeight: 600,
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)", color: isSelectedAppPinned ? 'red' : 'rgba(250, 250, 250, 0.5)',
}, },
}} }}
primary={`${isSelectedAppPinned ? "Unpin app" : "Pin app"}`} primary={`${isSelectedAppPinned ? 'Unpin app' : 'Pin app'}`}
/> />
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
executeEvent("refreshApp", { executeEvent('refreshApp', {
tabId: selectedTab?.tabId, tabId: selectedTab?.tabId,
}); });
handleClose(); handleClose();
@ -325,23 +338,24 @@ export const AppsNavBar = () => {
> >
<ListItemIcon <ListItemIcon
sx={{ sx={{
minWidth: "24px !important", minWidth: '24px !important',
marginRight: "5px", marginRight: '5px',
}} }}
> >
<RefreshIcon <RefreshIcon
height={20} height={20}
sx={{ sx={{
color: "rgba(250, 250, 250, 0.5)", color: 'rgba(250, 250, 250, 0.5)',
}} }}
/> />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
sx={{ sx={{
"& .MuiTypography-root": { '& .MuiTypography-root': {
fontSize: "12px", fontSize: '12px',
fontWeight: 600, fontWeight: 600,
color: "rgba(250, 250, 250, 0.5)", color: 'rgba(250, 250, 250, 0.5)',
}, },
}} }}
primary="Refresh" primary="Refresh"

View File

@ -1,12 +1,12 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from 'react';
import { import {
AppsNavBarLeft, AppsNavBarLeft,
AppsNavBarParent, AppsNavBarParent,
AppsNavBarRight, AppsNavBarRight,
} from "./Apps-styles"; } from './Apps-styles';
import NavBack from "../../assets/svgs/NavBack.svg"; import { NavBack } from '../../assets/Icons/NavBack.tsx';
import NavAdd from "../../assets/svgs/NavAdd.svg"; import { NavAdd } from '../../assets/Icons/NavAdd.tsx';
import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg"; import { NavMoreMenu } from '../../assets/Icons/NavMoreMenu.tsx';
import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { import {
ButtonBase, ButtonBase,
@ -16,21 +16,22 @@ import {
MenuItem, MenuItem,
Tab, Tab,
Tabs, Tabs,
} from "@mui/material"; useTheme,
} from '@mui/material';
import { import {
executeEvent, executeEvent,
subscribeToEvent, subscribeToEvent,
unsubscribeFromEvent, unsubscribeFromEvent,
} from "../../utils/events"; } from '../../utils/events';
import TabComponent from "./TabComponent"; import TabComponent from './TabComponent';
import PushPinIcon from "@mui/icons-material/PushPin"; import PushPinIcon from '@mui/icons-material/PushPin';
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from '@mui/icons-material/Refresh';
import { useRecoilState, useSetRecoilState } from "recoil"; import { useRecoilState, useSetRecoilState } from 'recoil';
import { import {
navigationControllerAtom, navigationControllerAtom,
settingsLocalLastUpdatedAtom, settingsLocalLastUpdatedAtom,
sortablePinnedAppsAtom, sortablePinnedAppsAtom,
} from "../../atoms/global"; } from '../../atoms/global';
export function saveToLocalStorage(key, subKey, newValue) { export function saveToLocalStorage(key, subKey, newValue) {
try { try {
@ -59,14 +60,17 @@ export function saveToLocalStorage(key, subKey, newValue) {
const serializedValue = JSON.stringify(combinedData); const serializedValue = JSON.stringify(combinedData);
localStorage.setItem(key, serializedValue); localStorage.setItem(key, serializedValue);
} catch (error) { } catch (error) {
console.error("Error saving to localStorage:", error); console.error('Error saving to localStorage:', error);
} }
} }
export const AppsNavBarDesktop = ({disableBack}) => { export const AppsNavBarDesktop = ({ disableBack }) => {
const [tabs, setTabs] = useState([]); const [tabs, setTabs] = useState([]);
const [selectedTab, setSelectedTab] = useState(null); const [selectedTab, setSelectedTab] = useState(null);
const [navigationController, setNavigationController] = useRecoilState(navigationControllerAtom) const [navigationController, setNavigationController] = useRecoilState(
navigationControllerAtom
);
const theme = useTheme();
const [isNewTabWindow, setIsNewTabWindow] = useState(false); const [isNewTabWindow, setIsNewTabWindow] = useState(false);
const tabsRef = useRef(null); const tabsRef = useRef(null);
@ -76,7 +80,6 @@ export const AppsNavBarDesktop = ({disableBack}) => {
sortablePinnedAppsAtom sortablePinnedAppsAtom
); );
const setSettingsLocalLastUpdated = useSetRecoilState( const setSettingsLocalLastUpdated = useSetRecoilState(
settingsLocalLastUpdatedAtom settingsLocalLastUpdatedAtom
); );
@ -92,29 +95,26 @@ export const AppsNavBarDesktop = ({disableBack}) => {
useEffect(() => { useEffect(() => {
// Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added) // Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added)
if (tabsRef.current) { if (tabsRef.current) {
const tabElements = tabsRef.current.querySelectorAll(".MuiTab-root"); const tabElements = tabsRef.current.querySelectorAll('.MuiTab-root');
if (tabElements.length > 0) { if (tabElements.length > 0) {
const lastTab = tabElements[tabElements.length - 1]; const lastTab = tabElements[tabElements.length - 1];
lastTab.scrollIntoView({ lastTab.scrollIntoView({
behavior: "smooth", behavior: 'smooth',
block: "nearest", block: 'nearest',
inline: "end", inline: 'end',
}); });
} }
} }
}, [tabs.length]); // Dependency on the number of tabs }, [tabs.length]); // Dependency on the number of tabs
const isDisableBackButton = useMemo(() => {
if (disableBack) return true;
const isDisableBackButton = useMemo(()=> { if (selectedTab && navigationController[selectedTab?.tabId]?.hasBack)
if(disableBack) return true return false;
if(selectedTab && navigationController[selectedTab?.tabId]?.hasBack) return false if (selectedTab && !navigationController[selectedTab?.tabId]?.hasBack)
if(selectedTab && !navigationController[selectedTab?.tabId]?.hasBack) return true return true;
return false return false;
}, [navigationController, selectedTab, disableBack]) }, [navigationController, selectedTab, disableBack]);
const setTabsToNav = (e) => { const setTabsToNav = (e) => {
const { tabs, selectedTab, isNewTabWindow } = e.detail?.data; const { tabs, selectedTab, isNewTabWindow } = e.detail?.data;
@ -124,57 +124,61 @@ export const AppsNavBarDesktop = ({disableBack}) => {
}; };
useEffect(() => { useEffect(() => {
subscribeToEvent("setTabsToNav", setTabsToNav); subscribeToEvent('setTabsToNav', setTabsToNav);
return () => { return () => {
unsubscribeFromEvent("setTabsToNav", setTabsToNav); unsubscribeFromEvent('setTabsToNav', setTabsToNav);
}; };
}, []); }, []);
const isSelectedAppPinned = useMemo(() => {
if (selectedTab?.isPrivate) {
const isSelectedAppPinned = useMemo(()=> {
if(selectedTab?.isPrivate){
return !!sortablePinnedApps?.find( return !!sortablePinnedApps?.find(
(item) => (item) =>
item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name && item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service && item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier item?.privateAppProperties?.name ===
selectedTab?.privateAppProperties?.name &&
item?.privateAppProperties?.service ===
selectedTab?.privateAppProperties?.service &&
item?.privateAppProperties?.identifier ===
selectedTab?.privateAppProperties?.identifier
); );
} else { } else {
return !!sortablePinnedApps?.find( return !!sortablePinnedApps?.find(
(item) => (item) =>
item?.name === selectedTab?.name && item?.service === selectedTab?.service item?.name === selectedTab?.name &&
item?.service === selectedTab?.service
); );
} }
}, [selectedTab,sortablePinnedApps]) }, [selectedTab, sortablePinnedApps]);
return ( return (
<AppsNavBarParent <AppsNavBarParent
sx={{ sx={{
position: "relative", borderRadius: '0px 30px 30px 0px',
flexDirection: "column", flexDirection: 'column',
width: "60px", height: 'unset',
height: "unset", maxHeight: '70vh',
maxHeight: "70vh", padding: '10px',
borderRadius: "0px 30px 30px 0px", position: 'relative',
padding: "10px", width: '60px',
}} }}
> >
<AppsNavBarLeft <AppsNavBarLeft
sx={{ sx={{
flexDirection: "column", flexDirection: 'column',
}} }}
> >
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
executeEvent("navigateBack", selectedTab?.tabId); executeEvent('navigateBack', selectedTab?.tabId);
}} }}
disabled={isDisableBackButton} disabled={isDisableBackButton}
sx={{ sx={{
opacity: !isDisableBackButton ? 1 : 0.1, opacity: !isDisableBackButton ? 1 : 0.1,
cursor: !isDisableBackButton ? 'pointer': 'default' cursor: !isDisableBackButton ? 'pointer' : 'default',
}} }}
> >
<img src={NavBack} /> <NavBack />
</ButtonBase> </ButtonBase>
<Tabs <Tabs
orientation="vertical" orientation="vertical"
@ -183,11 +187,11 @@ export const AppsNavBarDesktop = ({disableBack}) => {
variant="scrollable" // Make tabs scrollable variant="scrollable" // Make tabs scrollable
scrollButtons={true} scrollButtons={true}
sx={{ sx={{
"& .MuiTabs-indicator": { '& .MuiTabs-indicator': {
backgroundColor: "white", backgroundColor: theme.palette.background.default,
}, },
maxHeight: `320px`, // Ensure the tabs container fits within the available space maxHeight: `320px`, // Ensure the tabs container fits within the available space
overflow: "hidden", // Prevents overflow on small screens overflow: 'hidden', // Prevents overflow on small screens
}} }}
> >
{tabs?.map((tab) => ( {tabs?.map((tab) => (
@ -202,84 +206,83 @@ export const AppsNavBarDesktop = ({disableBack}) => {
/> />
} // Pass custom component } // Pass custom component
sx={{ sx={{
"&.Mui-selected": { '&.Mui-selected': {
color: "white", color: theme.palette.text.primary,
}, },
padding: "0px", padding: '0px',
margin: "0px", margin: '0px',
minWidth: "0px", minWidth: '0px',
width: "50px", width: '50px',
}} }}
/> />
))} ))}
</Tabs> </Tabs>
</AppsNavBarLeft> </AppsNavBarLeft>
{selectedTab && ( {selectedTab && (
<AppsNavBarRight <AppsNavBarRight
sx={{ sx={{
gap: "10px", gap: '10px',
flexDirection: "column", flexDirection: 'column',
}}
>
<ButtonBase
onClick={() => {
setSelectedTab(null);
executeEvent("newTabWindow", {});
}} }}
> >
<img <ButtonBase
style={{ onClick={() => {
height: "40px", setSelectedTab(null);
width: "40px", executeEvent('newTabWindow', {});
}} }}
src={NavAdd} >
/> <NavAdd
</ButtonBase> style={{
<ButtonBase height: '40px',
onClick={(e) => { width: '40px',
if (!selectedTab) return; }}
handleClick(e); />
}} </ButtonBase>
> <ButtonBase
<img onClick={(e) => {
style={{ if (!selectedTab) return;
height: "34px", handleClick(e);
width: "34px",
}} }}
src={NavMoreMenu} >
/> <NavMoreMenu
</ButtonBase> style={{
</AppsNavBarRight> height: '34px',
width: '34px',
}}
/>
</ButtonBase>
</AppsNavBarRight>
)} )}
<Menu <Menu
id="navbar-more-mobile" id="navbar-more-mobile"
anchorEl={anchorEl} anchorEl={anchorEl}
open={open} open={open}
onClose={handleClose} onClose={handleClose}
MenuListProps={{ MenuListProps={{
"aria-labelledby": "basic-button", 'aria-labelledby': 'basic-button',
}} }}
anchorOrigin={{ anchorOrigin={{
vertical: "bottom", vertical: 'bottom',
horizontal: "center", horizontal: 'center',
}} }}
transformOrigin={{ transformOrigin={{
vertical: "top", vertical: 'top',
horizontal: "center", horizontal: 'center',
}} }}
slotProps={{ slotProps={{
paper: { paper: {
sx: { sx: {
backgroundColor: "var(--bg-primary)", backgroundColor: theme.palette.background.default,
color: "#fff", color: theme.palette.text.primary,
width: "148px", width: '148px',
borderRadius: "5px", borderRadius: '5px',
}, },
}, },
}} }}
sx={{ sx={{
marginTop: "10px", marginTop: '10px',
}} }}
> >
<MenuItem <MenuItem
@ -291,13 +294,16 @@ export const AppsNavBarDesktop = ({disableBack}) => {
if (isSelectedAppPinned) { if (isSelectedAppPinned) {
// Remove the selected app if it is pinned // Remove the selected app if it is pinned
if(selectedTab?.isPrivate){ if (selectedTab?.isPrivate) {
updatedApps = prev.filter( updatedApps = prev.filter(
(item) => (item) =>
!( !(
item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name && item?.privateAppProperties?.name ===
item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service && selectedTab?.privateAppProperties?.name &&
item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier item?.privateAppProperties?.service ===
selectedTab?.privateAppProperties?.service &&
item?.privateAppProperties?.identifier ===
selectedTab?.privateAppProperties?.identifier
) )
); );
} else { } else {
@ -309,21 +315,19 @@ export const AppsNavBarDesktop = ({disableBack}) => {
) )
); );
} }
} else { } else {
// Add the selected app if it is not pinned // Add the selected app if it is not pinned
if(selectedTab?.isPrivate){ if (selectedTab?.isPrivate) {
updatedApps = [ updatedApps = [
...prev, ...prev,
{ {
isPreview: true, isPreview: true,
isPrivate: true, isPrivate: true,
privateAppProperties: { privateAppProperties: {
...(selectedTab?.privateAppProperties || {}) ...(selectedTab?.privateAppProperties || {}),
} },
},
}, ];
];
} else { } else {
updatedApps = [ updatedApps = [
...prev, ...prev,
@ -333,12 +337,11 @@ export const AppsNavBarDesktop = ({disableBack}) => {
}, },
]; ];
} }
} }
saveToLocalStorage( saveToLocalStorage(
"ext_saved_settings", 'ext_saved_settings',
"sortablePinnedApps", 'sortablePinnedApps',
updatedApps updatedApps
); );
return updatedApps; return updatedApps;
@ -350,70 +353,74 @@ export const AppsNavBarDesktop = ({disableBack}) => {
> >
<ListItemIcon <ListItemIcon
sx={{ sx={{
minWidth: "24px !important", minWidth: '24px !important',
marginRight: "5px", marginRight: '5px',
}} }}
> >
<PushPinIcon <PushPinIcon
height={20} height={20}
sx={{ sx={{
color: isSelectedAppPinned ? "var(--danger)" : "rgba(250, 250, 250, 0.5)", color: isSelectedAppPinned
? 'var(--danger)'
: theme.palette.text.primary,
}} }}
/> />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
sx={{ sx={{
"& .MuiTypography-root": { '& .MuiTypography-root': {
fontSize: "12px", fontSize: '12px',
fontWeight: 600, fontWeight: 600,
color: isSelectedAppPinned ? "var(--danger)" : "rgba(250, 250, 250, 0.5)", color: isSelectedAppPinned
? 'var(--danger)'
: theme.palette.text.primary,
}, },
}} }}
primary={`${isSelectedAppPinned ? "Unpin app" : "Pin app"}`} primary={`${isSelectedAppPinned ? 'Unpin app' : 'Pin app'}`}
/> />
</MenuItem> </MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
if (selectedTab?.refreshFunc) { if (selectedTab?.refreshFunc) {
selectedTab.refreshFunc(selectedTab?.tabId); selectedTab.refreshFunc(selectedTab?.tabId);
} else { } else {
executeEvent("refreshApp", { executeEvent('refreshApp', {
tabId: selectedTab?.tabId, tabId: selectedTab?.tabId,
}); });
} }
handleClose(); handleClose();
}} }}
> >
<ListItemIcon <ListItemIcon
sx={{ sx={{
minWidth: "24px !important", minWidth: '24px !important',
marginRight: "5px", marginRight: '5px',
}} }}
> >
<RefreshIcon <RefreshIcon
height={20} height={20}
sx={{ sx={{
color: "rgba(250, 250, 250, 0.5)", color: theme.palette.text.primary,
}} }}
/> />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
sx={{ sx={{
"& .MuiTypography-root": { '& .MuiTypography-root': {
fontSize: "12px", fontSize: '12px',
fontWeight: 600, fontWeight: 600,
color: "rgba(250, 250, 250, 0.5)", color: theme.palette.text.primary,
}, },
}} }}
primary="Refresh" primary="Refresh"
/> />
</MenuItem> </MenuItem>
{!selectedTab?.isPrivate && ( {!selectedTab?.isPrivate && (
<MenuItem <MenuItem
onClick={() => { onClick={() => {
executeEvent("copyLink", { executeEvent('copyLink', {
tabId: selectedTab?.tabId, tabId: selectedTab?.tabId,
}); });
handleClose(); handleClose();
@ -421,23 +428,24 @@ export const AppsNavBarDesktop = ({disableBack}) => {
> >
<ListItemIcon <ListItemIcon
sx={{ sx={{
minWidth: "24px !important", minWidth: '24px !important',
marginRight: "5px", marginRight: '5px',
}} }}
> >
<ContentCopyIcon <ContentCopyIcon
height={20} height={20}
sx={{ sx={{
color: "rgba(250, 250, 250, 0.5)", color: theme.palette.text.primary,
}} }}
/> />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
sx={{ sx={{
"& .MuiTypography-root": { '& .MuiTypography-root': {
fontSize: "12px", fontSize: '12px',
fontWeight: 600, fontWeight: 600,
color: "rgba(250, 250, 250, 0.5)", color: theme.palette.text.primary,
}, },
}} }}
primary="Copy link" primary="Copy link"

View File

@ -1,6 +1,5 @@
import React, { useContext, useMemo, useState } from "react"; import React, { useContext, useMemo, useState } from 'react';
import { import {
Avatar,
Box, Box,
Button, Button,
ButtonBase, ButtonBase,
@ -13,14 +12,17 @@ import {
Select, Select,
Tab, Tab,
Tabs, Tabs,
Typography, useTheme,
} from "@mui/material"; } from '@mui/material';
import { useDropzone } from "react-dropzone"; import { useDropzone } from 'react-dropzone';
import { useHandlePrivateApps } from "./useHandlePrivateApps"; import { useHandlePrivateApps } from './useHandlePrivateApps';
import { useRecoilState, useSetRecoilState } from "recoil"; import { useRecoilState } from 'recoil';
import { groupsPropertiesAtom, myGroupsWhereIAmAdminAtom } from "../../atoms/global"; import {
import { Label } from "../Group/AddGroup"; groupsPropertiesAtom,
import { Spacer } from "../../common/Spacer"; myGroupsWhereIAmAdminAtom,
} from '../../atoms/global';
import { Label } from '../Group/AddGroup';
import { Spacer } from '../../common/Spacer';
import { import {
Add, Add,
AppCircle, AppCircle,
@ -28,52 +30,59 @@ import {
AppCircleLabel, AppCircleLabel,
PublishQAppChoseFile, PublishQAppChoseFile,
PublishQAppInfo, PublishQAppInfo,
} from "./Apps-styles"; } from './Apps-styles';
import ImageUploader from "../../common/ImageUploader"; import ImageUploader from '../../common/ImageUploader';
import { isMobile, MyContext } from "../../App"; import { isMobile, MyContext } from '../../App';
import { fileToBase64 } from "../../utils/fileReading"; import { fileToBase64 } from '../../utils/fileReading';
import { objectToBase64 } from "../../qdn/encryption/group-encryption"; import { objectToBase64 } from '../../qdn/encryption/group-encryption';
import { getFee } from "../../background"; import { getFee } from '../../background';
const maxFileSize = 50 * 1024 * 1024; // 50MB const maxFileSize = 50 * 1024 * 1024; // 50MB
export const AppsPrivate = ({myName}) => { export const AppsPrivate = ({ myName }) => {
const { openApp } = useHandlePrivateApps(); const { openApp } = useHandlePrivateApps();
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [logo, setLogo] = useState(null); const [logo, setLogo] = useState(null);
const [qortalUrl, setQortalUrl] = useState(""); const [qortalUrl, setQortalUrl] = useState('');
const [selectedGroup, setSelectedGroup] = useState(0); const [selectedGroup, setSelectedGroup] = useState(0);
const [groupsProperties] = useRecoilState(groupsPropertiesAtom) const [groupsProperties] = useRecoilState(groupsPropertiesAtom);
const [valueTabPrivateApp, setValueTabPrivateApp] = useState(0); const [valueTabPrivateApp, setValueTabPrivateApp] = useState(0);
const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState( const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState(
myGroupsWhereIAmAdminAtom myGroupsWhereIAmAdminAtom
); );
const myGroupsWhereIAmAdmin = useMemo(()=> { const myGroupsWhereIAmAdmin = useMemo(() => {
return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false) return myGroupsWhereIAmAdminFromGlobal?.filter(
}, [myGroupsWhereIAmAdminFromGlobal, groupsProperties]) (group) => groupsProperties[group?.groupId]?.isOpen === false
const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false); );
const { show, setInfoSnackCustom, setOpenSnackGlobal, memberGroups } = useContext(MyContext); }, [myGroupsWhereIAmAdminFromGlobal, groupsProperties]);
const myGroupsPrivate = useMemo(()=> { const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false);
return memberGroups?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false) const { show, setInfoSnackCustom, setOpenSnackGlobal, memberGroups } =
}, [memberGroups, groupsProperties]) useContext(MyContext);
const theme = useTheme();
const myGroupsPrivate = useMemo(() => {
return memberGroups?.filter(
(group) => groupsProperties[group?.groupId]?.isOpen === false
);
}, [memberGroups, groupsProperties]);
const [privateAppValues, setPrivateAppValues] = useState({ const [privateAppValues, setPrivateAppValues] = useState({
name: "", name: '',
service: "DOCUMENT", service: 'DOCUMENT',
identifier: "", identifier: '',
groupId: 0, groupId: 0,
}); });
const [newPrivateAppValues, setNewPrivateAppValues] = useState({ const [newPrivateAppValues, setNewPrivateAppValues] = useState({
service: "DOCUMENT", service: 'DOCUMENT',
identifier: "", identifier: '',
name: "", name: '',
}); });
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
accept: { accept: {
"application/zip": [".zip"], // Only accept zip files 'application/zip': ['.zip'], // Only accept zip files
}, },
maxSize: maxFileSize, maxSize: maxFileSize,
multiple: false, // Disable multiple file uploads multiple: false, // Disable multiple file uploads
@ -85,7 +94,7 @@ export const AppsPrivate = ({myName}) => {
onDropRejected: (fileRejections) => { onDropRejected: (fileRejections) => {
fileRejections.forEach(({ file, errors }) => { fileRejections.forEach(({ file, errors }) => {
errors.forEach((error) => { errors.forEach((error) => {
if (error.code === "file-too-large") { if (error.code === 'file-too-large') {
console.error( console.error(
`File ${file.name} is too large. Max size allowed is ${ `File ${file.name} is too large. Max size allowed is ${
maxFileSize / (1024 * 1024) maxFileSize / (1024 * 1024)
@ -100,25 +109,24 @@ export const AppsPrivate = ({myName}) => {
const addPrivateApp = async () => { const addPrivateApp = async () => {
try { try {
if (privateAppValues?.groupId === 0) return; if (privateAppValues?.groupId === 0) return;
await openApp(privateAppValues, true); await openApp(privateAppValues, true);
} catch (error) { } catch (error) {
console.error(error) console.error(error);
} }
}; };
const clearFields = () => { const clearFields = () => {
setPrivateAppValues({ setPrivateAppValues({
name: "", name: '',
service: "DOCUMENT", service: 'DOCUMENT',
identifier: "", identifier: '',
groupId: 0, groupId: 0,
}); });
setNewPrivateAppValues({ setNewPrivateAppValues({
service: "DOCUMENT", service: 'DOCUMENT',
identifier: "", identifier: '',
name: "", name: '',
}); });
setFile(null); setFile(null);
setValueTabPrivateApp(0); setValueTabPrivateApp(0);
@ -129,9 +137,9 @@ export const AppsPrivate = ({myName}) => {
const publishPrivateApp = async () => { const publishPrivateApp = async () => {
try { try {
if (selectedGroup === 0) return; if (selectedGroup === 0) return;
if (!logo) throw new Error("Please select an image for a logo"); 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 (!myName) throw new Error('You need a Qortal name to publish');
if (!newPrivateAppValues?.name) throw new Error("Your app needs a name"); if (!newPrivateAppValues?.name) throw new Error('Your app needs a name');
const base64Logo = await fileToBase64(logo); const base64Logo = await fileToBase64(logo);
const base64App = await fileToBase64(file); const base64App = await fileToBase64(file);
const objectToSave = { const objectToSave = {
@ -141,27 +149,29 @@ export const AppsPrivate = ({myName}) => {
}; };
const object64 = await objectToBase64(objectToSave); const object64 = await objectToBase64(objectToSave);
const decryptedData = await window.sendMessage( const decryptedData = await window.sendMessage(
"ENCRYPT_QORTAL_GROUP_DATA", 'ENCRYPT_QORTAL_GROUP_DATA',
{ {
base64: object64, base64: object64,
groupId: selectedGroup, groupId: selectedGroup,
} }
); );
if (decryptedData?.error) { if (decryptedData?.error) {
throw new Error( throw new Error(
decryptedData?.error || "Unable to encrypt app. App not published" decryptedData?.error || 'Unable to encrypt app. App not published'
); );
} }
const fee = await getFee("ARBITRARY");
const fee = await getFee('ARBITRARY');
await show({ await show({
message: "Would you like to publish this app?", message: 'Would you like to publish this app?',
publishFee: fee.fee + " QORT", publishFee: fee.fee + ' QORT',
}); });
await new Promise((res, rej) => { await new Promise((res, rej) => {
window window
.sendMessage("publishOnQDN", { .sendMessage('publishOnQDN', {
data: decryptedData, data: decryptedData,
identifier: newPrivateAppValues?.identifier, identifier: newPrivateAppValues?.identifier,
service: newPrivateAppValues?.service, service: newPrivateAppValues?.service,
@ -174,9 +184,10 @@ export const AppsPrivate = ({myName}) => {
rej(response.error); rej(response.error);
}) })
.catch((error) => { .catch((error) => {
rej(error.message || "An error occurred"); rej(error.message || 'An error occurred');
}); });
}); });
openApp( openApp(
{ {
identifier: newPrivateAppValues?.identifier, identifier: newPrivateAppValues?.identifier,
@ -188,10 +199,10 @@ export const AppsPrivate = ({myName}) => {
); );
clearFields(); clearFields();
} catch (error) { } catch (error) {
setOpenSnackGlobal(true) setOpenSnackGlobal(true);
setInfoSnackCustom({ setInfoSnackCustom({
type: "error", type: 'error',
message: error?.message || "Unable to publish app", message: error?.message || 'Unable to publish app',
}); });
} }
}; };
@ -203,9 +214,10 @@ export const AppsPrivate = ({myName}) => {
function a11yProps(index: number) { function a11yProps(index: number) {
return { return {
id: `simple-tab-${index}`, id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`, 'aria-controls': `simple-tabpanel-${index}`,
}; };
} }
return ( return (
<> <>
<ButtonBase <ButtonBase
@ -213,17 +225,18 @@ export const AppsPrivate = ({myName}) => {
setIsOpenPrivateModal(true); setIsOpenPrivateModal(true);
}} }}
sx={{ sx={{
width: "80px", width: '80px',
}} }}
> >
<AppCircleContainer <AppCircleContainer
sx={{ sx={{
gap: !isMobile ? "10px" : "5px", gap: !isMobile ? '10px' : '5px',
}} }}
> >
<AppCircle> <AppCircle>
<Add>+</Add> <Add>+</Add>
</AppCircle> </AppCircle>
<AppCircleLabel>Private</AppCircleLabel> <AppCircleLabel>Private</AppCircleLabel>
</AppCircleContainer> </AppCircleContainer>
</ButtonBase> </ButtonBase>
@ -233,7 +246,7 @@ export const AppsPrivate = ({myName}) => {
aria-labelledby="alert-dialog-title" aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === 'Enter') {
if (valueTabPrivateApp === 0) { if (valueTabPrivateApp === 0) {
if ( if (
!privateAppValues.name || !privateAppValues.name ||
@ -249,23 +262,17 @@ export const AppsPrivate = ({myName}) => {
maxWidth="md" maxWidth="md"
fullWidth={true} fullWidth={true}
> >
<DialogTitle id="alert-dialog-title">
{valueTabPrivateApp === 0
? "Access private app"
: "Publish private app"}
</DialogTitle>
<Box> <Box>
<Tabs <Tabs
value={valueTabPrivateApp} value={valueTabPrivateApp}
onChange={handleChange} onChange={handleChange}
aria-label="basic tabs example" aria-label="basic tabs example"
variant={isMobile ? "scrollable" : "fullWidth"} // Scrollable on mobile, full width on desktop variant={isMobile ? 'scrollable' : 'fullWidth'} // Scrollable on mobile, full width on desktop
scrollButtons="auto" scrollButtons="auto"
allowScrollButtonsMobile allowScrollButtonsMobile
sx={{ sx={{
"& .MuiTabs-indicator": { '& .MuiTabs-indicator': {
backgroundColor: "white", backgroundColor: theme.palette.background.default,
}, },
}} }}
> >
@ -273,20 +280,20 @@ export const AppsPrivate = ({myName}) => {
label="Access app" label="Access app"
{...a11yProps(0)} {...a11yProps(0)}
sx={{ sx={{
"&.Mui-selected": { '&.Mui-selected': {
color: "white", color: theme.palette.text.primary,
}, },
fontSize: isMobile ? "0.75rem" : "1rem", // Adjust font size for mobile fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile
}} }}
/> />
<Tab <Tab
label="Publish app" label="Publish app"
{...a11yProps(1)} {...a11yProps(1)}
sx={{ sx={{
"&.Mui-selected": { '&.Mui-selected': {
color: "white", color: theme.palette.text.primary,
}, },
fontSize: isMobile ? "0.75rem" : "1rem", // Adjust font size for mobile fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile
}} }}
/> />
</Tabs> </Tabs>
@ -296,9 +303,9 @@ export const AppsPrivate = ({myName}) => {
<DialogContent> <DialogContent>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '5px',
}} }}
> >
<Label>Select a group</Label> <Label>Select a group</Label>
@ -333,10 +340,10 @@ export const AppsPrivate = ({myName}) => {
<Spacer height="10px" /> <Spacer height="10px" />
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '5px',
marginTop: "15px", marginTop: '15px',
}} }}
> >
<Label>name</Label> <Label>name</Label>
@ -355,10 +362,10 @@ export const AppsPrivate = ({myName}) => {
</Box> </Box>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '5px',
marginTop: "15px", marginTop: '15px',
}} }}
> >
<Label>identifier</Label> <Label>identifier</Label>
@ -376,6 +383,7 @@ export const AppsPrivate = ({myName}) => {
/> />
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
variant="contained" variant="contained"
@ -406,15 +414,19 @@ export const AppsPrivate = ({myName}) => {
<DialogContent> <DialogContent>
<PublishQAppInfo <PublishQAppInfo
sx={{ sx={{
fontSize: "14px", backgroundColor: theme.palette.background.paper,
fontSize: '14px',
}} }}
> >
Select .zip file containing static content:{" "} Select .zip file containing static content:{' '}
</PublishQAppInfo> </PublishQAppInfo>
<Spacer height="10px" /> <Spacer height="10px" />
<PublishQAppInfo <PublishQAppInfo
sx={{ sx={{
fontSize: "14px", backgroundColor: theme.palette.background.paper,
fontSize: '14px',
}} }}
>{` >{`
50mb MB maximum`}</PublishQAppInfo> 50mb MB maximum`}</PublishQAppInfo>
@ -426,17 +438,26 @@ export const AppsPrivate = ({myName}) => {
)} )}
<Spacer height="18px" /> <Spacer height="18px" />
<PublishQAppChoseFile {...getRootProps()}>
{" "} <PublishQAppChoseFile
sx={{
backgroundColor: theme.palette.background.default,
fontSize: '14px',
}}
{...getRootProps()}
>
{' '}
<input {...getInputProps()} /> <input {...getInputProps()} />
{file ? "Change" : "Choose"} File {file ? 'Change' : 'Choose'} File
</PublishQAppChoseFile> </PublishQAppChoseFile>
<Spacer height="20px" /> <Spacer height="20px" />
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '5px',
}} }}
> >
<Label>Select a group</Label> <Label>Select a group</Label>
@ -462,14 +483,15 @@ export const AppsPrivate = ({myName}) => {
})} })}
</Select> </Select>
</Box> </Box>
<Spacer height="20px" /> <Spacer height="20px" />
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '5px',
marginTop: "15px", marginTop: '15px',
}} }}
> >
<Label>identifier</Label> <Label>identifier</Label>
@ -486,13 +508,15 @@ export const AppsPrivate = ({myName}) => {
} }
/> />
</Box> </Box>
<Spacer height="10px" /> <Spacer height="10px" />
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '5px',
marginTop: "15px", marginTop: '15px',
}} }}
> >
<Label>App name</Label> <Label>App name</Label>
@ -511,12 +535,15 @@ export const AppsPrivate = ({myName}) => {
</Box> </Box>
<Spacer height="10px" /> <Spacer height="10px" />
<ImageUploader onPick={(file) => setLogo(file)}> <ImageUploader onPick={(file) => setLogo(file)}>
<Button variant="contained">Choose logo</Button> <Button variant="contained">Choose logo</Button>
</ImageUploader> </ImageUploader>
{logo?.name} {logo?.name}
<Spacer height="25px" /> <Spacer height="25px" />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
variant="contained" variant="contained"
@ -527,6 +554,7 @@ export const AppsPrivate = ({myName}) => {
> >
Close Close
</Button> </Button>
<Button <Button
disabled={ disabled={
!newPrivateAppValues.name || !newPrivateAppValues.name ||

View File

@ -1,205 +1,251 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import { useMemo } from 'react';
import { DndContext, closestCenter } from '@dnd-kit/core'; import { DndContext, closestCenter } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable'; import {
import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; 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 { CSS } from '@dnd-kit/utilities';
import { Avatar, ButtonBase } from '@mui/material'; import { Avatar, ButtonBase } from '@mui/material';
import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles'; import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles';
import { getBaseApiReact, MyContext } from '../../App'; import { getBaseApiReact } from '../../App';
import { executeEvent } from '../../utils/events'; import { executeEvent } from '../../utils/events';
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global'; import {
settingsLocalLastUpdatedAtom,
sortablePinnedAppsAtom,
} from '../../atoms/global';
import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilState, useSetRecoilState } from 'recoil';
import { saveToLocalStorage } from './AppsNavBar'; import { saveToLocalStorage } from './AppsNavBar';
import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps'; import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps';
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from '@mui/icons-material/Lock';
import { useHandlePrivateApps } from './useHandlePrivateApps'; import { useHandlePrivateApps } from './useHandlePrivateApps';
const SortableItem = ({ id, name, app, isDesktop }) => { const SortableItem = ({ id, name, app, isDesktop }) => {
const {openApp} = useHandlePrivateApps() const { openApp } = useHandlePrivateApps();
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); const { attributes, listeners, setNodeRef, transform, transition } =
const style = { useSortable({ id });
transform: CSS.Transform.toString(transform),
transition,
padding: '10px',
border: '1px solid #ccc',
marginBottom: '5px',
borderRadius: '4px',
backgroundColor: '#f9f9f9',
cursor: 'grab',
color: 'black'
};
return ( const style = {
<ContextMenuPinnedApps app={app} isMine={!!app?.isMine}> backgroundColor: '#f9f9f9',
<ButtonBase border: '1px solid #ccc',
ref={setNodeRef} {...attributes} {...listeners} borderRadius: '4px',
sx={{ color: 'black',
width: "80px", cursor: 'grab',
transform: CSS.Transform.toString(transform), marginBottom: '5px',
transition, padding: '10px',
}} transform: CSS.Transform.toString(transform),
onClick={async ()=> { transition,
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: "42px",
width: "42px",
}}
/>
) : (
<Avatar
sx={{
height: "42px",
width: "42px",
'& 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",
}}
// 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 = [] }) => { return (
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom); <ContextMenuPinnedApps app={app} isMine={!!app?.isMine}>
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); <ButtonBase
ref={setNodeRef}
const transformPinnedApps = useMemo(() => { {...attributes}
{...listeners}
// Clone the existing pinned apps list sx={{
let pinned = [...pinnedApps]; width: '80px',
transform: CSS.Transform.toString(transform),
// Function to add or update `isMine` property transition,
const addOrUpdateIsMine = (pinnedList, appToCheck) => { }}
if (!appToCheck) return pinnedList; onClick={async () => {
if (app?.isPrivate) {
const existingIndex = pinnedList.findIndex( try {
(item) => item?.service === appToCheck?.service && item?.name === appToCheck?.name await openApp(app?.privateAppProperties);
); } catch (error) {
console.error(error);
if (existingIndex !== -1) { }
// If the app is already in the list, update it with `isMine: true`
pinnedList[existingIndex] = { ...pinnedList[existingIndex], isMine: true };
} else { } else {
// If not in the list, add it with `isMine: true` at the beginning executeEvent('addTab', {
pinnedList.unshift({ ...appToCheck, isMine: true }); data: app,
});
} }
}}
return pinnedList; >
}; <AppCircleContainer
sx={{
// Update or add `myWebsite` and `myApp` while preserving their positions border: 'none',
pinned = addOrUpdateIsMine(pinned, myWebsite); gap: isDesktop ? '10px' : '5px',
pinned = addOrUpdateIsMine(pinned, myApp); }}
>
// Update pinned list based on availableQapps <AppCircle
pinned = pinned.map((pin) => { sx={{
const findIndex = availableQapps?.findIndex( border: 'none',
(item) => item?.service === pin?.service && item?.name === pin?.name }}
); >
if (findIndex !== -1) return { {app?.isPrivate && !app?.privateAppProperties?.logo ? (
...availableQapps[findIndex], <LockIcon
...pin sx={{
} height: '42px',
width: '42px',
return pin; }}
}); />
) : (
return pinned; <Avatar
}, [myApp, myWebsite, pinnedApps, availableQapps]); sx={{
height: '42px',
width: '42px',
const sensors = useSensors( '& img': {
useSensor(PointerSensor, { objectFit: 'fill',
activationConstraint: { },
distance: 10, // Set a distance to avoid triggering drag on small movements }}
}, alt={app?.metadata?.title || app?.name}
}), src={
useSensor(TouchSensor, { app?.privateAppProperties?.logo
activationConstraint: { ? app?.privateAppProperties?.logo
distance: 10, // Also apply to touch : `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
}, app?.name
}), }/qortal_avatar?async=true`
useSensor(KeyboardSensor, { }
coordinateGetter: sortableKeyboardCoordinates, >
}) <img
); style={{
width: '31px',
const handleDragEnd = (event) => { height: 'auto',
const { active, over } = event; }}
// src={LogoSelected}
if (!over) return; // Make sure the drop target exists alt="center-icon"
/>
if (active.id !== over.id) { </Avatar>
const oldIndex = transformPinnedApps.findIndex((item) => `${item?.service}-${item?.name}` === active.id); )}
const newIndex = transformPinnedApps.findIndex((item) => `${item?.service}-${item?.name}` === over.id); </AppCircle>
{app?.isPrivate ? (
const newOrder = arrayMove(transformPinnedApps, oldIndex, newIndex); <AppCircleLabel>
setPinnedApps(newOrder); {`${app?.privateAppProperties?.appName || 'Private'}`}
saveToLocalStorage('ext_saved_settings','sortablePinnedApps', newOrder) </AppCircleLabel>
setSettingsLocalLastUpdated(Date.now()) ) : (
} <AppCircleLabel>{app?.metadata?.title || app?.name}</AppCircleLabel>
}; )}
return ( </AppCircleContainer>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}> </ButtonBase>
<SortableContext items={transformPinnedApps.map((app) => `${app?.service}-${app?.name}`)}> </ContextMenuPinnedApps>
{transformPinnedApps.map((app) => ( );
<SortableItem isDesktop={isDesktop} key={`${app?.service}-${app?.name}`} id={`${app?.service}-${app?.name}`} name={app?.name} app={app} />
))}
</SortableContext>
</DndContext>
);
}; };
export const SortablePinnedApps = ({
isDesktop,
myWebsite,
myApp,
availableQapps = [],
}) => {
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom);
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;
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 });
}
return pinnedList;
};
// Update or add `myWebsite` and `myApp` while preserving their positions
pinned = addOrUpdateIsMine(pinned, myWebsite);
pinned = addOrUpdateIsMine(pinned, myApp);
// 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 {
...availableQapps[findIndex],
...pin,
};
return pin;
});
return pinned;
}, [myApp, myWebsite, pinnedApps, availableQapps]);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 10, // Set a distance to avoid triggering drag on small movements
},
}),
useSensor(TouchSensor, {
activationConstraint: {
distance: 10, // Also apply to touch
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const handleDragEnd = (event) => {
const { active, over } = event;
if (!over) return; // Make sure the drop target exists
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());
}
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<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>
);
};

View File

@ -1,54 +1,57 @@
import { TabParent } from "./Apps-styles"; import { TabParent } from './Apps-styles';
import NavCloseTab from "../../assets/svgs/NavCloseTab.svg"; import { NavCloseTab } from '../../assets/Icons/NavCloseTab.tsx';
import { getBaseApiReact } from "../../App"; import { getBaseApiReact } from '../../App';
import { Avatar, ButtonBase } from "@mui/material"; import { Avatar, ButtonBase, useTheme } from '@mui/material';
import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import LogoSelected from '../../assets/svgs/LogoSelected.svg';
import { executeEvent } from "../../utils/events"; import { executeEvent } from '../../utils/events';
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from '@mui/icons-material/Lock';
const TabComponent = ({ isSelected, app }) => { const TabComponent = ({ isSelected, app }) => {
const theme = useTheme();
return ( return (
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
if (isSelected) { if (isSelected) {
executeEvent("removeTab", { executeEvent('removeTab', {
data: app, data: app,
}); });
return; return;
} }
executeEvent("setSelectedTab", { executeEvent('setSelectedTab', {
data: app, data: app,
}); });
}} }}
> >
<TabParent <TabParent
sx={{ sx={{
border: isSelected && "1px solid #FFFFFF", borderStyle: isSelected && 'solid',
borderWidth: isSelected && '1px',
borderColor: isSelected && theme.palette.text.primary,
}} }}
> >
{isSelected && ( {isSelected && (
<img <NavCloseTab
style={{ style={{
position: "absolute", position: 'absolute',
top: "-5px", top: '-5px',
right: "-5px", right: '-5px',
zIndex: 1, zIndex: 1,
}} }}
src={NavCloseTab}
/> />
)} )}
{app?.isPrivate && !app?.privateAppProperties?.logo ? ( {app?.isPrivate && !app?.privateAppProperties?.logo ? (
<LockIcon <LockIcon
sx={{ sx={{
height: "28px", height: '28px',
width: "28px", width: '28px',
}} }}
/> />
) : ( ) : (
<Avatar <Avatar
sx={{ sx={{
height: "28px", height: '28px',
width: "28px", width: '28px',
}} }}
alt={app?.name} alt={app?.name}
src={ src={
@ -61,8 +64,8 @@ const TabComponent = ({ isSelected, app }) => {
> >
<img <img
style={{ style={{
width: "28px", width: '28px',
height: "auto", height: 'auto',
}} }}
src={LogoSelected} src={LogoSelected}
alt="center-icon" alt="center-icon"

View File

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

View File

@ -1,21 +1,7 @@
import React, { import { useContext, useEffect, useState } from 'react';
useCallback, import { MyContext, isMobile } from '../../App';
useContext, import { Box, Typography } from '@mui/material';
useEffect, import { AdminSpaceInner } from './AdminSpaceInner';
useMemo,
useRef,
useState,
} from "react";
import { GroupMail } from "../Group/Forum/GroupMail";
import { MyContext, isMobile } from "../../App";
import { getRootHeight } from "../../utils/mobile/mobileUtils";
import { Box, Typography } from "@mui/material";
import { AdminSpaceInner } from "./AdminSpaceInner";
export const AdminSpace = ({ export const AdminSpace = ({
selectedGroup, selectedGroup,
@ -26,11 +12,11 @@ export const AdminSpace = ({
isAdmin, isAdmin,
myAddress, myAddress,
hide, hide,
defaultThread, defaultThread,
setDefaultThread, setDefaultThread,
setIsForceShowCreationKeyPopup setIsForceShowCreationKeyPopup,
}) => { }) => {
const { rootHeight } = useContext(MyContext); const { rootHeight } = useContext(MyContext);
const [isMoved, setIsMoved] = useState(false); const [isMoved, setIsMoved] = useState(false);
useEffect(() => { useEffect(() => {
if (hide) { if (hide) {
@ -42,26 +28,37 @@ export const AdminSpace = ({
return ( return (
<div <div
style={{ style={{
// reference to change height // reference to change height
height: isMobile ? `calc(${rootHeight} - 127px` : "calc(100vh - 70px)", display: 'flex',
display: "flex", flexDirection: 'column',
flexDirection: "column", height: isMobile ? `calc(${rootHeight} - 127px` : 'calc(100vh - 70px)',
width: "100%", left: hide && '-1000px',
opacity: hide ? 0 : 1, opacity: hide ? 0 : 1,
visibility: hide && 'hidden', position: hide ? 'fixed' : 'relative',
position: hide ? 'fixed' : 'relative', visibility: hide && 'hidden',
left: hide && '-1000px' width: '100%',
}} }}
> >
{!isAdmin && <Box sx={{ {!isAdmin && (
width: '100%', <Box
display: 'flex', sx={{
justifyContent: 'center', display: 'flex',
paddingTop: '25px' justifyContent: 'center',
}}><Typography>Sorry, this space is only for Admins.</Typography></Box>} paddingTop: '25px',
{isAdmin && <AdminSpaceInner setIsForceShowCreationKeyPopup={setIsForceShowCreationKeyPopup} adminsWithNames={adminsWithNames} selectedGroup={selectedGroup} />} width: '100%',
}}
</div> >
<Typography>Sorry, this space is only for Admins.</Typography>
</Box>
)}
{isAdmin && (
<AdminSpaceInner
setIsForceShowCreationKeyPopup={setIsForceShowCreationKeyPopup}
adminsWithNames={adminsWithNames}
selectedGroup={selectedGroup}
/>
)}
</div>
); );
}; };

View File

@ -1,39 +1,42 @@
import React, { useCallback, useContext, useEffect, useState } from "react"; import { useCallback, useContext, useEffect, useState } from 'react';
import { import {
MyContext, MyContext,
getArbitraryEndpointReact, getArbitraryEndpointReact,
getBaseApiReact, getBaseApiReact,
} from "../../App"; } from '../../App';
import { Box, Button, Typography } from "@mui/material"; import { Box, Button, Typography } from '@mui/material';
import { import {
decryptResource, decryptResource,
getPublishesFromAdmins, getPublishesFromAdmins,
validateSecretKey, validateSecretKey,
} from "../Group/Group"; } from '../Group/Group';
import { getFee } from "../../background"; import { getFee } from '../../background';
import { base64ToUint8Array } from "../../qdn/encryption/group-encryption"; import { base64ToUint8Array } from '../../qdn/encryption/group-encryption';
import { uint8ArrayToObject } from "../../backgroundFunctions/encryption"; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
import { formatTimestampForum } from "../../utils/time"; import { formatTimestampForum } from '../../utils/time';
import { Spacer } from "../../common/Spacer"; import { Spacer } from '../../common/Spacer';
export const getPublishesFromAdminsAdminSpace = async ( export const getPublishesFromAdminsAdminSpace = async (
admins: string[], admins: string[],
groupId groupId
) => { ) => {
const queryString = admins.map((name) => `name=${name}`).join("&"); const queryString = admins.map((name) => `name=${name}`).join('&');
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=admins-symmetric-qchat-group-${groupId}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=admins-symmetric-qchat-group-${groupId}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`;
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error("network error"); throw new Error('network error');
} }
const adminData = await response.json(); const adminData = await response.json();
const filterId = adminData.filter( const filterId = adminData.filter(
(data: any) => data.identifier === `admins-symmetric-qchat-group-${groupId}` (data: any) => data.identifier === `admins-symmetric-qchat-group-${groupId}`
); );
if (filterId?.length === 0) { if (filterId?.length === 0) {
return false; return false;
} }
const sortedData = filterId.sort((a: any, b: any) => { const sortedData = filterId.sort((a: any, b: any) => {
// Get the most recent date for both a and b // Get the most recent date for both a and b
const dateA = a.updated ? new Date(a.updated) : new Date(a.created); const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
@ -87,10 +90,11 @@ export const AdminSpaceInner = ({
const dataint8Array = base64ToUint8Array(decryptedKey.data); const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
if (!validateSecretKey(decryptedKeyToObject)) if (!validateSecretKey(decryptedKeyToObject))
throw new Error("SecretKey is not valid"); throw new Error('SecretKey is not valid');
setAdminGroupSecretKey(decryptedKeyToObject); setAdminGroupSecretKey(decryptedKeyToObject);
setAdminGroupSecretKeyPublishDetails(getLatestPublish); setAdminGroupSecretKeyPublishDetails(getLatestPublish);
} catch (error) { } catch (error) {
console.log(error);
} finally { } finally {
setIsFetchingAdminGroupSecretKey(false); setIsFetchingAdminGroupSecretKey(false);
} }
@ -106,6 +110,7 @@ export const AdminSpaceInner = ({
if (getLatestPublish === false) setGroupSecretKeyPublishDetails(false); if (getLatestPublish === false) setGroupSecretKeyPublishDetails(false);
setGroupSecretKeyPublishDetails(getLatestPublish); setGroupSecretKeyPublishDetails(getLatestPublish);
} catch (error) { } catch (error) {
console.log(error);
} finally { } finally {
setIsFetchingGroupSecretKey(false); setIsFetchingGroupSecretKey(false);
} }
@ -113,15 +118,17 @@ export const AdminSpaceInner = ({
const createCommonSecretForAdmins = async () => { const createCommonSecretForAdmins = async () => {
try { try {
const fee = await getFee("ARBITRARY"); const fee = await getFee('ARBITRARY');
await show({ await show({
message: "Would you like to perform an ARBITRARY transaction?", message: 'Would you like to perform an ARBITRARY transaction?',
publishFee: fee.fee + " QORT", publishFee: fee.fee + ' QORT',
}); });
setIsLoadingPublishKey(true); setIsLoadingPublishKey(true);
window window
.sendMessage("encryptAndPublishSymmetricKeyGroupChatForAdmins", { .sendMessage('encryptAndPublishSymmetricKeyGroupChatForAdmins', {
groupId: selectedGroup, groupId: selectedGroup,
previousData: adminGroupSecretKey, previousData: adminGroupSecretKey,
admins: adminsWithNames, admins: adminsWithNames,
@ -129,27 +136,29 @@ export const AdminSpaceInner = ({
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
setInfoSnackCustom({ setInfoSnackCustom({
type: "success", type: 'success',
message: message:
"Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.", 'Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.',
}); });
setOpenSnackGlobal(true); setOpenSnackGlobal(true);
return; return;
} }
setInfoSnackCustom({ setInfoSnackCustom({
type: "error", type: 'error',
message: response?.error || "unable to re-encrypt secret key", message: response?.error || 'unable to re-encrypt secret key',
}); });
setOpenSnackGlobal(true); setOpenSnackGlobal(true);
}) })
.catch((error) => { .catch((error) => {
setInfoSnackCustom({ setInfoSnackCustom({
type: "error", type: 'error',
message: error?.message || "unable to re-encrypt secret key", message: error?.message || 'unable to re-encrypt secret key',
}); });
setOpenSnackGlobal(true); setOpenSnackGlobal(true);
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
useEffect(() => { useEffect(() => {
@ -159,27 +168,32 @@ export const AdminSpaceInner = ({
return ( return (
<Box <Box
sx={{ sx={{
width: "100%", alignItems: 'center',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
padding: "10px", padding: '10px',
alignItems: 'center' width: '100%',
}} }}
> >
<Typography sx={{ <Typography
fontSize: '14px' sx={{
}}>Reminder: After publishing the key, it will take a couple of minutes for it to appear. Please just wait.</Typography> fontSize: '14px',
}}
>
Reminder: After publishing the key, it will take a couple of minutes for
it to appear. Please just wait.
</Typography>
<Spacer height="25px" /> <Spacer height="25px" />
<Box <Box
sx={{ sx={{
display: "flex",
flexDirection: "column",
gap: "20px",
width: "300px",
maxWidth: "90%",
padding: '10px',
border: '1px solid gray', border: '1px solid gray',
borderRadius: '6px' borderRadius: '6px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
maxWidth: '90%',
padding: '10px',
width: '300px',
}} }}
> >
{isFetchingGroupSecretKey && ( {isFetchingGroupSecretKey && (
@ -191,33 +205,47 @@ export const AdminSpaceInner = ({
)} )}
{groupSecretKeyPublishDetails && ( {groupSecretKeyPublishDetails && (
<Typography> <Typography>
Last encryption date:{" "} Last encryption date:{' '}
{formatTimestampForum( {formatTimestampForum(
groupSecretKeyPublishDetails?.updated || groupSecretKeyPublishDetails?.updated ||
groupSecretKeyPublishDetails?.created groupSecretKeyPublishDetails?.created
)}{" "} )}{' '}
{` by ${groupSecretKeyPublishDetails?.name}`} {` by ${groupSecretKeyPublishDetails?.name}`}
</Typography> </Typography>
)} )}
<Button disabled={isFetchingGroupSecretKey} onClick={()=> setIsForceShowCreationKeyPopup(true)} variant="contained"> <Button
disabled={isFetchingGroupSecretKey}
onClick={() => setIsForceShowCreationKeyPopup(true)}
variant="contained"
>
Publish group secret key Publish group secret key
</Button> </Button>
<Spacer height="20px" /> <Spacer height="20px" />
<Typography sx={{
fontSize: '14px' <Typography
}}>This key is to encrypt GROUP related content. This is the only one used in this UI as of now. All group members will be able to see content encrypted with this key.</Typography> sx={{
fontSize: '14px',
}}
>
This key is to encrypt GROUP related content. This is the only one
used in this UI as of now. All group members will be able to see
content encrypted with this key.
</Typography>
</Box> </Box>
<Spacer height="25px" /> <Spacer height="25px" />
<Box <Box
sx={{ sx={{
display: "flex",
flexDirection: "column",
gap: "20px",
width: "300px",
maxWidth: "90%",
padding: '10px',
border: '1px solid gray', border: '1px solid gray',
borderRadius: '6px' borderRadius: '6px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
maxWidth: '90%',
padding: '10px',
width: '300px',
}} }}
> >
{isFetchingAdminGroupSecretKey && ( {isFetchingAdminGroupSecretKey && (
@ -228,20 +256,31 @@ export const AdminSpaceInner = ({
)} )}
{adminGroupSecretKeyPublishDetails && ( {adminGroupSecretKeyPublishDetails && (
<Typography> <Typography>
Last encryption date:{" "} Last encryption date:{' '}
{formatTimestampForum( {formatTimestampForum(
adminGroupSecretKeyPublishDetails?.updated || adminGroupSecretKeyPublishDetails?.updated ||
adminGroupSecretKeyPublishDetails?.created adminGroupSecretKeyPublishDetails?.created
)} )}
</Typography> </Typography>
)} )}
<Button disabled={isFetchingAdminGroupSecretKey} onClick={createCommonSecretForAdmins} variant="contained"> <Button
disabled={isFetchingAdminGroupSecretKey}
onClick={createCommonSecretForAdmins}
variant="contained"
>
Publish admin secret key Publish admin secret key
</Button> </Button>
<Spacer height="20px" /> <Spacer height="20px" />
<Typography sx={{
fontSize: '14px' <Typography
}}>This key is to encrypt ADMIN related content. Only admins would see content encrypted with it.</Typography> sx={{
fontSize: '14px',
}}
>
This key is to encrypt ADMIN related content. Only admins would see
content encrypted with it.
</Typography>
</Box> </Box>
</Box> </Box>
); );

View File

@ -4,7 +4,7 @@ import {
AuthenticatedContainerInnerTop, AuthenticatedContainerInnerTop,
CustomButton, CustomButton,
} from '../../styles/App-styles'; } from '../../styles/App-styles';
import { Box, CircularProgress } from '@mui/material'; import { Box, CircularProgress, useTheme } from '@mui/material';
import { objectToBase64 } from '../../qdn/encryption/group-encryption'; import { objectToBase64 } from '../../qdn/encryption/group-encryption';
import ShortUniqueId from 'short-unique-id'; import ShortUniqueId from 'short-unique-id';
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
@ -39,6 +39,7 @@ export const AnnouncementDiscussion = ({
myName, myName,
isPrivate, isPrivate,
}) => { }) => {
const theme = useTheme();
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isFocusedParent, setIsFocusedParent] = useState(false); const [isFocusedParent, setIsFocusedParent] = useState(false);
@ -83,7 +84,9 @@ export const AnnouncementDiscussion = ({
[`${identifier}-${name}`]: messageData, [`${identifier}-${name}`]: messageData,
}; };
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const publishAnc = async ({ encryptedData, identifier }: any) => { const publishAnc = async ({ encryptedData, identifier }: any) => {
@ -107,7 +110,9 @@ export const AnnouncementDiscussion = ({
rej(error.message || 'An error occurred'); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const setTempData = async () => { const setTempData = async () => {
@ -123,7 +128,9 @@ export const AnnouncementDiscussion = ({
}); });
setTempPublishedList(tempData); setTempPublishedList(tempData);
} }
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const publishComment = async () => { const publishComment = async () => {
@ -206,6 +213,7 @@ export const AnnouncementDiscussion = ({
getData({ name: data.name, identifier: data.identifier }, isPrivate); getData({ name: data.name, identifier: data.identifier }, isPrivate);
} }
} catch (error) { } catch (error) {
console.log(error);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@ -235,7 +243,9 @@ export const AnnouncementDiscussion = ({
for (const data of responseData) { for (const data of responseData) {
getData({ name: data.name, identifier: data.identifier }, isPrivate); getData({ name: data.name, identifier: data.identifier }, isPrivate);
} }
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const combinedListTempAndReal = useMemo(() => { const combinedListTempAndReal = useMemo(() => {
@ -266,19 +276,19 @@ export const AnnouncementDiscussion = ({
return ( return (
<div <div
style={{ style={{
height: isMobile ? '100%' : '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: isMobile ? '100%' : '100%',
width: '100%', width: '100%',
}} }}
> >
<div <div
style={{ style={{
position: 'relative',
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexShrink: 0, flexShrink: 0,
position: 'relative',
width: '100%',
}} }}
> >
<AuthenticatedContainerInnerTop <AuthenticatedContainerInnerTop
@ -293,6 +303,7 @@ export const AnnouncementDiscussion = ({
}} }}
/> />
</AuthenticatedContainerInnerTop> </AuthenticatedContainerInnerTop>
<Spacer height="20px" /> <Spacer height="20px" />
</div> </div>
<AnnouncementList <AnnouncementList
@ -306,30 +317,27 @@ export const AnnouncementDiscussion = ({
/> />
<div <div
style={{ style={{
// position: 'fixed', backgroundColor: theme.palette.background.default,
// bottom: '0px', bottom: isFocusedParent ? '0px' : 'unset',
backgroundColor: '#232428', boxSizing: 'border-box',
minHeight: isMobile ? '0px' : '150px',
maxHeight: isMobile ? 'auto' : '400px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
overflow: 'hidden',
width: '100%',
boxSizing: 'border-box',
padding: isMobile ? '10px' : '20px',
position: isFocusedParent ? 'fixed' : 'relative',
bottom: isFocusedParent ? '0px' : 'unset',
top: isFocusedParent ? '0px' : 'unset',
zIndex: isFocusedParent ? 5 : 'unset',
flexShrink: 0, flexShrink: 0,
maxHeight: '400px',
minHeight: '150px',
overflow: 'hidden',
padding: '20px',
position: isFocusedParent ? 'fixed' : 'relative',
top: isFocusedParent ? '0px' : 'unset',
width: '100%',
zIndex: isFocusedParent ? 5 : 'unset',
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
// height: '100%', flexGrow: 1,
flexGrow: isMobile && 1,
overflow: 'auto', overflow: 'auto',
}} }}
> >
@ -345,11 +353,11 @@ export const AnnouncementDiscussion = ({
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
width: '100&', flexShrink: 0,
gap: '10px', gap: '10px',
justifyContent: 'center', justifyContent: 'center',
flexShrink: 0,
position: 'relative', position: 'relative',
width: '100&',
}} }}
> >
{isFocusedParent && ( {isFocusedParent && (
@ -361,13 +369,13 @@ export const AnnouncementDiscussion = ({
// Unfocus the editor // Unfocus the editor
}} }}
style={{ style={{
marginTop: 'auto',
alignSelf: 'center', alignSelf: 'center',
background: 'red',
cursor: isSending ? 'default' : 'pointer', cursor: isSending ? 'default' : 'pointer',
flexShrink: 0, flexShrink: 0,
padding: isMobile && '5px', fontSize: '14px',
fontSize: isMobile && '14px', marginTop: 'auto',
background: 'red', padding: '5px',
}} }}
> >
{` Close`} {` Close`}
@ -379,25 +387,25 @@ export const AnnouncementDiscussion = ({
publishComment(); publishComment();
}} }}
style={{ style={{
marginTop: 'auto',
alignSelf: 'center', alignSelf: 'center',
background: theme.palette.background.default,
cursor: isSending ? 'default' : 'pointer', cursor: isSending ? 'default' : 'pointer',
background: isSending && 'rgba(0, 0, 0, 0.8)',
flexShrink: 0, flexShrink: 0,
padding: isMobile && '5px', fontSize: '14px',
fontSize: isMobile && '14px', marginTop: 'auto',
padding: '5px',
}} }}
> >
{isSending && ( {isSending && (
<CircularProgress <CircularProgress
size={18} size={18}
sx={{ sx={{
color: theme.palette.text.primary,
left: '50%',
marginLeft: '-12px',
marginTop: '-12px',
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: '50%',
marginTop: '-12px',
marginLeft: '-12px',
color: 'white',
}} }}
/> />
)} )}

View File

@ -1,173 +1,206 @@
import { Message } from "@chatscope/chat-ui-kit-react"; import React, { useEffect, useState } from 'react';
import React, { useEffect, useState } from "react"; import { MessageDisplay } from './MessageDisplay';
import { useInView } from "react-intersection-observer"; import { Avatar, Box, Typography, useTheme } from '@mui/material';
import { MessageDisplay } from "./MessageDisplay"; import { formatTimestamp } from '../../utils/time';
import { Avatar, Box, Typography } from "@mui/material";
import { formatTimestamp } from "../../utils/time";
import ChatBubbleIcon from '@mui/icons-material/ChatBubble'; import ChatBubbleIcon from '@mui/icons-material/ChatBubble';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import { getBaseApi } from "../../background"; import { getBaseApi } from '../../background';
import { requestQueueCommentCount } from "./GroupAnnouncements"; import { requestQueueCommentCount } from './GroupAnnouncements';
import { CustomLoader } from "../../common/CustomLoader"; import { CustomLoader } from '../../common/CustomLoader';
import { getArbitraryEndpointReact, getBaseApiReact } from "../../App"; import { getArbitraryEndpointReact, getBaseApiReact } from '../../App';
import { WrapperUserAction } from "../WrapperUserAction"; import { WrapperUserAction } from '../WrapperUserAction';
export const AnnouncementItem = ({ message, messageData, setSelectedAnnouncement, disableComment, myName }) => {
const [commentLength, setCommentLength] = useState(0) export const AnnouncementItem = ({
const getNumberOfComments = React.useCallback( message,
async () => { messageData,
try { setSelectedAnnouncement,
const offset = 0; disableComment,
myName,
}) => {
const theme = useTheme();
const [commentLength, setCommentLength] = useState(0);
const getNumberOfComments = React.useCallback(async () => {
try {
const offset = 0;
// dispatch(setIsLoadingGlobal(true)) // dispatch(setIsLoadingGlobal(true))
const identifier = `cm-${message.identifier}`; const identifier = `cm-${message.identifier}`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=0&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=0&includemetadata=false&offset=${offset}&reverse=true&prefix=true`;
const response = await requestQueueCommentCount.enqueue(() => { const response = await requestQueueCommentCount.enqueue(() => {
return fetch(url, { return fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
}) });
const responseData = await response.json(); const responseData = await response.json();
setCommentLength(responseData?.length);
} catch (error) {
console.log(error);
}
}, []);
useEffect(() => {
if (disableComment) return;
getNumberOfComments();
}, []);
setCommentLength(responseData?.length);
} catch (error) {
} finally {
// dispatch(setIsLoadingGlobal(false))
}
},
[]
);
useEffect(()=> {
if(disableComment) return
getNumberOfComments()
}, [])
return ( return (
<div <div
style={{ style={{
padding: "10px", backgroundColor: theme.palette.background.default,
backgroundColor: "#232428", borderRadius: '7px',
borderRadius: "7px", display: 'flex',
width: "95%", flexDirection: 'column',
display: "flex",
gap: '7px', gap: '7px',
flexDirection: 'column' padding: '10px',
width: '95%',
}} }}
> >
<Box sx={{
display: "flex",
gap: '7px',
width: '100%',
wordBreak: 'break-word'
}}>
<WrapperUserAction disabled={myName === message?.name} address={undefined} name={message?.name}>
<Avatar
sx={{
backgroundColor: '#27282c',
color: 'white'
}}
alt={message?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${message?.name}/qortal_avatar?async=true`}
>
{message?.name?.charAt(0)}
</Avatar>
</WrapperUserAction>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", gap: '7px',
gap: "7px", width: '100%',
width: '100%' wordBreak: 'break-word',
}} }}
> >
<WrapperUserAction disabled={myName === message?.name} address={undefined} name={message?.name}> <WrapperUserAction
<Typography disabled={myName === message?.name}
address={undefined}
name={message?.name}
>
<Avatar
sx={{
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}}
alt={message?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${message?.name}/qortal_avatar?async=true`}
>
{message?.name?.charAt(0)}
</Avatar>
</WrapperUserAction>
<Box
sx={{ sx={{
fontWight: 600, display: 'flex',
fontFamily: "Inter", flexDirection: 'column',
color: "cadetBlue", gap: '7px',
width: '100%',
}} }}
> >
{message?.name} <WrapperUserAction
</Typography> disabled={myName === message?.name}
</WrapperUserAction> address={undefined}
{!messageData?.decryptedData && ( name={message?.name}
<Box sx={{ >
width: '100%', <Typography
display: 'flex', sx={{
justifyContent: 'center' fontWight: 600,
}}> fontFamily: 'Inter',
<CustomLoader /> color: 'cadetBlue',
</Box> }}
)} >
{messageData?.decryptedData?.message && ( {message?.name}
<> </Typography>
{messageData?.type === "notification" ? ( </WrapperUserAction>
<MessageDisplay htmlContent={messageData?.decryptedData?.message} /> {!messageData?.decryptedData && (
) : ( <Box
<MessageDisplay htmlContent={messageData?.decryptedData?.message} /> sx={{
)} width: '100%',
</> display: 'flex',
)} justifyContent: 'center',
}}
>
<CustomLoader />
</Box>
)}
{messageData?.decryptedData?.message && (
<>
{messageData?.type === 'notification' ? (
<MessageDisplay
htmlContent={messageData?.decryptedData?.message}
/>
) : (
<MessageDisplay
htmlContent={messageData?.decryptedData?.message}
/>
)}
</>
)}
<Box
<Box sx={{ sx={{
display: 'flex', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'flex-end',
width: '100%' width: '100%',
}}> }}
<Typography sx={{ >
fontSize: '14px', <Typography
color: 'gray', sx={{
fontFamily: 'Inter' color: theme.palette.text.secondary,
}}>{formatTimestamp(message.created)}</Typography> fontFamily: 'Inter',
fontSize: '14px',
}}
>
{formatTimestamp(message.created)}
</Typography>
</Box>
</Box> </Box>
</Box> </Box>
</Box> {!disableComment && (
{!disableComment && ( <Box
<Box sx={{ sx={{
display: 'flex', alignItems: 'center',
width: '100%', borderTop: '1px solid white',
alignItems: 'center', cursor: 'pointer',
justifyContent: 'space-between', display: 'flex',
padding: '20px', justifyContent: 'space-between',
cursor: 'pointer', opacity: 0.4,
opacity: 0.4, padding: '20px',
borderTop: '1px solid white', width: '100%',
}}
}} onClick={()=> setSelectedAnnouncement(message)}> onClick={() => setSelectedAnnouncement(message)}
>
<Box sx={{ <Box
display: 'flex', sx={{
width: '100%', alignItems: 'center',
gap: '25px', display: 'flex',
alignItems: 'center', gap: '25px',
width: '100%',
}}> }}
<ChatBubbleIcon sx={{ >
fontSize: '20px' <ChatBubbleIcon
}} /> sx={{
{commentLength ? ( fontSize: '20px',
<Typography sx={{ }}
fontSize: '14px' />
}}>{`${commentLength > 1 ? `${commentLength} comments` : `${commentLength} comment`}`}</Typography> {commentLength ? (
) : ( <Typography
<Typography sx={{ sx={{
fontSize: '14px' fontSize: '14px',
}}>Leave comment</Typography> }}
)} >{`${commentLength > 1 ? `${commentLength} comments` : `${commentLength} comment`}`}</Typography>
) : (
<Typography
sx={{
fontSize: '14px',
}}
>
Leave comment
</Typography>
)}
</Box>
<ArrowForwardIosIcon
sx={{
fontSize: '20px',
}}
/>
</Box> </Box>
<ArrowForwardIosIcon sx={{ )}
fontSize: '20px'
}} />
</Box>
)}
</div> </div>
); );
}; };

View File

@ -1,10 +1,5 @@
import React, { useCallback, useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { import { CellMeasurerCache } from 'react-virtualized';
List,
AutoSizer,
CellMeasurerCache,
CellMeasurer,
} from 'react-virtualized';
import { AnnouncementItem } from './AnnouncementItem'; import { AnnouncementItem } from './AnnouncementItem';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { CustomButton } from '../../styles/App-styles'; import { CustomButton } from '../../styles/App-styles';
@ -37,12 +32,12 @@ export const AnnouncementList = ({
return ( return (
<div <div
style={{ style={{
position: 'relative',
flexGrow: 1,
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: 1,
flexShrink: 1, flexShrink: 1,
position: 'relative',
width: '100%',
overflow: 'auto', overflow: 'auto',
}} }}
> >
@ -57,11 +52,11 @@ export const AnnouncementList = ({
<div <div
key={message?.identifier} key={message?.identifier}
style={{ style={{
marginBottom: '10px', alignItems: 'center',
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', marginBottom: '10px',
width: '100%',
}} }}
> >
<AnnouncementItem <AnnouncementItem
@ -89,10 +84,10 @@ export const AnnouncementList = ({
</AutoSizer> */} </AutoSizer> */}
<Box <Box
sx={{ sx={{
width: '100%',
marginTop: '25px',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
marginTop: '25px',
width: '100%',
}} }}
> >
{showLoadMore && ( {showLoadMore && (

View File

@ -1,16 +1,14 @@
import React, { useState } from "react";
import InfiniteScroll from "react-infinite-scroller";
import { import {
MainContainer, MainContainer,
ChatContainer, ChatContainer,
MessageList, MessageList,
Message, Message,
MessageInput, MessageInput,
Avatar Avatar,
} from "@chatscope/chat-ui-kit-react"; } from '@chatscope/chat-ui-kit-react';
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
export const ChatContainerComp = ({messages}) => { export const ChatContainerComp = ({ messages }) => {
// const [messages, setMessages] = useState([ // const [messages, setMessages] = useState([
// { id: 1, text: "Hello! How are you?", sender: "Joe"}, // { id: 1, text: "Hello! How are you?", sender: "Joe"},
// { id: 2, text: "I'm good, thank you!", sender: "Me" } // { id: 2, text: "I'm good, thank you!", sender: "Me" }
@ -26,31 +24,31 @@ export const ChatContainerComp = ({messages}) => {
// }; // };
return ( return (
<div style={{ height: "500px", width: "300px" }}> <div style={{ height: '500px', width: '300px' }}>
<MainContainer> <MainContainer>
<ChatContainer> <ChatContainer>
<MessageList> <MessageList>
{messages.map((msg) => ( {messages.map((msg) => (
<Message <Message
key={msg.id} key={msg.id}
model={{ model={{
message: msg.text, message: msg.text,
sentTime: "just now", sentTime: 'just now',
sender: msg.senderName, sender: msg.senderName,
direction: 'incoming', direction: 'incoming',
position: "single" position: 'single',
}} }}
> >
{msg.direction === "incoming" && <Avatar name={msg.senderName} />} {msg.direction === 'incoming' && (
</Message> <Avatar name={msg.senderName} />
))} )}
</MessageList> </Message>
))}
</MessageList>
<MessageInput placeholder="Type a message..." /> <MessageInput placeholder="Type a message..." />
</ChatContainer> </ChatContainer>
</MainContainer> </MainContainer>
</div> </div>
); );
}; };

View File

@ -7,13 +7,12 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import { objectToBase64 } from '../../qdn/encryption/group-encryption';
import { ChatList } from './ChatList'; import { ChatList } from './ChatList';
import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css'; import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
import Tiptap from './TipTap'; import Tiptap from './TipTap';
import { CustomButton } from '../../styles/App-styles'; import { CustomButton } from '../../styles/App-styles';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import { Box, ButtonBase, Input, Typography } from '@mui/material'; import { Box, ButtonBase, Input, Typography, useTheme } from '@mui/material';
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
import { getNameInfo } from '../Group/Group'; import { getNameInfo } from '../Group/Group';
import { Spacer } from '../../common/Spacer'; import { Spacer } from '../../common/Spacer';
@ -36,7 +35,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ShortUniqueId from 'short-unique-id'; import ShortUniqueId from 'short-unique-id';
import { ReturnIcon } from '../../assets/Icons/ReturnIcon'; import { ReturnIcon } from '../../assets/Icons/ReturnIcon';
import { ExitIcon } from '../../assets/Icons/ExitIcon'; import { ExitIcon } from '../../assets/Icons/ExitIcon';
import { MessageItem, ReplyPreview } from './MessageItem'; import { ReplyPreview } from './MessageItem';
const uid = new ShortUniqueId({ length: 5 }); const uid = new ShortUniqueId({ length: 5 });
@ -52,6 +51,7 @@ export const ChatDirect = ({
close, close,
setMobileViewModeKeepOpen, setMobileViewModeKeepOpen,
}) => { }) => {
const theme = useTheme();
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue(); const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
const [isFocusedParent, setIsFocusedParent] = useState(false); const [isFocusedParent, setIsFocusedParent] = useState(false);
const [onEditMessage, setOnEditMessage] = useState(null); const [onEditMessage, setOnEditMessage] = useState(null);
@ -87,7 +87,9 @@ export const ChatDirect = ({
const publicKey = await getPublicKey(address); const publicKey = await getPublicKey(address);
if (publicKeyOfRecipientRef.current !== selectedDirect?.address) return; if (publicKeyOfRecipientRef.current !== selectedDirect?.address) return;
setPublicKeyOfRecipient(publicKey); setPublicKeyOfRecipient(publicKey);
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const tempMessages = useMemo(() => { const tempMessages = useMemo(() => {
@ -167,6 +169,7 @@ export const ChatDirect = ({
text: item.message, text: item.message,
unread: item?.sender === myAddress ? false : true, unread: item?.sender === myAddress ? false : true,
})); }));
setMessages((prev) => [...prev, ...formatted]); setMessages((prev) => [...prev, ...formatted]);
setChatReferences((prev) => { setChatReferences((prev) => {
const organizedChatReferences = { ...prev }; const organizedChatReferences = { ...prev };
@ -183,7 +186,9 @@ export const ChatDirect = ({
{}), {}),
edit: item, edit: item,
}; };
} catch (error) {} } catch (error) {
console.log(error);
}
}); });
return organizedChatReferences; return organizedChatReferences;
}); });
@ -214,7 +219,9 @@ export const ChatDirect = ({
{}), {}),
edit: item, edit: item,
}; };
} catch (error) {} } catch (error) {
console.log(error);
}
}); });
return organizedChatReferences; return organizedChatReferences;
}); });
@ -227,7 +234,9 @@ export const ChatDirect = ({
rej(error.message || 'An error occurred'); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const forceCloseWebSocket = () => { const forceCloseWebSocket = () => {
@ -368,7 +377,6 @@ export const ChatDirect = ({
senderName: myName, senderName: myName,
}); });
setNewChat(null); setNewChat(null);
window window
.sendMessage('addTimestampEnterChat', { .sendMessage('addTimestampEnterChat', {
timestamp: Date.now(), timestamp: Date.now(),
@ -396,7 +404,6 @@ export const ChatDirect = ({
}); });
} catch (error) { } catch (error) {
throw new Error(error); throw new Error(error);
} finally {
} }
}; };
const clearEditorContent = () => { const clearEditorContent = () => {
@ -537,39 +544,39 @@ export const ChatDirect = ({
return ( return (
<div <div
style={{ style={{
height: isMobile ? '100%' : '100vh', background: theme.palette.background.default,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: isMobile ? '100%' : '100vh',
width: '100%', width: '100%',
background: !isMobile && 'var(--bg-2)',
}} }}
> >
{!isMobile && ( {!isMobile && (
<Box <Box
onClick={close} onClick={close}
sx={{ sx={{
display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '5px', alignSelf: 'center',
background: theme.palette.background.default,
borderRadius: '3px',
cursor: 'pointer', cursor: 'pointer',
display: 'flex',
gap: '5px',
margin: '10px 0px',
padding: '4px 6px', padding: '4px 6px',
width: 'fit-content', width: 'fit-content',
borderRadius: '3px',
background: 'rgb(35, 36, 40)',
margin: '10px 0px',
alignSelf: 'center',
}} }}
> >
<ArrowBackIcon <ArrowBackIcon
sx={{ sx={{
color: 'white', color: theme.palette.text.primary,
fontSize: isMobile ? '20px' : '20px', fontSize: '20px',
}} }}
/> />
<Typography <Typography
sx={{ sx={{
color: 'white', color: theme.palette.text.primary,
fontSize: isMobile ? '14px' : '14px', fontSize: '14px',
}} }}
> >
Close Direct Chat Close Direct Chat
@ -579,26 +586,26 @@ export const ChatDirect = ({
{isMobile && ( {isMobile && (
<Box <Box
sx={{ sx={{
display: 'flex',
alignItems: 'center', alignItems: 'center',
width: '100%', display: 'flex',
marginTop: '14px',
justifyContent: 'center',
height: '15px', height: '15px',
justifyContent: 'center',
marginTop: '14px',
width: '100%',
}} }}
> >
<Box <Box
sx={{ sx={{
display: 'flex',
alignItems: 'center', alignItems: 'center',
display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
width: '320px', width: '320px',
}} }}
> >
<Box <Box
sx={{ sx={{
display: 'flex',
alignItems: 'center', alignItems: 'center',
display: 'flex',
width: '50px', width: '50px',
}} }}
> >
@ -623,10 +630,10 @@ export const ChatDirect = ({
</Typography> </Typography>
<Box <Box
sx={{ sx={{
display: 'flex',
alignItems: 'center', alignItems: 'center',
width: '50px', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'flex-end',
width: '50px',
}} }}
> >
<ButtonBase <ButtonBase
@ -670,42 +677,40 @@ export const ChatDirect = ({
<div <div
style={{ style={{
// position: 'fixed', backgroundColor: theme.palette.background.default,
// bottom: '0px', bottom: isFocusedParent ? '0px' : 'unset',
backgroundColor: '#232428', boxSizing: 'border-box',
minHeight: isMobile ? '0px' : '150px',
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
flexShrink: 0,
minHeight: '150px',
overflow: 'hidden', overflow: 'hidden',
width: '100%',
boxSizing: 'border-box',
padding: isMobile ? '10px' : '20px', padding: isMobile ? '10px' : '20px',
position: isFocusedParent ? 'fixed' : 'relative', position: isFocusedParent ? 'fixed' : 'relative',
bottom: isFocusedParent ? '0px' : 'unset',
top: isFocusedParent ? '0px' : 'unset', top: isFocusedParent ? '0px' : 'unset',
width: '100%',
zIndex: isFocusedParent ? 5 : 'unset', zIndex: isFocusedParent ? 5 : 'unset',
flexShrink: 0,
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: isMobile && 1, flexGrow: 1,
overflow: !isMobile && 'auto',
flexShrink: 0, flexShrink: 0,
width: 'calc(100% - 100px)',
justifyContent: 'flex-end', justifyContent: 'flex-end',
overflow: 'auto',
width: 'calc(100% - 100px)',
}} }}
> >
{replyMessage && ( {replyMessage && (
<Box <Box
sx={{ sx={{
alignItems: 'flex-start',
display: 'flex', display: 'flex',
gap: '5px', gap: '5px',
alignItems: 'flex-start',
width: 'calc(100% - 100px)',
justifyContent: 'flex-end', justifyContent: 'flex-end',
width: 'calc(100% - 100px)',
}} }}
> >
<ReplyPreview message={replyMessage} /> <ReplyPreview message={replyMessage} />
@ -723,9 +728,9 @@ export const ChatDirect = ({
{onEditMessage && ( {onEditMessage && (
<Box <Box
sx={{ sx={{
alignItems: 'flex-start',
display: 'flex', display: 'flex',
gap: '5px', gap: '5px',
alignItems: 'flex-start',
width: '100%', width: '100%',
}} }}
> >
@ -735,7 +740,6 @@ export const ChatDirect = ({
onClick={() => { onClick={() => {
setReplyMessage(null); setReplyMessage(null);
setOnEditMessage(null); setOnEditMessage(null);
clearEditorContent(); clearEditorContent();
}} }}
> >
@ -756,9 +760,9 @@ export const ChatDirect = ({
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
width: '100%',
justifyContent: 'flex-start', justifyContent: 'flex-start',
position: 'relative', position: 'relative',
width: '100%',
}} }}
> >
<Typography <Typography
@ -774,12 +778,11 @@ export const ChatDirect = ({
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
width: '100px', flexShrink: 0,
gap: '10px', gap: '10px',
justifyContent: 'center', justifyContent: 'center',
flexShrink: 0,
position: 'relative', position: 'relative',
width: '100px',
}} }}
> >
<CustomButton <CustomButton
@ -788,26 +791,28 @@ export const ChatDirect = ({
sendMessage(); sendMessage();
}} }}
style={{ style={{
marginTop: 'auto',
alignSelf: 'center', alignSelf: 'center',
background: isSending
? theme.palette.background.default
: theme.palette.background.paper,
cursor: isSending ? 'default' : 'pointer', cursor: isSending ? 'default' : 'pointer',
background: isSending && 'rgba(0, 0, 0, 0.8)',
flexShrink: 0, flexShrink: 0,
marginTop: 'auto',
minWidth: 'auto',
padding: '5px', padding: '5px',
width: '100px', width: '100px',
minWidth: 'auto',
}} }}
> >
{isSending && ( {isSending && (
<CircularProgress <CircularProgress
size={18} size={18}
sx={{ sx={{
color: theme.palette.text.primary,
left: '50%',
marginLeft: '-12px',
marginTop: '-12px',
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: '50%',
marginTop: '-12px',
marginLeft: '-12px',
color: 'white',
}} }}
/> />
)} )}
@ -815,12 +820,14 @@ export const ChatDirect = ({
</CustomButton> </CustomButton>
</Box> </Box>
</div> </div>
<LoadingSnackbar <LoadingSnackbar
open={isLoading} open={isLoading}
info={{ info={{
message: 'Loading chat... please wait.', message: 'Loading chat... please wait.',
}} }}
/> />
<CustomizedSnackbars <CustomizedSnackbars
open={openSnack} open={openSnack}
setOpen={setOpenSnack} setOpen={setOpenSnack}

View File

@ -7,15 +7,10 @@ import React, {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { CreateCommonSecret } from './CreateCommonSecret';
import { reusableGet } from '../../qdn/publish/pubish';
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
import { import {
base64ToUint8Array,
decodeBase64ForUIChatMessages, decodeBase64ForUIChatMessages,
objectToBase64, objectToBase64,
} from '../../qdn/encryption/group-encryption'; } from '../../qdn/encryption/group-encryption';
import { ChatContainerComp } from './ChatContainer';
import { ChatList } from './ChatList'; import { ChatList } from './ChatList';
import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css'; import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
import Tiptap from './TipTap'; import Tiptap from './TipTap';
@ -38,7 +33,7 @@ import {
subscribeToEvent, subscribeToEvent,
unsubscribeFromEvent, unsubscribeFromEvent,
} from '../../utils/events'; } from '../../utils/events';
import { Box, ButtonBase, Divider, Typography } from '@mui/material'; import { Box, ButtonBase, Divider, Typography, useTheme } from '@mui/material';
import ShortUniqueId from 'short-unique-id'; import ShortUniqueId from 'short-unique-id';
import { ReplyPreview } from './MessageItem'; import { ReplyPreview } from './MessageItem';
import { ExitIcon } from '../../assets/Icons/ExitIcon'; import { ExitIcon } from '../../assets/Icons/ExitIcon';
@ -128,7 +123,9 @@ export const ChatGroup = ({
rej(error.message || 'An error occurred'); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
useEffect(() => { useEffect(() => {
@ -192,7 +189,9 @@ export const ChatGroup = ({
handleSecretKeyCreationInProgress(); handleSecretKeyCreationInProgress();
return; return;
} }
} catch (error) {} } catch (error) {
console.log(error);
}
}); });
}; };
@ -578,7 +577,9 @@ export const ChatGroup = ({
rej(error.message || 'An error occurred'); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const forceCloseWebSocket = () => { const forceCloseWebSocket = () => {
@ -621,7 +622,9 @@ export const ChatGroup = ({
middletierFunc(JSON.parse(e.data), selectedGroup); middletierFunc(JSON.parse(e.data), selectedGroup);
setIsLoading(false); setIsLoading(false);
} }
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
socketRef.current.onclose = () => { socketRef.current.onclose = () => {
clearTimeout(groupSocketTimeoutRef.current); clearTimeout(groupSocketTimeoutRef.current);
@ -700,7 +703,9 @@ export const ChatGroup = ({
rej(error.message || 'An error occurred'); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const sendChatGroup = async ({ const sendChatGroup = async ({
@ -991,16 +996,18 @@ export const ChatGroup = ({
setIsOpenQManager(true); setIsOpenQManager(true);
}, []); }, []);
const theme = useTheme();
return ( return (
<div <div
style={{ style={{
height: isMobile ? '100%' : '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
width: '100%', height: '100%',
left: hide && '-100000px',
opacity: hide ? 0 : 1, opacity: hide ? 0 : 1,
position: hide ? 'absolute' : 'relative', position: hide ? 'absolute' : 'relative',
left: hide && '-100000px', width: '100%',
}} }}
> >
<ChatList <ChatList
@ -1025,40 +1032,38 @@ export const ChatGroup = ({
{(!!secretKey || isPrivate === false) && ( {(!!secretKey || isPrivate === false) && (
<div <div
style={{ style={{
// position: 'fixed', backgroundColor: theme.palette.background.default,
// bottom: '0px', bottom: isFocusedParent ? '0px' : 'unset',
backgroundColor: '#232428', boxSizing: 'border-box',
minHeight: isMobile ? '0px' : '150px',
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
overflow: 'hidden',
width: '100%',
boxSizing: 'border-box',
padding: isMobile ? '10px' : '20px',
position: isFocusedParent ? 'fixed' : 'relative',
bottom: isFocusedParent ? '0px' : 'unset',
top: isFocusedParent ? '0px' : 'unset',
zIndex: isFocusedParent ? 5 : 'unset',
flexShrink: 0, flexShrink: 0,
minHeight: '150px',
overflow: 'hidden',
padding: '20px',
position: isFocusedParent ? 'fixed' : 'relative',
top: isFocusedParent ? '0px' : 'unset',
width: '100%',
zIndex: isFocusedParent ? 5 : 'unset',
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: isMobile && 1, flexGrow: 1,
overflow: !isMobile && 'auto',
flexShrink: 0, flexShrink: 0,
width: 'calc(100% - 100px)',
justifyContent: 'flex-end', justifyContent: 'flex-end',
overflow: 'auto',
width: 'calc(100% - 100px)',
}} }}
> >
{replyMessage && ( {replyMessage && (
<Box <Box
sx={{ sx={{
alignItems: 'flex-start',
display: 'flex', display: 'flex',
gap: '5px', gap: '5px',
alignItems: 'flex-start',
width: '100%', width: '100%',
}} }}
> >
@ -1078,9 +1083,9 @@ export const ChatGroup = ({
{onEditMessage && ( {onEditMessage && (
<Box <Box
sx={{ sx={{
alignItems: 'flex-start',
display: 'flex', display: 'flex',
gap: '5px', gap: '5px',
alignItems: 'flex-start',
width: '100%', width: '100%',
}} }}
> >
@ -1113,9 +1118,9 @@ export const ChatGroup = ({
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
width: '100%',
justifyContent: 'flex-start', justifyContent: 'flex-start',
position: 'relative', position: 'relative',
width: '100%',
}} }}
> >
<Typography <Typography
@ -1131,11 +1136,11 @@ export const ChatGroup = ({
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
width: '100px', flexShrink: 0,
gap: '10px', gap: '10px',
justifyContent: 'center', justifyContent: 'center',
flexShrink: 0,
position: 'relative', position: 'relative',
width: '100px',
}} }}
> >
<CustomButton <CustomButton
@ -1144,26 +1149,28 @@ export const ChatGroup = ({
sendMessage(); sendMessage();
}} }}
style={{ style={{
marginTop: 'auto',
alignSelf: 'center', alignSelf: 'center',
background: isSending
? theme.palette.background.default
: theme.palette.background.paper,
cursor: isSending ? 'default' : 'pointer', cursor: isSending ? 'default' : 'pointer',
background: isSending && 'rgba(0, 0, 0, 0.8)',
flexShrink: 0, flexShrink: 0,
marginTop: 'auto',
minWidth: 'auto',
padding: '5px', padding: '5px',
width: '100px', width: '100px',
minWidth: 'auto',
}} }}
> >
{isSending && ( {isSending && (
<CircularProgress <CircularProgress
size={18} size={18}
sx={{ sx={{
color: theme.palette.text.primary,
left: '50%',
marginLeft: '-12px',
marginTop: '-12px',
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: '50%',
marginTop: '-12px',
marginLeft: '-12px',
color: 'white',
}} }}
/> />
)} )}
@ -1175,25 +1182,24 @@ export const ChatGroup = ({
{isOpenQManager !== null && ( {isOpenQManager !== null && (
<Box <Box
sx={{ sx={{
position: 'fixed', backgroundColor: theme.palette.background.default,
height: '600px',
maxHeight: '100vh',
width: '400px',
maxWidth: '100vw',
backgroundColor: '#27282c',
zIndex: 100,
bottom: 0,
right: 0,
overflow: 'hidden',
borderTopLeftRadius: '10px', borderTopLeftRadius: '10px',
borderTopRightRadius: '10px', borderTopRightRadius: '10px',
bottom: 0,
boxShadow: 4,
display: hideView display: hideView
? 'none' ? 'none'
: isOpenQManager === true : isOpenQManager === true
? 'block' ? 'block'
: 'none', : 'none',
boxShadow: 4, height: '600px',
maxHeight: '100vh',
maxWidth: '100vw',
overflow: 'hidden',
position: 'fixed',
right: 0,
width: '400px',
zIndex: 100,
}} }}
> >
<Box <Box
@ -1204,12 +1210,11 @@ export const ChatGroup = ({
> >
<Box <Box
sx={{ sx={{
height: '40px',
display: 'flex',
alignItems: 'center', alignItems: 'center',
padding: '5px', display: 'flex',
height: '40px',
justifyContent: 'space-between', justifyContent: 'space-between',
padding: '5px',
}} }}
> >
<Typography>Q-Manager</Typography> <Typography>Q-Manager</Typography>
@ -1241,12 +1246,14 @@ export const ChatGroup = ({
)} )}
{/* <ChatContainerComp messages={formatMessages} /> */} {/* <ChatContainerComp messages={formatMessages} /> */}
<LoadingSnackbar <LoadingSnackbar
open={isLoading} open={isLoading}
info={{ info={{
message: 'Loading chat... please wait.', message: 'Loading chat... please wait.',
}} }}
/> />
<CustomizedSnackbars <CustomizedSnackbars
open={openSnack} open={openSnack}
setOpen={setOpenSnack} setOpen={setOpenSnack}

View File

@ -1,23 +1,15 @@
import React, { import { useCallback, useState, useEffect, useRef, useMemo } from 'react';
useCallback, import { useVirtualizer } from '@tanstack/react-virtual';
useState, import { MessageItem } from './MessageItem';
useEffect, import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
useRef, import { Box, Typography, useTheme } from '@mui/material';
useMemo, import { ChatOptions } from './ChatOptions';
} from "react"; import ErrorBoundary from '../../common/ErrorBoundary';
import { useVirtualizer } from "@tanstack/react-virtual";
import { MessageItem } from "./MessageItem";
import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
import { useInView } from "react-intersection-observer";
import { Box, Typography } from "@mui/material";
import { ChatOptions } from "./ChatOptions";
import ErrorBoundary from "../../common/ErrorBoundary";
export const ChatList = ({ export const ChatList = ({
initialMessages, initialMessages,
myAddress, myAddress,
tempMessages, tempMessages,
chatId,
onReply, onReply,
onEdit, onEdit,
handleReaction, handleReaction,
@ -29,7 +21,7 @@ export const ChatList = ({
enableMentions, enableMentions,
openQManager, openQManager,
hasSecretKey, hasSecretKey,
isPrivate isPrivate,
}) => { }) => {
const parentRef = useRef(); const parentRef = useRef();
const [messages, setMessages] = useState(initialMessages); const [messages, setMessages] = useState(initialMessages);
@ -42,33 +34,32 @@ export const ChatList = ({
// Initialize the virtualizer // Initialize the virtualizer
const rowVirtualizer = useVirtualizer({ const rowVirtualizer = useVirtualizer({
count: messages.length, count: messages.length,
getItemKey: (index) => messages[index]?.tempSignature || messages[index].signature, getItemKey: (index) =>
messages[index]?.tempSignature || messages[index].signature,
getScrollElement: () => parentRef?.current, getScrollElement: () => parentRef?.current,
estimateSize: useCallback(() => 80, []), // Provide an estimated height of items, adjust this as needed estimateSize: useCallback(() => 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 overscan: 10, // Number of items to render outside the visible area to improve smoothness
}); });
const isAtBottom = useMemo(()=> { const isAtBottom = useMemo(() => {
if (parentRef.current && rowVirtualizer?.isScrolling !== undefined) { if (parentRef.current && rowVirtualizer?.isScrolling !== undefined) {
const { scrollTop, scrollHeight, clientHeight } = parentRef.current; const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed
return atBottom return atBottom;
} }
return false return false;
}, [rowVirtualizer?.isScrolling]);
}, [rowVirtualizer?.isScrolling])
useEffect(() => { useEffect(() => {
if (!parentRef.current || rowVirtualizer?.isScrolling === undefined) return; if (!parentRef.current || rowVirtualizer?.isScrolling === undefined) return;
if(isAtBottom){ if (isAtBottom) {
if (scrollingIntervalRef.current) { if (scrollingIntervalRef.current) {
clearTimeout(scrollingIntervalRef.current); clearTimeout(scrollingIntervalRef.current);
} }
setShowScrollDownButton(false); setShowScrollDownButton(false);
return; return;
} else } else if (rowVirtualizer?.isScrolling) {
if (rowVirtualizer?.isScrolling) {
if (scrollingIntervalRef.current) { if (scrollingIntervalRef.current) {
clearTimeout(scrollingIntervalRef.current); clearTimeout(scrollingIntervalRef.current);
} }
@ -108,7 +99,13 @@ export const ChatList = ({
setTimeout(() => { setTimeout(() => {
const hasUnreadMessages = totalMessages.some( const hasUnreadMessages = totalMessages.some(
(msg) => msg.unread && !msg?.chatReference && !msg?.isTemp && (!msg?.chatReference && msg?.timestamp > lastSeenUnreadMessageTimestamp.current || 0) (msg) =>
msg.unread &&
!msg?.chatReference &&
!msg?.isTemp &&
((!msg?.chatReference &&
msg?.timestamp > lastSeenUnreadMessageTimestamp.current) ||
0)
); );
if (parentRef.current) { if (parentRef.current) {
const { scrollTop, scrollHeight, clientHeight } = parentRef.current; const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
@ -136,9 +133,9 @@ export const ChatList = ({
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1; const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1;
if (rowVirtualizer) { if (rowVirtualizer) {
if (divideIndex) { if (divideIndex) {
rowVirtualizer.scrollToIndex(divideIndex, { align: "start" }); rowVirtualizer.scrollToIndex(divideIndex, { align: 'start' });
} else { } else {
rowVirtualizer.scrollToIndex(index, { align: "end" }); rowVirtualizer.scrollToIndex(index, { align: 'end' });
} }
} }
handleMessageSeen(); handleMessageSeen();
@ -152,7 +149,7 @@ export const ChatList = ({
})) }))
); );
setShowScrollButton(false); setShowScrollButton(false);
lastSeenUnreadMessageTimestamp.current = Date.now() lastSeenUnreadMessageTimestamp.current = Date.now();
}, []); }, []);
const sentNewMessageGroupFunc = useCallback(() => { const sentNewMessageGroupFunc = useCallback(() => {
@ -166,9 +163,9 @@ export const ChatList = ({
}, [messages]); }, [messages]);
useEffect(() => { useEffect(() => {
subscribeToEvent("sent-new-message-group", sentNewMessageGroupFunc); subscribeToEvent('sent-new-message-group', sentNewMessageGroupFunc);
return () => { return () => {
unsubscribeFromEvent("sent-new-message-group", sentNewMessageGroupFunc); unsubscribeFromEvent('sent-new-message-group', sentNewMessageGroupFunc);
}; };
}, [sentNewMessageGroupFunc]); }, [sentNewMessageGroupFunc]);
@ -181,21 +178,24 @@ export const ChatList = ({
const goToMessage = useCallback((idx) => { const goToMessage = useCallback((idx) => {
rowVirtualizer.scrollToIndex(idx); rowVirtualizer.scrollToIndex(idx);
}, []); }, []);
const theme = useTheme();
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
width: "100%", height: '100%',
height: "100%", width: '100%',
}} }}
> >
<div <div
style={{ style={{
height: "100%", display: 'flex',
position: "relative", flexDirection: 'column',
display: "flex", height: '100%',
flexDirection: "column", position: 'relative',
width: "100%", width: '100%',
}} }}
> >
<div <div
@ -203,27 +203,26 @@ export const ChatList = ({
className="List" className="List"
style={{ style={{
flexGrow: 1, flexGrow: 1,
overflow: "auto", overflow: 'auto',
position: "relative", position: 'relative',
display: "flex", display: 'flex',
height: "0px", height: '0px',
}} }}
> >
<div <div
style={{ style={{
height: rowVirtualizer.getTotalSize(), height: rowVirtualizer.getTotalSize(),
width: "100%", width: '100%',
}} }}
> >
<div <div
style={{ style={{
position: "absolute",
top: 0,
left: 0, left: 0,
width: "100%", position: 'absolute',
top: 0,
width: '100%',
}} }}
> >
{rowVirtualizer.getVirtualItems().map((virtualRow) => { {rowVirtualizer.getVirtualItems().map((virtualRow) => {
const index = virtualRow.index; const index = virtualRow.index;
let message = messages[index] || null; // Safeguard against undefined let message = messages[index] || null; // Safeguard against undefined
@ -231,7 +230,7 @@ export const ChatList = ({
let reply = null; let reply = null;
let reactions = null; let reactions = null;
let isUpdating = false; let isUpdating = false;
try { try {
// Safeguard for message existence // Safeguard for message existence
if (message) { if (message) {
@ -239,16 +238,19 @@ export const ChatList = ({
replyIndex = messages.findIndex( replyIndex = messages.findIndex(
(msg) => msg?.signature === message?.repliedTo (msg) => msg?.signature === message?.repliedTo
); );
if (message?.repliedTo && replyIndex !== -1) { if (message?.repliedTo && replyIndex !== -1) {
reply = { ...(messages[replyIndex] || {}) }; reply = { ...(messages[replyIndex] || {}) };
if (chatReferences?.[reply?.signature]?.edit) { if (chatReferences?.[reply?.signature]?.edit) {
reply.decryptedData = chatReferences[reply?.signature]?.edit; reply.decryptedData =
reply.text = chatReferences[reply?.signature]?.edit?.message; chatReferences[reply?.signature]?.edit;
reply.editTimestamp = chatReferences[reply?.signature]?.edit?.timestamp reply.text =
chatReferences[reply?.signature]?.edit?.message;
reply.editTimestamp =
chatReferences[reply?.signature]?.edit?.timestamp;
} }
} }
// GroupDirectId logic // GroupDirectId logic
if (message?.message && message?.groupDirectId) { if (message?.message && message?.groupDirectId) {
replyIndex = messages.findIndex( replyIndex = messages.findIndex(
@ -264,24 +266,34 @@ export const ChatList = ({
status: message?.status, status: message?.status,
}; };
} }
// Check for reactions and edits // Check for reactions and edits
if (chatReferences?.[message.signature]) { if (chatReferences?.[message.signature]) {
reactions = chatReferences[message.signature]?.reactions || null; reactions =
chatReferences[message.signature]?.reactions || null;
if (chatReferences[message.signature]?.edit?.message && message?.text) {
message.text = chatReferences[message.signature]?.edit?.message; if (
message.isEdit = true chatReferences[message.signature]?.edit?.message &&
message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp message?.text
) {
message.text =
chatReferences[message.signature]?.edit?.message;
message.isEdit = true;
message.editTimestamp =
chatReferences[message.signature]?.edit?.timestamp;
} }
if (chatReferences[message.signature]?.edit?.messageText && message?.messageText) { if (
message.messageText = chatReferences[message.signature]?.edit?.messageText; chatReferences[message.signature]?.edit?.messageText &&
message.isEdit = true message?.messageText
message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp ) {
message.messageText =
chatReferences[message.signature]?.edit?.messageText;
message.isEdit = true;
message.editTimestamp =
chatReferences[message.signature]?.edit?.timestamp;
} }
} }
// Check if message is updating // Check if message is updating
if ( if (
tempChatReferences?.some( tempChatReferences?.some(
@ -292,34 +304,37 @@ export const ChatList = ({
} }
} }
} catch (error) { } catch (error) {
console.error("Error processing message:", error, { index, message }); console.error('Error processing message:', error, {
index,
message,
});
// Gracefully handle the error by providing fallback data // Gracefully handle the error by providing fallback data
message = null; message = null;
reply = null; reply = null;
reactions = null; reactions = null;
} }
// Render fallback if message is null // Render fallback if message is null
if (!message) { if (!message) {
return ( return (
<div <div
key={virtualRow.index} key={virtualRow.index}
style={{ style={{
position: "absolute", position: 'absolute',
top: 0, top: 0,
left: "50%", left: '50%',
transform: `translateY(${virtualRow.start}px) translateX(-50%)`, transform: `translateY(${virtualRow.start}px) translateX(-50%)`,
width: "100%", width: '100%',
padding: "10px 0", padding: '10px 0',
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '5px',
}} }}
> >
<Typography>Error loading message.</Typography> <Typography>Error loading message.</Typography>
</div> </div>
); );
} }
return ( return (
<div <div
@ -327,49 +342,47 @@ export const ChatList = ({
ref={rowVirtualizer.measureElement} //measure dynamic row height ref={rowVirtualizer.measureElement} //measure dynamic row height
key={message.signature} key={message.signature}
style={{ style={{
position: "absolute", alignItems: 'center',
display: 'flex',
flexDirection: 'column',
gap: '5px',
left: '50%', // Move to the center horizontally
overscrollBehavior: 'none',
padding: '10px 0',
position: 'absolute',
top: 0, top: 0,
left: "50%", // Move to the center horizontally
transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering
width: "100%", // Control width (90% of the parent) width: '100%', // Control width (90% of the parent)
padding: "10px 0",
display: "flex",
alignItems: "center",
overscrollBehavior: "none",
flexDirection: "column",
gap: "5px",
}} }}
> >
<ErrorBoundary <ErrorBoundary
fallback={ fallback={
<Typography> <Typography>
Error loading content: Invalid Data Error loading content: Invalid Data
</Typography> </Typography>
} }
> >
<MessageItem <MessageItem
isLast={index === messages.length - 1} isLast={index === messages.length - 1}
lastSignature={lastSignature} lastSignature={lastSignature}
message={message} message={message}
onSeen={handleMessageSeen} onSeen={handleMessageSeen}
isTemp={!!message?.isTemp} isTemp={!!message?.isTemp}
myAddress={myAddress} myAddress={myAddress}
onReply={onReply} onReply={onReply}
onEdit={onEdit} onEdit={onEdit}
reply={reply} reply={reply}
replyIndex={replyIndex} replyIndex={replyIndex}
scrollToItem={goToMessage} scrollToItem={goToMessage}
handleReaction={handleReaction} handleReaction={handleReaction}
reactions={reactions} reactions={reactions}
isUpdating={isUpdating} isUpdating={isUpdating}
isPrivate={isPrivate} isPrivate={isPrivate}
/>
/> </ErrorBoundary>
</ErrorBoundary>
</div> </div>
); );
})} })}
</div> </div>
</div> </div>
</div> </div>
@ -377,17 +390,17 @@ export const ChatList = ({
<button <button
onClick={() => scrollToBottom()} onClick={() => scrollToBottom()}
style={{ style={{
backgroundColor: 'var(--unread)',
border: 'none',
borderRadius: '20px',
bottom: 20, bottom: 20,
position: "absolute", color: theme.palette.text.primary,
cursor: 'pointer',
outline: 'none',
padding: '10px 20px',
position: 'absolute',
right: 20, right: 20,
backgroundColor: "var(--unread)",
color: "black",
padding: "10px 20px",
borderRadius: "20px",
cursor: "pointer",
zIndex: 10, zIndex: 10,
border: "none",
outline: "none",
}} }}
> >
Scroll to Unread Messages Scroll to Unread Messages
@ -397,18 +410,18 @@ export const ChatList = ({
<button <button
onClick={() => scrollToBottom()} onClick={() => scrollToBottom()}
style={{ style={{
backgroundColor: theme.palette.background.default,
border: 'none',
borderRadius: '20px',
bottom: 20, bottom: 20,
position: "absolute", color: theme.palette.text.primary,
cursor: 'pointer',
fontSize: '16px',
outline: 'none',
padding: '10px 20px',
position: 'absolute',
right: 20, right: 20,
backgroundColor: "var(--Mail-Background)",
color: "white",
padding: "10px 20px",
borderRadius: "20px",
cursor: "pointer",
zIndex: 10, zIndex: 10,
border: "none",
outline: "none",
fontSize: "16px",
}} }}
> >
Scroll to bottom Scroll to bottom
@ -417,7 +430,7 @@ export const ChatList = ({
</div> </div>
{enableMentions && (hasSecretKey || isPrivate === false) && ( {enableMentions && (hasSecretKey || isPrivate === false) && (
<ChatOptions <ChatOptions
openQManager={openQManager} openQManager={openQManager}
messages={messages} messages={messages}
goToMessage={goToMessage} goToMessage={goToMessage}
members={members} members={members}

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,56 @@
import { Box, Button, Typography } from '@mui/material' import React, { useContext } from 'react';
import React, { useContext } from 'react' import { Box, Button, Typography, useTheme } from '@mui/material';
import { CustomizedSnackbars } from '../Snackbar/Snackbar'; import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { MyContext, getArbitraryEndpointReact, getBaseApiReact, pauseAllQueues } from '../../App'; import {
MyContext,
getArbitraryEndpointReact,
getBaseApiReact,
pauseAllQueues,
} from '../../App';
import { getFee } from '../../background'; import { getFee } from '../../background';
import { decryptResource, getGroupAdmins, validateSecretKey } from '../Group/Group'; import {
decryptResource,
getGroupAdmins,
validateSecretKey,
} from '../Group/Group';
import { base64ToUint8Array } from '../../qdn/encryption/group-encryption'; import { base64ToUint8Array } from '../../qdn/encryption/group-encryption';
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, secretKeyDetails, userInfo, noSecretKey, setHideCommonKeyPopup, setIsForceShowCreationKeyPopup, isForceShowCreationKeyPopup}) => { export const CreateCommonSecret = ({
groupId,
secretKey,
isOwner,
myAddress,
secretKeyDetails,
userInfo,
noSecretKey,
setHideCommonKeyPopup,
setIsForceShowCreationKeyPopup,
isForceShowCreationKeyPopup,
}) => {
const { show, setTxList } = useContext(MyContext); const { show, setTxList } = useContext(MyContext);
const [openSnack, setOpenSnack] = React.useState(false); const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null); const [infoSnack, setInfoSnack] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false) const [isLoading, setIsLoading] = React.useState(false);
const theme = useTheme();
const getPublishesFromAdmins = async (admins: string[]) => { const getPublishesFromAdmins = async (admins: string[]) => {
// const validApi = await findUsableApi(); // const validApi = await findUsableApi();
const queryString = admins.map((name) => `name=${name}`).join("&"); const queryString = admins.map((name) => `name=${name}`).join('&');
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${ const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${
groupId groupId
}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`; }&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`;
const response = await fetch(url); const response = await fetch(url);
if(!response.ok){ if (!response.ok) {
throw new Error('network error') throw new Error('network error');
} }
const adminData = await response.json(); const adminData = await response.json();
const filterId = adminData.filter( const filterId = adminData.filter(
(data: any) => (data: any) => data.identifier === `symmetric-qchat-group-${groupId}`
data.identifier === `symmetric-qchat-group-${groupId}`
); );
if (filterId?.length === 0) { if (filterId?.length === 0) {
return false; return false;
@ -38,149 +59,182 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec
// Get the most recent date for both a and b // Get the most recent date for both a and b
const dateA = a.updated ? new Date(a.updated) : new Date(a.created); const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
const dateB = b.updated ? new Date(b.updated) : new Date(b.created); const dateB = b.updated ? new Date(b.updated) : new Date(b.created);
// Sort by most recent // Sort by most recent
return dateB.getTime() - dateA.getTime(); return dateB.getTime() - dateA.getTime();
}); });
return sortedData[0]; return sortedData[0];
}; };
const getSecretKey = async (loadingGroupParam?: boolean, secretKeyToPublish?: boolean) => {
const getSecretKey = async (
loadingGroupParam?: boolean,
secretKeyToPublish?: boolean
) => {
try { try {
pauseAllQueues() pauseAllQueues();
const { names } = await getGroupAdmins(groupId);
if (!names.length) {
const {names} = await getGroupAdmins(groupId); throw new Error('Network error');
if(!names.length){
throw new Error('Network error')
} }
const publish = await getPublishesFromAdmins(names); const publish = await getPublishesFromAdmins(names);
if (publish === false) { if (publish === false) {
return false; return false;
} }
const res = await fetch( const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${ `${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
publish.identifier publish.identifier
}?encoding=base64&rebuild=true` }?encoding=base64&rebuild=true`
); );
const data = await res.text(); const data = await res.text();
const decryptedKey: any = await decryptResource(data); const decryptedKey: any = await decryptResource(data);
const dataint8Array = base64ToUint8Array(decryptedKey.data); const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array); const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
if (!validateSecretKey(decryptedKeyToObject)) if (!validateSecretKey(decryptedKeyToObject))
throw new Error("SecretKey is not valid"); throw new Error('SecretKey is not valid');
if (decryptedKeyToObject) { if (decryptedKeyToObject) {
return decryptedKeyToObject; return decryptedKeyToObject;
} else {
} }
} catch (error) { } catch (error) {
console.log(error);
} finally {
} }
}; };
const createCommonSecret = async ()=> { const createCommonSecret = async () => {
try { try {
const fee = await getFee('ARBITRARY') const fee = await getFee('ARBITRARY');
await show({ await show({
message: "Would you like to perform an ARBITRARY transaction?" , message: 'Would you like to perform an ARBITRARY transaction?',
publishFee: fee.fee + ' QORT' publishFee: fee.fee + ' QORT',
}) });
setIsLoading(true) setIsLoading(true);
const secretKey2 = await getSecretKey() const secretKey2 = await getSecretKey();
if((!secretKey2 && secretKey2 !== false)) throw new Error('invalid secret key') if (!secretKey2 && secretKey2 !== false)
if (secretKey2 && !validateSecretKey(secretKey2)) throw new Error('invalid secret key') throw new Error('invalid secret key');
if (secretKey2 && !validateSecretKey(secretKey2))
throw new Error('invalid secret key');
const secretKeyToSend = !secretKey2 ? null : secretKey2 const secretKeyToSend = !secretKey2 ? null : secretKey2;
window
window.sendMessage("encryptAndPublishSymmetricKeyGroupChat", { .sendMessage('encryptAndPublishSymmetricKeyGroupChat', {
groupId: groupId, groupId: groupId,
previousData: secretKeyToSend, previousData: secretKeyToSend,
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
setInfoSnack({ setInfoSnack({
type: "success", type: 'success',
message: "Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.", message:
}); 'Successfully re-encrypted secret key. It may take a couple of minutes for the changes to propagate. Refresh the group in 5 mins.',
setOpenSnack(true);
setTxList((prev) => [
{
...response,
type: 'created-common-secret',
label: `Published secret key for group ${groupId}: awaiting confirmation`,
labelDone: `Published secret key for group ${groupId}: success!`,
done: false,
groupId,
},
...prev,
]);
}
setIsLoading(false);
setTimeout(() => {
setIsForceShowCreationKeyPopup(false)
}, 1000);
})
.catch((error) => {
console.error("Failed to encrypt and publish symmetric key for group chat:", error.message || "An error occurred");
setIsLoading(false);
}); });
setOpenSnack(true);
} catch (error) { setTxList((prev) => [
{
} ...response,
type: 'created-common-secret',
label: `Published secret key for group ${groupId}: awaiting confirmation`,
labelDone: `Published secret key for group ${groupId}: success!`,
done: false,
groupId,
},
...prev,
]);
}
setIsLoading(false);
setTimeout(() => {
setIsForceShowCreationKeyPopup(false);
}, 1000);
})
.catch((error) => {
console.error(
'Failed to encrypt and publish symmetric key for group chat:',
error.message || 'An error occurred'
);
setIsLoading(false);
});
} catch (error) {
console.log(error);
} }
};
return ( return (
<Box sx={{ <Box
padding: '25px', sx={{
display: 'flex', background: theme.palette.background.default,
flexDirection: 'column',
gap: '25px',
maxWidth: '350px',
background: '#444444'
}}>
<LoadingButton loading={isLoading} loadingPosition="start" color="warning" variant='contained' onClick={createCommonSecret}>Re-encrypt key</LoadingButton>
{noSecretKey ? (
<Box>
<Typography>There is no group secret key. Be the first admin to publish one!</Typography>
</Box>
) : isOwner && secretKeyDetails && userInfo?.name && userInfo.name !== secretKeyDetails?.name ? (
<Box>
<Typography>The latest group secret key was published by a non-owner. As the owner of the group please re-encrypt the key as a safeguard</Typography>
</Box>
): isForceShowCreationKeyPopup ? null : (
<Box>
<Typography>The group member list has changed. Please re-encrypt the secret key.</Typography>
</Box>
)}
<Box sx={{
display: 'flex', display: 'flex',
width: '100%', flexDirection: 'column',
justifyContent: 'flex-end' gap: '25px',
}}> maxWidth: '350px',
<Button onClick={()=> { padding: '25px',
setHideCommonKeyPopup(true) }}
setIsForceShowCreationKeyPopup(false) >
}} size='small'>Hide</Button> <LoadingButton
loading={isLoading}
loadingPosition="start"
color="warning"
variant="contained"
onClick={createCommonSecret}
>
Re-encrypt key
</LoadingButton>
{noSecretKey ? (
<Box>
<Typography>
There is no group secret key. Be the first admin to publish one!
</Typography>
</Box>
) : isOwner &&
secretKeyDetails &&
userInfo?.name &&
userInfo.name !== secretKeyDetails?.name ? (
<Box>
<Typography>
The latest group secret key was published by a non-owner. As the
owner of the group please re-encrypt the key as a safeguard
</Typography>
</Box>
) : isForceShowCreationKeyPopup ? null : (
<Box>
<Typography>
The group member list has changed. Please re-encrypt the secret key.
</Typography>
</Box>
)}
<Box
sx={{
display: 'flex',
justifyContent: 'flex-end',
width: '100%',
}}
>
<Button
onClick={() => {
setHideCommonKeyPopup(true);
setIsForceShowCreationKeyPopup(false);
}}
size="small"
>
Hide
</Button>
</Box> </Box>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} />
<CustomizedSnackbars
open={openSnack}
setOpen={setOpenSnack}
info={infoSnack}
setInfo={setInfoSnack}
/>
</Box> </Box>
);
) };
}

View File

@ -5,31 +5,22 @@ import React, {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { CreateCommonSecret } from './CreateCommonSecret';
import { reusableGet } from '../../qdn/publish/pubish';
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
import { import {
base64ToUint8Array, base64ToUint8Array,
objectToBase64, objectToBase64,
} from '../../qdn/encryption/group-encryption'; } from '../../qdn/encryption/group-encryption';
import { ChatContainerComp } from './ChatContainer';
import { ChatList } from './ChatList';
import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css'; import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
import Tiptap from './TipTap'; import Tiptap from './TipTap';
import { import { CustomButton } from '../../styles/App-styles';
AuthenticatedContainerInnerTop,
CustomButton,
} from '../../styles/App-styles';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import { getBaseApi, getFee } from '../../background'; import { getFee } from '../../background';
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
import { Box, Typography } from '@mui/material'; import { Box, Typography, useTheme } from '@mui/material';
import { Spacer } from '../../common/Spacer'; import { Spacer } from '../../common/Spacer';
import ShortUniqueId from 'short-unique-id'; import ShortUniqueId from 'short-unique-id';
import { AnnouncementList } from './AnnouncementList'; import { AnnouncementList } from './AnnouncementList';
const uid = new ShortUniqueId({ length: 8 });
import CampaignIcon from '@mui/icons-material/Campaign'; import CampaignIcon from '@mui/icons-material/Campaign';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { AnnouncementDiscussion } from './AnnouncementDiscussion'; import { AnnouncementDiscussion } from './AnnouncementDiscussion';
import { import {
MyContext, MyContext,
@ -42,9 +33,11 @@ import {
import { RequestQueueWithPromise } from '../../utils/queue/queue'; import { RequestQueueWithPromise } from '../../utils/queue/queue';
import { CustomizedSnackbars } from '../Snackbar/Snackbar'; import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { addDataPublishesFunc, getDataPublishesFunc } from '../Group/Group'; import { addDataPublishesFunc, getDataPublishesFunc } from '../Group/Group';
import { getRootHeight } from '../../utils/mobile/mobileUtils';
const uid = new ShortUniqueId({ length: 8 });
export const requestQueueCommentCount = new RequestQueueWithPromise(3); export const requestQueueCommentCount = new RequestQueueWithPromise(3);
export const requestQueuePublishedAccouncements = new RequestQueueWithPromise( export const requestQueuePublishedAccouncements = new RequestQueueWithPromise(
3 3
); );
@ -106,7 +99,9 @@ export const decryptPublishes = async (encryptedMessages: any[], secretKey) => {
rej(error.message || 'An error occurred'); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
export const handleUnencryptedPublishes = (publishes) => { export const handleUnencryptedPublishes = (publishes) => {
let publishesData = []; let publishesData = [];
@ -117,10 +112,13 @@ export const handleUnencryptedPublishes = (publishes) => {
if (decodedData) { if (decodedData) {
publishesData.push({ decryptedData: decodedData }); publishesData.push({ decryptedData: decodedData });
} }
} catch (error) {} } catch (error) {
console.log(error);
}
}); });
return publishesData; return publishesData;
}; };
export const GroupAnnouncements = ({ export const GroupAnnouncements = ({
selectedGroup, selectedGroup,
secretKey, secretKey,
@ -236,7 +234,9 @@ export const GroupAnnouncements = ({
rej(error.message || 'An error occurred'); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const publishAnc = async ({ encryptedData, identifier }: any) => { const publishAnc = async ({ encryptedData, identifier }: any) => {
@ -258,6 +258,7 @@ export const GroupAnnouncements = ({
}); });
}); });
}; };
const clearEditorContent = () => { const clearEditorContent = () => {
if (editorRef.current) { if (editorRef.current) {
editorRef.current.chain().focus().clearContent().run(); editorRef.current.chain().focus().clearContent().run();
@ -286,17 +287,21 @@ export const GroupAnnouncements = ({
}); });
setTempPublishedList(tempData); setTempPublishedList(tempData);
} }
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const publishAnnouncement = async () => { const publishAnnouncement = async () => {
try { try {
pauseAllQueues(); pauseAllQueues();
const fee = await getFee('ARBITRARY'); const fee = await getFee('ARBITRARY');
await show({ await show({
message: 'Would you like to perform a ARBITRARY transaction?', message: 'Would you like to perform a ARBITRARY transaction?',
publishFee: fee.fee + ' QORT', publishFee: fee.fee + ' QORT',
}); });
if (isSending) return; if (isSending) return;
if (editorRef.current) { if (editorRef.current) {
const htmlContent = editorRef.current.getHTML(); const htmlContent = editorRef.current.getHTML();
@ -379,8 +384,7 @@ export const GroupAnnouncements = ({
); );
} }
} catch (error) { } catch (error) {
} finally { console.log(error);
// dispatch(setIsLoadingGlobal(false))
} }
}, },
[secretKey] [secretKey]
@ -422,11 +426,15 @@ export const GroupAnnouncements = ({
isPrivate isPrivate
); );
} }
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const interval = useRef<any>(null); const interval = useRef<any>(null);
const theme = useTheme();
const checkNewMessages = React.useCallback(async () => { const checkNewMessages = React.useCallback(async () => {
try { try {
const identifier = `grp-${selectedGroup}-anc-`; const identifier = `grp-${selectedGroup}-anc-`;
@ -449,7 +457,9 @@ export const GroupAnnouncements = ({
}, },
isPrivate isPrivate
); );
} catch (error) {} } catch (error) {
console.log(error);
}
} }
setAnnouncements(responseData); setAnnouncements(responseData);
return; return;
@ -467,11 +477,13 @@ export const GroupAnnouncements = ({
{ name: data.name, identifier: data.identifier }, { name: data.name, identifier: data.identifier },
isPrivate isPrivate
); );
} catch (error) {} } catch (error) {
console.log(error);
}
} }
setAnnouncements((prev) => [...newArray, ...prev]); setAnnouncements((prev) => [...newArray, ...prev]);
} catch (error) { } catch (error) {
} finally { console.log(error);
} }
}, [announcements, secretKey, selectedGroup]); }, [announcements, secretKey, selectedGroup]);
@ -523,10 +535,10 @@ export const GroupAnnouncements = ({
: 'calc(100vh - 70px)', : 'calc(100vh - 70px)',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
width: '100%',
visibility: hide && 'hidden',
position: hide && 'fixed',
left: hide && '-1000px', left: hide && '-1000px',
position: hide && 'fixed',
visibility: hide && 'hidden',
width: '100%',
}} }}
> >
<AnnouncementDiscussion <AnnouncementDiscussion
@ -546,54 +558,54 @@ export const GroupAnnouncements = ({
return ( return (
<div <div
style={{ style={{
// reference to change height
height: isMobile ? `calc(${rootHeight} - 127px` : 'calc(100vh - 70px)',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
width: '100%', height: 'calc(100vh - 70px)',
visibility: hide && 'hidden',
position: hide && 'fixed',
left: hide && '-1000px', left: hide && '-1000px',
position: hide && 'fixed',
visibility: hide && 'hidden',
width: '100%',
}} }}
> >
<div <div
style={{ style={{
position: 'relative',
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexShrink: 0, flexShrink: 0,
position: 'relative',
width: '100%',
}} }}
> >
{!isMobile && ( {!isMobile && (
<Box <Box
sx={{ sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
padding: isMobile ? '8px' : '25px',
fontSize: isMobile ? '16px' : '20px',
gap: '20px',
alignItems: 'center', alignItems: 'center',
display: 'flex',
fontSize: '20px',
gap: '20px',
justifyContent: 'center',
padding: '25px',
width: '100%',
}} }}
> >
<CampaignIcon <CampaignIcon
sx={{ sx={{
fontSize: isMobile ? '16px' : '30px', fontSize: '30px',
}} }}
/> />
Group Announcements Group Announcements
</Box> </Box>
)} )}
<Spacer height={isMobile ? '0px' : '25px'} /> <Spacer height={'25px'} />
</div> </div>
{!isLoading && combinedListTempAndReal?.length === 0 && ( {!isLoading && combinedListTempAndReal?.length === 0 && (
<Box <Box
sx={{ sx={{
width: '100%',
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
width: '100%',
}} }}
> >
<Typography <Typography
@ -620,31 +632,28 @@ export const GroupAnnouncements = ({
{isAdmin && ( {isAdmin && (
<div <div
style={{ style={{
// position: 'fixed', backgroundColor: theme.palette.background.default,
// bottom: '0px', bottom: isFocusedParent ? '0px' : 'unset',
backgroundColor: '#232428', boxSizing: 'border-box',
minHeight: isMobile ? '0px' : '150px',
maxHeight: isMobile ? 'auto' : '400px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
overflow: 'hidden',
width: '100%',
boxSizing: 'border-box',
padding: isMobile ? '10px' : '20px',
position: isFocusedParent ? 'fixed' : 'relative',
bottom: isFocusedParent ? '0px' : 'unset',
top: isFocusedParent ? '0px' : 'unset',
zIndex: isFocusedParent ? 5 : 'unset',
flexShrink: 0, flexShrink: 0,
maxHeight: '400px',
minHeight: '150px',
overflow: 'hidden',
padding: '20px',
position: isFocusedParent ? 'fixed' : 'relative',
top: isFocusedParent ? '0px' : 'unset',
width: '100%',
zIndex: isFocusedParent ? 5 : 'unset',
}} }}
> >
<div <div
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: isMobile && 1, flexGrow: 1,
overflow: 'auto', overflow: 'auto',
// height: '100%',
}} }}
> >
<Tiptap <Tiptap
@ -656,14 +665,15 @@ export const GroupAnnouncements = ({
setIsFocusedParent={setIsFocusedParent} setIsFocusedParent={setIsFocusedParent}
/> />
</div> </div>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
width: '100&', flexShrink: 0,
gap: '10px', gap: '10px',
justifyContent: 'center', justifyContent: 'center',
flexShrink: 0,
position: 'relative', position: 'relative',
width: '100&',
}} }}
> >
{isFocusedParent && ( {isFocusedParent && (
@ -678,43 +688,46 @@ export const GroupAnnouncements = ({
// Unfocus the editor // Unfocus the editor
}} }}
style={{ style={{
marginTop: 'auto',
alignSelf: 'center', alignSelf: 'center',
cursor: isSending ? 'default' : 'pointer',
background: 'var(--danger)', background: 'var(--danger)',
cursor: isSending ? 'default' : 'pointer',
flexShrink: 0, flexShrink: 0,
padding: isMobile && '5px', fontSize: '14px',
fontSize: isMobile && '14px', marginTop: 'auto',
padding: '5px',
}} }}
> >
{` Close`} {` Close`}
</CustomButton> </CustomButton>
)} )}
<CustomButton <CustomButton
onClick={() => { onClick={() => {
if (isSending) return; if (isSending) return;
publishAnnouncement(); publishAnnouncement();
}} }}
style={{ style={{
marginTop: 'auto',
alignSelf: 'center', alignSelf: 'center',
background: isSending
? theme.palette.background.default
: theme.palette.background.paper,
cursor: isSending ? 'default' : 'pointer', cursor: isSending ? 'default' : 'pointer',
background: isSending && 'rgba(0, 0, 0, 0.8)',
flexShrink: 0, flexShrink: 0,
padding: isMobile && '5px', fontSize: '14px',
fontSize: isMobile && '14px', marginTop: 'auto',
padding: '5px',
}} }}
> >
{isSending && ( {isSending && (
<CircularProgress <CircularProgress
size={18} size={18}
sx={{ sx={{
color: theme.palette.text.primary,
left: '50%',
marginLeft: '-12px',
marginTop: '-12px',
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: '50%',
marginTop: '-12px',
marginLeft: '-12px',
color: 'white',
}} }}
/> />
)} )}

View File

@ -1,19 +1,6 @@
import React, { import { useContext, useEffect, useState } from 'react';
useCallback, import { GroupMail } from '../Group/Forum/GroupMail';
useContext, import { MyContext, isMobile } from '../../App';
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { GroupMail } from "../Group/Forum/GroupMail";
import { MyContext, isMobile } from "../../App";
import { getRootHeight } from "../../utils/mobile/mobileUtils";
export const GroupForum = ({ export const GroupForum = ({
selectedGroup, selectedGroup,
@ -23,12 +10,13 @@ export const GroupForum = ({
isAdmin, isAdmin,
myAddress, myAddress,
hide, hide,
defaultThread, defaultThread,
setDefaultThread, setDefaultThread,
isPrivate isPrivate,
}) => { }) => {
const { rootHeight } = useContext(MyContext); const { rootHeight } = useContext(MyContext);
const [isMoved, setIsMoved] = useState(false); const [isMoved, setIsMoved] = useState(false);
useEffect(() => { useEffect(() => {
if (hide) { if (hide) {
setTimeout(() => setIsMoved(true), 300); // Wait for the fade-out to complete before moving setTimeout(() => setIsMoved(true), 300); // Wait for the fade-out to complete before moving
@ -39,20 +27,27 @@ export const GroupForum = ({
return ( return (
<div <div
style={{ style={{
// reference to change height display: 'flex',
height: isMobile ? `calc(${rootHeight} - 127px` : "calc(100vh - 70px)", flexDirection: 'column',
display: "flex", height: 'calc(100vh - 70px)',
flexDirection: "column", left: hide && '-1000px',
width: "100%", opacity: hide ? 0 : 1,
opacity: hide ? 0 : 1, position: hide ? 'fixed' : 'relative',
visibility: hide && 'hidden', visibility: hide && 'hidden',
position: hide ? 'fixed' : 'relative', width: '100%',
left: hide && '-1000px' }}
}} >
> <GroupMail
<GroupMail isPrivate={isPrivate} hide={hide} getSecretKey={getSecretKey} selectedGroup={selectedGroup} userInfo={userInfo} secretKey={secretKey} defaultThread={defaultThread} setDefaultThread={setDefaultThread} /> isPrivate={isPrivate}
hide={hide}
</div> getSecretKey={getSecretKey}
selectedGroup={selectedGroup}
userInfo={userInfo}
secretKey={secretKey}
defaultThread={defaultThread}
setDefaultThread={setDefaultThread}
/>
</div>
); );
}; };

View File

@ -1,69 +1,68 @@
import React, { import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
forwardRef, useEffect, useImperativeHandle,
useState, export default forwardRef((props, ref) => {
} from 'react' const [selectedIndex, setSelectedIndex] = useState(0);
export default forwardRef((props, ref) => { const selectItem = (index) => {
const [selectedIndex, setSelectedIndex] = useState(0) const item = props.items[index];
const selectItem = index => { if (item) {
const item = props.items[index] props.command(item);
}
if (item) { };
props.command(item)
const upHandler = () => {
setSelectedIndex(
(selectedIndex + props.items.length - 1) % props.items.length
);
};
const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items.length);
};
const enterHandler = () => {
selectItem(selectedIndex);
};
useEffect(() => setSelectedIndex(0), [props.items]);
useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
if (event.key === 'ArrowUp') {
upHandler();
return true;
} }
}
if (event.key === 'ArrowDown') {
const upHandler = () => { downHandler();
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length) return true;
} }
const downHandler = () => { if (event.key === 'Enter') {
setSelectedIndex((selectedIndex + 1) % props.items.length) enterHandler();
} return true;
}
const enterHandler = () => {
selectItem(selectedIndex) return false;
} },
}));
useEffect(() => setSelectedIndex(0), [props.items])
return (
useImperativeHandle(ref, () => ({ <div className="dropdown-menu">
onKeyDown: ({ event }) => { {props.items.length ? (
if (event.key === 'ArrowUp') { props.items.map((item, index) => (
upHandler() <button
return true className={index === selectedIndex ? 'is-selected' : ''}
} key={item.id || index}
onClick={() => selectItem(index)}
if (event.key === 'ArrowDown') { >
downHandler() {item.label}
return true </button>
} ))
) : (
if (event.key === 'Enter') { <div className="item">No result</div>
enterHandler() )}
return true </div>
} );
});
return false
},
}))
return (
<div className="dropdown-menu">
{props.items.length
? props.items.map((item, index) => (
<button
className={index === selectedIndex ? 'is-selected' : ''}
key={item.id || index}
onClick={() => selectItem(index)}
>
{item.label}
</button>
))
: <div className="item">No result</div>
}
</div>
)
})

View File

@ -1,28 +1,29 @@
import React, { useEffect, useMemo } from 'react'; import { useMemo } from 'react';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import './styles.css'; import './chat.css';
import { executeEvent } from '../../utils/events'; import { executeEvent } from '../../utils/events';
import { Embed } from '../Embeds/Embed'; import { Embed } from '../Embeds/Embed';
import { Box, useTheme } from '@mui/material';
export const extractComponents = (url) => { export const extractComponents = (url) => {
if (!url || !url.startsWith("qortal://")) { if (!url || !url.startsWith('qortal://')) {
return null; return null;
} }
// Skip links starting with "qortal://use-" // Skip links starting with "qortal://use-"
if (url.startsWith("qortal://use-")) { if (url.startsWith('qortal://use-')) {
return null; return null;
} }
url = url.replace(/^(qortal\:\/\/)/, ""); url = url.replace(/^(qortal\:\/\/)/, '');
if (url.includes("/")) { if (url.includes('/')) {
let parts = url.split("/"); let parts = url.split('/');
const service = parts[0].toUpperCase(); const service = parts[0].toUpperCase();
parts.shift(); parts.shift();
const name = parts[0]; const name = parts[0];
parts.shift(); parts.shift();
let identifier; let identifier;
const path = parts.join("/"); const path = parts.join('/');
return { service, name, identifier, path }; return { service, name, identifier, path };
} }
@ -64,8 +65,7 @@ function processText(input) {
} }
const linkify = (text) => { const linkify = (text) => {
if (!text) return ""; // Return an empty string if text is null or undefined if (!text) return ''; // Return an empty string if text is null or undefined
let textFormatted = text; let textFormatted = text;
const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g; const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g;
textFormatted = text.replace(urlPattern, (url) => { textFormatted = text.replace(urlPattern, (url) => {
@ -75,22 +75,68 @@ const linkify = (text) => {
return processText(textFormatted); return processText(textFormatted);
}; };
export const MessageDisplay = ({ htmlContent, isReply }) => { export const MessageDisplay = ({ htmlContent, isReply }) => {
const theme = useTheme();
const sanitizedContent = useMemo(() => {
const sanitizedContent = useMemo(()=> {
return DOMPurify.sanitize(linkify(htmlContent), { return DOMPurify.sanitize(linkify(htmlContent), {
ALLOWED_TAGS: [ ALLOWED_TAGS: [
'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'div', 'span', 'img', 'a',
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 's', 'hr' '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: [ ALLOWED_ATTR: [
'href', 'target', 'rel', 'class', 'src', 'alt', 'title', 'href',
'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url' '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, ''); }).replace(
}, [htmlContent]) /<span[^>]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g,
''
);
}, [htmlContent]);
const handleClick = async (e) => { const handleClick = async (e) => {
e.preventDefault(); e.preventDefault();
@ -98,7 +144,7 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
const target = e.target; const target = e.target;
if (target.tagName === 'A') { if (target.tagName === 'A') {
const href = target.getAttribute('href'); const href = target.getAttribute('href');
if(window?.electronAPI){ if (window?.electronAPI) {
window.electronAPI.openExternal(href); window.electronAPI.openExternal(href);
} else { } else {
window.open(href, '_system'); window.open(href, '_system');
@ -106,32 +152,32 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
} else if (target.getAttribute('data-url')) { } else if (target.getAttribute('data-url')) {
const url = target.getAttribute('data-url'); const url = target.getAttribute('data-url');
let copyUrl = url let copyUrl = url;
try { try {
copyUrl = copyUrl.replace(/^(qortal:\/\/)/, '') copyUrl = copyUrl.replace(/^(qortal:\/\/)/, '');
if (copyUrl.startsWith('use-')) { if (copyUrl.startsWith('use-')) {
// Handle the new 'use' format // Handle the new 'use' format
const parts = copyUrl.split('/') const parts = copyUrl.split('/');
const type = parts[0].split('-')[1] // e.g., 'group' from 'use-group' const type = parts[0].split('-')[1]; // e.g., 'group' from 'use-group'
parts.shift() parts.shift();
const action = parts.length > 0 ? parts[0].split('-')[1] : null // e.g., 'invite' from 'action-invite' const action = parts.length > 0 ? parts[0].split('-')[1] : null; // e.g., 'invite' from 'action-invite'
parts.shift() parts.shift();
const idPrefix = parts.length > 0 ? parts[0].split('-')[0] : null // e.g., 'groupid' from 'groupid-321' const idPrefix = parts.length > 0 ? parts[0].split('-')[0] : null; // e.g., 'groupid' from 'groupid-321'
const id = parts.length > 0 ? parts[0].split('-')[1] : null // e.g., '321' from 'groupid-321' const id = parts.length > 0 ? parts[0].split('-')[1] : null; // e.g., '321' from 'groupid-321'
if(action === 'join'){ if (action === 'join') {
executeEvent("globalActionJoinGroup", { groupId: id}); executeEvent('globalActionJoinGroup', { groupId: id });
return return;
}
} }
} catch (error) {
//error
} }
} catch (error) {
//error
}
const res = extractComponents(url); const res = extractComponents(url);
if (res) { if (res) {
const { service, name, identifier, path } = res; const { service, name, identifier, path } = res;
executeEvent("addTab", { data: { service, name, identifier, path } }); executeEvent('addTab', { data: { service, name, identifier, path } });
executeEvent("open-apps-mode", { }); executeEvent('open-apps-mode', {});
} }
} }
}; };
@ -141,19 +187,24 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
let embedData = null; let embedData = null;
if (embedLink) { if (embedLink) {
embedData = embedLink[0] embedData = embedLink[0];
} }
return ( return (
<> <Box
{embedLink && ( sx={{
<Embed embedLink={embedData} /> '--text-primary': theme.palette.text.primary,
)} '--text-secondary': theme.palette.text.secondary,
<div '--background-default': theme.palette.background.default,
className={`tiptap ${isReply ? 'isReply' : ''}`} '--background-secondary': theme.palette.background.paper,
dangerouslySetInnerHTML={{ __html: sanitizedContent }} }}
onClick={handleClick} >
/> {embedLink && <Embed embedLink={embedData} />}
</> <div
className={`tiptap ${isReply ? 'isReply' : ''}`}
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
onClick={handleClick}
/>
</Box>
); );
}; };

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,10 @@
import React, { useRef } from 'react'; import { useRef } from 'react';
import { NodeViewWrapper } from '@tiptap/react'; import { NodeViewWrapper } from '@tiptap/react';
import { useTheme } from '@mui/material';
const ResizableImage = ({ node, updateAttributes, selected }) => { const ResizableImage = ({ node, updateAttributes, selected }) => {
const imgRef = useRef(null); const imgRef = useRef(null);
const theme = useTheme();
const startResizing = (e) => { const startResizing = (e) => {
e.preventDefault(); e.preventDefault();
@ -40,18 +42,23 @@ const ResizableImage = ({ node, updateAttributes, selected }) => {
src={node.attrs.src} src={node.attrs.src}
alt={node.attrs.alt || ''} alt={node.attrs.alt || ''}
title={node.attrs.title || ''} title={node.attrs.title || ''}
style={{ width: node.attrs.width || 'auto', display: 'block', margin: '0 auto' }} style={{
width: node.attrs.width || 'auto',
display: 'block',
margin: '0 auto',
}}
draggable={false} // Prevent image dragging draggable={false} // Prevent image dragging
/> />
<div <div
style={{ style={{
backgroundColor: theme.palette.background.paper,
bottom: 0,
cursor: 'nwse-resize',
height: '10px',
position: 'absolute', position: 'absolute',
right: 0, right: 0,
bottom: 0,
width: '10px', width: '10px',
height: '10px',
backgroundColor: 'gray',
cursor: 'nwse-resize',
zIndex: 1, // Ensure the resize handle is above other content zIndex: 1, // Ensure the resize handle is above other content
}} }}
onMouseDown={startResizing} onMouseDown={startResizing}

View File

@ -1,42 +1,36 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef } from 'react';
import { EditorProvider, useCurrentEditor, useEditor } from "@tiptap/react"; import { EditorProvider, useCurrentEditor } from '@tiptap/react';
import StarterKit from "@tiptap/starter-kit"; import StarterKit from '@tiptap/starter-kit';
import { Color } from "@tiptap/extension-color"; import { Color } from '@tiptap/extension-color';
import ListItem from "@tiptap/extension-list-item"; import ListItem from '@tiptap/extension-list-item';
import TextStyle from "@tiptap/extension-text-style"; import TextStyle from '@tiptap/extension-text-style';
import Placeholder from "@tiptap/extension-placeholder"; import Placeholder from '@tiptap/extension-placeholder';
import Image from "@tiptap/extension-image"; import IconButton from '@mui/material/IconButton';
import IconButton from "@mui/material/IconButton"; import FormatBoldIcon from '@mui/icons-material/FormatBold';
import FormatBoldIcon from "@mui/icons-material/FormatBold"; import FormatItalicIcon from '@mui/icons-material/FormatItalic';
import FormatItalicIcon from "@mui/icons-material/FormatItalic"; import StrikethroughSIcon from '@mui/icons-material/StrikethroughS';
import StrikethroughSIcon from "@mui/icons-material/StrikethroughS"; import FormatClearIcon from '@mui/icons-material/FormatClear';
import FormatClearIcon from "@mui/icons-material/FormatClear"; import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted"; import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
import FormatListNumberedIcon from "@mui/icons-material/FormatListNumbered"; import CodeIcon from '@mui/icons-material/Code';
import CodeIcon from "@mui/icons-material/Code"; import ImageIcon from '@mui/icons-material/Image'; // Import Image icon
import ImageIcon from "@mui/icons-material/Image"; // Import Image icon import FormatQuoteIcon from '@mui/icons-material/FormatQuote';
import FormatQuoteIcon from "@mui/icons-material/FormatQuote"; import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule';
import HorizontalRuleIcon from "@mui/icons-material/HorizontalRule"; import UndoIcon from '@mui/icons-material/Undo';
import UndoIcon from "@mui/icons-material/Undo"; import RedoIcon from '@mui/icons-material/Redo';
import RedoIcon from "@mui/icons-material/Redo"; import FormatHeadingIcon from '@mui/icons-material/FormatSize';
import FormatHeadingIcon from "@mui/icons-material/FormatSize"; import DeveloperModeIcon from '@mui/icons-material/DeveloperMode';
import DeveloperModeIcon from "@mui/icons-material/DeveloperMode"; import Compressor from 'compressorjs';
import Compressor from "compressorjs";
import Mention from '@tiptap/extension-mention'; import Mention from '@tiptap/extension-mention';
import ImageResize from "tiptap-extension-resize-image"; // Import the ResizeImage extension import ImageResize from 'tiptap-extension-resize-image'; // Import the ResizeImage extension
import { isMobile } from "../../App"; import { isMobile } from '../../App';
import tippy from "tippy.js"; import tippy from 'tippy.js';
import "tippy.js/dist/tippy.css"; import 'tippy.js/dist/tippy.css';
import Popover from '@mui/material/Popover'; import { ReactRenderer } from '@tiptap/react';
import List from '@mui/material/List'; import MentionList from './MentionList.jsx';
import ListItemMui from '@mui/material/ListItem'; import { useRecoilState } from 'recoil';
import ListItemButton from '@mui/material/ListItemButton'; import { isDisabledEditorEnterAtom } from '../../atoms/global.js';
import ListItemText from '@mui/material/ListItemText'; import { Box, Checkbox, Typography, useTheme } from '@mui/material';
import { ReactRenderer } from '@tiptap/react'
import MentionList from './MentionList.jsx'
import { useRecoilState } from "recoil";
import { isDisabledEditorEnterAtom } from "../../atoms/global.js";
import { Box, Checkbox, Typography } from "@mui/material";
function textMatcher(doc, from) { function textMatcher(doc, from) {
const textBeforeCursor = doc.textBetween(0, from, ' ', ' '); const textBeforeCursor = doc.textBetween(0, from, ' ', ' ');
@ -47,9 +41,16 @@ function textMatcher(doc, from) {
const query = match[0]; const query = match[0];
return { start, query }; return { start, query };
} }
const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEditorEnter }) => {
const MenuBar = ({
setEditorRef,
isChat,
isDisabledEditorEnter,
setIsDisabledEditorEnter,
}) => {
const { editor } = useCurrentEditor(); const { editor } = useCurrentEditor();
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const theme = useTheme();
if (!editor) { if (!editor) {
return null; return null;
@ -67,15 +68,15 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
new Compressor(file, { new Compressor(file, {
quality: 0.6, quality: 0.6,
maxWidth: 1200, maxWidth: 1200,
mimeType: "image/webp", mimeType: 'image/webp',
success(result) { success(result) {
compressedFile = new File([result], "image.webp", { compressedFile = new File([result], 'image.webp', {
type: "image/webp", type: 'image/webp',
}); });
resolve(); resolve();
}, },
error(err) { error(err) {
console.error("Image compression error:", err); console.error('Image compression error:', err);
}, },
}); });
}); });
@ -87,9 +88,9 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
editor editor
.chain() .chain()
.focus() .focus()
.setImage({ src: url, style: "width: auto" }) .setImage({ src: url, style: 'width: auto' })
.run(); .run();
fileInputRef.current.value = ""; fileInputRef.current.value = '';
}; };
reader.readAsDataURL(compressedFile); reader.readAsDataURL(compressedFile);
} }
@ -102,7 +103,7 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
const handlePaste = (event) => { const handlePaste = (event) => {
const items = event.clipboardData.items; const items = event.clipboardData.items;
for (const item of items) { for (const item of items) {
if (item.type.startsWith("image/")) { if (item.type.startsWith('image/')) {
const file = item.getAsFile(); const file = item.getAsFile();
if (file) { if (file) {
event.preventDefault(); // Prevent the default paste behavior event.preventDefault(); // Prevent the default paste behavior
@ -114,24 +115,29 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
useEffect(() => { useEffect(() => {
if (editor) { if (editor) {
editor.view.dom.addEventListener("paste", handlePaste); editor.view.dom.addEventListener('paste', handlePaste);
return () => { return () => {
editor.view.dom.removeEventListener("paste", handlePaste); editor.view.dom.removeEventListener('paste', handlePaste);
}; };
} }
}, [editor]); }, [editor]);
return ( return (
<div className="control-group"> <div className="control-group">
<div className="button-group" style={{ <div
display: 'flex' className="button-group"
}}> style={{
display: 'flex',
}}
>
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleBold().run()} onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()} disabled={!editor.can().chain().focus().toggleBold().run()}
sx={{ sx={{
color: editor.isActive("bold") ? "white" : "gray", color: editor.isActive('bold')
padding: isMobile ? "5px" : "revert", ? theme.palette.text.primary
: theme.palette.text.secondary,
padding: isMobile ? '5px' : 'revert',
}} }}
> >
<FormatBoldIcon /> <FormatBoldIcon />
@ -140,8 +146,10 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
onClick={() => editor.chain().focus().toggleItalic().run()} onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()} disabled={!editor.can().chain().focus().toggleItalic().run()}
sx={{ sx={{
color: editor.isActive("italic") ? "white" : "gray", color: editor.isActive('italic')
padding: isMobile ? "5px" : "revert", ? theme.palette.text.primary
: theme.palette.text.secondary,
padding: isMobile ? '5px' : 'revert',
}} }}
> >
<FormatItalicIcon /> <FormatItalicIcon />
@ -150,8 +158,10 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
onClick={() => editor.chain().focus().toggleStrike().run()} onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={!editor.can().chain().focus().toggleStrike().run()} disabled={!editor.can().chain().focus().toggleStrike().run()}
sx={{ sx={{
color: editor.isActive("strike") ? "white" : "gray", color: editor.isActive('strike')
padding: isMobile ? "5px" : "revert", ? theme.palette.text.primary
: theme.palette.text.secondary,
padding: isMobile ? '5px' : 'revert',
}} }}
> >
<StrikethroughSIcon /> <StrikethroughSIcon />
@ -160,8 +170,10 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
onClick={() => editor.chain().focus().toggleCode().run()} onClick={() => editor.chain().focus().toggleCode().run()}
disabled={!editor.can().chain().focus().toggleCode().run()} disabled={!editor.can().chain().focus().toggleCode().run()}
sx={{ sx={{
color: editor.isActive("code") ? "white" : "gray", color: editor.isActive('code')
padding: isMobile ? "5px" : "revert", ? theme.palette.text.primary
: theme.palette.text.secondary,
padding: isMobile ? '5px' : 'revert',
}} }}
> >
<CodeIcon /> <CodeIcon />
@ -170,13 +182,13 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
onClick={() => editor.chain().focus().unsetAllMarks().run()} onClick={() => editor.chain().focus().unsetAllMarks().run()}
sx={{ sx={{
color: color:
editor.isActive("bold") || editor.isActive('bold') ||
editor.isActive("italic") || editor.isActive('italic') ||
editor.isActive("strike") || editor.isActive('strike') ||
editor.isActive("code") editor.isActive('code')
? "white" ? theme.palette.text.primary
: "gray", : theme.palette.text.secondary,
padding: isMobile ? "5px" : "revert", padding: isMobile ? '5px' : 'revert',
}} }}
> >
<FormatClearIcon /> <FormatClearIcon />
@ -184,8 +196,10 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleBulletList().run()} onClick={() => editor.chain().focus().toggleBulletList().run()}
sx={{ sx={{
color: editor.isActive("bulletList") ? "white" : "gray", color: editor.isActive('bulletList')
padding: isMobile ? "5px" : "revert", ? theme.palette.text.primary
: theme.palette.text.secondary,
padding: isMobile ? '5px' : 'revert',
}} }}
> >
<FormatListBulletedIcon /> <FormatListBulletedIcon />
@ -193,8 +207,10 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleOrderedList().run()} onClick={() => editor.chain().focus().toggleOrderedList().run()}
sx={{ sx={{
color: editor.isActive("orderedList") ? "white" : "gray", color: editor.isActive('orderedList')
padding: isMobile ? "5px" : "revert", ? theme.palette.text.primary
: theme.palette.text.secondary,
padding: isMobile ? '5px' : 'revert',
}} }}
> >
<FormatListNumberedIcon /> <FormatListNumberedIcon />
@ -202,8 +218,10 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleCodeBlock().run()} onClick={() => editor.chain().focus().toggleCodeBlock().run()}
sx={{ sx={{
color: editor.isActive("codeBlock") ? "white" : "gray", color: editor.isActive('codeBlock')
padding: isMobile ? "5px" : "revert", ? theme.palette.text.primary
: theme.palette.text.secondary,
padding: isMobile ? '5px' : 'revert',
}} }}
> >
<DeveloperModeIcon /> <DeveloperModeIcon />
@ -211,8 +229,10 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
<IconButton <IconButton
onClick={() => editor.chain().focus().toggleBlockquote().run()} onClick={() => editor.chain().focus().toggleBlockquote().run()}
sx={{ sx={{
color: editor.isActive("blockquote") ? "white" : "gray", color: editor.isActive('blockquote')
padding: isMobile ? "5px" : "revert", ? theme.palette.text.primary
: theme.palette.text.secondary,
padding: isMobile ? '5px' : 'revert',
}} }}
> >
<FormatQuoteIcon /> <FormatQuoteIcon />
@ -220,7 +240,7 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
<IconButton <IconButton
onClick={() => editor.chain().focus().setHorizontalRule().run()} onClick={() => editor.chain().focus().setHorizontalRule().run()}
disabled={!editor.can().chain().focus().setHorizontalRule().run()} disabled={!editor.can().chain().focus().setHorizontalRule().run()}
sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }} sx={{ color: 'gray', padding: isMobile ? '5px' : 'revert' }}
> >
<HorizontalRuleIcon /> <HorizontalRuleIcon />
</IconButton> </IconButton>
@ -229,8 +249,10 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
editor.chain().focus().toggleHeading({ level: 1 }).run() editor.chain().focus().toggleHeading({ level: 1 }).run()
} }
sx={{ sx={{
color: editor.isActive("heading", { level: 1 }) ? "white" : "gray", color: editor.isActive('heading', { level: 1 })
padding: isMobile ? "5px" : "revert", ? theme.palette.text.primary
: theme.palette.text.secondary,
padding: isMobile ? '5px' : 'revert',
}} }}
> >
<FormatHeadingIcon fontSize="small" /> <FormatHeadingIcon fontSize="small" />
@ -238,66 +260,68 @@ const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEdi
<IconButton <IconButton
onClick={() => editor.chain().focus().undo().run()} onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().chain().focus().undo().run()} disabled={!editor.can().chain().focus().undo().run()}
sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }} sx={{ color: 'gray', padding: isMobile ? '5px' : 'revert' }}
> >
<UndoIcon /> <UndoIcon />
</IconButton> </IconButton>
<IconButton <IconButton
onClick={() => editor.chain().focus().redo().run()} onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().chain().focus().redo().run()} disabled={!editor.can().chain().focus().redo().run()}
sx={{ color: "gray" }} sx={{ color: 'gray' }}
> >
<RedoIcon /> <RedoIcon />
</IconButton> </IconButton>
{isChat && ( {isChat && (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
marginLeft: '5px', marginLeft: '5px',
cursor: 'pointer' cursor: 'pointer',
}} }}
onClick={()=> { onClick={() => {
setIsDisabledEditorEnter(!isDisabledEditorEnter) setIsDisabledEditorEnter(!isDisabledEditorEnter);
}} }}
> >
<Checkbox <Checkbox
edge="start" edge="start"
tabIndex={-1} tabIndex={-1}
disableRipple disableRipple
checked={isDisabledEditorEnter}
checked={isDisabledEditorEnter} sx={{
sx={{ '&.Mui-checked': {
"&.Mui-checked": { color: theme.palette.text.secondary,
color: "gray", // Customize the color when checked },
}, '& .MuiSvgIcon-root': {
"& .MuiSvgIcon-root": { color: theme.palette.text.secondary,
color: "gray", },
}, }}
}} />
/> <Typography
<Typography sx={{
sx={{ fontSize: '14px',
fontSize: "14px", color: theme.palette.text.primary,
color: 'gray' }}
}} >
> disable enter
disable enter </Typography>
</Typography> </Box>
</Box>
)} )}
{!isChat && ( {!isChat && (
<> <>
<IconButton <IconButton
onClick={triggerImageUpload} onClick={triggerImageUpload}
sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }} sx={{
color: theme.palette.text.secondary,
padding: isMobile ? '5px' : 'revert',
}}
> >
<ImageIcon /> <ImageIcon />
</IconButton> </IconButton>
<input <input
type="file" type="file"
ref={fileInputRef} ref={fileInputRef}
style={{ display: "none" }} style={{ display: 'none' }}
onChange={(event) => handleImageUpload(event.target.files[0])} onChange={(event) => handleImageUpload(event.target.files[0])}
accept="image/*" accept="image/*"
/> />
@ -322,7 +346,7 @@ const extensions = [
}, },
}), }),
Placeholder.configure({ Placeholder.configure({
placeholder: "Start typing here...", placeholder: 'Start typing here...',
}), }),
ImageResize, ImageResize,
]; ];
@ -340,12 +364,13 @@ export default ({
overrideMobile, overrideMobile,
customEditorHeight, customEditorHeight,
membersWithNames, membersWithNames,
enableMentions enableMentions,
}) => { }) => {
const [isDisabledEditorEnter, setIsDisabledEditorEnter] = useRecoilState(isDisabledEditorEnterAtom) const [isDisabledEditorEnter, setIsDisabledEditorEnter] = useRecoilState(
isDisabledEditorEnterAtom
);
const extensionsFiltered = isChat const extensionsFiltered = isChat
? extensions.filter((item) => item?.name !== "image") ? extensions.filter((item) => item?.name !== 'image')
: extensions; : extensions;
const editorRef = useRef(null); const editorRef = useRef(null);
const setEditorRefFunc = (editorInstance) => { const setEditorRefFunc = (editorInstance) => {
@ -359,20 +384,14 @@ export default ({
// { id: 3, label: 'Charlie' }, // { id: 3, label: 'Charlie' },
// ]; // ];
const users = useMemo(() => {
return (membersWithNames || [])?.map((item) => {
const users = useMemo(()=> {
return (membersWithNames || [])?.map((item)=> {
return { return {
id: item, id: item,
label: item label: item,
} };
}) });
}, [membersWithNames]) }, [membersWithNames]);
const usersRef = useRef([]); const usersRef = useRef([]);
useEffect(() => { useEffect(() => {
@ -386,13 +405,13 @@ export default ({
const handleBlur = () => { const handleBlur = () => {
const htmlContent = editorRef.current.getHTML(); const htmlContent = editorRef.current.getHTML();
if (!htmlContent?.trim() || htmlContent?.trim() === "<p></p>") { if (!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') {
// Set focus state based on content // Set focus state based on content
} }
}; };
const additionalExtensions = useMemo(()=> { const additionalExtensions = useMemo(() => {
if(!enableMentions) return [] if (!enableMentions) return [];
return [ return [
Mention.configure({ Mention.configure({
HTMLAttributes: { HTMLAttributes: {
@ -409,122 +428,129 @@ export default ({
let popup; // Reference to the Tippy.js instance let popup; // Reference to the Tippy.js instance
let component; let component;
return { return {
onStart: props => { onStart: (props) => {
component = new ReactRenderer(MentionList, { component = new ReactRenderer(MentionList, {
props, props,
editor: props.editor, editor: props.editor,
}) });
if (!props.clientRect) { if (!props.clientRect) {
return return;
} }
popup = tippy('body', { popup = tippy('body', {
getReferenceClientRect: props.clientRect, getReferenceClientRect: props.clientRect,
appendTo: () => document.body, appendTo: () => document.body,
content: component.element, content: component.element,
showOnCreate: true, showOnCreate: true,
interactive: true, interactive: true,
trigger: 'manual', trigger: 'manual',
placement: 'bottom-start', placement: 'bottom-start',
}) });
}, },
onUpdate(props) { onUpdate(props) {
component.updateProps(props) component.updateProps(props);
if (!props.clientRect) { if (!props.clientRect) {
return return;
} }
popup[0].setProps({ popup[0].setProps({
getReferenceClientRect: props.clientRect, getReferenceClientRect: props.clientRect,
}) });
}, },
onKeyDown(props) { onKeyDown(props) {
if (props.event.key === 'Escape') { if (props.event.key === 'Escape') {
popup[0].hide() popup[0].hide();
return true return true;
} }
return component.ref?.onKeyDown(props) return component.ref?.onKeyDown(props);
}, },
onExit() { onExit() {
popup[0].destroy() popup[0].destroy();
component.destroy() component.destroy();
}, },
} };
}, },
}, },
}) }),
] ];
}, [enableMentions]) }, [enableMentions]);
const handleSetIsDisabledEditorEnter = useCallback((val)=> { const handleSetIsDisabledEditorEnter = useCallback((val) => {
setIsDisabledEditorEnter(val) setIsDisabledEditorEnter(val);
localStorage.setItem('settings-disable-editor-enter', JSON.stringify(val)); localStorage.setItem('settings-disable-editor-enter', JSON.stringify(val));
}, []);
}, [])
return ( return (
<div style={{ <div
display: 'flex', style={{
flexDirection: 'column', display: 'flex',
justifyContent: 'space-between', flexDirection: 'column',
height: '100%' height: '100%',
}}> justifyContent: 'space-between',
<EditorProvider
slotBefore={
(isFocusedParent || !isMobile || overrideMobile) && (
<MenuBar setEditorRef={setEditorRefFunc} isChat={isChat} isDisabledEditorEnter={isDisabledEditorEnter} setIsDisabledEditorEnter={handleSetIsDisabledEditorEnter} />
)
}
extensions={[...extensionsFiltered, ...additionalExtensions
]}
content={content}
onCreate={({ editor }) => {
editor.on("focus", handleFocus); // Listen for focus event
editor.on("blur", handleBlur); // Listen for blur event
}} }}
onUpdate={({ editor }) => { >
editor.on('focus', handleFocus); // Ensure focus is updated <EditorProvider
editor.on('blur', handleBlur); // Ensure blur is updated slotBefore={
}} (isFocusedParent || !isMobile || overrideMobile) && (
editorProps={{ <MenuBar
attributes: { setEditorRef={setEditorRefFunc}
class: "tiptap-prosemirror", isChat={isChat}
style: isDisabledEditorEnter={isDisabledEditorEnter}
isMobile ? setIsDisabledEditorEnter={handleSetIsDisabledEditorEnter}
`overflow: auto; min-height: ${ />
customEditorHeight ? "200px" : "0px" )
}; max-height:calc(100svh - ${customEditorHeight || "140px"})`: `overflow: auto; max-height: 250px`, }
}, extensions={[...extensionsFiltered, ...additionalExtensions]}
handleKeyDown(view, event) { content={content}
if (!disableEnter && !isDisabledEditorEnter && event.key === "Enter") { onCreate={({ editor }) => {
if (event.shiftKey) { editor.on('focus', handleFocus); // Listen for focus event
view.dispatch( editor.on('blur', handleBlur); // Listen for blur event
view.state.tr.replaceSelectionWith( }}
view.state.schema.nodes.hardBreak.create() onUpdate={({ editor }) => {
) editor.on('focus', handleFocus); // Ensure focus is updated
); editor.on('blur', handleBlur); // Ensure blur is updated
return true; }}
} else { editorProps={{
if (typeof onEnter === "function") { attributes: {
onEnter(); class: 'tiptap-prosemirror',
style: isMobile
? `overflow: auto; min-height: ${
customEditorHeight ? '200px' : '0px'
}; max-height:calc(100svh - ${customEditorHeight || '140px'})`
: `overflow: auto; max-height: 250px`,
},
handleKeyDown(view, event) {
if (
!disableEnter &&
!isDisabledEditorEnter &&
event.key === 'Enter'
) {
if (event.shiftKey) {
view.dispatch(
view.state.tr.replaceSelectionWith(
view.state.schema.nodes.hardBreak.create()
)
);
return true;
} else {
if (typeof onEnter === 'function') {
onEnter();
}
return true;
} }
return true;
} }
} return false;
return false; },
}, }}
}} />
/>
</div> </div>
); );
}; };

View File

@ -1,6 +1,6 @@
.tiptap { .tiptap {
margin-top: 0; margin-top: 0;
color: white; /* Set default font color to white */ color: var(--text-primary);
width: 100%; width: 100%;
} }
@ -26,7 +26,7 @@
line-height: 1.1; line-height: 1.1;
margin-top: 2.5rem; margin-top: 2.5rem;
text-wrap: pretty; text-wrap: pretty;
color: white; /* Ensure heading font color is white */ color: var(--text-primary);
} }
.tiptap h1, .tiptap h1,
@ -55,18 +55,18 @@
/* Code and preformatted text styles */ /* Code and preformatted text styles */
.tiptap code { .tiptap code {
background-color: #27282c; /* Set code background color to #27282c */ background-color: var(--background-default);
border-radius: 0.4rem; border-radius: 0.4rem;
color: white; /* Ensure inline code text color is white */ color: var(--text-primary);
font-size: 0.85rem; font-size: 0.85rem;
padding: 0.25em 0.3em; padding: 0.25em 0.3em;
text-wrap: pretty; text-wrap: pretty;
} }
.tiptap pre { .tiptap pre {
background: #27282c; /* Set code block background color to #27282c */ background: var(--background-default);
border-radius: 0.5rem; border-radius: 0.5rem;
color: white; /* Ensure code block text color is white */ color: var(--text-primary);
font-family: 'JetBrainsMono', monospace; font-family: 'JetBrainsMono', monospace;
margin: 1.5rem 0; margin: 1.5rem 0;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
@ -86,7 +86,7 @@
border-left: 3px solid var(--gray-3); border-left: 3px solid var(--gray-3);
margin: 1.5rem 0; margin: 1.5rem 0;
padding-left: 1rem; padding-left: 1rem;
color: white; /* Ensure blockquote text color is white */ color: var(--text-primary);
text-wrap: pretty; text-wrap: pretty;
} }
@ -102,49 +102,49 @@
.tiptap p { .tiptap p {
font-size: 16px; font-size: 16px;
color: white; /* Ensure paragraph text color is white */ color: var(--text-primary);
margin: 0px; margin: 0px;
} }
.tiptap p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.tiptap p:empty::before { .tiptap p.is-editor-empty:first-child::before {
content: ''; color: var(--text-primary);
display: inline-block; content: attr(data-placeholder);
} float: left;
height: 0;
pointer-events: none;
}
.tiptap p:empty::before {
content: '';
display: inline-block;
}
.tiptap a { .tiptap a {
color: cadetblue color: cadetblue;
} }
.tiptap img { .tiptap img {
display: block; display: block;
max-width: 100%; max-width: 100%;
} }
.isReply p { .isReply p {
font-size: 12px !important; font-size: 12px !important;
} }
.tiptap [data-type="mention"] { .tiptap [data-type='mention'] {
box-decoration-break: clone; box-decoration-break: clone;
color: lightblue; color: var(--text-secondary);
padding: 0.1rem 0.3rem; padding: 0.1rem 0.3rem;
} }
.unread-divider { .unread-divider {
border-bottom: 1px solid var(--text-primary);
border-radius: 2px;
color: var(--text-primary);
display: flex;
justify-content: center;
width: 90%; width: 90%;
color: white;
border-bottom: 1px solid white;
display: flex;
justify-content: center;
border-radius: 2px;
} }
.mention-item { .mention-item {
@ -169,11 +169,10 @@
font-size: 16px; font-size: 16px;
width: 100%; width: 100%;
border: none; border: none;
color: white; color: var(--text-primary);
cursor: pointer;
&:hover, &:hover,
&:hover.is-selected { &:hover.is-selected {
background-color: gray; background-color: var(--background-default);
} }
} }
} }

View File

@ -73,6 +73,7 @@ export const CoreSyncStatus = () => {
let imagePath = syncingImg; let imagePath = syncingImg;
let message = `Synchronizing`; let message = `Synchronizing`;
if (isMintingPossible && !isUsingGateway) { if (isMintingPossible && !isUsingGateway) {
imagePath = syncedMintingImg; imagePath = syncedMintingImg;
message = `${isSynchronizing ? 'Synchronizing' : 'Synchronized'} ${'(Minting)'}`; message = `${isSynchronizing ? 'Synchronizing' : 'Synchronized'} ${'(Minting)'}`;
@ -101,15 +102,17 @@ export const CoreSyncStatus = () => {
<span> <span>
<img <img
src={imagePath} src={imagePath}
style={{ height: 'auto', width: '24px' }} style={{ height: 'auto', width: '35px' }}
alt="sync status" alt="sync status"
/> />
</span> </span>
<div <div
className="bottom" className="bottom"
style={{ style={{
right: 'unset', right: 'unset',
left: '0px', left: '55px',
top: '10px',
}} }}
> >
<h3>Core Information</h3> <h3>Core Information</h3>
@ -134,7 +137,6 @@ export const CoreSyncStatus = () => {
{isUsingGateway?.toString()} {isUsingGateway?.toString()}
</span> </span>
</h4> </h4>
<i></i>
</div> </div>
</div> </div>
); );

View File

@ -22,27 +22,27 @@ export const IconWrapper = ({
return ( return (
<Box <Box
sx={{ sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
gap: '5px',
flexDirection: 'column',
height: customWidth ? customWidth : disableWidth ? 'auto' : '89px',
width: customWidth ? customWidth : disableWidth ? 'auto' : '89px',
borderRadius: '50%',
backgroundColor: selected backgroundColor: selected
? theme.palette.background.default ? theme.palette.background.default
: 'transparent', : 'transparent',
borderRadius: '50%',
color: color ? color : theme.palette.text.primary, color: color ? color : theme.palette.text.primary,
display: 'flex',
flexDirection: 'column',
gap: '5px',
height: customWidth ? customWidth : disableWidth ? 'auto' : '89px',
justifyContent: 'center',
width: customWidth ? customWidth : disableWidth ? 'auto' : '89px',
}} }}
> >
{children} {children}
<Typography <Typography
sx={{ sx={{
color: theme.palette.text.primary,
fontFamily: 'Inter', fontFamily: 'Inter',
fontSize: '12px', fontSize: '12px',
fontWeight: 500, fontWeight: 500,
color: theme.palette.text.primary,
}} }}
> >
{label} {label}
@ -68,18 +68,20 @@ export const DesktopFooter = ({
const [isEnabledDevMode, setIsEnabledDevMode] = const [isEnabledDevMode, setIsEnabledDevMode] =
useRecoilState(enabledDevModeAtom); useRecoilState(enabledDevModeAtom);
const theme = useTheme();
if (hide) return; if (hide) return;
return ( return (
<Box <Box
sx={{ sx={{
width: '100%', alignItems: 'center',
position: 'absolute',
bottom: 0, bottom: 0,
display: 'flex', display: 'flex',
alignItems: 'center',
height: '100px', // Footer height height: '100px', // Footer height
zIndex: 1,
justifyContent: 'center', justifyContent: 'center',
position: 'absolute',
width: '100%',
zIndex: 1,
}} }}
> >
<Box <Box
@ -122,8 +124,8 @@ export const DesktopFooter = ({
hasUnreadGroups hasUnreadGroups
? 'var(--danger)' ? 'var(--danger)'
: isGroups : isGroups
? 'white' ? theme.palette.text.primary
: 'rgba(250, 250, 250, 0.5)' : theme.palette.text.secondary
} }
/> />
</IconWrapper> </IconWrapper>
@ -141,8 +143,8 @@ export const DesktopFooter = ({
hasUnreadDirects hasUnreadDirects
? 'var(--danger)' ? 'var(--danger)'
: isDirects : isDirects
? 'white' ? theme.palette.text.primary
: 'rgba(250, 250, 250, 0.5)' : theme.palette.text.secondary
} }
/> />
</IconWrapper> </IconWrapper>

View File

@ -7,6 +7,7 @@ import { useRecoilState } from 'recoil';
import { enabledDevModeAtom } from '../atoms/global'; import { enabledDevModeAtom } from '../atoms/global';
import { AppsIcon } from '../assets/Icons/AppsIcon'; import { AppsIcon } from '../assets/Icons/AppsIcon';
import ThemeSelector from './Theme/ThemeSelector'; import ThemeSelector from './Theme/ThemeSelector';
import { CoreSyncStatus } from './CoreSyncStatus';
export const DesktopSideBar = ({ export const DesktopSideBar = ({
goToHome, goToHome,
@ -30,19 +31,28 @@ export const DesktopSideBar = ({
return ( return (
<Box <Box
sx={{ sx={{
width: '60px',
flexDirection: 'column',
height: '100vh',
alignItems: 'center', alignItems: 'center',
display: 'flex', display: 'flex',
flexDirection: 'column',
gap: '25px', gap: '25px',
height: '100vh',
width: '60px',
}} }}
> >
<ButtonBase
sx={{
width: '70px',
height: '70px',
paddingTop: '23px',
}}
>
<CoreSyncStatus />
</ButtonBase>
<ButtonBase <ButtonBase
sx={{ sx={{
width: '60px', width: '60px',
height: '60px', height: '60px',
paddingTop: '23px',
}} }}
onClick={() => { onClick={() => {
goToHome(); goToHome();
@ -51,10 +61,13 @@ export const DesktopSideBar = ({
<HomeIcon <HomeIcon
height={34} height={34}
color={ color={
desktopViewMode === 'home' ? 'white' : 'rgba(250, 250, 250, 0.5)' desktopViewMode === 'home'
? theme.palette.text.primary
: theme.palette.text.secondary
} }
/> />
</ButtonBase> </ButtonBase>
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
setDesktopViewMode('apps'); setDesktopViewMode('apps');
@ -63,17 +76,22 @@ export const DesktopSideBar = ({
}} }}
> >
<IconWrapper <IconWrapper
color={isApps ? 'white' : 'rgba(250, 250, 250, 0.5)'} color={
isApps ? theme.palette.text.primary : theme.palette.text.secondary
}
label="Apps" label="Apps"
selected={isApps} selected={isApps}
disableWidth disableWidth
> >
<AppsIcon <AppsIcon
color={isApps ? 'white' : 'rgba(250, 250, 250, 0.5)'} color={
isApps ? theme.palette.text.primary : theme.palette.text.secondary
}
height={30} height={30}
/> />
</IconWrapper> </IconWrapper>
</ButtonBase> </ButtonBase>
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
setDesktopViewMode('chat'); setDesktopViewMode('chat');
@ -98,24 +116,7 @@ export const DesktopSideBar = ({
/> />
</IconWrapper> </IconWrapper>
</ButtonBase> </ButtonBase>
{/* <ButtonBase
onClick={() => {
setDesktopSideView("groups");
toggleSideViewGroups()
}}
>
<HubsIcon
height={30}
color={
hasUnreadGroups
? "var(--danger)"
: isGroups
? "white"
: "rgba(250, 250, 250, 0.5)"
}
/>
</ButtonBase> */}
<Save isDesktop disableWidth myName={myName} /> <Save isDesktop disableWidth myName={myName} />
{/* <CoreSyncStatus imageSize="30px" position="left" /> */} {/* <CoreSyncStatus imageSize="30px" position="left" /> */}
{isEnabledDevMode && ( {isEnabledDevMode && (
@ -126,7 +127,9 @@ export const DesktopSideBar = ({
> >
<IconWrapper <IconWrapper
color={ color={
desktopViewMode === 'dev' ? 'white' : 'rgba(250, 250, 250, 0.5)' desktopViewMode === 'dev'
? theme.palette.text.primary
: theme.palette.text.secondary
} }
label="Dev" label="Dev"
disableWidth disableWidth

View File

@ -1,32 +1,21 @@
import * as React from 'react';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import Drawer from '@mui/material/Drawer'; import Drawer from '@mui/material/Drawer';
import Button from '@mui/material/Button';
import List from '@mui/material/List';
import Divider from '@mui/material/Divider';
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 InboxIcon from '@mui/icons-material/MoveToInbox';
import MailIcon from '@mui/icons-material/Mail';
import CloseIcon from '@mui/icons-material/Close';
import { isMobile } from '../../App'; import { isMobile } from '../../App';
export const DrawerComponent = ({open, setOpen, children}) => { export const DrawerComponent = ({ open, setOpen, children }) => {
const toggleDrawer = (newOpen: boolean) => () => { const toggleDrawer = (newOpen: boolean) => () => {
setOpen(newOpen); setOpen(newOpen);
}; };
return ( return (
<div> <div>
<Drawer open={open} onClose={toggleDrawer(false)}> <Drawer open={open} onClose={toggleDrawer(false)}>
<Box sx={{ width: isMobile ? '100vw' : '400px', height: '100%' }} role="presentation"> <Box
sx={{ width: isMobile ? '100vw' : '400px', height: '100%' }}
{children} role="presentation"
</Box> >
{children}
</Box>
</Drawer> </Drawer>
</div> </div>
); );
} };

View File

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

View File

@ -1,42 +1,46 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from 'react';
import { getBaseApiReact } from "../../App"; import { getBaseApiReact } from '../../App';
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { extractComponents } from '../Chat/MessageDisplay';
import { executeEvent } from '../../utils/events';
import { extractComponents } from "../Chat/MessageDisplay"; import { base64ToBlobUrl } from '../../utils/fileReading';
import { executeEvent } from "../../utils/events"; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import {
import { base64ToBlobUrl } from "../../utils/fileReading"; blobControllerAtom,
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; blobKeySelector,
import { blobControllerAtom, blobKeySelector, resourceKeySelector, selectedGroupIdAtom } from "../../atoms/global"; resourceKeySelector,
import { parseQortalLink } from "./embed-utils"; selectedGroupIdAtom,
import { PollCard } from "./PollEmbed"; } from '../../atoms/global';
import { ImageCard } from "./ImageEmbed"; import { parseQortalLink } from './embed-utils';
import { AttachmentCard } from "./AttachmentEmbed"; import { PollCard } from './PollEmbed';
import { decodeIfEncoded } from "../../utils/decode"; import { ImageCard } from './ImageEmbed';
import { AttachmentCard } from './AttachmentEmbed';
import { decodeIfEncoded } from '../../utils/decode';
const getPoll = async (name) => { const getPoll = async (name) => {
const pollName = name; const pollName = name;
const url = `${getBaseApiReact()}/polls/${pollName}`; const url = `${getBaseApiReact()}/polls/${pollName}`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
const responseData = await response.json(); const responseData = await response.json();
if (responseData?.message?.includes("POLL_NO_EXISTS")) { if (responseData?.message?.includes('POLL_NO_EXISTS')) {
throw new Error("POLL_NO_EXISTS"); throw new Error('POLL_NO_EXISTS');
} else if (responseData?.pollName) { } else if (responseData?.pollName) {
const urlVotes = `${getBaseApiReact()}/polls/votes/${pollName}`; const urlVotes = `${getBaseApiReact()}/polls/votes/${pollName}`;
const responseVotes = await fetch(urlVotes, { const responseVotes = await fetch(urlVotes, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
@ -49,56 +53,64 @@ const getPoll = async (name) => {
}; };
export const Embed = ({ embedLink }) => { export const Embed = ({ embedLink }) => {
const [errorMsg, setErrorMsg] = useState(""); const [errorMsg, setErrorMsg] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [poll, setPoll] = useState(null); const [poll, setPoll] = useState(null);
const [type, setType] = useState(""); const [type, setType] = useState('');
const hasFetched = useRef(false); const hasFetched = useRef(false);
const [openSnack, setOpenSnack] = useState(false); const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null); const [infoSnack, setInfoSnack] = useState(null);
const [external, setExternal] = useState(null); const [external, setExternal] = useState(null);
const [imageUrl, setImageUrl] = useState(""); const [imageUrl, setImageUrl] = useState('');
const [parsedData, setParsedData] = useState(null); const [parsedData, setParsedData] = useState(null);
const setBlobs = useSetRecoilState(blobControllerAtom); const setBlobs = useSetRecoilState(blobControllerAtom);
const [selectedGroupId] = useRecoilState(selectedGroupIdAtom) const [selectedGroupId] = useRecoilState(selectedGroupIdAtom);
const resourceData = useMemo(()=> { const resourceData = useMemo(() => {
const parsedDataOnTheFly = parseQortalLink(embedLink); const parsedDataOnTheFly = parseQortalLink(embedLink);
if(parsedDataOnTheFly?.service && parsedDataOnTheFly?.name && parsedDataOnTheFly?.identifier){ if (
parsedDataOnTheFly?.service &&
parsedDataOnTheFly?.name &&
parsedDataOnTheFly?.identifier
) {
return { return {
service : parsedDataOnTheFly?.service, service: parsedDataOnTheFly?.service,
name: parsedDataOnTheFly?.name, name: parsedDataOnTheFly?.name,
identifier: parsedDataOnTheFly?.identifier, identifier: parsedDataOnTheFly?.identifier,
fileName: parsedDataOnTheFly?.fileName ? decodeURIComponent(parsedDataOnTheFly?.fileName) : null, fileName: parsedDataOnTheFly?.fileName
mimeType: parsedDataOnTheFly?.mimeType ? decodeURIComponent(parsedDataOnTheFly?.mimeType) : null, ? decodeURIComponent(parsedDataOnTheFly?.fileName)
key: parsedDataOnTheFly?.key ? decodeURIComponent(parsedDataOnTheFly?.key) : null, : null,
} mimeType: parsedDataOnTheFly?.mimeType
? decodeURIComponent(parsedDataOnTheFly?.mimeType)
: null,
key: parsedDataOnTheFly?.key
? decodeURIComponent(parsedDataOnTheFly?.key)
: null,
};
} else { } else {
return null return null;
} }
}, [embedLink]) }, [embedLink]);
const keyIdentifier = useMemo(()=> { const keyIdentifier = useMemo(() => {
if (resourceData) {
if(resourceData){ return `${resourceData.service}-${resourceData.name}-${resourceData.identifier}`;
return `${resourceData.service}-${resourceData.name}-${resourceData.identifier}`
} else { } else {
return undefined return undefined;
} }
}, [resourceData]) }, [resourceData]);
const blobUrl = useRecoilValue(blobKeySelector(keyIdentifier)); const blobUrl = useRecoilValue(blobKeySelector(keyIdentifier));
const handlePoll = async (parsedData) => { const handlePoll = async (parsedData) => {
try { try {
setIsLoading(true); setIsLoading(true);
setErrorMsg(""); setErrorMsg('');
setType("POLL"); setType('POLL');
if (!parsedData?.name) if (!parsedData?.name)
throw new Error("Invalid poll embed link. Missing name."); throw new Error('Invalid poll embed link. Missing name.');
const pollRes = await getPoll(parsedData.name); const pollRes = await getPoll(parsedData.name);
setPoll(pollRes); setPoll(pollRes);
} catch (error) { } catch (error) {
setErrorMsg(error?.message || "Invalid embed link"); setErrorMsg(error?.message || 'Invalid embed link');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -106,8 +118,8 @@ export const Embed = ({ embedLink }) => {
const getImage = async ({ identifier, name, service }, key, parsedData) => { const getImage = async ({ identifier, name, service }, key, parsedData) => {
try { try {
if(blobUrl?.blobUrl){ if (blobUrl?.blobUrl) {
return blobUrl?.blobUrl return blobUrl?.blobUrl;
} }
let numberOfTries = 0; let numberOfTries = 0;
let imageFinalUrl = null; let imageFinalUrl = null;
@ -116,76 +128,76 @@ export const Embed = ({ embedLink }) => {
const urlStatus = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}?build=true`; const urlStatus = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}?build=true`;
const responseStatus = await fetch(urlStatus, { const responseStatus = await fetch(urlStatus, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
const responseData = await responseStatus.json(); const responseData = await responseStatus.json();
if (responseData?.status === "READY") { if (responseData?.status === 'READY') {
if (parsedData?.encryptionType) { if (parsedData?.encryptionType) {
const urlData = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?encoding=base64`; const urlData = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?encoding=base64`;
const responseData = await fetch(urlData, { const responseData = await fetch(urlData, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
const data = await responseData.text(); const data = await responseData.text();
if (data) { if (data) {
let decryptedData let decryptedData;
try { try {
if(key && encryptionType === 'private'){ if (key && encryptionType === 'private') {
decryptedData = await window.sendMessage( decryptedData = await window.sendMessage(
"DECRYPT_DATA_WITH_SHARING_KEY", 'DECRYPT_DATA_WITH_SHARING_KEY',
{ {
encryptedData: data, encryptedData: data,
key: decodeURIComponent(key), key: decodeURIComponent(key),
} }
); );
} }
if(encryptionType === 'group'){ if (encryptionType === 'group') {
decryptedData = await window.sendMessage( decryptedData = await window.sendMessage(
"DECRYPT_QORTAL_GROUP_DATA", 'DECRYPT_QORTAL_GROUP_DATA',
{
data64: data,
groupId: selectedGroupId,
}
);
} {
data64: data,
groupId: selectedGroupId,
}
);
}
} catch (error) { } catch (error) {
throw new Error('Unable to decrypt') throw new Error('Unable to decrypt');
} }
if (!decryptedData || decryptedData?.error) throw new Error("Could not decrypt data"); if (!decryptedData || decryptedData?.error)
imageFinalUrl = base64ToBlobUrl(decryptedData, parsedData?.mimeType ? decodeURIComponent(parsedData?.mimeType) : undefined) throw new Error('Could not decrypt data');
setBlobs((prev=> { imageFinalUrl = base64ToBlobUrl(
decryptedData,
parsedData?.mimeType
? decodeURIComponent(parsedData?.mimeType)
: undefined
);
setBlobs((prev) => {
return { return {
...prev, ...prev,
[`${service}-${name}-${identifier}`]: { [`${service}-${name}-${identifier}`]: {
blobUrl: imageFinalUrl, blobUrl: imageFinalUrl,
timestamp: Date.now() timestamp: Date.now(),
} },
} };
})) });
} else { } else {
throw new Error('No data for image') throw new Error('No data for image');
} }
} else { } else {
imageFinalUrl = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?async=true`; imageFinalUrl = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?async=true`;
// If parsedData is used here, it must be defined somewhere // If parsedData is used here, it must be defined somewhere
}
}
} }
}; };
@ -203,18 +215,19 @@ export const Embed = ({ embedLink }) => {
} }
if (imageFinalUrl) { if (imageFinalUrl) {
return imageFinalUrl; return imageFinalUrl;
} else { } else {
setErrorMsg( setErrorMsg(
"Unable to download IMAGE. Please try again later by clicking the refresh button" 'Unable to download IMAGE. Please try again later by clicking the refresh button'
); );
return null; return null;
} }
} catch (error) { } catch (error) {
console.error("Error fetching image:", error); console.error('Error fetching image:', error);
setErrorMsg( setErrorMsg(
error?.error || error?.message || "An unexpected error occurred while trying to download the image" error?.error ||
error?.message ||
'An unexpected error occurred while trying to download the image'
); );
return null; return null;
} }
@ -223,25 +236,27 @@ export const Embed = ({ embedLink }) => {
const handleImage = async (parsedData) => { const handleImage = async (parsedData) => {
try { try {
setIsLoading(true); setIsLoading(true);
setErrorMsg(""); setErrorMsg('');
if (!parsedData?.name || !parsedData?.service || !parsedData?.identifier) if (!parsedData?.name || !parsedData?.service || !parsedData?.identifier)
throw new Error("Invalid image embed link. Missing param."); throw new Error('Invalid image embed link. Missing param.');
let image = await getImage({ let image = await getImage(
name: parsedData.name, {
service: parsedData.service, name: parsedData.name,
identifier: parsedData?.identifier, service: parsedData.service,
}, parsedData?.key, parsedData); identifier: parsedData?.identifier,
},
setImageUrl(image); parsedData?.key,
parsedData
);
setImageUrl(image);
} catch (error) { } catch (error) {
setErrorMsg(error?.message || "Invalid embed link"); setErrorMsg(error?.message || 'Invalid embed link');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
const handleLink = () => { const handleLink = () => {
try { try {
const parsedData = parseQortalLink(embedLink); const parsedData = parseQortalLink(embedLink);
@ -254,28 +269,26 @@ export const Embed = ({ embedLink }) => {
setExternal(res); setExternal(res);
} }
} }
} catch (error) { } catch (error) {}
}
switch (type) { switch (type) {
case "POLL": case 'POLL':
{ {
handlePoll(parsedData); handlePoll(parsedData);
} }
break; break;
case "IMAGE": case 'IMAGE':
setType("IMAGE"); setType('IMAGE');
break;
case 'ATTACHMENT':
setType('ATTACHMENT');
break; break;
case "ATTACHMENT":
setType("ATTACHMENT");
break;
default: default:
break; break;
} }
} catch (error) { } catch (error) {
setErrorMsg(error?.message || "Invalid embed link"); setErrorMsg(error?.message || 'Invalid embed link');
} }
}; };
@ -284,13 +297,13 @@ export const Embed = ({ embedLink }) => {
const parsedData = parseQortalLink(embedLink); const parsedData = parseQortalLink(embedLink);
handleImage(parsedData); handleImage(parsedData);
} catch (error) { } catch (error) {
setErrorMsg(error?.message || "Invalid embed link"); setErrorMsg(error?.message || 'Invalid embed link');
} }
}; };
const openExternal = () => { const openExternal = () => {
executeEvent("addTab", { data: external }); executeEvent('addTab', { data: external });
executeEvent("open-apps-mode", {}); executeEvent('open-apps-mode', {});
}; };
useEffect(() => { useEffect(() => {
@ -299,8 +312,6 @@ export const Embed = ({ embedLink }) => {
hasFetched.current = true; hasFetched.current = true;
}, [embedLink]); }, [embedLink]);
const resourceDetails = useRecoilValue(resourceKeySelector(keyIdentifier)); const resourceDetails = useRecoilValue(resourceKeySelector(keyIdentifier));
const { parsedType, encryptionType } = useMemo(() => { const { parsedType, encryptionType } = useMemo(() => {
@ -312,15 +323,17 @@ export const Embed = ({ embedLink }) => {
parsedType = parsedDataOnTheFly.type; parsedType = parsedDataOnTheFly.type;
} }
if (parsedDataOnTheFly?.encryptionType) { if (parsedDataOnTheFly?.encryptionType) {
encryptionType = parsedDataOnTheFly?.encryptionType encryptionType = parsedDataOnTheFly?.encryptionType;
} }
} catch (error) {} } catch (error) {
console.log(error);
}
return { parsedType, encryptionType }; return { parsedType, encryptionType };
}, [embedLink]); }, [embedLink]);
return ( return (
<div> <div>
{parsedType === "POLL" && ( {parsedType === 'POLL' && (
<PollCard <PollCard
poll={poll} poll={poll}
refresh={handleLink} refresh={handleLink}
@ -332,7 +345,7 @@ export const Embed = ({ embedLink }) => {
errorMsg={errorMsg} errorMsg={errorMsg}
/> />
)} )}
{parsedType === "IMAGE" && ( {parsedType === 'IMAGE' && (
<ImageCard <ImageCard
image={imageUrl} image={imageUrl}
owner={parsedData?.name} owner={parsedData?.name}
@ -349,8 +362,8 @@ export const Embed = ({ embedLink }) => {
)} )}
{parsedType === 'ATTACHMENT' && ( {parsedType === 'ATTACHMENT' && (
<AttachmentCard <AttachmentCard
resourceData={resourceData} resourceData={resourceData}
resourceDetails={resourceDetails} resourceDetails={resourceDetails}
owner={parsedData?.name} owner={parsedData?.name}
refresh={fetchImage} refresh={fetchImage}
setInfoSnack={setInfoSnack} setInfoSnack={setInfoSnack}
@ -373,11 +386,3 @@ export const Embed = ({ embedLink }) => {
</div> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
import React, { useContext, useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from 'react';
import { MyContext } from "../../App"; import { MyContext } from '../../App';
import { import {
Card, Card,
CardContent, CardContent,
@ -12,384 +12,389 @@ import {
Box, Box,
ButtonBase, ButtonBase,
Divider, Divider,
} from '@mui/material';
} from "@mui/material"; import { getNameInfo } from '../Group/Group';
import { getNameInfo } from "../Group/Group"; import PollIcon from '@mui/icons-material/Poll';
import PollIcon from "@mui/icons-material/Poll"; import { getFee } from '../../background';
import { getFee } from "../../background"; import RefreshIcon from '@mui/icons-material/Refresh';
import RefreshIcon from "@mui/icons-material/Refresh"; import { Spacer } from '../../common/Spacer';
import { Spacer } from "../../common/Spacer"; import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import { CustomLoader } from '../../common/CustomLoader';
import { CustomLoader } from "../../common/CustomLoader";
export const PollCard = ({ export const PollCard = ({
poll, poll,
setInfoSnack, setInfoSnack,
setOpenSnack, setOpenSnack,
refresh, refresh,
openExternal, openExternal,
external, external,
isLoadingParent, isLoadingParent,
errorMsg, errorMsg,
}) => { }) => {
const [selectedOption, setSelectedOption] = useState(""); const [selectedOption, setSelectedOption] = useState('');
const [ownerName, setOwnerName] = useState(""); const [ownerName, setOwnerName] = useState('');
const [showResults, setShowResults] = useState(false); const [showResults, setShowResults] = useState(false);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { show, userInfo } = useContext(MyContext); const { show, userInfo } = useContext(MyContext);
const [isLoadingSubmit, setIsLoadingSubmit] = useState(false); const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
const handleVote = async () => { const handleVote = async () => {
const fee = await getFee("VOTE_ON_POLL"); const fee = await getFee('VOTE_ON_POLL');
await show({ await show({
message: `Do you accept this VOTE_ON_POLL transaction? POLLS are public!`, message: `Do you accept this VOTE_ON_POLL transaction? POLLS are public!`,
publishFee: fee.fee + " QORT", publishFee: fee.fee + ' QORT',
}); });
setIsLoadingSubmit(true); setIsLoadingSubmit(true);
window window
.sendMessage( .sendMessage(
"voteOnPoll", 'voteOnPoll',
{ {
pollName: poll?.info?.pollName, pollName: poll?.info?.pollName,
optionIndex: +selectedOption, optionIndex: +selectedOption,
}, },
60000 60000
) )
.then((response) => { .then((response) => {
setIsLoadingSubmit(false); setIsLoadingSubmit(false);
if (response.error) { if (response.error) {
setInfoSnack({
type: "error",
message: response?.error || "Unable to vote.",
});
setOpenSnack(true);
return;
} else {
setInfoSnack({
type: "success",
message:
"Successfully voted. Please wait a couple minutes for the network to propogate the changes.",
});
setOpenSnack(true);
}
})
.catch((error) => {
setIsLoadingSubmit(false);
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: error?.message || "Unable to vote.", message: response?.error || 'Unable to vote.',
});
setOpenSnack(true);
return;
} else {
setInfoSnack({
type: 'success',
message:
'Successfully voted. Please wait a couple minutes for the network to propogate the changes.',
}); });
setOpenSnack(true); setOpenSnack(true);
});
};
const getName = async (owner) => {
try {
const res = await getNameInfo(owner);
if (res) {
setOwnerName(res);
} }
} catch (error) {} })
}; .catch((error) => {
setIsLoadingSubmit(false);
useEffect(() => { setInfoSnack({
if (poll?.info?.owner) { type: 'error',
getName(poll.info.owner); message: error?.message || 'Unable to vote.',
});
setOpenSnack(true);
});
};
const getName = async (owner) => {
try {
const res = await getNameInfo(owner);
if (res) {
setOwnerName(res);
} }
}, [poll?.info?.owner]); } catch (error) {
console.log(error);
return ( }
<Card };
useEffect(() => {
if (poll?.info?.owner) {
getName(poll.info.owner);
}
}, [poll?.info?.owner]);
return (
<Card
sx={{
backgroundColor: '#1F2023',
height: isOpen ? 'auto' : '150px',
}}
>
<Box
sx={{ sx={{
backgroundColor: "#1F2023", display: 'flex',
height: isOpen ? "auto" : "150px", alignItems: 'center',
justifyContent: 'space-between',
padding: '16px 16px 0px 16px',
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
justifyContent: "space-between", gap: '10px',
padding: "16px 16px 0px 16px",
}} }}
> >
<Box <PollIcon
sx={{ sx={{
display: "flex", color: 'white',
alignItems: "center",
gap: "10px",
}} }}
> />
<PollIcon <Typography>POLL embed</Typography>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<ButtonBase>
<RefreshIcon
onClick={refresh}
sx={{ sx={{
color: "white", fontSize: '24px',
color: 'white',
}} }}
/> />
<Typography>POLL embed</Typography> </ButtonBase>
</Box> {external && (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
<ButtonBase> <ButtonBase>
<RefreshIcon <OpenInNewIcon
onClick={refresh} onClick={openExternal}
sx={{ sx={{
fontSize: "24px", fontSize: '24px',
color: "white", color: 'white',
}} }}
/> />
</ButtonBase> </ButtonBase>
{external && ( )}
<ButtonBase>
<OpenInNewIcon
onClick={openExternal}
sx={{
fontSize: "24px",
color: "white",
}}
/>
</ButtonBase>
)}
</Box>
</Box> </Box>
<Box </Box>
<Box
sx={{
padding: '8px 16px 8px 16px',
}}
>
<Typography
sx={{ sx={{
padding: "8px 16px 8px 16px", fontSize: '12px',
}} }}
> >
<Typography Created by {ownerName || poll?.info?.owner}
</Typography>
</Box>
<Divider sx={{ borderColor: 'rgb(255 255 255 / 10%)' }} />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
alignItems: 'center',
}}
>
{!isOpen && !errorMsg && (
<>
<Spacer height="5px" />
<Button
size="small"
variant="contained"
sx={{
backgroundColor: 'var(--green)',
}}
onClick={() => {
setIsOpen(true);
}}
>
Show poll
</Button>
</>
)}
{isLoadingParent && isOpen && (
<Box
sx={{ sx={{
fontSize: "12px", width: '100%',
display: 'flex',
justifyContent: 'center',
}} }}
> >
Created by {ownerName || poll?.info?.owner} {' '}
</Typography> <CustomLoader />{' '}
</Box> </Box>
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} /> )}
<Box {errorMsg && (
sx={{ <Box
display: "flex",
flexDirection: "column",
width: "100%",
alignItems: "center",
}}
>
{!isOpen && !errorMsg && (
<>
<Spacer height="5px" />
<Button
size="small"
variant="contained"
sx={{
backgroundColor: "var(--green)",
}}
onClick={() => {
setIsOpen(true);
}}
>
Show poll
</Button>
</>
)}
{isLoadingParent && isOpen && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{" "}
<CustomLoader />{" "}
</Box>
)}
{errorMsg && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{" "}
<Typography
sx={{
fontSize: "14px",
color: "var(--danger)",
}}
>
{errorMsg}
</Typography>{" "}
</Box>
)}
</Box>
<Box
sx={{
display: isOpen ? "block" : "none",
}}
>
<CardHeader
title={poll?.info?.pollName}
subheader={poll?.info?.description}
sx={{ sx={{
"& .MuiCardHeader-title": { width: '100%',
fontSize: "18px", // Custom font size for title display: 'flex',
}, justifyContent: 'center',
}} }}
/> >
<CardContent> {' '}
<Typography <Typography
sx={{ sx={{
fontSize: "18px", fontSize: '14px',
color: 'var(--danger)',
}} }}
> >
Options {errorMsg}
</Typography> </Typography>{' '}
<RadioGroup </Box>
value={selectedOption} )}
onChange={(e) => setSelectedOption(e.target.value)} </Box>
>
{poll?.info?.pollOptions?.map((option, index) => ( <Box
<FormControlLabel sx={{
key={index} display: isOpen ? 'block' : 'none',
value={index} }}
control={ >
<Radio <CardHeader
sx={{ title={poll?.info?.pollName}
color: "white", // Unchecked color subheader={poll?.info?.description}
"&.Mui-checked": { sx={{
color: "var(--green)", // Checked color '& .MuiCardHeader-title': {
}, fontSize: '18px', // Custom font size for title
}} },
/> }}
} />
label={option?.optionName} <CardContent>
sx={{ <Typography
"& .MuiFormControlLabel-label": { sx={{
fontSize: "14px", fontSize: '18px',
}}
}, >
}} Options
/> </Typography>
))} <RadioGroup
</RadioGroup> value={selectedOption}
<Box onChange={(e) => setSelectedOption(e.target.value)}
sx={{ >
display: "flex", {poll?.info?.pollOptions?.map((option, index) => (
alignItems: "center", <FormControlLabel
gap: "20px", key={index}
}} value={index}
> control={
<Button <Radio
variant="contained" sx={{
color="primary" color: 'white', // Unchecked color
disabled={!selectedOption || isLoadingSubmit} '&.Mui-checked': {
onClick={handleVote} color: 'var(--green)', // Checked color
> },
Vote }}
</Button> />
<Typography }
label={option?.optionName}
sx={{ sx={{
fontSize: "14px", '& .MuiFormControlLabel-label': {
fontStyle: "italic", fontSize: '14px',
},
}}
/>
))}
</RadioGroup>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '20px',
}}
>
<Button
variant="contained"
color="primary"
disabled={!selectedOption || isLoadingSubmit}
onClick={handleVote}
>
Vote
</Button>
<Typography
sx={{
fontSize: '14px',
fontStyle: 'italic',
}}
>
{' '}
{`${poll?.votes?.totalVotes} ${
poll?.votes?.totalVotes === 1 ? ' vote' : ' votes'
}`}
</Typography>
</Box>
<Spacer height="10px" />
<Typography
sx={{
fontSize: '14px',
visibility: poll?.votes?.votes?.find(
(item) => item?.voterPublicKey === userInfo?.publicKey
)
? 'visible'
: 'hidden',
}}
>
You've already voted.
</Typography>
<Spacer height="10px" />
{isLoadingSubmit && (
<Typography
sx={{
fontSize: '12px',
}}
>
Is processing transaction, please wait...
</Typography>
)}
<ButtonBase
onClick={() => {
setShowResults((prev) => !prev);
}}
>
{showResults ? 'hide ' : 'show '} results
</ButtonBase>
</CardContent>
{showResults && <PollResults votes={poll?.votes} />}
</Box>
</Card>
);
};
const PollResults = ({ votes }) => {
const maxVotes = Math.max(
...votes?.voteCounts?.map((option) => option.voteCount)
);
const options = votes?.voteCounts;
return (
<Box sx={{ width: '100%', p: 2 }}>
{options
.sort((a, b) => b.voteCount - a.voteCount) // Sort options by votes (highest first)
.map((option, index) => (
<Box key={index} sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography
variant="body1"
sx={{
fontWeight: index === 0 ? 'bold' : 'normal',
fontSize: '14px',
}} }}
> >
{" "} {`${index + 1}. ${option.optionName}`}
{`${poll?.votes?.totalVotes} ${ </Typography>
poll?.votes?.totalVotes === 1 ? " vote" : " votes" <Typography
}`} variant="body1"
sx={{
fontWeight: index === 0 ? 'bold' : 'normal',
fontSize: '14px',
}}
>
{option.voteCount} votes
</Typography> </Typography>
</Box> </Box>
<Box
<Spacer height="10px" />
<Typography
sx={{ sx={{
fontSize: "14px", mt: 1,
visibility: poll?.votes?.votes?.find( height: 10,
(item) => item?.voterPublicKey === userInfo?.publicKey backgroundColor: '#e0e0e0',
) borderRadius: 5,
? "visible" overflow: 'hidden',
: "hidden",
}} }}
> >
You've already voted.
</Typography>
<Spacer height="10px" />
{isLoadingSubmit && (
<Typography
sx={{
fontSize: "12px",
}}
>
Is processing transaction, please wait...
</Typography>
)}
<ButtonBase
onClick={() => {
setShowResults((prev) => !prev);
}}
>
{showResults ? "hide " : "show "} results
</ButtonBase>
</CardContent>
{showResults && <PollResults votes={poll?.votes} />}
</Box>
</Card>
);
};
const PollResults = ({ votes }) => {
const maxVotes = Math.max(
...votes?.voteCounts?.map((option) => option.voteCount)
);
const options = votes?.voteCounts;
return (
<Box sx={{ width: "100%", p: 2 }}>
{options
.sort((a, b) => b.voteCount - a.voteCount) // Sort options by votes (highest first)
.map((option, index) => (
<Box key={index} sx={{ mb: 2 }}>
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Typography
variant="body1"
sx={{ fontWeight: index === 0 ? "bold" : "normal" , fontSize: "14px"}}
>
{`${index + 1}. ${option.optionName}`}
</Typography>
<Typography
variant="body1"
sx={{ fontWeight: index === 0 ? "bold" : "normal" , fontSize: "14px"}}
>
{option.voteCount} votes
</Typography>
</Box>
<Box <Box
sx={{ sx={{
mt: 1, width: `${(option.voteCount / maxVotes) * 100}%`,
height: 10, height: '100%',
backgroundColor: "#e0e0e0", backgroundColor: index === 0 ? '#3f51b5' : '#f50057',
borderRadius: 5, transition: 'width 0.3s ease-in-out',
overflow: "hidden",
}} }}
> />
<Box
sx={{
width: `${(option.voteCount / maxVotes) * 100}%`,
height: "100%",
backgroundColor: index === 0 ? "#3f51b5" : "#f50057",
transition: "width 0.3s ease-in-out",
}}
/>
</Box>
</Box> </Box>
))} </Box>
</Box> ))}
); </Box>
}; );
};

View File

@ -1,19 +1,15 @@
import * as React from "react"; import * as React from 'react';
import Button from "@mui/material/Button"; import Button from '@mui/material/Button';
import Dialog from "@mui/material/Dialog"; import Dialog from '@mui/material/Dialog';
import ListItemText from "@mui/material/ListItemText"; import AppBar from '@mui/material/AppBar';
import ListItemButton from "@mui/material/ListItemButton"; import Toolbar from '@mui/material/Toolbar';
import List from "@mui/material/List"; import IconButton from '@mui/material/IconButton';
import Divider from "@mui/material/Divider"; import Typography from '@mui/material/Typography';
import AppBar from "@mui/material/AppBar"; import CloseIcon from '@mui/icons-material/Close';
import Toolbar from "@mui/material/Toolbar"; import ExpandLess from '@mui/icons-material/ExpandLess';
import IconButton from "@mui/material/IconButton"; import ExpandMore from '@mui/icons-material/ExpandMore';
import Typography from "@mui/material/Typography"; import Slide from '@mui/material/Slide';
import CloseIcon from "@mui/icons-material/Close"; import { TransitionProps } from '@mui/material/transitions';
import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore";
import Slide from "@mui/material/Slide";
import { TransitionProps } from "@mui/material/transitions";
import { import {
Box, Box,
Collapse, Collapse,
@ -24,23 +20,23 @@ import {
Tab, Tab,
Tabs, Tabs,
styled, styled,
} from "@mui/material"; useTheme,
import { AddGroupList } from "./AddGroupList"; } from '@mui/material';
import { UserListOfInvites } from "./UserListOfInvites"; import { AddGroupList } from './AddGroupList';
import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { UserListOfInvites } from './UserListOfInvites';
import { getFee } from "../../background"; import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { MyContext, isMobile } from "../../App"; import { getFee } from '../../background';
import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; import { MyContext, isMobile } from '../../App';
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
export const Label = styled("label")( export const Label = styled('label')`
({ theme }) => ` display: block;
font-family: 'IBM Plex Sans', sans-serif; font-family: 'IBM Plex Sans', sans-serif;
font-size: 14px; font-size: 14px;
display: block;
margin-bottom: 4px;
font-weight: 400; font-weight: 400;
` margin-bottom: 4px;
); `;
const Transition = React.forwardRef(function Transition( const Transition = React.forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {
children: React.ReactElement; children: React.ReactElement;
@ -51,17 +47,15 @@ const Transition = React.forwardRef(function Transition(
}); });
export const AddGroup = ({ address, open, setOpen }) => { export const AddGroup = ({ address, open, setOpen }) => {
const {show, setTxList} = React.useContext(MyContext) const { show, setTxList } = React.useContext(MyContext);
const [tab, setTab] = React.useState('create');
const [tab, setTab] = React.useState("create");
const [openAdvance, setOpenAdvance] = React.useState(false); const [openAdvance, setOpenAdvance] = React.useState(false);
const [name, setName] = React.useState('');
const [name, setName] = React.useState(""); const [description, setDescription] = React.useState('');
const [description, setDescription] = React.useState(""); const [groupType, setGroupType] = React.useState('1');
const [groupType, setGroupType] = React.useState("1"); const [approvalThreshold, setApprovalThreshold] = React.useState('40');
const [approvalThreshold, setApprovalThreshold] = React.useState("40"); const [minBlock, setMinBlock] = React.useState('5');
const [minBlock, setMinBlock] = React.useState("5"); const [maxBlock, setMaxBlock] = React.useState('21600');
const [maxBlock, setMaxBlock] = React.useState("21600");
const [value, setValue] = React.useState(0); const [value, setValue] = React.useState(0);
const [openSnack, setOpenSnack] = React.useState(false); const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null); const [infoSnack, setInfoSnack] = React.useState(null);
@ -69,6 +63,7 @@ export const AddGroup = ({ address, open, setOpen }) => {
const handleChange = (event: React.SyntheticEvent, newValue: number) => { const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValue(newValue); setValue(newValue);
}; };
const handleClose = () => { const handleClose = () => {
setOpen(false); setOpen(false);
}; };
@ -89,58 +84,59 @@ export const AddGroup = ({ address, open, setOpen }) => {
setMaxBlock(event.target.value as string); setMaxBlock(event.target.value as string);
}; };
const theme = useTheme();
const handleCreateGroup = async () => { const handleCreateGroup = async () => {
try { try {
if(!name) throw new Error('Please provide a name') if (!name) throw new Error('Please provide a name');
if(!description) throw new Error('Please provide a description') if (!description) throw new Error('Please provide a description');
const fee = await getFee('CREATE_GROUP') const fee = await getFee('CREATE_GROUP');
await show({ await show({
message: "Would you like to perform an CREATE_GROUP transaction?" , message: 'Would you like to perform an CREATE_GROUP transaction?',
publishFee: fee.fee + ' QORT' publishFee: fee.fee + ' QORT',
}) });
await new Promise((res, rej) => { await new Promise((res, rej) => {
window.sendMessage("createGroup", { window
groupName: name, .sendMessage('createGroup', {
groupDescription: description, groupName: name,
groupType: +groupType, groupDescription: description,
groupApprovalThreshold: +approvalThreshold, groupType: +groupType,
minBlock: +minBlock, groupApprovalThreshold: +approvalThreshold,
maxBlock: +maxBlock, minBlock: +minBlock,
}) maxBlock: +maxBlock,
.then((response) => { })
if (!response?.error) { .then((response) => {
setInfoSnack({ if (!response?.error) {
type: "success", setInfoSnack({
message: "Successfully created group. It may take a couple of minutes for the changes to propagate", type: 'success',
}); message:
setOpenSnack(true); 'Successfully created group. It may take a couple of minutes for the changes to propagate',
setTxList((prev) => [ });
{ setOpenSnack(true);
...response, setTxList((prev) => [
type: 'created-group', {
label: `Created group ${name}: awaiting confirmation`, ...response,
labelDone: `Created group ${name}: success!`, type: 'created-group',
done: false, label: `Created group ${name}: awaiting confirmation`,
}, labelDone: `Created group ${name}: success!`,
...prev, done: false,
]); },
res(response); ...prev,
return; ]);
} res(response);
rej({ message: response.error }); return;
}) }
.catch((error) => { rej({ message: response.error });
rej({ message: error.message || "An error occurred" }); })
}); .catch((error) => {
rej({ message: error.message || 'An error occurred' });
});
}); });
} catch (error) { } catch (error) {
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: error?.message, message: error?.message,
}); });
setOpenSnack(true); setOpenSnack(true);
@ -166,20 +162,22 @@ export const AddGroup = ({ address, open, setOpen }) => {
function a11yProps(index: number) { function a11yProps(index: number) {
return { return {
id: `simple-tab-${index}`, id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`, 'aria-controls': `simple-tabpanel-${index}`,
}; };
} }
const openGroupInvitesRequestFunc = () => {
const openGroupInvitesRequestFunc = ()=> { setValue(2);
setValue(2) };
}
React.useEffect(() => { React.useEffect(() => {
subscribeToEvent("openGroupInvitesRequest", openGroupInvitesRequestFunc); subscribeToEvent('openGroupInvitesRequest', openGroupInvitesRequestFunc);
return () => { return () => {
unsubscribeFromEvent("openGroupInvitesRequest", openGroupInvitesRequestFunc); unsubscribeFromEvent(
'openGroupInvitesRequest',
openGroupInvitesRequestFunc
);
}; };
}, []); }, []);
@ -191,17 +189,22 @@ export const AddGroup = ({ address, open, setOpen }) => {
onClose={handleClose} onClose={handleClose}
TransitionComponent={Transition} TransitionComponent={Transition}
> >
<AppBar sx={{ position: "relative", bgcolor: "#232428" }}> <AppBar
sx={{
position: 'relative',
bgcolor: theme.palette.background.default,
}}
>
<Toolbar> <Toolbar>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div"> <Typography sx={{ ml: 2, flex: 1 }} variant="h4" component="div">
Group Mgmt Group Management
</Typography> </Typography>
<IconButton <IconButton
edge="start"
color="inherit"
onClick={handleClose}
aria-label="close" aria-label="close"
color="inherit"
edge="start"
onClick={handleClose}
> >
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
@ -213,275 +216,292 @@ export const AddGroup = ({ address, open, setOpen }) => {
</AppBar> </AppBar>
<Box <Box
sx={{ sx={{
bgcolor: "#27282c", bgcolor: theme.palette.background.default,
flexGrow: 1, color: theme.palette.text.primary,
overflowY: "auto", display: 'flex',
color: "white",
flexDirection: 'column', flexDirection: 'column',
display: 'flex' flexGrow: 1,
overflowY: 'auto',
}} }}
> >
<Box sx={{ borderBottom: 1, borderColor: "divider" }}> <Box
<Tabs sx={{ borderBottom: 1, borderColor: theme.palette.text.secondary }}
value={value} >
onChange={handleChange} <Tabs
aria-label="basic tabs example" value={value}
variant={isMobile ? 'scrollable' : 'fullWidth'} // Scrollable on mobile, full width on desktop onChange={handleChange}
scrollButtons="auto" aria-label="basic tabs example"
allowScrollButtonsMobile variant={isMobile ? 'scrollable' : 'fullWidth'} // Scrollable on mobile, full width on desktop
sx={{ scrollButtons="auto"
"& .MuiTabs-indicator": { allowScrollButtonsMobile
backgroundColor: "white", sx={{
}, '& .MuiTabs-indicator': {
}} backgroundColor: theme.palette.background.default,
> },
<Tab }}
label="Create Group" >
{...a11yProps(0)} <Tab
sx={{ label="Create Group"
"&.Mui-selected": { {...a11yProps(0)}
color: "white", sx={{
}, '&.Mui-selected': {
fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile color: theme.palette.text.primary,
}} },
/> fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile
<Tab }}
label="Find Group" />
{...a11yProps(1)} <Tab
sx={{ label="Find Group"
"&.Mui-selected": { {...a11yProps(1)}
color: "white", sx={{
}, '&.Mui-selected': {
fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile color: theme.palette.text.primary,
}} },
/> fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile
<Tab }}
label="Group Invites" />
{...a11yProps(2)} <Tab
sx={{ label="Group Invites"
"&.Mui-selected": { {...a11yProps(2)}
color: "white", sx={{
}, '&.Mui-selected': {
fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile color: theme.palette.text.primary,
}} },
/> fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile
</Tabs> }}
/>
</Tabs>
</Box> </Box>
{value === 0 && ( {value === 0 && (
<Box sx={{
width: '100%',
padding: '25px'
}}>
<Box <Box
sx={{ sx={{
display: "flex", width: '100%',
flexDirection: "column", padding: '25px',
gap: "20px",
maxWidth: "500px",
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '20px',
maxWidth: '500px',
}} }}
> >
<Label>Name of group</Label>
<Input
placeholder="Name of group"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
<Label>Description of group</Label>
<Input
placeholder="Description of group"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
}}
>
<Label>Group type</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={groupType}
label="Group Type"
onChange={handleChangeGroupType}
>
<MenuItem value={1}>Open (public)</MenuItem>
<MenuItem value={0}>
Closed (private) - users need permission to join
</MenuItem>
</Select>
</Box>
<Box
sx={{
display: "flex",
gap: "15px",
alignItems: "center",
cursor: "pointer",
}}
onClick={() => setOpenAdvance((prev) => !prev)}
>
<Typography>Advanced options</Typography>
{openAdvance ? <ExpandLess /> : <ExpandMore />}
</Box>
<Collapse in={openAdvance} timeout="auto" unmountOnExit>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '5px',
}} }}
> >
<Label> <Label>Name of group</Label>
Group Approval Threshold (number / percentage of Admins that <Input
must approve a transaction) placeholder="Name of group"
</Label> value={name}
onChange={(e) => setName(e.target.value)}
/>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>Description of group</Label>
<Input
placeholder="Description of group"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>Group type</Label>
<Select <Select
labelId="demo-simple-select-label" labelId="demo-simple-select-label"
id="demo-simple-select" id="demo-simple-select"
value={approvalThreshold} value={groupType}
label="Group Approval Threshold" label="Group Type"
onChange={handleChangeApprovalThreshold} onChange={handleChangeGroupType}
> >
<MenuItem value={0}>NONE</MenuItem> <MenuItem value={1}>Open (public)</MenuItem>
<MenuItem value={1}>ONE </MenuItem> <MenuItem value={0}>
Closed (private) - users need permission to join
<MenuItem value={20}>20% </MenuItem> </MenuItem>
<MenuItem value={40}>40% </MenuItem>
<MenuItem value={60}>60% </MenuItem>
<MenuItem value={80}>80% </MenuItem>
<MenuItem value={100}>100% </MenuItem>
</Select> </Select>
</Box> </Box>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", gap: '15px',
gap: "5px", alignItems: 'center',
cursor: 'pointer',
}} }}
onClick={() => setOpenAdvance((prev) => !prev)}
> >
<Label> <Typography>Advanced options</Typography>
Minimum Block delay for Group Transaction Approvals
</Label> {openAdvance ? <ExpandLess /> : <ExpandMore />}
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={minBlock}
label="Minimum Block delay"
onChange={handleChangeMinBlock}
>
<MenuItem value={5}>5 minutes</MenuItem>
<MenuItem value={10}>10 minutes</MenuItem>
<MenuItem value={30}>30 minutes</MenuItem>
<MenuItem value={60}>1 hour</MenuItem>
<MenuItem value={180}>3 hours</MenuItem>
<MenuItem value={300}>5 hours</MenuItem>
<MenuItem value={420}>7 hours</MenuItem>
<MenuItem value={720}>12 hours</MenuItem>
<MenuItem value={1440}>1 day</MenuItem>
<MenuItem value={4320}>3 days</MenuItem>
<MenuItem value={7200}>5 days</MenuItem>
<MenuItem value={10080}>7 days</MenuItem>
</Select>
</Box> </Box>
<Collapse in={openAdvance} timeout="auto" unmountOnExit>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>
Group Approval Threshold (number / percentage of Admins
that must approve a transaction)
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={approvalThreshold}
label="Group Approval Threshold"
onChange={handleChangeApprovalThreshold}
>
<MenuItem value={0}>NONE</MenuItem>
<MenuItem value={1}>ONE </MenuItem>
<MenuItem value={20}>20% </MenuItem>
<MenuItem value={40}>40% </MenuItem>
<MenuItem value={60}>60% </MenuItem>
<MenuItem value={80}>80% </MenuItem>
<MenuItem value={100}>100% </MenuItem>
</Select>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>
Minimum Block delay for Group Transaction Approvals
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={minBlock}
label="Minimum Block delay"
onChange={handleChangeMinBlock}
>
<MenuItem value={5}>5 minutes</MenuItem>
<MenuItem value={10}>10 minutes</MenuItem>
<MenuItem value={30}>30 minutes</MenuItem>
<MenuItem value={60}>1 hour</MenuItem>
<MenuItem value={180}>3 hours</MenuItem>
<MenuItem value={300}>5 hours</MenuItem>
<MenuItem value={420}>7 hours</MenuItem>
<MenuItem value={720}>12 hours</MenuItem>
<MenuItem value={1440}>1 day</MenuItem>
<MenuItem value={4320}>3 days</MenuItem>
<MenuItem value={7200}>5 days</MenuItem>
<MenuItem value={10080}>7 days</MenuItem>
</Select>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>
Maximum Block delay for Group Transaction Approvals
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={maxBlock}
label="Maximum Block delay"
onChange={handleChangeMaxBlock}
>
<MenuItem value={60}>1 hour</MenuItem>
<MenuItem value={180}>3 hours</MenuItem>
<MenuItem value={300}>5 hours</MenuItem>
<MenuItem value={420}>7 hours</MenuItem>
<MenuItem value={720}>12 hours</MenuItem>
<MenuItem value={1440}>1 day</MenuItem>
<MenuItem value={4320}>3 days</MenuItem>
<MenuItem value={7200}>5 days</MenuItem>
<MenuItem value={10080}>7 days</MenuItem>
<MenuItem value={14400}>10 days</MenuItem>
<MenuItem value={21600}>15 days</MenuItem>
</Select>
</Box>
</Collapse>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", width: '100%',
gap: "5px", justifyContent: 'center',
}} }}
> >
<Label> <Button
Maximum Block delay for Group Transaction Approvals variant="contained"
</Label> color="primary"
<Select onClick={handleCreateGroup}
labelId="demo-simple-select-label"
id="demo-simple-select"
value={maxBlock}
label="Maximum Block delay"
onChange={handleChangeMaxBlock}
> >
<MenuItem value={60}>1 hour</MenuItem> Create Group
<MenuItem value={180}>3 hours</MenuItem> </Button>
<MenuItem value={300}>5 hours</MenuItem>
<MenuItem value={420}>7 hours</MenuItem>
<MenuItem value={720}>12 hours</MenuItem>
<MenuItem value={1440}>1 day</MenuItem>
<MenuItem value={4320}>3 days</MenuItem>
<MenuItem value={7200}>5 days</MenuItem>
<MenuItem value={10080}>7 days</MenuItem>
<MenuItem value={14400}>10 days</MenuItem>
<MenuItem value={21600}>15 days</MenuItem>
</Select>
</Box> </Box>
</Collapse>
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "center",
}}
>
<Button
variant="contained"
color="primary"
onClick={handleCreateGroup}
>
Create Group
</Button>
</Box> </Box>
</Box> </Box>
</Box>
)} )}
{value === 1 && ( {value === 1 && (
<Box sx={{ <Box
width: '100%', sx={{
padding: '25px', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
display: 'flex' padding: '25px',
}}> width: '100%',
<AddGroupList setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} /> }}
>
<AddGroupList
setOpenSnack={setOpenSnack}
setInfoSnack={setInfoSnack}
/>
</Box> </Box>
)} )}
{value === 2 && (
<Box sx={{
width: '100%',
padding: '25px',
flexDirection: 'column',
flexGrow: 1,
display: 'flex'
}}>
<UserListOfInvites myAddress={address} setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} />
</Box>
)}
{value === 2 && (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
padding: '25px',
width: '100%',
}}
>
<UserListOfInvites
myAddress={address}
setOpenSnack={setOpenSnack}
setInfoSnack={setInfoSnack}
/>
</Box>
)}
</Box> </Box>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} />
<CustomizedSnackbars
open={openSnack}
setOpen={setOpenSnack}
info={infoSnack}
setInfo={setInfoSnack}
/>
</Dialog> </Dialog>
</React.Fragment> </React.Fragment>
); );

View File

@ -7,7 +7,7 @@ import {
Popover, Popover,
TextField, TextField,
Typography, Typography,
} from "@mui/material"; } from '@mui/material';
import React, { import React, {
useCallback, useCallback,
useContext, useContext,
@ -15,20 +15,20 @@ import React, {
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from "react"; } from 'react';
import { import {
AutoSizer, AutoSizer,
CellMeasurer, CellMeasurer,
CellMeasurerCache, CellMeasurerCache,
List, List,
} from "react-virtualized"; } from 'react-virtualized';
import _ from "lodash"; import _ from 'lodash';
import { MyContext, getBaseApiReact } from "../../App"; import { MyContext, getBaseApiReact } from '../../App';
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from '@mui/lab';
import { getBaseApi, getFee } from "../../background"; import { getBaseApi, getFee } from '../../background';
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from '@mui/icons-material/Lock';
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
import { Spacer } from "../../common/Spacer"; import { Spacer } from '../../common/Spacer';
const cache = new CellMeasurerCache({ const cache = new CellMeasurerCache({
fixedWidth: true, fixedWidth: true,
defaultHeight: 50, defaultHeight: 50,
@ -41,7 +41,7 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to const [popoverAnchor, setPopoverAnchor] = useState(null); // Track which list item the popover is anchored to
const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open
const listRef = useRef(); const listRef = useRef();
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState('');
const [filteredItems, setFilteredItems] = useState(groups); const [filteredItems, setFilteredItems] = useState(groups);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -72,9 +72,7 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
const getGroups = async () => { const getGroups = async () => {
try { try {
const response = await fetch( const response = await fetch(`${getBaseApiReact()}/groups/?limit=0`);
`${getBaseApiReact()}/groups/?limit=0`
);
const groupData = await response.json(); const groupData = await response.json();
const filteredGroup = groupData.filter( const filteredGroup = groupData.filter(
(item) => !memberGroups.find((group) => group.groupId === item.groupId) (item) => !memberGroups.find((group) => group.groupId === item.groupId)
@ -103,23 +101,25 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
const handleJoinGroup = async (group, isOpen) => { const handleJoinGroup = async (group, isOpen) => {
try { try {
const groupId = group.groupId; const groupId = group.groupId;
const fee = await getFee('JOIN_GROUP') const fee = await getFee('JOIN_GROUP');
await show({ await show({
message: "Would you like to perform an JOIN_GROUP transaction?" , message: 'Would you like to perform an JOIN_GROUP transaction?',
publishFee: fee.fee + ' QORT' publishFee: fee.fee + ' QORT',
}) });
setIsLoading(true); setIsLoading(true);
await new Promise((res, rej) => { await new Promise((res, rej) => {
window.sendMessage("joinGroup", { window
groupId, .sendMessage('joinGroup', {
}) groupId,
})
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
setInfoSnack({ setInfoSnack({
type: "success", type: 'success',
message: "Successfully requested to join group. It may take a couple of minutes for the changes to propagate", message:
'Successfully requested to join group. It may take a couple of minutes for the changes to propagate',
}); });
if (isOpen) { if (isOpen) {
setTxList((prev) => [ setTxList((prev) => [
{ {
@ -145,14 +145,14 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
...prev, ...prev,
]); ]);
} }
setOpenSnack(true); setOpenSnack(true);
handlePopoverClose(); handlePopoverClose();
res(response); res(response);
return; return;
} else { } else {
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: response?.error, message: response?.error,
}); });
setOpenSnack(true); setOpenSnack(true);
@ -161,18 +161,18 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
}) })
.catch((error) => { .catch((error) => {
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: error.message || "An error occurred", message: error.message || 'An error occurred',
}); });
setOpenSnack(true); setOpenSnack(true);
rej(error); rej(error);
}); });
}); });
setIsLoading(false); setIsLoading(false);
} catch (error) {} finally { } catch (error) {
console.log(error);
} finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
@ -195,30 +195,30 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
anchorEl={popoverAnchor} anchorEl={popoverAnchor}
onClose={handlePopoverClose} onClose={handlePopoverClose}
anchorOrigin={{ anchorOrigin={{
vertical: "bottom", vertical: 'bottom',
horizontal: "center", horizontal: 'center',
}} }}
transformOrigin={{ transformOrigin={{
vertical: "top", vertical: 'top',
horizontal: "center", horizontal: 'center',
}} }}
style={{ marginTop: "8px" }} style={{ marginTop: '8px' }}
> >
<Box <Box
sx={{ sx={{
width: "325px", width: '325px',
height: "250px", height: '250px',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
gap: "10px", gap: '10px',
padding: "10px", padding: '10px',
}} }}
> >
<Typography>Join {group?.groupName}</Typography> <Typography>Join {group?.groupName}</Typography>
<Typography> <Typography>
{group?.isOpen === false && {group?.isOpen === false &&
"This is a closed/private group, so you will need to wait until an admin accepts your request"} 'This is a closed/private group, so you will need to wait until an admin accepts your request'}
</Typography> </Typography>
<LoadingButton <LoadingButton
loading={isLoading} loading={isLoading}
@ -234,16 +234,20 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
onClick={(event) => handlePopoverOpen(event, index)} onClick={(event) => handlePopoverOpen(event, index)}
> >
{group?.isOpen === false && ( {group?.isOpen === false && (
<LockIcon sx={{ <LockIcon
color: 'var(--green)' sx={{
}} /> color: 'var(--green)',
)} }}
{group?.isOpen === true && ( />
<NoEncryptionGmailerrorredIcon sx={{ )}
color: 'var(--danger)' {group?.isOpen === true && (
}} /> <NoEncryptionGmailerrorredIcon
)} sx={{
<Spacer width="15px" /> color: 'var(--danger)',
}}
/>
)}
<Spacer width="15px" />
<ListItemText <ListItemText
primary={group?.groupName} primary={group?.groupName}
secondary={group?.description} secondary={group?.description}
@ -257,11 +261,13 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
}; };
return ( return (
<Box sx={{ <Box
display: 'flex', sx={{
flexDirection: 'column', display: 'flex',
flexGrow: 1 flexDirection: 'column',
}}> flexGrow: 1,
}}
>
<p>Groups list</p> <p>Groups list</p>
<TextField <TextField
label="Search for Groups" label="Search for Groups"
@ -272,10 +278,10 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
/> />
<div <div
style={{ style={{
position: "relative", position: 'relative',
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
}} }}
> >

View File

@ -8,64 +8,78 @@ import {
DialogTitle, DialogTitle,
TextField, TextField,
Typography, Typography,
} from "@mui/material"; useTheme,
import React, { useContext, useEffect, useState } from "react"; } from '@mui/material';
import { getBaseApiReact, MyContext } from "../../App"; import { useContext, useEffect, useState } from 'react';
import { Spacer } from "../../common/Spacer"; import { getBaseApiReact, MyContext } from '../../App';
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; import { Spacer } from '../../common/Spacer';
import { validateAddress } from "../../utils/validateAddress"; import {
import { getNameInfo, requestQueueMemberNames } from "./Group"; executeEvent,
import { useModal } from "../../common/useModal"; subscribeToEvent,
import { useRecoilState } from "recoil"; unsubscribeFromEvent,
import { isOpenBlockedModalAtom } from "../../atoms/global"; } from '../../utils/events';
import { validateAddress } from '../../utils/validateAddress';
import { getNameInfo, requestQueueMemberNames } from './Group';
import { useModal } from '../../common/useModal';
import { useRecoilState } from 'recoil';
import { isOpenBlockedModalAtom } from '../../atoms/global';
import InfoIcon from '@mui/icons-material/Info'; import InfoIcon from '@mui/icons-material/Info';
export const BlockedUsersModal = () => { export const BlockedUsersModal = () => {
const [isOpenBlockedModal, setIsOpenBlockedModal] = useRecoilState(isOpenBlockedModalAtom) const theme = useTheme();
const [isOpenBlockedModal, setIsOpenBlockedModal] = useRecoilState(
isOpenBlockedModalAtom
);
const [hasChanged, setHasChanged] = useState(false); const [hasChanged, setHasChanged] = useState(false);
const [value, setValue] = useState(""); const [value, setValue] = useState('');
const [addressesWithNames, setAddressesWithNames] = useState({}) const [addressesWithNames, setAddressesWithNames] = useState({});
const { isShow, onCancel, onOk, show, message } = useModal(); const { isShow, onCancel, onOk, show, message } = useModal();
const { getAllBlockedUsers, removeBlockFromList, addToBlockList, setOpenSnackGlobal, setInfoSnackCustom } = const {
useContext(MyContext); getAllBlockedUsers,
removeBlockFromList,
addToBlockList,
setOpenSnackGlobal,
setInfoSnackCustom,
} = useContext(MyContext);
const [blockedUsers, setBlockedUsers] = useState({ const [blockedUsers, setBlockedUsers] = useState({
addresses: {}, addresses: {},
names: {}, names: {},
}); });
const fetchBlockedUsers = () => { const fetchBlockedUsers = () => {
setBlockedUsers(getAllBlockedUsers()); setBlockedUsers(getAllBlockedUsers());
}; };
useEffect(() => { useEffect(() => {
if(!isOpenBlockedModal) return if (!isOpenBlockedModal) return;
fetchBlockedUsers(); fetchBlockedUsers();
}, [isOpenBlockedModal]); }, [isOpenBlockedModal]);
const getNames = async () => { const getNames = async () => {
// const validApi = await findUsableApi(); // const validApi = await findUsableApi();
const addresses = Object.keys(blockedUsers?.addresses) const addresses = Object.keys(blockedUsers?.addresses);
const addressNames = {} const addressNames = {};
const getMemNames = addresses.map(async (address) => { const getMemNames = addresses.map(async (address) => {
const name = await requestQueueMemberNames.enqueue(() => { const name = await requestQueueMemberNames.enqueue(() => {
return getNameInfo(address); return getNameInfo(address);
}); });
if (name) { if (name) {
addressNames[address] = name addressNames[address] = name;
} }
return true; return true;
}); });
await Promise.all(getMemNames); await Promise.all(getMemNames);
setAddressesWithNames(addressNames) setAddressesWithNames(addressNames);
}; };
const blockUser = async (e, user?: string) => { const blockUser = async (e, user?: string) => {
try { try {
const valUser = user || value const valUser = user || value;
if (!valUser) return; if (!valUser) return;
const isAddress = validateAddress(valUser); const isAddress = validateAddress(valUser);
let userName = null; let userName = null;
@ -80,62 +94,66 @@ export const BlockedUsersModal = () => {
if (!isAddress) { if (!isAddress) {
const response = await fetch(`${getBaseApiReact()}/names/${valUser}`); const response = await fetch(`${getBaseApiReact()}/names/${valUser}`);
const data = await response.json(); const data = await response.json();
if (!data?.owner) throw new Error("Name does not exist"); if (!data?.owner) throw new Error('Name does not exist');
if (data?.owner) { if (data?.owner) {
userAddress = data.owner; userAddress = data.owner;
userName = valUser; userName = valUser;
} }
} }
if(!userName){ if (!userName) {
await addToBlockList(userAddress, null); await addToBlockList(userAddress, null);
fetchBlockedUsers(); fetchBlockedUsers();
setHasChanged(true); setHasChanged(true);
executeEvent('updateChatMessagesWithBlocks', true) executeEvent('updateChatMessagesWithBlocks', true);
setValue('') setValue('');
return return;
} }
const responseModal = await show({ const responseModal = await show({
userName, userName,
userAddress, userAddress,
}); });
if (responseModal === "both") { if (responseModal === 'both') {
await addToBlockList(userAddress, userName); await addToBlockList(userAddress, userName);
} else if (responseModal === "address") { } else if (responseModal === 'address') {
await addToBlockList(userAddress, null); await addToBlockList(userAddress, null);
} else if (responseModal === "name") { } else if (responseModal === 'name') {
await addToBlockList(null, userName); await addToBlockList(null, userName);
} }
fetchBlockedUsers(); fetchBlockedUsers();
setHasChanged(true); setHasChanged(true);
setValue('') setValue('');
if(user){ if (user) {
setIsOpenBlockedModal(false) setIsOpenBlockedModal(false);
} }
if(responseModal === 'both' || responseModal === 'address'){ if (responseModal === 'both' || responseModal === 'address') {
executeEvent('updateChatMessagesWithBlocks', true) executeEvent('updateChatMessagesWithBlocks', true);
} }
} catch (error) { } catch (error) {
setOpenSnackGlobal(true); setOpenSnackGlobal(true);
setInfoSnackCustom({
setInfoSnackCustom({ type: 'error',
type: "error", message: error?.message || 'Unable to block user',
message: error?.message || "Unable to block user", });
});
} }
}; };
const blockUserFromOutsideModalFunc = (e) => { const blockUserFromOutsideModalFunc = (e) => {
const user = e.detail?.user; const user = e.detail?.user;
setIsOpenBlockedModal(true) setIsOpenBlockedModal(true);
blockUser(null, user) blockUser(null, user);
};
useEffect(() => {
subscribeToEvent('blockUserFromOutside', blockUserFromOutsideModalFunc);
return () => {
unsubscribeFromEvent(
'blockUserFromOutside',
blockUserFromOutsideModalFunc
);
}; };
}, []);
useEffect(() => {
subscribeToEvent("blockUserFromOutside", blockUserFromOutsideModalFunc);
return () => {
unsubscribeFromEvent("blockUserFromOutside", blockUserFromOutsideModalFunc);
};
}, []);
return ( return (
<Dialog <Dialog
open={isOpenBlockedModal} open={isOpenBlockedModal}
@ -145,14 +163,14 @@ export const BlockedUsersModal = () => {
<DialogTitle>Blocked Users</DialogTitle> <DialogTitle>Blocked Users</DialogTitle>
<DialogContent <DialogContent
sx={{ sx={{
padding: "20px", padding: '20px',
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", alignItems: 'center',
alignItems: "center", display: 'flex',
gap: "10px", gap: '10px',
}} }}
> >
<TextField <TextField
@ -180,16 +198,18 @@ export const BlockedUsersModal = () => {
Blocked addresses- blocks processing of txs Blocked addresses- blocks processing of txs
</DialogContentText> </DialogContentText>
<Spacer height="10px" /> <Spacer height="10px" />
<Button variant="contained" size="small" onClick={getNames}>Fetch names</Button> <Button variant="contained" size="small" onClick={getNames}>
Fetch names
</Button>
<Spacer height="10px" /> <Spacer height="10px" />
</> </>
)} )}
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "10px", gap: '10px',
}} }}
> >
{Object.entries(blockedUsers?.addresses || {})?.map( {Object.entries(blockedUsers?.addresses || {})?.map(
@ -197,11 +217,11 @@ export const BlockedUsersModal = () => {
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", alignItems: 'center',
alignItems: "center", display: 'flex',
gap: "10px", gap: '10px',
width: "100%", justifyContent: 'space-between',
justifyContent: "space-between", width: '100%',
}} }}
> >
<Typography>{addressesWithNames[key] || key}</Typography> <Typography>{addressesWithNames[key] || key}</Typography>
@ -215,7 +235,7 @@ export const BlockedUsersModal = () => {
try { try {
await removeBlockFromList(key, undefined); await removeBlockFromList(key, undefined);
setHasChanged(true); setHasChanged(true);
setValue(""); setValue('');
fetchBlockedUsers(); fetchBlockedUsers();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -241,20 +261,20 @@ export const BlockedUsersModal = () => {
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "10px", gap: '10px',
}} }}
> >
{Object.entries(blockedUsers?.names || {})?.map(([key, value]) => { {Object.entries(blockedUsers?.names || {})?.map(([key, value]) => {
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", alignItems: 'center',
alignItems: "center", display: 'flex',
gap: "10px", gap: '10px',
width: "100%", justifyContent: 'space-between',
justifyContent: "space-between", width: '100%',
}} }}
> >
<Typography>{key}</Typography> <Typography>{key}</Typography>
@ -284,20 +304,20 @@ export const BlockedUsersModal = () => {
<DialogActions> <DialogActions>
<Button <Button
sx={{ sx={{
backgroundColor: "var(--green)", backgroundColor: theme.palette.background.default,
color: "black", color: theme.palette.text.primary,
fontWeight: "bold", fontWeight: 'bold',
opacity: 0.7, opacity: 0.7,
"&:hover": { '&:hover': {
backgroundColor: "var(--green)", backgroundColor: theme.palette.background.paper,
color: "black", color: theme.palette.text.primary,
opacity: 1, opacity: 1,
}, },
}} }}
variant="contained" variant="contained"
onClick={() => { onClick={() => {
if (hasChanged) { if (hasChanged) {
executeEvent("updateChatMessagesWithBlocks", true); executeEvent('updateChatMessagesWithBlocks', true);
} }
setIsOpenBlockedModal(false); setIsOpenBlockedModal(false);
}} }}
@ -312,28 +332,37 @@ export const BlockedUsersModal = () => {
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
> >
<DialogTitle id="alert-dialog-title"> <DialogTitle id="alert-dialog-title">
{"Decide what to block"} {'Decide what to block'}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText id="alert-dialog-description"> <DialogContentText id="alert-dialog-description">
Blocking {message?.userName || message?.userAddress} Blocking {message?.userName || message?.userAddress}
</DialogContentText> </DialogContentText>
<Box sx={{ <Box
display: 'flex', sx={{
alignItems: 'center', alignItems: 'center',
gap: '10px', display: 'flex',
marginTop: '20px' gap: '10px',
}}> marginTop: '20px',
<InfoIcon sx={{ }}
color: 'fff' >
}}/> <Typography>Choose "block txs" or "all" to block chat messages </Typography> <InfoIcon
sx={{
color: theme.palette.text.primary,
}}
/>{' '}
<Typography>
Choose "block txs" or "all" to block chat messages{' '}
</Typography>
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
variant="contained" variant="contained"
onClick={() => { onClick={() => {
onOk("address"); onOk('address');
}} }}
> >
Block txs Block txs
@ -341,7 +370,7 @@ export const BlockedUsersModal = () => {
<Button <Button
variant="contained" variant="contained"
onClick={() => { onClick={() => {
onOk("name"); onOk('name');
}} }}
> >
Block QDN data Block QDN data
@ -349,7 +378,7 @@ export const BlockedUsersModal = () => {
<Button <Button
variant="contained" variant="contained"
onClick={() => { onClick={() => {
onOk("both"); onOk('both');
}} }}
> >
Block All Block All

View File

@ -5,10 +5,10 @@ import React, {
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from "react"; } from 'react';
import { Avatar, Box, Popover, Typography } from "@mui/material"; import { Avatar, Box, Popover, Typography } from '@mui/material';
// import { MAIL_SERVICE_TYPE, THREAD_SERVICE_TYPE } from "../../constants/mail"; // import { MAIL_SERVICE_TYPE, THREAD_SERVICE_TYPE } from "../../constants/mail";
import { Thread } from "./Thread"; import { Thread } from './Thread';
import { import {
AllThreadP, AllThreadP,
ArrowDownIcon, ArrowDownIcon,
@ -38,61 +38,73 @@ import {
ThreadSingleLastMessageP, ThreadSingleLastMessageP,
ThreadSingleLastMessageSpanP, ThreadSingleLastMessageSpanP,
ThreadSingleTitle, ThreadSingleTitle,
} from "./Mail-styles"; } from './Mail-styles';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import { Spacer } from "../../../common/Spacer"; import { Spacer } from '../../../common/Spacer';
import { formatDate, formatTimestamp } from "../../../utils/time"; import { formatDate, formatTimestamp } from '../../../utils/time';
import LazyLoad from "../../../common/LazyLoad"; import LazyLoad from '../../../common/LazyLoad';
import { delay } from "../../../utils/helpers"; import { delay } from '../../../utils/helpers';
import { NewThread } from "./NewThread"; import { NewThread } from './NewThread';
import { getBaseApi } from "../../../background"; import { getBaseApi } from '../../../background';
import { decryptPublishes, getTempPublish, handleUnencryptedPublishes } from "../../Chat/GroupAnnouncements"; import {
import CheckSVG from "../../../assets/svgs/Check.svg"; decryptPublishes,
import SortSVG from "../../../assets/svgs/Sort.svg"; getTempPublish,
import ArrowDownSVG from "../../../assets/svgs/ArrowDown.svg"; handleUnencryptedPublishes,
import { LoadingSnackbar } from "../../Snackbar/LoadingSnackbar"; } from '../../Chat/GroupAnnouncements';
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../../utils/events"; import CheckSVG from '../../../assets/svgs/Check.svg';
import SortSVG from '../../../assets/svgs/Sort.svg';
import ArrowDownSVG from '../../../assets/svgs/ArrowDown.svg';
import { LoadingSnackbar } from '../../Snackbar/LoadingSnackbar';
import {
executeEvent,
subscribeToEvent,
unsubscribeFromEvent,
} from '../../../utils/events';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import { getArbitraryEndpointReact, getBaseApiReact, isMobile } from "../../../App"; import {
import { WrapperUserAction } from "../../WrapperUserAction"; getArbitraryEndpointReact,
import { addDataPublishesFunc, getDataPublishesFunc } from "../Group"; getBaseApiReact,
const filterOptions = ["Recently active", "Newest", "Oldest"]; isMobile,
} from '../../../App';
import { WrapperUserAction } from '../../WrapperUserAction';
import { addDataPublishesFunc, getDataPublishesFunc } from '../Group';
const filterOptions = ['Recently active', 'Newest', 'Oldest'];
export const threadIdentifier = "DOCUMENT"; export const threadIdentifier = 'DOCUMENT';
export const GroupMail = ({ export const GroupMail = ({
selectedGroup, selectedGroup,
userInfo, userInfo,
getSecretKey, getSecretKey,
secretKey, secretKey,
defaultThread, defaultThread,
setDefaultThread, setDefaultThread,
hide, hide,
isPrivate isPrivate,
}) => { }) => {
const [viewedThreads, setViewedThreads] = React.useState<any>({}); const [viewedThreads, setViewedThreads] = React.useState<any>({});
const [filterMode, setFilterMode] = useState<string>("Recently active"); const [filterMode, setFilterMode] = useState<string>('Recently active');
const [currentThread, setCurrentThread] = React.useState(null); const [currentThread, setCurrentThread] = React.useState(null);
const [recentThreads, setRecentThreads] = useState<any[]>([]); const [recentThreads, setRecentThreads] = useState<any[]>([]);
const [allThreads, setAllThreads] = useState<any[]>([]); const [allThreads, setAllThreads] = useState<any[]>([]);
const [members, setMembers] = useState<any>(null); const [members, setMembers] = useState<any>(null);
const [isOpenFilterList, setIsOpenFilterList] = useState<boolean>(false); const [isOpenFilterList, setIsOpenFilterList] = useState<boolean>(false);
const anchorElInstanceFilter = useRef<any>(null); const anchorElInstanceFilter = useRef<any>(null);
const [tempPublishedList, setTempPublishedList] = useState([]) const [tempPublishedList, setTempPublishedList] = useState([]);
const dataPublishes = useRef({}) const dataPublishes = useRef({});
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false);
const groupIdRef = useRef<any>(null); const groupIdRef = useRef<any>(null);
const groupId = useMemo(() => { const groupId = useMemo(() => {
return selectedGroup?.groupId; return selectedGroup?.groupId;
}, [selectedGroup]); }, [selectedGroup]);
useEffect(()=> { useEffect(() => {
if(!groupId) return if (!groupId) return;
(async ()=> { (async () => {
const res = await getDataPublishesFunc(groupId, 'thread') const res = await getDataPublishesFunc(groupId, 'thread');
dataPublishes.current = res || {} dataPublishes.current = res || {};
})() })();
}, [groupId]) }, [groupId]);
useEffect(() => { useEffect(() => {
if (groupId !== groupIdRef?.current) { if (groupId !== groupIdRef?.current) {
@ -103,55 +115,66 @@ export const GroupMail = ({
} }
}, [groupId]); }, [groupId]);
const setTempData = async ()=> { const setTempData = async () => {
try { try {
const getTempAnnouncements = await getTempPublish() const getTempAnnouncements = await getTempPublish();
if(getTempAnnouncements?.thread){ if (getTempAnnouncements?.thread) {
let tempData = [] let tempData = [];
Object.keys(getTempAnnouncements?.thread || {}).map((key)=> { Object.keys(getTempAnnouncements?.thread || {}).map((key) => {
const value = getTempAnnouncements?.thread[key] const value = getTempAnnouncements?.thread[key];
if(value?.data?.groupId === groupIdRef?.current){ if (value?.data?.groupId === groupIdRef?.current) {
tempData.push(value.data) tempData.push(value.data);
}
});
setTempPublishedList(tempData);
} }
} catch (error) {}
}) };
setTempPublishedList(tempData)
}
} catch (error) {
}
}
const getEncryptedResource = async ({ name, identifier, resource }, isPrivate) => {
let data = dataPublishes.current[`${name}-${identifier}`]
if(!data || (data?.update || data?.created !== (resource?.updated || resource?.created))){
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64`
);
if(!res?.ok) return
data = await res.text();
await addDataPublishesFunc({...resource, data}, groupId, 'thread')
const getEncryptedResource = async (
{ name, identifier, resource },
isPrivate
) => {
let data = dataPublishes.current[`${name}-${identifier}`];
if (
!data ||
data?.update ||
data?.created !== (resource?.updated || resource?.created)
) {
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64`
);
if (!res?.ok) return;
data = await res.text();
await addDataPublishesFunc({ ...resource, data }, groupId, 'thread');
} else { } else {
data = data.data data = data.data;
} }
const response = isPrivate === false ? handleUnencryptedPublishes([data]) : await decryptPublishes([{ data }], secretKey); const response =
isPrivate === false
? handleUnencryptedPublishes([data])
: await decryptPublishes([{ data }], secretKey);
const messageData = response[0]; const messageData = response[0];
return messageData.decryptedData; return messageData.decryptedData;
}; };
const updateThreadActivity = async ({threadId, qortalName, groupId, thread}) => { const updateThreadActivity = async ({
threadId,
qortalName,
groupId,
thread,
}) => {
try { try {
await new Promise((res, rej) => { await new Promise((res, rej) => {
window.sendMessage("updateThreadActivity", { window
threadId, .sendMessage('updateThreadActivity', {
qortalName, threadId,
groupId, qortalName,
thread, groupId,
}) thread,
})
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
res(response); res(response);
@ -160,13 +183,10 @@ export const GroupMail = ({
rej(response.error); rej(response.error);
}) })
.catch((error) => { .catch((error) => {
rej(error.message || "An error occurred"); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) { } catch (error) {
} finally { } finally {
} }
}; };
@ -174,9 +194,9 @@ export const GroupMail = ({
const getAllThreads = React.useCallback( const getAllThreads = React.useCallback(
async (groupId: string, mode: string, isInitial?: boolean) => { async (groupId: string, mode: string, isInitial?: boolean) => {
try { try {
setIsLoading(true) setIsLoading(true);
const offset = isInitial ? 0 : allThreads.length; const offset = isInitial ? 0 : allThreads.length;
const isReverse = mode === "Newest" ? true : false; const isReverse = mode === 'Newest' ? true : false;
if (isInitial) { if (isInitial) {
// dispatch(setIsLoadingCustom("Loading threads")); // dispatch(setIsLoadingCustom("Loading threads"));
} }
@ -184,9 +204,9 @@ export const GroupMail = ({
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=${20}&includemetadata=false&offset=${offset}&reverse=${isReverse}&prefix=true`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=${20}&includemetadata=false&offset=${offset}&reverse=${isReverse}&prefix=true`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
const responseData = await response.json(); const responseData = await response.json();
@ -209,21 +229,26 @@ export const GroupMail = ({
let threadRes = null; let threadRes = null;
try { try {
threadRes = await Promise.race([ threadRes = await Promise.race([
getEncryptedResource({ getEncryptedResource(
name: message.name, {
identifier: message.identifier, name: message.name,
resource: message identifier: message.identifier,
}, isPrivate), resource: message,
},
isPrivate
),
delay(5000), delay(5000),
]); ]);
} catch (error) {} } catch (error) {
console.log(error);
}
if (threadRes?.title) { if (threadRes?.title) {
fullObject = { fullObject = {
...message, ...message,
threadData: threadRes, threadData: threadRes,
threadOwner: message?.name, threadOwner: message?.name,
threadId: message.identifier threadId: message.identifier,
}; };
} }
} }
@ -251,7 +276,7 @@ export const GroupMail = ({
console.log({ error }); console.log({ error });
} finally { } finally {
if (isInitial) { if (isInitial) {
setIsLoading(false) setIsLoading(false);
// dispatch(setIsLoadingCustom(null)); // dispatch(setIsLoadingCustom(null));
} }
} }
@ -261,21 +286,21 @@ export const GroupMail = ({
const getMailMessages = React.useCallback( const getMailMessages = React.useCallback(
async (groupId: string, members: any) => { async (groupId: string, members: any) => {
try { try {
setIsLoading(true) setIsLoading(true);
const identifier = `thmsg-grp-${groupId}-thread-`; const identifier = `thmsg-grp-${groupId}-thread-`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=100&includemetadata=false&offset=${0}&reverse=true&prefix=true`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=100&includemetadata=false&offset=${0}&reverse=true&prefix=true`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
const responseData = await response.json(); const responseData = await response.json();
const messagesForThread: any = {}; const messagesForThread: any = {};
for (const message of responseData) { for (const message of responseData) {
let str = message.identifier; let str = message.identifier;
const parts = str.split("-"); const parts = str.split('-');
// Get the second last element // Get the second last element
const secondLastId = parts[parts.length - 2]; const secondLastId = parts[parts.length - 2];
@ -295,16 +320,16 @@ export const GroupMail = ({
}) })
.sort((a, b) => b.created - a.created) .sort((a, b) => b.created - a.created)
.slice(0, 10); .slice(0, 10);
let fullThreadArray: any = []; let fullThreadArray: any = [];
const getMessageForThreads = newArray.map(async (message: any) => { const getMessageForThreads = newArray.map(async (message: any) => {
try { try {
const identifierQuery = message.threadId; const identifierQuery = message.threadId;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifierQuery}&limit=1&includemetadata=false&offset=${0}&reverse=true&prefix=true`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifierQuery}&limit=1&includemetadata=false&offset=${0}&reverse=true&prefix=true`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
const responseData = await response.json(); const responseData = await response.json();
@ -324,11 +349,14 @@ export const GroupMail = ({
fullThreadArray.push(fullObject); fullThreadArray.push(fullObject);
} else { } else {
let threadRes = await Promise.race([ let threadRes = await Promise.race([
getEncryptedResource({ getEncryptedResource(
name: thread.name, {
identifier: message.threadId, name: thread.name,
resource: thread identifier: message.threadId,
}, isPrivate), resource: thread,
},
isPrivate
),
delay(10000), delay(10000),
]); ]);
if (threadRes?.title) { if (threadRes?.title) {
@ -353,7 +381,7 @@ export const GroupMail = ({
setRecentThreads(sorted); setRecentThreads(sorted);
} catch (error) { } catch (error) {
} finally { } finally {
setIsLoading(false) setIsLoading(false);
// dispatch(setIsLoadingCustom(null)); // dispatch(setIsLoadingCustom(null));
} }
}, },
@ -361,7 +389,6 @@ export const GroupMail = ({
); );
const getMessages = React.useCallback(async () => { const getMessages = React.useCallback(async () => {
// if ( !groupId || members?.length === 0) return; // if ( !groupId || members?.length === 0) return;
if (!groupId || isPrivate === null) return; if (!groupId || isPrivate === null) return;
@ -371,23 +398,23 @@ export const GroupMail = ({
const interval = useRef<any>(null); const interval = useRef<any>(null);
const firstMount = useRef(false); const firstMount = useRef(false);
const filterModeRef = useRef(""); const filterModeRef = useRef('');
useEffect(() => { useEffect(() => {
if(hide) return if (hide) return;
if (filterModeRef.current !== filterMode) { if (filterModeRef.current !== filterMode) {
firstMount.current = false; firstMount.current = false;
} }
// if (groupId && !firstMount.current && members.length > 0) { // if (groupId && !firstMount.current && members.length > 0) {
if (groupId && !firstMount.current && isPrivate !== null) { if (groupId && !firstMount.current && isPrivate !== null) {
if (filterMode === "Recently active") { if (filterMode === 'Recently active') {
getMessages(); getMessages();
} else if (filterMode === "Newest") { } else if (filterMode === 'Newest') {
getAllThreads(groupId, "Newest", true); getAllThreads(groupId, 'Newest', true);
} else if (filterMode === "Oldest") { } else if (filterMode === 'Oldest') {
getAllThreads(groupId, "Oldest", true); getAllThreads(groupId, 'Oldest', true);
} }
setTempData() setTempData();
firstMount.current = true; firstMount.current = true;
} }
}, [groupId, members, filterMode, hide, isPrivate]); }, [groupId, members, filterMode, hide, isPrivate]);
@ -428,19 +455,16 @@ export const GroupMail = ({
} }
}, []); }, []);
let listOfThreadsToDisplay = recentThreads; let listOfThreadsToDisplay = recentThreads;
if (filterMode === "Newest" || filterMode === "Oldest") { if (filterMode === 'Newest' || filterMode === 'Oldest') {
listOfThreadsToDisplay = allThreads; listOfThreadsToDisplay = allThreads;
} }
const onSubmitNewThread = useCallback( const onSubmitNewThread = useCallback(
(val: any) => { (val: any) => {
if (filterMode === "Recently active") { if (filterMode === 'Recently active') {
setRecentThreads((prev) => [val, ...prev]); setRecentThreads((prev) => [val, ...prev]);
} else if (filterMode === "Newest") { } else if (filterMode === 'Newest') {
setAllThreads((prev) => [val, ...prev]); setAllThreads((prev) => [val, ...prev]);
} }
}, },
@ -461,72 +485,77 @@ export const GroupMail = ({
setIsOpenFilterList(false); setIsOpenFilterList(false);
}; };
const refetchThreadsLists = useCallback(()=> { const refetchThreadsLists = useCallback(() => {
if (filterMode === "Recently active") { if (filterMode === 'Recently active') {
getMessages(); getMessages();
} else if (filterMode === "Newest") { } else if (filterMode === 'Newest') {
getAllThreads(groupId, "Newest", true); getAllThreads(groupId, 'Newest', true);
} else if (filterMode === "Oldest") { } else if (filterMode === 'Oldest') {
getAllThreads(groupId, "Oldest", true); getAllThreads(groupId, 'Oldest', true);
} }
}, [filterMode, isPrivate]) }, [filterMode, isPrivate]);
const updateThreadActivityCurrentThread = ()=> { const updateThreadActivityCurrentThread = () => {
if(!currentThread) return if (!currentThread) return;
const thread = currentThread const thread = currentThread;
updateThreadActivity({ updateThreadActivity({
threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread threadId: thread?.threadId,
}) qortalName: thread?.threadData?.name,
} groupId: groupId,
thread: thread,
});
};
const setThreadFunc = (data)=> { const setThreadFunc = (data) => {
const thread = data const thread = data;
setCurrentThread(thread); setCurrentThread(thread);
if(thread?.threadId && thread?.threadData?.name){ if (thread?.threadId && thread?.threadData?.name) {
updateThreadActivity({ updateThreadActivity({
threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread threadId: thread?.threadId,
}) qortalName: thread?.threadData?.name,
} groupId: groupId,
thread: thread,
});
}
setTimeout(() => { setTimeout(() => {
executeEvent("threadFetchMode", { executeEvent('threadFetchMode', {
mode: "last-page" mode: 'last-page',
}); });
}, 300); }, 300);
} };
useEffect(() => {
useEffect(()=> { if (defaultThread) {
if(defaultThread){ setThreadFunc(defaultThread);
setThreadFunc(defaultThread) setDefaultThread(null);
setDefaultThread(null)
} }
}, [defaultThread]) }, [defaultThread]);
const combinedListTempAndReal = useMemo(() => { const combinedListTempAndReal = useMemo(() => {
// Combine the two lists // Combine the two lists
const transformTempPublishedList = tempPublishedList.map((item)=> { const transformTempPublishedList = tempPublishedList.map((item) => {
return { return {
...item, ...item,
threadData: item.tempData, threadData: item.tempData,
threadOwner: item?.name, threadOwner: item?.name,
threadId: item.identifier threadId: item.identifier,
} };
}) });
const combined = [...transformTempPublishedList, ...listOfThreadsToDisplay]; const combined = [...transformTempPublishedList, ...listOfThreadsToDisplay];
// Remove duplicates based on the "identifier" // Remove duplicates based on the "identifier"
const uniqueItems = new Map(); const uniqueItems = new Map();
combined.forEach(item => { combined.forEach((item) => {
uniqueItems.set(item.threadId, item); // This will overwrite duplicates, keeping the last occurrence uniqueItems.set(item.threadId, item); // This will overwrite duplicates, keeping the last occurrence
}); });
// Convert the map back to an array and sort by "created" timestamp in descending order // Convert the map back to an array and sort by "created" timestamp in descending order
const sortedList = Array.from(uniqueItems.values()).sort((a, b) => const sortedList = Array.from(uniqueItems.values()).sort((a, b) =>
filterMode === 'Oldest' filterMode === 'Oldest'
? a.threadData?.createdAt - b.threadData?.createdAt ? a.threadData?.createdAt - b.threadData?.createdAt
: b.threadData?.createdAt - a.threadData?.createdAt : b.threadData?.createdAt - a.threadData?.createdAt
); );
return sortedList; return sortedList;
}, [tempPublishedList, listOfThreadsToDisplay, filterMode]); }, [tempPublishedList, listOfThreadsToDisplay, filterMode]);
@ -548,9 +577,9 @@ export const GroupMail = ({
return ( return (
<GroupContainer <GroupContainer
sx={{ sx={{
position: "relative", position: 'relative',
overflow: "auto", overflow: 'auto',
width: "100%", width: '100%',
}} }}
> >
<Popover <Popover
@ -558,19 +587,19 @@ export const GroupMail = ({
anchorEl={anchorElInstanceFilter.current} anchorEl={anchorElInstanceFilter.current}
onClose={handleCloseThreadFilterList} onClose={handleCloseThreadFilterList}
anchorOrigin={{ anchorOrigin={{
vertical: "bottom", vertical: 'bottom',
horizontal: "right", horizontal: 'right',
}} }}
transformOrigin={{ transformOrigin={{
vertical: "top", vertical: 'top',
horizontal: "right", horizontal: 'right',
}} }}
> >
<InstanceListParent <InstanceListParent
sx={{ sx={{
minHeight: "unset", minHeight: 'unset',
width: "auto", width: 'auto',
padding: "0px", padding: '0px',
}} }}
> >
<InstanceListHeader></InstanceListHeader> <InstanceListHeader></InstanceListHeader>
@ -583,7 +612,7 @@ export const GroupMail = ({
}} }}
sx={{ sx={{
backgroundColor: backgroundColor:
filterMode === filter ? "rgba(74, 158, 244, 1)" : "unset", filterMode === filter ? 'rgba(74, 158, 244, 1)' : 'unset',
}} }}
key={filter} key={filter}
> >
@ -606,12 +635,11 @@ export const GroupMail = ({
</Popover> </Popover>
<ThreadContainerFullWidth> <ThreadContainerFullWidth>
<ThreadContainer> <ThreadContainer>
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "center", alignItems: 'center',
}} }}
> >
<NewThread <NewThread
@ -626,7 +654,7 @@ export const GroupMail = ({
/> />
<ComposeContainerBlank <ComposeContainerBlank
sx={{ sx={{
height: "auto", height: 'auto',
}} }}
> >
{selectedGroup && !currentThread && ( {selectedGroup && !currentThread && (
@ -647,17 +675,22 @@ export const GroupMail = ({
</ComposeContainerBlank> </ComposeContainerBlank>
</Box> </Box>
<Spacer height="30px" /> <Spacer height="30px" />
<Box sx={{ <Box
display: 'flex', sx={{
alignItems: 'center', display: 'flex',
justifyContent: 'space-between' alignItems: 'center',
}}> justifyContent: 'space-between',
<AllThreadP>{filterMode}</AllThreadP> }}
>
<AllThreadP>{filterMode}</AllThreadP>
<RefreshIcon onClick={refetchThreadsLists} sx={{ <RefreshIcon
color: 'white', onClick={refetchThreadsLists}
cursor: 'pointer' sx={{
}} /> color: 'white',
cursor: 'pointer',
}}
/>
</Box> </Box>
<Spacer height="30px" /> <Spacer height="30px" />
@ -668,112 +701,119 @@ export const GroupMail = ({
]; ];
const shouldAppearLighter = const shouldAppearLighter =
hasViewedRecent && hasViewedRecent &&
filterMode === "Recently active" && filterMode === 'Recently active' &&
thread?.threadData?.createdAt < hasViewedRecent?.timestamp; thread?.threadData?.createdAt < hasViewedRecent?.timestamp;
return ( return (
<SingleThreadParent <SingleThreadParent
sx={{ sx={{
flexWrap: 'wrap', flexWrap: 'wrap',
gap: '15px', gap: '15px',
height: 'auto' height: 'auto',
}} }}
onClick={() => { onClick={() => {
setCurrentThread(thread); setCurrentThread(thread);
if(thread?.threadId && thread?.threadData?.name){ if (thread?.threadId && thread?.threadData?.name) {
updateThreadActivity({ updateThreadActivity({
threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread threadId: thread?.threadId,
}) qortalName: thread?.threadData?.name,
groupId: groupId,
thread: thread,
});
} }
}} }}
> >
<Avatar <Avatar
sx={{ sx={{
height: "50px", height: '50px',
width: "50px", width: '50px',
}} }}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${thread?.threadData?.name}/qortal_avatar?async=true`} src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${thread?.threadData?.name}/qortal_avatar?async=true`}
alt={thread?.threadData?.name} alt={thread?.threadData?.name}
> >
{thread?.threadData?.name?.charAt(0)} {thread?.threadData?.name?.charAt(0)}
</Avatar> </Avatar>
<ThreadInfoColumn> <ThreadInfoColumn>
<ThreadInfoColumnNameP> <ThreadInfoColumnNameP>
<ThreadInfoColumnbyP>by </ThreadInfoColumnbyP> <ThreadInfoColumnbyP>by </ThreadInfoColumnbyP>
{thread?.threadData?.name} {thread?.threadData?.name}
</ThreadInfoColumnNameP> </ThreadInfoColumnNameP>
<ThreadInfoColumnTime> <ThreadInfoColumnTime>
{formatTimestamp(thread?.threadData?.createdAt)} {formatTimestamp(thread?.threadData?.createdAt)}
</ThreadInfoColumnTime> </ThreadInfoColumnTime>
</ThreadInfoColumn> </ThreadInfoColumn>
<div <div
style={{ style={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
justifyContent: "center", justifyContent: 'center',
width: '100%' width: '100%',
}} }}
> >
<ThreadSingleTitle <ThreadSingleTitle
sx={{ sx={{
fontWeight: shouldAppearLighter && 300, fontWeight: shouldAppearLighter && 300,
fontSize: isMobile && '18px' fontSize: isMobile && '18px',
}} }}
> >
{thread?.threadData?.title} {thread?.threadData?.title}
</ThreadSingleTitle> </ThreadSingleTitle>
<Spacer height="10px" /> <Spacer height="10px" />
{filterMode === "Recently active" && ( {filterMode === 'Recently active' && (
<div <div
style={{ style={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
}} }}
> >
<ThreadSingleLastMessageP> <ThreadSingleLastMessageP>
<ThreadSingleLastMessageSpanP> <ThreadSingleLastMessageSpanP>
last message:{" "} last message:{' '}
</ThreadSingleLastMessageSpanP> </ThreadSingleLastMessageSpanP>
{formatDate(thread?.created)} {formatDate(thread?.created)}
</ThreadSingleLastMessageP> </ThreadSingleLastMessageP>
</div> </div>
)} )}
</div> </div>
<Box onClick={()=> { <Box
setTimeout(() => { onClick={() => {
executeEvent("threadFetchMode", { setTimeout(() => {
mode: "last-page" executeEvent('threadFetchMode', {
}); mode: 'last-page',
}, 300); });
}, 300);
}}
}} sx={{ sx={{
position: 'absolute', position: 'absolute',
bottom: '2px', bottom: '2px',
right: '2px', right: '2px',
borderRadius: '5px', borderRadius: '5px',
backgroundColor: '#27282c', backgroundColor: '#27282c',
display: 'flex', display: 'flex',
gap: '10px', gap: '10px',
alignItems: 'center', alignItems: 'center',
padding: '5px', padding: '5px',
cursor: 'pointer', cursor: 'pointer',
'&:hover': { '&:hover': {
background: 'rgba(255, 255, 255, 0.60)' background: 'rgba(255, 255, 255, 0.60)',
} },
}}> }}
<Typography sx={{ >
color: 'white', <Typography
fontSize: '12px' sx={{
}}>Last page</Typography> color: 'white',
<ArrowForwardIosIcon sx={{ fontSize: '12px',
color: 'white', }}
fontSize: '12px' >
}} /> Last page
</Typography>
<ArrowForwardIosIcon
sx={{
color: 'white',
fontSize: '12px',
}}
/>
</Box> </Box>
</SingleThreadParent> </SingleThreadParent>
); );
@ -781,12 +821,12 @@ export const GroupMail = ({
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
justifyContent: "center", justifyContent: 'center',
}} }}
> >
{listOfThreadsToDisplay.length >= 20 && {listOfThreadsToDisplay.length >= 20 &&
filterMode !== "Recently active" && ( filterMode !== 'Recently active' && (
<LazyLoad <LazyLoad
onLoadMore={() => getAllThreads(groupId, filterMode, false)} onLoadMore={() => getAllThreads(groupId, filterMode, false)}
></LazyLoad> ></LazyLoad>
@ -797,7 +837,7 @@ export const GroupMail = ({
<LoadingSnackbar <LoadingSnackbar
open={isLoading} open={isLoading}
info={{ info={{
message: "Loading threads... please wait.", message: 'Loading threads... please wait.',
}} }}
/> />
</GroupContainer> </GroupContainer>

View File

@ -1,11 +1,17 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from 'react';
import { Box, Button, CircularProgress, Input, Typography } from "@mui/material"; import {
import ShortUniqueId from "short-unique-id"; Box,
import CloseIcon from "@mui/icons-material/Close"; Button,
CircularProgress,
Input,
Typography,
} from '@mui/material';
import ShortUniqueId from 'short-unique-id';
import CloseIcon from '@mui/icons-material/Close';
import ModalCloseSVG from "../../../assets/svgs/ModalClose.svg"; import ModalCloseSVG from '../../../assets/svgs/ModalClose.svg';
import ComposeIconSVG from "../../../assets/svgs/ComposeIcon.svg"; import ComposeIconSVG from '../../../assets/svgs/ComposeIcon.svg';
import { import {
AttachmentContainer, AttachmentContainer,
@ -22,20 +28,25 @@ import {
NewMessageInputRow, NewMessageInputRow,
NewMessageSendButton, NewMessageSendButton,
NewMessageSendP, NewMessageSendP,
} from "./Mail-styles"; } from './Mail-styles';
import { ReusableModal } from "./ReusableModal"; import { ReusableModal } from './ReusableModal';
import { Spacer } from "../../../common/Spacer"; import { Spacer } from '../../../common/Spacer';
import { formatBytes } from "../../../utils/Size"; import { formatBytes } from '../../../utils/Size';
import { CreateThreadIcon } from "../../../assets/svgs/CreateThreadIcon"; import { CreateThreadIcon } from '../../../assets/Icons/CreateThreadIcon';
import { SendNewMessage } from "../../../assets/svgs/SendNewMessage"; import { SendNewMessage } from '../../../assets/Icons/SendNewMessage';
import { TextEditor } from "./TextEditor"; import { TextEditor } from './TextEditor';
import { MyContext, isMobile, pauseAllQueues, resumeAllQueues } from "../../../App"; import {
import { getFee } from "../../../background"; MyContext,
import TipTap from "../../Chat/TipTap"; isMobile,
import { MessageDisplay } from "../../Chat/MessageDisplay"; pauseAllQueues,
import { CustomizedSnackbars } from "../../Snackbar/Snackbar"; resumeAllQueues,
import { saveTempPublish } from "../../Chat/GroupAnnouncements"; } from '../../../App';
import { getFee } from '../../../background';
import TipTap from '../../Chat/TipTap';
import { MessageDisplay } from '../../Chat/MessageDisplay';
import { CustomizedSnackbars } from '../../Snackbar/Snackbar';
import { saveTempPublish } from '../../Chat/GroupAnnouncements';
const uid = new ShortUniqueId({ length: 8 }); const uid = new ShortUniqueId({ length: 8 });
@ -54,21 +65,21 @@ export function objectToBase64(obj: any) {
const jsonString = JSON.stringify(obj); const jsonString = JSON.stringify(obj);
// Step 2: Create a Blob from the JSON string // Step 2: Create a Blob from the JSON string
const blob = new Blob([jsonString], { type: "application/json" }); const blob = new Blob([jsonString], { type: 'application/json' });
// Step 3: Create a FileReader to read the Blob as a base64-encoded string // Step 3: Create a FileReader to read the Blob as a base64-encoded string
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onloadend = () => { reader.onloadend = () => {
if (typeof reader.result === "string") { if (typeof reader.result === 'string') {
// Remove 'data:application/json;base64,' prefix // Remove 'data:application/json;base64,' prefix
const base64 = reader.result.replace( const base64 = reader.result.replace(
"data:application/json;base64,", 'data:application/json;base64,',
"" ''
); );
resolve(base64); resolve(base64);
} else { } else {
reject(new Error("Failed to read the Blob as a base64-encoded string")); reject(new Error('Failed to read the Blob as a base64-encoded string'));
} }
}; };
reader.onerror = () => { reader.onerror = () => {
@ -94,10 +105,11 @@ export const publishGroupEncryptedResource = async ({
identifier, identifier,
}) => { }) => {
return new Promise((res, rej) => { return new Promise((res, rej) => {
window.sendMessage("publishGroupEncryptedResource", { window
encryptedData, .sendMessage('publishGroupEncryptedResource', {
identifier, encryptedData,
}) identifier,
})
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
res(response); res(response);
@ -106,19 +118,19 @@ export const publishGroupEncryptedResource = async ({
rej(response.error); rej(response.error);
}) })
.catch((error) => { .catch((error) => {
rej(error.message || "An error occurred"); rej(error.message || 'An error occurred');
}); });
}); });
}; };
export const encryptSingleFunc = async (data: string, secretKeyObject: any) => { export const encryptSingleFunc = async (data: string, secretKeyObject: any) => {
try { try {
return new Promise((res, rej) => { return new Promise((res, rej) => {
window.sendMessage("encryptSingle", { window
data, .sendMessage('encryptSingle', {
secretKeyObject, data,
}) secretKeyObject,
})
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
res(response); res(response);
@ -127,11 +139,12 @@ export const encryptSingleFunc = async (data: string, secretKeyObject: any) => {
rej(response.error); rej(response.error);
}) })
.catch((error) => { .catch((error) => {
rej(error.message || "An error occurred"); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
export const NewThread = ({ export const NewThread = ({
groupInfo, groupInfo,
@ -145,14 +158,14 @@ export const NewThread = ({
postReply, postReply,
myName, myName,
setPostReply, setPostReply,
isPrivate isPrivate,
}: NewMessageProps) => { }: NewMessageProps) => {
const { show } = React.useContext(MyContext); const { show } = React.useContext(MyContext);
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);
const [value, setValue] = useState(""); const [value, setValue] = useState('');
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
const [threadTitle, setThreadTitle] = useState<string>(""); const [threadTitle, setThreadTitle] = useState<string>('');
const [openSnack, setOpenSnack] = React.useState(false); const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null); const [infoSnack, setInfoSnack] = React.useState(null);
const editorRef = useRef(null); const editorRef = useRef(null);
@ -168,44 +181,42 @@ export const NewThread = ({
const closeModal = () => { const closeModal = () => {
setIsOpen(false); setIsOpen(false);
setValue(""); setValue('');
if(setPostReply){ if (setPostReply) {
setPostReply(null) setPostReply(null);
} }
}; };
async function publishQDNResource() { async function publishQDNResource() {
try { try {
pauseAllQueues() pauseAllQueues();
if(isSending) return if (isSending) return;
setIsSending(true) setIsSending(true);
let name: string = ""; let name: string = '';
let errorMsg = ""; let errorMsg = '';
name = userInfo?.name || ""; name = userInfo?.name || '';
const missingFields: string[] = []; const missingFields: string[] = [];
if (!isMessage && !threadTitle) { if (!isMessage && !threadTitle) {
errorMsg = "Please provide a thread title"; errorMsg = 'Please provide a thread title';
} }
if (!name) { if (!name) {
errorMsg = "Cannot send a message without a access to your name"; errorMsg = 'Cannot send a message without a access to your name';
} }
if (!groupInfo) { if (!groupInfo) {
errorMsg = "Cannot access group information"; errorMsg = 'Cannot access group information';
} }
// if (!description) missingFields.push('subject') // if (!description) missingFields.push('subject')
if (missingFields.length > 0) { if (missingFields.length > 0) {
const missingFieldsString = missingFields.join(", "); const missingFieldsString = missingFields.join(', ');
const errMsg = `Missing: ${missingFieldsString}`; const errMsg = `Missing: ${missingFieldsString}`;
errorMsg = errMsg; errorMsg = errMsg;
} }
if (errorMsg) { if (errorMsg) {
// dispatch( // dispatch(
// setNotification({ // setNotification({
@ -217,17 +228,17 @@ export const NewThread = ({
} }
const htmlContent = editorRef.current.getHTML(); const htmlContent = editorRef.current.getHTML();
if (!htmlContent?.trim() || htmlContent?.trim() === "<p></p>") if (!htmlContent?.trim() || htmlContent?.trim() === '<p></p>')
throw new Error("Please provide a first message to the thread"); throw new Error('Please provide a first message to the thread');
const fee = await getFee("ARBITRARY"); const fee = await getFee('ARBITRARY');
let feeToShow = fee.fee; let feeToShow = fee.fee;
if (!isMessage) { if (!isMessage) {
feeToShow = +feeToShow * 2; feeToShow = +feeToShow * 2;
} }
await show({ await show({
message: "Would you like to perform a ARBITRARY transaction?", message: 'Would you like to perform a ARBITRARY transaction?',
publishFee: feeToShow + " QORT", publishFee: feeToShow + ' QORT',
}); });
let reply = null; let reply = null;
@ -245,20 +256,21 @@ export const NewThread = ({
threadOwner: currentThread?.threadData?.name || name, threadOwner: currentThread?.threadData?.name || name,
reply, reply,
}; };
const secretKey = isPrivate === false ? null : await getSecretKey(false, true); const secretKey =
isPrivate === false ? null : await getSecretKey(false, true);
if (!secretKey && isPrivate) { if (!secretKey && isPrivate) {
throw new Error("Cannot get group secret key"); throw new Error('Cannot get group secret key');
} }
if (!isMessage) { if (!isMessage) {
const idThread = uid.rnd(); const idThread = uid.rnd();
const idMsg = uid.rnd(); const idMsg = uid.rnd();
const messageToBase64 = await objectToBase64(mailObject); const messageToBase64 = await objectToBase64(mailObject);
const encryptSingleFirstPost = isPrivate === false ? messageToBase64 : await encryptSingleFunc( const encryptSingleFirstPost =
messageToBase64, isPrivate === false
secretKey ? messageToBase64
); : await encryptSingleFunc(messageToBase64, secretKey);
const threadObject = { const threadObject = {
title: threadTitle, title: threadTitle,
groupId: groupInfo.id, groupId: groupInfo.id,
@ -267,10 +279,10 @@ export const NewThread = ({
}; };
const threadToBase64 = await objectToBase64(threadObject); const threadToBase64 = await objectToBase64(threadObject);
const encryptSingleThread = isPrivate === false ? threadToBase64 : await encryptSingleFunc( const encryptSingleThread =
threadToBase64, isPrivate === false
secretKey ? threadToBase64
); : await encryptSingleFunc(threadToBase64, secretKey);
let identifierThread = `grp-${groupInfo.groupId}-thread-${idThread}`; let identifierThread = `grp-${groupInfo.groupId}-thread-${idThread}`;
await publishGroupEncryptedResource({ await publishGroupEncryptedResource({
identifier: identifierThread, identifier: identifierThread,
@ -288,23 +300,27 @@ export const NewThread = ({
service: 'DOCUMENT', service: 'DOCUMENT',
tempData: threadObject, tempData: threadObject,
created: Date.now(), created: Date.now(),
groupId: groupInfo.groupId groupId: groupInfo.groupId,
} };
const dataToSaveToStoragePost = { const dataToSaveToStoragePost = {
name: myName, name: myName,
identifier: identifierPost, identifier: identifierPost,
service: 'DOCUMENT', service: 'DOCUMENT',
tempData: mailObject, tempData: mailObject,
created: Date.now(), created: Date.now(),
threadId: identifierThread threadId: identifierThread,
} };
await saveTempPublish({data: dataToSaveToStorage, key: 'thread'}) await saveTempPublish({ data: dataToSaveToStorage, key: 'thread' });
await saveTempPublish({data: dataToSaveToStoragePost, key: 'thread-post'}) await saveTempPublish({
setInfoSnack({ data: dataToSaveToStoragePost,
type: "success", key: 'thread-post',
message: "Successfully created thread. It may take some time for the publish to propagate",
}); });
setOpenSnack(true) setInfoSnack({
type: 'success',
message:
'Successfully created thread. It may take some time for the publish to propagate',
});
setOpenSnack(true);
// dispatch( // dispatch(
// setNotification({ // setNotification({
@ -313,35 +329,36 @@ export const NewThread = ({
// }) // })
// ); // );
if (publishCallback) { if (publishCallback) {
publishCallback() publishCallback();
} }
closeModal(); closeModal();
} else { } else {
if (!currentThread) throw new Error('unable to locate thread Id');
if (!currentThread) throw new Error("unable to locate thread Id");
const idThread = currentThread.threadId; const idThread = currentThread.threadId;
const messageToBase64 = await objectToBase64(mailObject); const messageToBase64 = await objectToBase64(mailObject);
const encryptSinglePost = isPrivate === false ? messageToBase64 : await encryptSingleFunc( const encryptSinglePost =
messageToBase64, isPrivate === false
secretKey ? messageToBase64
); : await encryptSingleFunc(messageToBase64, secretKey);
const idMsg = uid.rnd(); const idMsg = uid.rnd();
let identifier = `thmsg-${idThread}-${idMsg}`; let identifier = `thmsg-${idThread}-${idMsg}`;
const res = await publishGroupEncryptedResource({ const res = await publishGroupEncryptedResource({
identifier: identifier, identifier: identifier,
encryptedData: encryptSinglePost, encryptedData: encryptSinglePost,
}); });
const dataToSaveToStoragePost = { const dataToSaveToStoragePost = {
threadId: idThread, threadId: idThread,
name: myName, name: myName,
identifier: identifier, identifier: identifier,
service: 'DOCUMENT', service: 'DOCUMENT',
tempData: mailObject, tempData: mailObject,
created: Date.now() created: Date.now(),
} };
await saveTempPublish({data: dataToSaveToStoragePost, key: 'thread-post'}) await saveTempPublish({
data: dataToSaveToStoragePost,
key: 'thread-post',
});
// await qortalRequest(multiplePublishMsg); // await qortalRequest(multiplePublishMsg);
// dispatch( // dispatch(
// setNotification({ // setNotification({
@ -350,12 +367,13 @@ export const NewThread = ({
// }) // })
// ); // );
setInfoSnack({ setInfoSnack({
type: "success", type: 'success',
message: "Successfully created post. It may take some time for the publish to propagate", message:
'Successfully created post. It may take some time for the publish to propagate',
}); });
setOpenSnack(true) setOpenSnack(true);
if(publishCallback){ if (publishCallback) {
publishCallback() publishCallback();
} }
// messageCallback({ // messageCallback({
// identifier, // identifier,
@ -369,17 +387,16 @@ export const NewThread = ({
closeModal(); closeModal();
} catch (error: any) { } catch (error: any) {
if(error?.message){ if (error?.message) {
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: error?.message, message: error?.message,
}); });
setOpenSnack(true) setOpenSnack(true);
} }
} finally { } finally {
setIsSending(false); setIsSending(false);
resumeAllQueues() resumeAllQueues();
} }
} }
@ -389,56 +406,59 @@ export const NewThread = ({
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
}} }}
> >
<ComposeContainer <ComposeContainer
sx={{ sx={{
padding: isMobile ? '5px' : "15px", padding: isMobile ? '5px' : '15px',
justifyContent: isMobile ? 'flex-start' : 'revert' justifyContent: isMobile ? 'flex-start' : 'revert',
}} }}
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
> >
<ComposeIcon src={ComposeIconSVG} /> <ComposeIcon src={ComposeIconSVG} />
<ComposeP>{currentThread ? "New Post" : "New Thread"}</ComposeP> <ComposeP>{currentThread ? 'New Post' : 'New Thread'}</ComposeP>
</ComposeContainer> </ComposeContainer>
<ReusableModal <ReusableModal
open={isOpen} open={isOpen}
customStyles={{ customStyles={{
maxHeight: isMobile ? '95svh' : "95vh", maxHeight: isMobile ? '95svh' : '95vh',
maxWidth: "950px", maxWidth: '950px',
height: "700px", height: '700px',
borderRadius: "12px 12px 0px 0px", borderRadius: '12px 12px 0px 0px',
background: "#434448", background: '#434448',
padding: "0px", padding: '0px',
gap: "0px", gap: '0px',
}} }}
> >
<InstanceListHeader <InstanceListHeader
sx={{ sx={{
height: isMobile ? 'auto' : "50px", height: isMobile ? 'auto' : '50px',
padding: isMobile ? '5px' : "20px 42px", padding: isMobile ? '5px' : '20px 42px',
flexDirection: "row", flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: "space-between", justifyContent: 'space-between',
backgroundColor: "#434448", backgroundColor: '#434448',
}} }}
> >
<NewMessageHeaderP> <NewMessageHeaderP>
{isMessage ? "Post Message" : "New Thread"} {isMessage ? 'Post Message' : 'New Thread'}
</NewMessageHeaderP> </NewMessageHeaderP>
<CloseContainer sx={{ <CloseContainer
height: '40px' sx={{
}} onClick={closeModal}> height: '40px',
}}
onClick={closeModal}
>
<NewMessageCloseImg src={ModalCloseSVG} /> <NewMessageCloseImg src={ModalCloseSVG} />
</CloseContainer> </CloseContainer>
</InstanceListHeader> </InstanceListHeader>
<InstanceListContainer <InstanceListContainer
sx={{ sx={{
backgroundColor: "#434448", backgroundColor: '#434448',
padding: isMobile ? '5px' : "20px 42px", padding: isMobile ? '5px' : '20px 42px',
height: "calc(100% - 165px)", height: 'calc(100% - 165px)',
flexShrink: 0, flexShrink: 0,
}} }}
> >
@ -457,19 +477,19 @@ export const NewThread = ({
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
sx={{ sx={{
width: "100%", width: '100%',
color: "white", color: 'white',
"& .MuiInput-input::placeholder": { '& .MuiInput-input::placeholder': {
color: "rgba(255,255,255, 0.70) !important", color: 'rgba(255,255,255, 0.70) !important',
fontSize: isMobile ? '14px' : "20px", fontSize: isMobile ? '14px' : '20px',
fontStyle: "normal", fontStyle: 'normal',
fontWeight: 400, fontWeight: 400,
lineHeight: "120%", // 24px lineHeight: '120%', // 24px
letterSpacing: "0.15px", letterSpacing: '0.15px',
opacity: 1, opacity: 1,
}, },
"&:focus": { '&:focus': {
outline: "none", outline: 'none',
}, },
// Add any additional styles for the input here // Add any additional styles for the input here
}} }}
@ -481,21 +501,18 @@ export const NewThread = ({
{postReply && postReply.textContentV2 && ( {postReply && postReply.textContentV2 && (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
maxHeight: "120px", maxHeight: '120px',
overflow: "auto", overflow: 'auto',
}} }}
> >
<MessageDisplay htmlContent={postReply?.textContentV2} /> <MessageDisplay htmlContent={postReply?.textContentV2} />
</Box> </Box>
)} )}
{!isMobile && ( {!isMobile && <Spacer height="30px" />}
<Spacer height="30px" />
)}
<Box <Box
sx={{ sx={{
maxHeight: "40vh", maxHeight: '40vh',
}} }}
> >
<TipTap <TipTap
@ -515,41 +532,44 @@ export const NewThread = ({
</InstanceListContainer> </InstanceListContainer>
<InstanceFooter <InstanceFooter
sx={{ sx={{
backgroundColor: "#434448", backgroundColor: '#434448',
padding: isMobile ? '5px' : "20px 42px", padding: isMobile ? '5px' : '20px 42px',
alignItems: "center", alignItems: 'center',
height: isMobile ? 'auto' : "90px", height: isMobile ? 'auto' : '90px',
}} }}
> >
<NewMessageSendButton onClick={sendMail}> <NewMessageSendButton onClick={sendMail}>
{isSending && ( {isSending && (
<Box sx={{height: '100%', position: 'absolute', width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center'}}> <Box
<CircularProgress sx={{ sx={{
height: '100%',
}} size={'12px'} /> position: 'absolute',
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<CircularProgress sx={{}} size={'12px'} />
</Box> </Box>
)} )}
<NewMessageSendP> <NewMessageSendP>
{isMessage ? "Post" : "Create Thread"} {isMessage ? 'Post' : 'Create Thread'}
</NewMessageSendP> </NewMessageSendP>
{isMessage ? ( {isMessage ? (
<SendNewMessage <SendNewMessage opacity={1} height="25px" width="25px" />
opacity={1}
height="25px"
width="25px"
/>
) : ( ) : (
<CreateThreadIcon <CreateThreadIcon opacity={1} height="25px" width="25px" />
opacity={1}
height="25px"
width="25px"
/>
)} )}
</NewMessageSendButton> </NewMessageSendButton>
</InstanceFooter> </InstanceFooter>
</ReusableModal> </ReusableModal>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} /> <CustomizedSnackbars
open={openSnack}
setOpen={setOpenSnack}
info={infoSnack}
setInfo={setInfoSnack}
/>
</Box> </Box>
); );
}; };

View File

@ -195,7 +195,9 @@ export const Thread = ({
[message.identifier]: fullObject, [message.identifier]: fullObject,
}; };
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const setTempData = async () => { const setTempData = async () => {
@ -216,7 +218,9 @@ export const Thread = ({
}); });
setTempPublishedList(tempData); setTempPublishedList(tempData);
} }
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const getMailMessages = React.useCallback( const getMailMessages = React.useCallback(
@ -461,7 +465,9 @@ export const Thread = ({
} else { } else {
fullArrayMsg.unshift(fullObject); fullArrayMsg.unshift(fullObject);
} }
} catch (error) {} } catch (error) {
console.log(error);
}
} }
setMessages(fullArrayMsg); setMessages(fullArrayMsg);
} catch (error) { } catch (error) {

View File

@ -7,6 +7,7 @@ import {
ListItemAvatar, ListItemAvatar,
ListItemText, ListItemText,
Typography, Typography,
useTheme,
} from '@mui/material'; } from '@mui/material';
import React, { import React, {
useCallback, useCallback,
@ -24,7 +25,6 @@ import CampaignIcon from '@mui/icons-material/Campaign';
import { AddGroup } from './AddGroup'; import { AddGroup } from './AddGroup';
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import CreateIcon from '@mui/icons-material/Create'; import CreateIcon from '@mui/icons-material/Create';
import { import {
AuthenticatedContainerInnerRight, AuthenticatedContainerInnerRight,
CustomButton, CustomButton,
@ -46,7 +46,6 @@ import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
import { GroupAnnouncements } from '../Chat/GroupAnnouncements'; import { GroupAnnouncements } from '../Chat/GroupAnnouncements';
import { GroupForum } from '../Chat/GroupForum'; import { GroupForum } from '../Chat/GroupForum';
import { import {
executeEvent, executeEvent,
@ -56,9 +55,7 @@ import {
import { RequestQueueWithPromise } from '../../utils/queue/queue'; import { RequestQueueWithPromise } from '../../utils/queue/queue';
import { WebSocketActive } from './WebsocketActive'; import { WebSocketActive } from './WebsocketActive';
import { useMessageQueue } from '../../MessageQueueContext'; import { useMessageQueue } from '../../MessageQueueContext';
import { isExtMsg, isUpdateMsg } from '../../background';
import { ContextMenu } from '../ContextMenu'; import { ContextMenu } from '../ContextMenu';
import { ReturnIcon } from '../../assets/Icons/ReturnIcon'; import { ReturnIcon } from '../../assets/Icons/ReturnIcon';
import { ExitIcon } from '../../assets/Icons/ExitIcon'; import { ExitIcon } from '../../assets/Icons/ExitIcon';
import { HomeDesktop } from './HomeDesktop'; import { HomeDesktop } from './HomeDesktop';
@ -113,6 +110,7 @@ export const getPublishesFromAdmins = async (admins: string[], groupId) => {
return sortedData[0]; return sortedData[0];
}; };
interface GroupProps { interface GroupProps {
myAddress: string; myAddress: string;
isFocused: boolean; isFocused: boolean;
@ -134,7 +132,7 @@ export const getGroupAdminsAddress = async (groupNumber: number) => {
`${getBaseApiReact()}/groups/members/${groupNumber}?limit=0&onlyAdmins=true` `${getBaseApiReact()}/groups/members/${groupNumber}?limit=0&onlyAdmins=true`
); );
const groupData = await response.json(); const groupData = await response.json();
let members: any = []; const members: any = [];
if (groupData && Array.isArray(groupData?.members)) { if (groupData && Array.isArray(groupData?.members)) {
for (const member of groupData.members) { for (const member of groupData.members) {
if (member.member) { if (member.member) {
@ -153,7 +151,7 @@ export function validateSecretKey(obj) {
} }
// Iterate over each key in the object // Iterate over each key in the object
for (let key in obj) { for (const key in obj) {
// Ensure the key is a string representation of a positive integer // Ensure the key is a string representation of a positive integer
if (!/^\d+$/.test(key)) { if (!/^\d+$/.test(key)) {
return false; return false;
@ -224,7 +222,9 @@ export const decryptResource = async (data: string, fromQortalRequest) => {
} }
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
export const addDataPublishesFunc = async (data: string, groupId, type) => { export const addDataPublishesFunc = async (data: string, groupId, type) => {
@ -247,7 +247,9 @@ export const addDataPublishesFunc = async (data: string, groupId, type) => {
rej(error.message || 'An error occurred'); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
export const getDataPublishesFunc = async (groupId, type) => { export const getDataPublishesFunc = async (groupId, type) => {
@ -269,7 +271,9 @@ export const getDataPublishesFunc = async (groupId, type) => {
rej(error.message || 'An error occurred'); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
export async function getNameInfo(address: string) { export async function getNameInfo(address: string) {
@ -337,6 +341,7 @@ export const getNames = async (listOfMembers) => {
return members; return members;
}; };
export const getNamesForAdmins = async (admins) => { export const getNamesForAdmins = async (admins) => {
let members: any = []; let members: any = [];
@ -473,22 +478,20 @@ export const Group = ({
} }
setIsOpenSideViewGroups((prev) => !prev); setIsOpenSideViewGroups((prev) => !prev);
}; };
useEffect(() => { useEffect(() => {
timestampEnterDataRef.current = timestampEnterData; timestampEnterDataRef.current = timestampEnterData;
}, [timestampEnterData]); }, [timestampEnterData]);
useEffect(() => { useEffect(() => {
isFocusedRef.current = isFocused; isFocusedRef.current = isFocused;
}, [isFocused]); }, [isFocused]);
useEffect(() => { useEffect(() => {
groupSectionRef.current = groupSection; groupSectionRef.current = groupSection;
}, [groupSection]); }, [groupSection]);
useEffect(() => { useEffect(() => {
selectedGroupRef.current = selectedGroup; selectedGroupRef.current = selectedGroup;
setSelectedGroupId(selectedGroup?.groupId); setSelectedGroupId(selectedGroup?.groupId);
}, [selectedGroup]); }, [selectedGroup]);
useEffect(() => { useEffect(() => {
selectedDirectRef.current = selectedDirect; selectedDirectRef.current = selectedDirect;
}, [selectedDirect]); }, [selectedDirect]);
@ -538,7 +541,9 @@ export const Group = ({
rej(error.message || 'An error occurred'); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const refreshHomeDataFunc = () => { const refreshHomeDataFunc = () => {
@ -565,7 +570,9 @@ export const Group = ({
rej(error.message || 'An error occurred'); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
useEffect(() => { useEffect(() => {
@ -586,7 +593,9 @@ export const Group = ({
data.name = name; data.name = name;
} }
setGroupOwner(data); setGroupOwner(data);
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const directChatHasUnread = useMemo(() => { const directChatHasUnread = useMemo(() => {
@ -755,7 +764,7 @@ export const Group = ({
setAdmins(addresses); setAdmins(addresses);
setAdminsWithNames(both); setAdminsWithNames(both);
} catch (error) { } catch (error) {
//error console.log(error);
} }
}; };
@ -783,7 +792,9 @@ export const Group = ({
); );
const data = await response.json(); const data = await response.json();
if (data && data[0]) return data[0].timestamp; if (data && data[0]) return data[0].timestamp;
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const getLatestRegularChat = async (groups) => { const getLatestRegularChat = async (groups) => {
@ -811,7 +822,9 @@ export const Group = ({
await Promise.all(getGroupData); await Promise.all(getGroupData);
setGroupChatTimestamps(groupData); setGroupChatTimestamps(groupData);
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const getGroupsProperties = useCallback(async (address) => { const getGroupsProperties = useCallback(async (address) => {
@ -826,7 +839,7 @@ export const Group = ({
}, {}); }, {});
setGroupsProperties(transformToObject); setGroupsProperties(transformToObject);
} catch (error) { } catch (error) {
// error console.log(error);
} }
}, []); }, []);
@ -838,6 +851,7 @@ export const Group = ({
Object.keys(groupsProperties) Object.keys(groupsProperties)
) )
) { ) {
// TODO: empty block. Check it!
} else { } else {
getGroupsProperties(myAddress); getGroupsProperties(myAddress);
} }
@ -958,8 +972,11 @@ export const Group = ({
const res = await getGroupMembers(groupId); const res = await getGroupMembers(groupId);
if (groupId !== selectedGroupRef.current?.groupId) return; if (groupId !== selectedGroupRef.current?.groupId) return;
setMembers(res); setMembers(res);
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
useEffect(() => { useEffect(() => {
if ( if (
!initiatedGetMembers.current && !initiatedGetMembers.current &&
@ -1508,6 +1525,7 @@ export const Group = ({
setMobileViewMode('home'); setMobileViewMode('home');
} }
if (!isMobile) { if (!isMobile) {
// TODO: empty block. Check it!
} }
setDesktopViewMode('home'); setDesktopViewMode('home');
@ -1585,27 +1603,29 @@ export const Group = ({
} }
}; };
const theme = useTheme();
const renderDirects = () => { const renderDirects = () => {
return ( return (
<div <div
style={{ style={{
display: 'flex',
width: isMobile ? '100%' : '380px',
flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
background: theme.palette.background.default,
borderRadius: '0px 15px 15px 0px',
display: 'flex',
flexDirection: 'column',
height: isMobile ? `calc(${rootHeight} - 45px)` : '100%', height: isMobile ? `calc(${rootHeight} - 45px)` : '100%',
background: !isMobile && 'var(--bg-primary)', width: isMobile ? '100%' : '380px',
borderRadius: !isMobile && '0px 15px 15px 0px',
}} }}
> >
{!isMobile && ( {!isMobile && (
<Box <Box
sx={{ sx={{
width: '100%',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center',
display: 'flex', display: 'flex',
gap: '10px', gap: '10px',
justifyContent: 'center',
width: '100%',
}} }}
> >
<ButtonBase <ButtonBase
@ -1618,8 +1638,8 @@ export const Group = ({
groupChatHasUnread || groupsAnnHasUnread groupChatHasUnread || groupsAnnHasUnread
? 'var(--unread)' ? 'var(--unread)'
: desktopSideView === 'groups' : desktopSideView === 'groups'
? 'white' ? theme.palette.text.primary
: 'rgba(250, 250, 250, 0.5)' : theme.palette.text.secondary
} }
label="Groups" label="Groups"
selected={desktopSideView === 'groups'} selected={desktopSideView === 'groups'}
@ -1631,8 +1651,8 @@ export const Group = ({
groupChatHasUnread || groupsAnnHasUnread groupChatHasUnread || groupsAnnHasUnread
? 'var(--unread)' ? 'var(--unread)'
: desktopSideView === 'groups' : desktopSideView === 'groups'
? 'white' ? theme.palette.text.primary
: 'rgba(250, 250, 250, 0.5)' : theme.palette.text.secondary
} }
/> />
</IconWrapper> </IconWrapper>
@ -1648,8 +1668,8 @@ export const Group = ({
directChatHasUnread directChatHasUnread
? 'var(--unread)' ? 'var(--unread)'
: desktopSideView === 'directs' : desktopSideView === 'directs'
? 'white' ? theme.palette.text.primary
: 'rgba(250, 250, 250, 0.5)' : theme.palette.text.secondary
} }
label="Messaging" label="Messaging"
selected={desktopSideView === 'directs'} selected={desktopSideView === 'directs'}
@ -1660,8 +1680,8 @@ export const Group = ({
directChatHasUnread directChatHasUnread
? 'var(--unread)' ? 'var(--unread)'
: desktopSideView === 'directs' : desktopSideView === 'directs'
? 'white' ? theme.palette.text.primary
: 'rgba(250, 250, 250, 0.5)' : theme.palette.text.secondary
} }
/> />
</IconWrapper> </IconWrapper>
@ -1671,12 +1691,12 @@ export const Group = ({
<div <div
style={{ style={{
display: 'flex',
width: '100%',
flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
display: 'flex',
flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
overflowY: 'auto', overflowY: 'auto',
width: '100%',
}} }}
> >
{directs.map((direct: any) => ( {directs.map((direct: any) => (
@ -1715,11 +1735,14 @@ export const Group = ({
width: '100%', width: '100%',
flexDirection: 'column', flexDirection: 'column',
cursor: 'pointer', cursor: 'pointer',
border: '1px #232428 solid', borderColor: theme.palette.primary,
borderWidth: '1px',
borderStyle: 'solid',
padding: '2px', padding: '2px',
borderRadius: '2px', borderRadius: '2px',
background: background:
direct?.address === selectedDirect?.address && 'white', direct?.address === selectedDirect?.address &&
theme.palette.background.default,
}} }}
> >
<Box <Box
@ -1732,8 +1755,8 @@ export const Group = ({
<ListItemAvatar> <ListItemAvatar>
<Avatar <Avatar
sx={{ sx={{
background: '#232428', background: theme.palette.background.default,
color: 'white', color: theme.palette.text.primary,
}} }}
alt={direct?.name || direct?.address} alt={direct?.name || direct?.address}
> >
@ -1751,7 +1774,7 @@ export const Group = ({
style: { style: {
color: color:
direct?.address === selectedDirect?.address && direct?.address === selectedDirect?.address &&
'black', theme.palette.text.primary,
textWrap: 'wrap', textWrap: 'wrap',
overflow: 'hidden', overflow: 'hidden',
}, },
@ -1760,7 +1783,7 @@ export const Group = ({
style: { style: {
color: color:
direct?.address === selectedDirect?.address && direct?.address === selectedDirect?.address &&
'black', theme.palette.text.primary,
fontSize: '12px', fontSize: '12px',
}, },
}} }}
@ -1805,7 +1828,7 @@ export const Group = ({
> >
<CreateIcon <CreateIcon
sx={{ sx={{
color: 'white', color: theme.palette.text.primary,
}} }}
/> />
New Chat New Chat
@ -1824,7 +1847,7 @@ export const Group = ({
flexDirection: 'column', flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
height: isMobile ? `calc(${rootHeight} - 45px)` : '100%', height: isMobile ? `calc(${rootHeight} - 45px)` : '100%',
background: !isMobile && 'var(--bg-primary)', background: !isMobile && theme.palette.background.default,
borderRadius: !isMobile && '0px 15px 15px 0px', borderRadius: !isMobile && '0px 15px 15px 0px',
}} }}
> >
@ -1848,8 +1871,8 @@ export const Group = ({
groupChatHasUnread || groupsAnnHasUnread groupChatHasUnread || groupsAnnHasUnread
? 'var(--unread)' ? 'var(--unread)'
: desktopSideView === 'groups' : desktopSideView === 'groups'
? 'white' ? theme.palette.text.primary
: 'rgba(250, 250, 250, 0.5)' : theme.palette.text.secondary
} }
label="Groups" label="Groups"
selected={desktopSideView === 'groups'} selected={desktopSideView === 'groups'}
@ -1861,8 +1884,8 @@ export const Group = ({
groupChatHasUnread || groupsAnnHasUnread groupChatHasUnread || groupsAnnHasUnread
? 'var(--unread)' ? 'var(--unread)'
: desktopSideView === 'groups' : desktopSideView === 'groups'
? 'white' ? theme.palette.text.primary
: 'rgba(250, 250, 250, 0.5)' : theme.palette.text.secondary
} }
/> />
</IconWrapper> </IconWrapper>
@ -1878,8 +1901,8 @@ export const Group = ({
directChatHasUnread directChatHasUnread
? 'var(--unread)' ? 'var(--unread)'
: desktopSideView === 'directs' : desktopSideView === 'directs'
? 'white' ? theme.palette.text.primary
: 'rgba(250, 250, 250, 0.5)' : theme.palette.text.secondary
} }
label="Messaging" label="Messaging"
selected={desktopSideView === 'directs'} selected={desktopSideView === 'directs'}
@ -1890,8 +1913,8 @@ export const Group = ({
directChatHasUnread directChatHasUnread
? 'var(--unread)' ? 'var(--unread)'
: desktopSideView === 'directs' : desktopSideView === 'directs'
? 'white' ? theme.palette.text.primary
: 'rgba(250, 250, 250, 0.5)' : theme.palette.text.secondary
} }
/> />
</IconWrapper> </IconWrapper>
@ -1901,15 +1924,15 @@ export const Group = ({
<div <div
style={{ style={{
display: 'flex',
width: '100%',
flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
display: 'flex',
flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
overflowY: 'auto',
visibility: chatMode === 'directs' && 'hidden',
position: chatMode === 'directs' && 'fixed',
left: chatMode === 'directs' && '-1000px', left: chatMode === 'directs' && '-1000px',
overflowY: 'auto',
position: chatMode === 'directs' && 'fixed',
visibility: chatMode === 'directs' && 'hidden',
width: '100%',
}} }}
> >
{groups.map((group: any) => ( {groups.map((group: any) => (
@ -1951,14 +1974,17 @@ export const Group = ({
}} }}
sx={{ sx={{
display: 'flex', display: 'flex',
width: '100%',
flexDirection: 'column',
cursor: 'pointer',
border: '1px #232428 solid',
padding: '2px',
borderRadius: '2px',
background: background:
group?.groupId === selectedGroup?.groupId && 'white', group?.groupId === selectedGroup?.groupId &&
theme.palette.background.default,
borderColor: theme.palette.primary,
borderRadius: '2px',
borderStyle: 'solid',
borderWidth: '1px',
cursor: 'pointer',
flexDirection: 'column',
padding: '2px',
width: '100%',
}} }}
> >
<ContextMenu <ContextMenu
@ -1968,22 +1994,22 @@ export const Group = ({
> >
<Box <Box
sx={{ sx={{
alignItems: 'center',
display: 'flex', display: 'flex',
width: '100%', width: '100%',
alignItems: 'center',
}} }}
> >
<ListItemAvatar> <ListItemAvatar>
{groupsProperties[group?.groupId]?.isOpen === false ? ( {groupsProperties[group?.groupId]?.isOpen === false ? (
<Box <Box
sx={{ sx={{
width: '40px',
height: '40px',
borderRadius: '50%',
background: '#232428',
display: 'flex',
alignItems: 'center', alignItems: 'center',
background: theme.palette.background.default,
borderRadius: '50%',
display: 'flex',
height: '40px',
justifyContent: 'center', justifyContent: 'center',
width: '40px',
}} }}
> >
<LockIcon <LockIcon
@ -1995,13 +2021,13 @@ export const Group = ({
) : ( ) : (
<Box <Box
sx={{ sx={{
width: '40px',
height: '40px',
borderRadius: '50%',
background: '#232428',
display: 'flex',
alignItems: 'center', alignItems: 'center',
background: theme.palette.background.default,
borderRadius: '50%',
display: 'flex',
height: '40px',
justifyContent: 'center', justifyContent: 'center',
width: '40px',
}} }}
> >
<NoEncryptionGmailerrorredIcon <NoEncryptionGmailerrorredIcon
@ -2010,15 +2036,6 @@ export const Group = ({
}} }}
/> />
</Box> </Box>
// <Avatar
// sx={{
// background: "#232428",
// color: "white",
// }}
// alt={group?.groupName}
// >
// {group.groupName?.charAt(0)}
// </Avatar>
)} )}
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
@ -2034,14 +2051,14 @@ export const Group = ({
style: { style: {
color: color:
group?.groupId === selectedGroup?.groupId && group?.groupId === selectedGroup?.groupId &&
'black', theme.palette.text.primary,
}, },
}} // Change the color of the primary text }} // Change the color of the primary text
secondaryTypographyProps={{ secondaryTypographyProps={{
style: { style: {
color: color:
group?.groupId === selectedGroup?.groupId && group?.groupId === selectedGroup?.groupId &&
'black', theme.palette.text.primary,
fontSize: '12px', fontSize: '12px',
}, },
}} }}
@ -2084,10 +2101,10 @@ export const Group = ({
<div <div
style={{ style={{
display: 'flex', display: 'flex',
width: '100%', gap: '10px',
justifyContent: 'center', justifyContent: 'center',
padding: '10px', padding: '10px',
gap: '10px', width: '100%',
}} }}
> >
{chatMode === 'groups' && ( {chatMode === 'groups' && (
@ -2099,11 +2116,12 @@ export const Group = ({
> >
<AddCircleOutlineIcon <AddCircleOutlineIcon
sx={{ sx={{
color: 'white', color: theme.palette.text.primary,
}} }}
/> />
Group Mgmt Group
</CustomButton> </CustomButton>
{!isRunningPublicNode && ( {!isRunningPublicNode && (
<CustomButton <CustomButton
onClick={() => { onClick={() => {
@ -2116,7 +2134,7 @@ export const Group = ({
> >
<PersonOffIcon <PersonOffIcon
sx={{ sx={{
color: 'white', color: theme.palette.text.primary,
}} }}
/> />
</CustomButton> </CustomButton>
@ -2133,7 +2151,7 @@ export const Group = ({
> >
<CreateIcon <CreateIcon
sx={{ sx={{
color: 'white', color: theme.palette.text.primary,
}} }}
/> />
New Chat New Chat
@ -2150,6 +2168,7 @@ export const Group = ({
myAddress={myAddress} myAddress={myAddress}
setIsLoadingGroups={setIsLoadingGroups} setIsLoadingGroups={setIsLoadingGroups}
/> />
<CustomizedSnackbars <CustomizedSnackbars
open={openSnack} open={openSnack}
setOpen={setOpenSnack} setOpen={setOpenSnack}
@ -2159,11 +2178,11 @@ export const Group = ({
<div <div
style={{ style={{
display: 'flex',
width: '100%',
height: isMobile ? '100%' : '100%',
flexDirection: 'row',
alignItems: 'flex-start', alignItems: 'flex-start',
display: 'flex',
flexDirection: 'row',
height: isMobile ? '100%' : '100%',
width: '100%',
}} }}
> >
{!isMobile && {!isMobile &&
@ -2191,6 +2210,7 @@ export const Group = ({
desktopViewMode === 'chat' && desktopViewMode === 'chat' &&
desktopSideView !== 'directs' && desktopSideView !== 'directs' &&
renderGroups()} renderGroups()}
{!isMobile && {!isMobile &&
desktopViewMode === 'chat' && desktopViewMode === 'chat' &&
desktopSideView === 'directs' && desktopSideView === 'directs' &&
@ -2214,26 +2234,26 @@ export const Group = ({
{isMobile && ( {isMobile && (
<Box <Box
sx={{ sx={{
display: 'flex',
alignItems: 'center', alignItems: 'center',
width: '100%', display: 'flex',
marginTop: '14px',
justifyContent: 'center',
height: '15px', height: '15px',
justifyContent: 'center',
marginTop: '14px',
width: '100%',
}} }}
> >
<Box <Box
sx={{ sx={{
display: 'flex',
alignItems: 'center', alignItems: 'center',
display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
width: '320px', width: '320px',
}} }}
> >
<Box <Box
sx={{ sx={{
display: 'flex',
alignItems: 'center', alignItems: 'center',
display: 'flex',
width: '50px', width: '50px',
}} }}
> >
@ -2248,10 +2268,10 @@ export const Group = ({
<Box <Box
sx={{ sx={{
display: 'flex',
alignItems: 'center', alignItems: 'center',
width: '50px', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'flex-end',
width: '50px',
}} }}
> >
<ButtonBase <ButtonBase
@ -2268,17 +2288,15 @@ export const Group = ({
)} )}
<Box <Box
sx={{ sx={{
position: 'absolute', background: theme.palette.background.default,
right: !(desktopViewMode === 'chat') ? 'unset' : '0px',
bottom: !(desktopViewMode === 'chat') ? 'unset' : '0px', bottom: !(desktopViewMode === 'chat') ? 'unset' : '0px',
top: !(desktopViewMode === 'chat') ? 'unset' : '0px',
background: '#27282c',
zIndex: 5,
height: isMobile && `calc(${rootHeight} - 45px)`, height: isMobile && `calc(${rootHeight} - 45px)`,
opacity: !(desktopViewMode === 'chat') ? 0 : 1,
left: !(desktopViewMode === 'chat') ? '-100000px' : '0px', left: !(desktopViewMode === 'chat') ? '-100000px' : '0px',
opacity: !(desktopViewMode === 'chat') ? 0 : 1,
position: 'absolute',
right: !(desktopViewMode === 'chat') ? 'unset' : '0px',
top: !(desktopViewMode === 'chat') ? 'unset' : '0px',
zIndex: 5,
}} }}
> >
<ChatDirect <ChatDirect
@ -2292,7 +2310,6 @@ export const Group = ({
balance={balance} balance={balance}
close={() => { close={() => {
setSelectedDirect(null); setSelectedDirect(null);
setNewChat(false); setNewChat(false);
}} }}
setMobileViewModeKeepOpen={setMobileViewModeKeepOpen} setMobileViewModeKeepOpen={setMobileViewModeKeepOpen}
@ -2303,18 +2320,18 @@ export const Group = ({
{desktopViewMode === 'chat' && !selectedGroup && ( {desktopViewMode === 'chat' && !selectedGroup && (
<Box <Box
sx={{ sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
display: 'flex',
height: '100%', height: '100%',
justifyContent: 'center',
width: '100%',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: '14px', fontSize: '14px',
fontWeight: 400, fontWeight: 400,
color: 'rgba(255, 255, 255, 0.2)', color: theme.palette.text.primary,
}} }}
> >
No group selected No group selected
@ -2407,12 +2424,12 @@ export const Group = ({
!secretKeyPublishDate && ( !secretKeyPublishDate && (
<div <div
style={{ style={{
display: 'flex',
width: '100%',
height: '100%',
flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
display: 'flex',
flexDirection: 'column',
height: '100%',
padding: '20px', padding: '20px',
width: '100%',
}} }}
> >
{' '} {' '}
@ -2534,9 +2551,9 @@ export const Group = ({
<Box <Box
sx={{ sx={{
bottom: '25px',
display: 'flex', display: 'flex',
position: 'absolute', position: 'absolute',
bottom: '25px',
right: '25px', right: '25px',
zIndex: 100, zIndex: 100,
}} }}
@ -2586,24 +2603,23 @@ export const Group = ({
<> <>
<Box <Box
sx={{ sx={{
background: theme.palette.background.default,
bottom: !(desktopViewMode === 'chat') ? 'unset' : '0px',
height: isMobile && `calc(${rootHeight} - 45px)`,
left: !(desktopViewMode === 'chat') ? '-100000px' : '0px',
opacity: !(desktopViewMode === 'chat') ? 0 : 1,
position: 'absolute', position: 'absolute',
right: !(desktopViewMode === 'chat') ? 'unset' : '0px', right: !(desktopViewMode === 'chat') ? 'unset' : '0px',
bottom: !(desktopViewMode === 'chat') ? 'unset' : '0px',
top: !(desktopViewMode === 'chat') ? 'unset' : '0px', top: !(desktopViewMode === 'chat') ? 'unset' : '0px',
background: '#27282c',
zIndex: 5, zIndex: 5,
height: isMobile && `calc(${rootHeight} - 45px)`,
opacity: !(desktopViewMode === 'chat') ? 0 : 1,
left: !(desktopViewMode === 'chat') ? '-100000px' : '0px',
}} }}
> >
<Box <Box
sx={{ sx={{
position: 'relative',
flexGrow: 1,
display: 'flex', display: 'flex',
flexGrow: 1,
height: '100%', height: '100%',
position: 'relative',
}} }}
> >
<ChatDirect <ChatDirect

View File

@ -1,22 +1,17 @@
import * as React from "react"; import * as React from 'react';
import List from "@mui/material/List"; import List from '@mui/material/List';
import ListItem from "@mui/material/ListItem"; import ListItem from '@mui/material/ListItem';
import ListItemButton from "@mui/material/ListItemButton"; import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from '@mui/material/ListItemText';
import ListItemText from "@mui/material/ListItemText"; import IconButton from '@mui/material/IconButton';
import Checkbox from "@mui/material/Checkbox"; import GroupAddIcon from '@mui/icons-material/GroupAdd';
import IconButton from "@mui/material/IconButton"; import { executeEvent } from '../../utils/events';
import CommentIcon from "@mui/icons-material/Comment"; import { Box, ButtonBase, Collapse, Typography, useTheme } from '@mui/material';
import InfoIcon from "@mui/icons-material/Info"; import { getGroupNames } from './UserListOfInvites';
import GroupAddIcon from "@mui/icons-material/GroupAdd"; import { CustomLoader } from '../../common/CustomLoader';
import { executeEvent } from "../../utils/events"; import { getBaseApiReact, isMobile } from '../../App';
import { Box, ButtonBase, Collapse, Typography } from "@mui/material"; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { Spacer } from "../../common/Spacer"; import ExpandLessIcon from '@mui/icons-material/ExpandLess';
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 }) => { export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState( const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState(
@ -37,11 +32,14 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
setGroupsWithJoinRequests(resMoreData); setGroupsWithJoinRequests(resMoreData);
} catch (error) { } catch (error) {
console.log(error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const theme = useTheme();
React.useEffect(() => { React.useEffect(() => {
if (myAddress) { if (myAddress) {
getJoinRequests(); getJoinRequests();
@ -51,57 +49,65 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
return ( return (
<Box <Box
sx={{ sx={{
width: "100%", alignItems: 'center',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", width: '100%',
}} }}
> >
<ButtonBase <ButtonBase
sx={{ sx={{
width: "322px", display: 'flex',
display: "flex", flexDirection: 'row',
flexDirection: "row",
padding: "0px 20px",
gap: '10px', gap: '10px',
justifyContent: 'flex-start' justifyContent: 'flex-start',
padding: '0px 20px',
width: '322px',
}} }}
onClick={()=> setIsExpanded((prev)=> !prev)} onClick={() => setIsExpanded((prev) => !prev)}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "1rem", fontSize: '1rem',
}} }}
> >
Group Invites {groupsWithJoinRequests?.length > 0 && ` (${groupsWithJoinRequests?.length})`} Group Invites{' '}
{groupsWithJoinRequests?.length > 0 &&
` (${groupsWithJoinRequests?.length})`}
</Typography> </Typography>
{isExpanded ? <ExpandLessIcon sx={{ {isExpanded ? (
marginLeft: 'auto' <ExpandLessIcon
}} /> : ( sx={{
<ExpandMoreIcon sx={{ marginLeft: 'auto',
marginLeft: 'auto' }}
}}/> />
)} ) : (
<ExpandMoreIcon
sx={{
marginLeft: 'auto',
}}
/>
)}
</ButtonBase> </ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit> <Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box <Box
sx={{ sx={{
width: "322px", bgcolor: theme.palette.background.paper,
height: isMobile ? "165px" : "250px", borderRadius: '19px',
display: 'flex',
display: "flex", flexDirection: 'column',
flexDirection: "column", height: isMobile ? '165px' : '250px',
bgcolor: "background.paper", padding: '20px',
padding: "20px", width: '322px',
borderRadius: "19px",
}} }}
> >
{loading && groupsWithJoinRequests.length === 0 && ( {loading && groupsWithJoinRequests.length === 0 && (
<Box <Box
sx={{ sx={{
width: "100%", display: 'flex',
display: "flex", justifyContent: 'center',
justifyContent: "center", width: '100%',
}} }}
> >
<CustomLoader /> <CustomLoader />
@ -110,18 +116,18 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
{!loading && groupsWithJoinRequests.length === 0 && ( {!loading && groupsWithJoinRequests.length === 0 && (
<Box <Box
sx={{ sx={{
width: "100%", alignItems: 'center',
display: "flex", display: 'flex',
justifyContent: "center", height: '100%',
alignItems: "center", justifyContent: 'center',
height: "100%", width: '100%',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "11px", color: theme.palette.text.primary,
fontSize: '11px',
fontWeight: 400, fontWeight: 400,
color: "rgba(255, 255, 255, 0.2)",
}} }}
> >
Nothing to display Nothing to display
@ -130,11 +136,11 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
)} )}
<List <List
sx={{ sx={{
width: "100%", width: '100%',
maxWidth: 360, maxWidth: 360,
bgcolor: "background.paper", bgcolor: theme.palette.background.paper,
maxHeight: "300px", maxHeight: '300px',
overflow: "auto", overflow: 'auto',
}} }}
className="scrollable-container" className="scrollable-container"
> >
@ -142,13 +148,13 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
return ( return (
<ListItem <ListItem
sx={{ sx={{
marginBottom: "20px", marginBottom: '20px',
}} }}
key={group?.groupId} key={group?.groupId}
onClick={() => { onClick={() => {
setOpenAddGroup(true); setOpenAddGroup(true);
setTimeout(() => { setTimeout(() => {
executeEvent("openGroupInvitesRequest", {}); executeEvent('openGroupInvitesRequest', {});
}, 300); }, 300);
}} }}
disablePadding disablePadding
@ -156,8 +162,8 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
<IconButton edge="end" aria-label="comments"> <IconButton edge="end" aria-label="comments">
<GroupAddIcon <GroupAddIcon
sx={{ sx={{
color: "white", color: theme.palette.text.primary,
fontSize: "18px", fontSize: '18px',
}} }}
/> />
</IconButton> </IconButton>
@ -166,8 +172,8 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
<ListItemButton disableRipple role={undefined} dense> <ListItemButton disableRipple role={undefined} dense>
<ListItemText <ListItemText
sx={{ sx={{
"& .MuiTypography-root": { '& .MuiTypography-root': {
fontSize: "13px", fontSize: '13px',
fontWeight: 400, fontWeight: 400,
}, },
}} }}

View File

@ -1,4 +1,4 @@
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from '@mui/lab';
import { import {
Box, Box,
Button, Button,
@ -6,45 +6,46 @@ import {
MenuItem, MenuItem,
Select, Select,
SelectChangeEvent, SelectChangeEvent,
} from "@mui/material"; } from '@mui/material';
import React, { useState } from "react"; import React, { useState } from 'react';
import { Spacer } from "../../common/Spacer"; import { Spacer } from '../../common/Spacer';
import { Label } from "./AddGroup"; import { Label } from './AddGroup';
import { getFee } from "../../background"; import { getFee } from '../../background';
export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => { export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
const [value, setValue] = useState(""); const [value, setValue] = useState('');
const [expiryTime, setExpiryTime] = useState<string>('259200'); const [expiryTime, setExpiryTime] = useState<string>('259200');
const [isLoadingInvite, setIsLoadingInvite] = useState(false) const [isLoadingInvite, setIsLoadingInvite] = useState(false);
const inviteMember = async () => { const inviteMember = async () => {
try { try {
const fee = await getFee('GROUP_INVITE') const fee = await getFee('GROUP_INVITE');
await show({ await show({
message: "Would you like to perform a GROUP_INVITE transaction?" , message: 'Would you like to perform a GROUP_INVITE transaction?',
publishFee: fee.fee + ' QORT' publishFee: fee.fee + ' QORT',
}) });
setIsLoadingInvite(true) setIsLoadingInvite(true);
if (!expiryTime || !value) return; if (!expiryTime || !value) return;
new Promise((res, rej) => { new Promise((res, rej) => {
window.sendMessage("inviteToGroup", { window
groupId, .sendMessage('inviteToGroup', {
qortalAddress: value, groupId,
inviteTime: +expiryTime, qortalAddress: value,
}) inviteTime: +expiryTime,
})
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
setInfoSnack({ setInfoSnack({
type: "success", type: 'success',
message: `Successfully invited ${value}. It may take a couple of minutes for the changes to propagate`, message: `Successfully invited ${value}. It may take a couple of minutes for the changes to propagate`,
}); });
setOpenSnack(true); setOpenSnack(true);
res(response); res(response);
setValue(""); setValue('');
return; return;
} }
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: response?.error, message: response?.error,
}); });
setOpenSnack(true); setOpenSnack(true);
@ -52,16 +53,17 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
}) })
.catch((error) => { .catch((error) => {
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: error?.message || "An error occurred", message: error?.message || 'An error occurred',
}); });
setOpenSnack(true); setOpenSnack(true);
rej(error); rej(error);
}); });
}); });
} catch (error) {} finally { } catch (error) {
setIsLoadingInvite(false) console.log(error);
} finally {
setIsLoadingInvite(false);
} }
}; };
@ -72,8 +74,8 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
}} }}
> >
Invite member Invite member
@ -83,8 +85,7 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
placeholder="Name or address" placeholder="Name or address"
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
/> />
<Spacer height="20px" /> <Spacer height="20px" />
<Label>Invitation Expiry Time</Label> <Label>Invitation Expiry Time</Label>
<Select <Select
labelId="demo-simple-select-label" labelId="demo-simple-select-label"
@ -105,7 +106,14 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
<MenuItem value={2592000}>30 days</MenuItem> <MenuItem value={2592000}>30 days</MenuItem>
</Select> </Select>
<Spacer height="20px" /> <Spacer height="20px" />
<LoadingButton variant="contained" loadingPosition="start" loading={isLoadingInvite} onClick={inviteMember}>Invite</LoadingButton> <LoadingButton
variant="contained"
loadingPosition="start"
loading={isLoadingInvite}
onClick={inviteMember}
>
Invite
</LoadingButton>
</Box> </Box>
); );
}; };

View File

@ -4,7 +4,7 @@ import React, {
useEffect, useEffect,
useRef, useRef,
useState, useState,
} from "react"; } from 'react';
import { import {
Avatar, Avatar,
Box, Box,
@ -25,44 +25,44 @@ import {
Select, Select,
TextField, TextField,
Typography, Typography,
} from "@mui/material"; } from '@mui/material';
import { getNameInfo } from "./Group"; import { getNameInfo } from './Group';
import { getBaseApi, getFee } from "../../background"; import { getBaseApi, getFee } from '../../background';
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from '@mui/lab';
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from '@mui/icons-material/Lock';
import NoEncryptionGmailerrorredIcon from "@mui/icons-material/NoEncryptionGmailerrorred"; import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
import { import {
MyContext, MyContext,
getArbitraryEndpointReact, getArbitraryEndpointReact,
getBaseApiReact, getBaseApiReact,
isMobile, isMobile,
} from "../../App"; } from '../../App';
import { Spacer } from "../../common/Spacer"; import { Spacer } from '../../common/Spacer';
import { CustomLoader } from "../../common/CustomLoader"; import { CustomLoader } from '../../common/CustomLoader';
import { RequestQueueWithPromise } from "../../utils/queue/queue"; import { RequestQueueWithPromise } from '../../utils/queue/queue';
import { useRecoilState } from "recoil"; import { useRecoilState } from 'recoil';
import { import {
myGroupsWhereIAmAdminAtom, myGroupsWhereIAmAdminAtom,
promotionTimeIntervalAtom, promotionTimeIntervalAtom,
promotionsAtom, promotionsAtom,
} from "../../atoms/global"; } from '../../atoms/global';
import { Label } from "./AddGroup"; import { Label } from './AddGroup';
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from 'short-unique-id';
import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { getGroupNames } from "./UserListOfInvites"; import { getGroupNames } from './UserListOfInvites';
import { WrapperUserAction } from "../WrapperUserAction"; import { WrapperUserAction } from '../WrapperUserAction';
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from '@tanstack/react-virtual';
import ErrorBoundary from "../../common/ErrorBoundary"; import ErrorBoundary from '../../common/ErrorBoundary';
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from "@mui/icons-material/ExpandLess"; import ExpandLessIcon from '@mui/icons-material/ExpandLess';
export const requestQueuePromos = new RequestQueueWithPromise(20); export const requestQueuePromos = new RequestQueueWithPromise(20);
export function utf8ToBase64(inputString: string): string { export function utf8ToBase64(inputString: string): string {
// Encode the string as UTF-8 // Encode the string as UTF-8
const utf8String = encodeURIComponent(inputString).replace( const utf8String = encodeURIComponent(inputString).replace(
/%([0-9A-F]{2})/g, /%([0-9A-F]{2})/g,
(match, p1) => String.fromCharCode(Number("0x" + p1)) (match, p1) => String.fromCharCode(Number('0x' + p1))
); );
// Convert the UTF-8 encoded string to base64 // Convert the UTF-8 encoded string to base64
@ -83,7 +83,7 @@ export const ListOfGroupPromotions = () => {
const [selectedGroup, setSelectedGroup] = useState(null); const [selectedGroup, setSelectedGroup] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isShowModal, setIsShowModal] = useState(false); const [isShowModal, setIsShowModal] = useState(false);
const [text, setText] = useState(""); const [text, setText] = useState('');
const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState( const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState(
myGroupsWhereIAmAdminAtom myGroupsWhereIAmAdminAtom
); );
@ -115,10 +115,12 @@ export const ListOfGroupPromotions = () => {
useEffect(() => { useEffect(() => {
try { try {
(async () => { (async () => {
const feeRes = await getFee("ARBITRARY"); const feeRes = await getFee('ARBITRARY');
setFee(feeRes?.fee); setFee(feeRes?.fee);
})(); })();
} catch (error) {} } catch (error) {
console.log(error);
}
}, []); }, []);
const getPromotions = useCallback(async () => { const getPromotions = useCallback(async () => {
try { try {
@ -126,9 +128,9 @@ export const ListOfGroupPromotions = () => {
const identifier = `group-promotions-ui24-`; const identifier = `group-promotions-ui24-`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
const responseData = await response.json(); const responseData = await response.json();
@ -142,7 +144,7 @@ export const ListOfGroupPromotions = () => {
promo.name promo.name
}/${promo.identifier}`; }/${promo.identifier}`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
}); });
try { try {
@ -164,7 +166,7 @@ export const ListOfGroupPromotions = () => {
} }
} }
} catch (error) { } catch (error) {
console.error("Error fetching promo:", error); console.error('Error fetching promo:', error);
} }
}); });
} }
@ -222,10 +224,10 @@ export const ListOfGroupPromotions = () => {
await new Promise((res, rej) => { await new Promise((res, rej) => {
window window
.sendMessage("publishOnQDN", { .sendMessage('publishOnQDN', {
data: data, data: data,
identifier: identifier, identifier: identifier,
service: "DOCUMENT", service: 'DOCUMENT',
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
@ -235,23 +237,23 @@ export const ListOfGroupPromotions = () => {
rej(response.error); rej(response.error);
}) })
.catch((error) => { .catch((error) => {
rej(error.message || "An error occurred"); rej(error.message || 'An error occurred');
}); });
}); });
setInfoSnack({ setInfoSnack({
type: "success", type: 'success',
message: message:
"Successfully published promotion. It may take a couple of minutes for the promotion to appear", 'Successfully published promotion. It may take a couple of minutes for the promotion to appear',
}); });
setOpenSnack(true); setOpenSnack(true);
setText(""); setText('');
setSelectedGroup(null); setSelectedGroup(null);
setIsShowModal(false); setIsShowModal(false);
} catch (error) { } catch (error) {
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: message:
error?.message || "Error publishing the promotion. Please try again", error?.message || 'Error publishing the promotion. Please try again',
}); });
setOpenSnack(true); setOpenSnack(true);
} finally { } finally {
@ -262,30 +264,30 @@ export const ListOfGroupPromotions = () => {
const handleJoinGroup = async (group, isOpen) => { const handleJoinGroup = async (group, isOpen) => {
try { try {
const groupId = group.groupId; const groupId = group.groupId;
const fee = await getFee("JOIN_GROUP"); const fee = await getFee('JOIN_GROUP');
await show({ await show({
message: "Would you like to perform an JOIN_GROUP transaction?", message: 'Would you like to perform an JOIN_GROUP transaction?',
publishFee: fee.fee + " QORT", publishFee: fee.fee + ' QORT',
}); });
setIsLoadingJoinGroup(true); setIsLoadingJoinGroup(true);
await new Promise((res, rej) => { await new Promise((res, rej) => {
window window
.sendMessage("joinGroup", { .sendMessage('joinGroup', {
groupId, groupId,
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
setInfoSnack({ setInfoSnack({
type: "success", type: 'success',
message: message:
"Successfully requested to join group. It may take a couple of minutes for the changes to propagate", 'Successfully requested to join group. It may take a couple of minutes for the changes to propagate',
}); });
if (isOpen) { if (isOpen) {
setTxList((prev) => [ setTxList((prev) => [
{ {
...response, ...response,
type: "joined-group", type: 'joined-group',
label: `Joined Group ${group?.groupName}: awaiting confirmation`, label: `Joined Group ${group?.groupName}: awaiting confirmation`,
labelDone: `Joined Group ${group?.groupName}: success!`, labelDone: `Joined Group ${group?.groupName}: success!`,
done: false, done: false,
@ -297,7 +299,7 @@ export const ListOfGroupPromotions = () => {
setTxList((prev) => [ setTxList((prev) => [
{ {
...response, ...response,
type: "joined-group-request", type: 'joined-group-request',
label: `Requested to join Group ${group?.groupName}: awaiting confirmation`, label: `Requested to join Group ${group?.groupName}: awaiting confirmation`,
labelDone: `Requested to join Group ${group?.groupName}: success!`, labelDone: `Requested to join Group ${group?.groupName}: success!`,
done: false, done: false,
@ -313,7 +315,7 @@ export const ListOfGroupPromotions = () => {
return; return;
} else { } else {
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: response?.error, message: response?.error,
}); });
setOpenSnack(true); setOpenSnack(true);
@ -322,8 +324,8 @@ export const ListOfGroupPromotions = () => {
}) })
.catch((error) => { .catch((error) => {
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: error.message || "An error occurred", message: error.message || 'An error occurred',
}); });
setOpenSnack(true); setOpenSnack(true);
rej(error); rej(error);
@ -339,55 +341,58 @@ export const ListOfGroupPromotions = () => {
return ( return (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
display: "flex", display: 'flex',
marginTop: "20px", marginTop: '20px',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
}} }}
> >
<Box sx={{ <Box
display: 'flex', sx={{
gap: '20px', display: 'flex',
width: '100%', gap: '20px',
justifyContent: 'space-between' width: '100%',
}}> justifyContent: 'space-between',
}}
>
<ButtonBase <ButtonBase
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'row',
padding: `0px ${isExpanded ? "24px" : "20px"}`, padding: `0px ${isExpanded ? '24px' : '20px'}`,
gap: "10px", gap: '10px',
justifyContent: "flex-start", justifyContent: 'flex-start',
alignSelf: isExpanded && "flex-start", alignSelf: isExpanded && 'flex-start',
}} }}
onClick={() => setIsExpanded((prev) => !prev)} onClick={() => setIsExpanded((prev) => !prev)}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "1rem", fontSize: '1rem',
}} }}
> >
Group promotions {promotions.length > 0 && ` (${promotions.length})`} Group promotions{' '}
{promotions.length > 0 && ` (${promotions.length})`}
</Typography> </Typography>
{isExpanded ? ( {isExpanded ? (
<ExpandLessIcon <ExpandLessIcon
sx={{ sx={{
marginLeft: "auto", marginLeft: 'auto',
}} }}
/> />
) : ( ) : (
<ExpandMoreIcon <ExpandMoreIcon
sx={{ sx={{
marginLeft: "auto", marginLeft: 'auto',
}} }}
/> />
)} )}
</ButtonBase> </ButtonBase>
<Box <Box
style={{ style={{
width: "330px", width: '330px',
}} }}
/> />
</Box> </Box>
@ -396,24 +401,24 @@ export const ListOfGroupPromotions = () => {
<> <>
<Box <Box
sx={{ sx={{
width: isMobile ? "320px" : "750px", width: isMobile ? '320px' : '750px',
maxWidth: "90%", maxWidth: '90%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
padding: "0px 20px", padding: '0px 20px',
}} }}
> >
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
display: "flex", display: 'flex',
justifyContent: "space-between", justifyContent: 'space-between',
alignItems: "center", alignItems: 'center',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: '13px',
fontWeight: 600, fontWeight: 600,
}} }}
></Typography> ></Typography>
@ -421,7 +426,7 @@ export const ListOfGroupPromotions = () => {
variant="contained" variant="contained"
onClick={() => setIsShowModal(true)} onClick={() => setIsShowModal(true)}
sx={{ sx={{
fontSize: "12px", fontSize: '12px',
}} }}
> >
Add Promotion Add Promotion
@ -431,22 +436,22 @@ export const ListOfGroupPromotions = () => {
</Box> </Box>
<Box <Box
sx={{ sx={{
width: isMobile ? "320px" : "750px", width: isMobile ? '320px' : '750px',
maxWidth: "90%", maxWidth: '90%',
maxHeight: "700px", maxHeight: '700px',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
bgcolor: "background.paper", bgcolor: 'background.paper',
padding: "20px 0px", padding: '20px 0px',
borderRadius: "19px", borderRadius: '19px',
}} }}
> >
{loading && promotions.length === 0 && ( {loading && promotions.length === 0 && (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
}} }}
> >
<CustomLoader /> <CustomLoader />
@ -455,18 +460,18 @@ export const ListOfGroupPromotions = () => {
{!loading && promotions.length === 0 && ( {!loading && promotions.length === 0 && (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
height: "100%", height: '100%',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "11px", fontSize: '11px',
fontWeight: 400, fontWeight: 400,
color: "rgba(255, 255, 255, 0.2)", color: 'rgba(255, 255, 255, 0.2)',
}} }}
> >
Nothing to display Nothing to display
@ -475,11 +480,11 @@ export const ListOfGroupPromotions = () => {
)} )}
<div <div
style={{ style={{
height: "600px", height: '600px',
position: "relative", position: 'relative',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
width: "100%", width: '100%',
}} }}
> >
<div <div
@ -487,24 +492,24 @@ export const ListOfGroupPromotions = () => {
className="scrollable-container" className="scrollable-container"
style={{ style={{
flexGrow: 1, flexGrow: 1,
overflow: "auto", overflow: 'auto',
position: "relative", position: 'relative',
display: "flex", display: 'flex',
height: "0px", height: '0px',
}} }}
> >
<div <div
style={{ style={{
height: rowVirtualizer.getTotalSize(), height: rowVirtualizer.getTotalSize(),
width: "100%", width: '100%',
}} }}
> >
<div <div
style={{ style={{
position: "absolute", position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
width: "100%", width: '100%',
}} }}
> >
{rowVirtualizer.getVirtualItems().map((virtualRow) => { {rowVirtualizer.getVirtualItems().map((virtualRow) => {
@ -516,17 +521,17 @@ export const ListOfGroupPromotions = () => {
ref={rowVirtualizer.measureElement} //measure dynamic row height ref={rowVirtualizer.measureElement} //measure dynamic row height
key={promotion?.identifier} key={promotion?.identifier}
style={{ style={{
position: "absolute", position: 'absolute',
top: 0, top: 0,
left: "50%", // Move to the center horizontally left: '50%', // Move to the center horizontally
transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering
width: "100%", // Control width (90% of the parent) width: '100%', // Control width (90% of the parent)
padding: "10px 0", padding: '10px 0',
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
overscrollBehavior: "none", overscrollBehavior: 'none',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '5px',
}} }}
> >
<ErrorBoundary <ErrorBoundary
@ -538,47 +543,47 @@ export const ListOfGroupPromotions = () => {
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
width: "100%", width: '100%',
padding: "0px 20px", padding: '0px 20px',
}} }}
> >
<Popover <Popover
open={openPopoverIndex === promotion?.groupId} open={openPopoverIndex === promotion?.groupId}
anchorEl={popoverAnchor} anchorEl={popoverAnchor}
onClose={(event, reason) => { onClose={(event, reason) => {
if (reason === "backdropClick") { if (reason === 'backdropClick') {
// Prevent closing on backdrop click // Prevent closing on backdrop click
return; return;
} }
handlePopoverClose(); // Close only on other events like Esc key press handlePopoverClose(); // Close only on other events like Esc key press
}} }}
anchorOrigin={{ anchorOrigin={{
vertical: "top", vertical: 'top',
horizontal: "center", horizontal: 'center',
}} }}
transformOrigin={{ transformOrigin={{
vertical: "bottom", vertical: 'bottom',
horizontal: "center", horizontal: 'center',
}} }}
style={{ marginTop: "8px" }} style={{ marginTop: '8px' }}
> >
<Box <Box
sx={{ sx={{
width: "325px", width: '325px',
height: "auto", height: 'auto',
maxHeight: "400px", maxHeight: '400px',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
gap: "10px", gap: '10px',
padding: "10px", padding: '10px',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: '13px',
fontWeight: 600, fontWeight: 600,
}} }}
> >
@ -586,17 +591,17 @@ export const ListOfGroupPromotions = () => {
</Typography> </Typography>
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: '13px',
fontWeight: 600, fontWeight: 600,
}} }}
> >
Number of members:{" "} Number of members:{' '}
{` ${promotion?.memberCount}`} {` ${promotion?.memberCount}`}
</Typography> </Typography>
{promotion?.description && ( {promotion?.description && (
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: '13px',
fontWeight: 600, fontWeight: 600,
}} }}
> >
@ -606,7 +611,7 @@ export const ListOfGroupPromotions = () => {
{promotion?.isOpen === false && ( {promotion?.isOpen === false && (
<Typography <Typography
sx={{ sx={{
fontSize: "13px", fontSize: '13px',
fontWeight: 600, fontWeight: 600,
}} }}
> >
@ -618,11 +623,11 @@ export const ListOfGroupPromotions = () => {
<Spacer height="5px" /> <Spacer height="5px" />
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
gap: "20px", gap: '20px',
alignItems: "center", alignItems: 'center',
width: "100%", width: '100%',
justifyContent: "center", justifyContent: 'center',
}} }}
> >
<LoadingButton <LoadingButton
@ -652,23 +657,23 @@ export const ListOfGroupPromotions = () => {
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
justifyContent: "space-between", justifyContent: 'space-between',
width: "100%", width: '100%',
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
gap: "15px", gap: '15px',
}} }}
> >
<Avatar <Avatar
sx={{ sx={{
backgroundColor: "#27282c", backgroundColor: '#27282c',
color: "white", color: 'white',
}} }}
alt={promotion?.name} alt={promotion?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${ src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
@ -680,8 +685,8 @@ export const ListOfGroupPromotions = () => {
<Typography <Typography
sx={{ sx={{
fontWight: 600, fontWight: 600,
fontFamily: "Inter", fontFamily: 'Inter',
color: "cadetBlue", color: 'cadetBlue',
}} }}
> >
{promotion?.name} {promotion?.name}
@ -690,8 +695,8 @@ export const ListOfGroupPromotions = () => {
<Typography <Typography
sx={{ sx={{
fontWight: 600, fontWight: 600,
fontFamily: "Inter", fontFamily: 'Inter',
color: "cadetBlue", color: 'cadetBlue',
}} }}
> >
{promotion?.groupName} {promotion?.groupName}
@ -700,42 +705,42 @@ export const ListOfGroupPromotions = () => {
<Spacer height="20px" /> <Spacer height="20px" />
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
gap: "20px", gap: '20px',
alignItems: "center", alignItems: 'center',
}} }}
> >
{promotion?.isOpen === false && ( {promotion?.isOpen === false && (
<LockIcon <LockIcon
sx={{ sx={{
color: "var(--green)", color: 'var(--green)',
}} }}
/> />
)} )}
{promotion?.isOpen === true && ( {promotion?.isOpen === true && (
<NoEncryptionGmailerrorredIcon <NoEncryptionGmailerrorredIcon
sx={{ sx={{
color: "var(--danger)", color: 'var(--danger)',
}} }}
/> />
)} )}
<Typography <Typography
sx={{ sx={{
fontSize: "15px", fontSize: '15px',
fontWeight: 600, fontWeight: 600,
}} }}
> >
{promotion?.isOpen {promotion?.isOpen
? "Public group" ? 'Public group'
: "Private group"} : 'Private group'}
</Typography> </Typography>
</Box> </Box>
<Spacer height="20px" /> <Spacer height="20px" />
<Typography <Typography
sx={{ sx={{
fontWight: 600, fontWight: 600,
fontFamily: "Inter", fontFamily: 'Inter',
color: "cadetBlue", color: 'cadetBlue',
}} }}
> >
{promotion?.data} {promotion?.data}
@ -743,9 +748,9 @@ export const ListOfGroupPromotions = () => {
<Spacer height="20px" /> <Spacer height="20px" />
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
justifyContent: "center", justifyContent: 'center',
width: "100%", width: '100%',
}} }}
> >
<Button <Button
@ -754,8 +759,8 @@ export const ListOfGroupPromotions = () => {
handlePopoverOpen(event, promotion?.groupId) handlePopoverOpen(event, promotion?.groupId)
} }
sx={{ sx={{
fontSize: "12px", fontSize: '12px',
color: "white", color: 'white',
}} }}
> >
Join Group: {` ${promotion?.groupName}`} Join Group: {` ${promotion?.groupName}`}
@ -783,7 +788,7 @@ export const ListOfGroupPromotions = () => {
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
> >
<DialogTitle id="alert-dialog-title"> <DialogTitle id="alert-dialog-title">
{"Promote your group to non-members"} {'Promote your group to non-members'}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText id="alert-dialog-description"> <DialogContentText id="alert-dialog-description">
@ -791,14 +796,14 @@ export const ListOfGroupPromotions = () => {
group. group.
</DialogContentText> </DialogContentText>
<DialogContentText id="alert-dialog-description2"> <DialogContentText id="alert-dialog-description2">
Max 200 characters. Publish Fee: {fee && fee} {" QORT"} Max 200 characters. Publish Fee: {fee && fee} {' QORT'}
</DialogContentText> </DialogContentText>
<Spacer height="20px" /> <Spacer height="20px" />
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: "5px", gap: '5px',
}} }}
> >
<Label>Select a group</Label> <Label>Select a group</Label>
@ -832,11 +837,11 @@ export const ListOfGroupPromotions = () => {
}} }}
multiline={true} multiline={true}
sx={{ sx={{
"& .MuiFormLabel-root": { '& .MuiFormLabel-root': {
color: "white", color: 'white',
}, },
"& .MuiFormLabel-root.Mui-focused": { '& .MuiFormLabel-root.Mui-focused': {
color: "white", color: 'white',
}, },
}} }}
/> />

View File

@ -1,36 +1,36 @@
import * as React from "react"; import * as React from 'react';
import Button from "@mui/material/Button"; import Button from '@mui/material/Button';
import Dialog from "@mui/material/Dialog"; import Dialog from '@mui/material/Dialog';
import ListItemText from "@mui/material/ListItemText"; import ListItemText from '@mui/material/ListItemText';
import ListItemButton from "@mui/material/ListItemButton"; import ListItemButton from '@mui/material/ListItemButton';
import List from "@mui/material/List"; import List from '@mui/material/List';
import Divider from "@mui/material/Divider"; import Divider from '@mui/material/Divider';
import AppBar from "@mui/material/AppBar"; import AppBar from '@mui/material/AppBar';
import Toolbar from "@mui/material/Toolbar"; import Toolbar from '@mui/material/Toolbar';
import IconButton from "@mui/material/IconButton"; import IconButton from '@mui/material/IconButton';
import Typography from "@mui/material/Typography"; import Typography from '@mui/material/Typography';
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from '@mui/icons-material/Close';
import Slide from "@mui/material/Slide"; import Slide from '@mui/material/Slide';
import { TransitionProps } from "@mui/material/transitions"; import { TransitionProps } from '@mui/material/transitions';
import ListOfMembers from "./ListOfMembers"; import ListOfMembers from './ListOfMembers';
import { InviteMember } from "./InviteMember"; import { InviteMember } from './InviteMember';
import { ListOfInvites } from "./ListOfInvites"; import { ListOfInvites } from './ListOfInvites';
import { ListOfBans } from "./ListOfBans"; import { ListOfBans } from './ListOfBans';
import { ListOfJoinRequests } from "./ListOfJoinRequests"; import { ListOfJoinRequests } from './ListOfJoinRequests';
import { Box, ButtonBase, Card, Tab, Tabs } from "@mui/material"; import { Box, ButtonBase, Card, Tab, Tabs } from '@mui/material';
import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { MyContext, getBaseApiReact, isMobile } from "../../App"; import { MyContext, getBaseApiReact, isMobile } from '../../App';
import { getGroupMembers, getNames } from "./Group"; import { getGroupMembers, getNames } from './Group';
import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
import { getFee } from "../../background"; import { getFee } from '../../background';
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from '@mui/lab';
import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
import { Spacer } from "../../common/Spacer"; import { Spacer } from '../../common/Spacer';
import InsertLinkIcon from '@mui/icons-material/InsertLink'; import InsertLinkIcon from '@mui/icons-material/InsertLink';
function a11yProps(index: number) { function a11yProps(index: number) {
return { return {
id: `simple-tab-${index}`, id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`, 'aria-controls': `simple-tabpanel-${index}`,
}; };
} }
@ -50,16 +50,16 @@ export const ManageMembers = ({
selectedGroup, selectedGroup,
isAdmin, isAdmin,
isOwner isOwner,
}) => { }) => {
const [membersWithNames, setMembersWithNames] = React.useState([]); const [membersWithNames, setMembersWithNames] = React.useState([]);
const [tab, setTab] = React.useState("create"); const [tab, setTab] = React.useState('create');
const [value, setValue] = React.useState(0); const [value, setValue] = React.useState(0);
const [openSnack, setOpenSnack] = React.useState(false); const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null); const [infoSnack, setInfoSnack] = React.useState(null);
const [isLoadingMembers, setIsLoadingMembers] = React.useState(false) const [isLoadingMembers, setIsLoadingMembers] = React.useState(false);
const [isLoadingLeave, setIsLoadingLeave] = React.useState(false) const [isLoadingLeave, setIsLoadingLeave] = React.useState(false);
const [groupInfo, setGroupInfo] = React.useState(null) const [groupInfo, setGroupInfo] = React.useState(null);
const handleChange = (event: React.SyntheticEvent, newValue: number) => { const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValue(newValue); setValue(newValue);
}; };
@ -69,20 +69,20 @@ export const ManageMembers = ({
setOpen(false); setOpen(false);
}; };
const handleLeaveGroup = async () => { const handleLeaveGroup = async () => {
try { try {
setIsLoadingLeave(true) setIsLoadingLeave(true);
const fee = await getFee('LEAVE_GROUP') const fee = await getFee('LEAVE_GROUP');
await show({ await show({
message: "Would you like to perform an LEAVE_GROUP transaction?" , message: 'Would you like to perform an LEAVE_GROUP transaction?',
publishFee: fee.fee + ' QORT' publishFee: fee.fee + ' QORT',
}) });
await new Promise((res, rej) => { await new Promise((res, rej) => {
window.sendMessage("leaveGroup", { window
groupId: selectedGroup?.groupId, .sendMessage('leaveGroup', {
}) groupId: selectedGroup?.groupId,
})
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
setTxList((prev) => [ setTxList((prev) => [
@ -98,8 +98,9 @@ export const ManageMembers = ({
]); ]);
res(response); res(response);
setInfoSnack({ setInfoSnack({
type: "success", type: 'success',
message: "Successfully requested to leave group. It may take a couple of minutes for the changes to propagate", message:
'Successfully requested to leave group. It may take a couple of minutes for the changes to propagate',
}); });
setOpenSnack(true); setOpenSnack(true);
return; return;
@ -107,57 +108,62 @@ export const ManageMembers = ({
rej(response.error); rej(response.error);
}) })
.catch((error) => { .catch((error) => {
rej(error.message || "An error occurred"); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) {} finally { } catch (error) {
setIsLoadingLeave(false) console.log(error);
} finally {
setIsLoadingLeave(false);
} }
}; };
const getMembersWithNames = React.useCallback(async (groupId) => { const getMembersWithNames = React.useCallback(async (groupId) => {
try { try {
setIsLoadingMembers(true) setIsLoadingMembers(true);
const res = await getGroupMembers(groupId); const res = await getGroupMembers(groupId);
const resWithNames = await getNames(res.members); const resWithNames = await getNames(res.members);
setMembersWithNames(resWithNames); setMembersWithNames(resWithNames);
setIsLoadingMembers(false) setIsLoadingMembers(false);
} catch (error) {} } catch (error) {
console.log(error);
}
}, []); }, []);
const getMembers = async (groupId) => { const getMembers = async (groupId) => {
try { try {
const res = await getGroupMembers(groupId); const res = await getGroupMembers(groupId);
setMembersWithNames(res?.members || []); setMembersWithNames(res?.members || []);
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const getGroupInfo = async (groupId) => { const getGroupInfo = async (groupId) => {
try { try {
const response = await fetch( const response = await fetch(`${getBaseApiReact()}/groups/${groupId}`);
`${getBaseApiReact()}/groups/${groupId}` const groupData = await response.json();
); setGroupInfo(groupData);
const groupData = await response.json(); } catch (error) {
setGroupInfo(groupData) console.log(error);
} catch (error) {} }
}; };
React.useEffect(()=> { React.useEffect(() => {
if(selectedGroup?.groupId){ if (selectedGroup?.groupId) {
getMembers(selectedGroup?.groupId) getMembers(selectedGroup?.groupId);
getGroupInfo(selectedGroup?.groupId) getGroupInfo(selectedGroup?.groupId);
} }
}, [selectedGroup?.groupId]) }, [selectedGroup?.groupId]);
const openGroupJoinRequestFunc = ()=> { const openGroupJoinRequestFunc = () => {
setValue(4) setValue(4);
} };
React.useEffect(() => { React.useEffect(() => {
subscribeToEvent("openGroupJoinRequest", openGroupJoinRequestFunc); subscribeToEvent('openGroupJoinRequest', openGroupJoinRequestFunc);
return () => { return () => {
unsubscribeFromEvent("openGroupJoinRequest", openGroupJoinRequestFunc); unsubscribeFromEvent('openGroupJoinRequest', openGroupJoinRequestFunc);
}; };
}, []); }, []);
@ -169,7 +175,7 @@ export const ManageMembers = ({
onClose={handleClose} onClose={handleClose}
TransitionComponent={Transition} TransitionComponent={Transition}
> >
<AppBar sx={{ position: "relative", bgcolor: "#232428" }}> <AppBar sx={{ position: 'relative', bgcolor: '#232428' }}>
<Toolbar> <Toolbar>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div"> <Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
Manage Members Manage Members
@ -186,117 +192,136 @@ export const ManageMembers = ({
</AppBar> </AppBar>
<Box <Box
sx={{ sx={{
bgcolor: "#27282c", bgcolor: '#27282c',
flexGrow: 1, flexGrow: 1,
overflowY: "auto", overflowY: 'auto',
color: "white", color: 'white',
}} }}
> >
<Box sx={{ borderBottom: 1, borderColor: "divider" }}> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs <Tabs
value={value} value={value}
onChange={handleChange} onChange={handleChange}
aria-label="basic tabs example" aria-label="basic tabs example"
variant="scrollable" // Make tabs scrollable variant="scrollable" // Make tabs scrollable
scrollButtons="auto" // Show scroll buttons automatically scrollButtons="auto" // Show scroll buttons automatically
allowScrollButtonsMobile // Show scroll buttons on mobile as well allowScrollButtonsMobile // Show scroll buttons on mobile as well
sx={{ sx={{
"& .MuiTabs-indicator": { '& .MuiTabs-indicator': {
backgroundColor: "white", backgroundColor: 'white',
}, },
maxWidth: '100%', // Ensure the tabs container fits within the available space maxWidth: '100%', // Ensure the tabs container fits within the available space
overflow: 'hidden', // Prevents overflow on small screens overflow: 'hidden', // Prevents overflow on small screens
}} }}
> >
<Tab <Tab
label="List of members" label="List of members"
{...a11yProps(0)} {...a11yProps(0)}
sx={{ sx={{
"&.Mui-selected": { '&.Mui-selected': {
color: "white", color: 'white',
}, },
fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile
}} }}
/> />
<Tab <Tab
label="Invite new member" label="Invite new member"
{...a11yProps(1)} {...a11yProps(1)}
sx={{ sx={{
"&.Mui-selected": { '&.Mui-selected': {
color: "white", color: 'white',
}, },
fontSize: isMobile ? '0.75rem' : '1rem', fontSize: isMobile ? '0.75rem' : '1rem',
}} }}
/> />
<Tab <Tab
label="List of invites" label="List of invites"
{...a11yProps(2)} {...a11yProps(2)}
sx={{ sx={{
"&.Mui-selected": { '&.Mui-selected': {
color: "white", color: 'white',
}, },
fontSize: isMobile ? '0.75rem' : '1rem', fontSize: isMobile ? '0.75rem' : '1rem',
}} }}
/> />
<Tab <Tab
label="List of bans" label="List of bans"
{...a11yProps(3)} {...a11yProps(3)}
sx={{ sx={{
"&.Mui-selected": { '&.Mui-selected': {
color: "white", color: 'white',
}, },
fontSize: isMobile ? '0.75rem' : '1rem', fontSize: isMobile ? '0.75rem' : '1rem',
}} }}
/> />
<Tab <Tab
label="Join requests" label="Join requests"
{...a11yProps(4)} {...a11yProps(4)}
sx={{ sx={{
"&.Mui-selected": { '&.Mui-selected': {
color: "white", color: 'white',
}, },
fontSize: isMobile ? '0.75rem' : '1rem', fontSize: isMobile ? '0.75rem' : '1rem',
}} }}
/> />
</Tabs> </Tabs>
</Box> </Box>
<Card sx={{ <Card
padding: '10px', sx={{
cursor: 'default', padding: '10px',
}}> cursor: 'default',
}}
>
<Box> <Box>
<Typography>GroupId: {groupInfo?.groupId}</Typography> <Typography>GroupId: {groupInfo?.groupId}</Typography>
<Typography>GroupName: {groupInfo?.groupName}</Typography> <Typography>GroupName: {groupInfo?.groupName}</Typography>
<Typography>Number of members: {groupInfo?.memberCount}</Typography> <Typography>
<ButtonBase sx={{ Number of members: {groupInfo?.memberCount}
gap: '10px' </Typography>
}} onClick={async ()=> { <ButtonBase
const link = `qortal://use-group/action-join/groupid-${groupInfo?.groupId}` sx={{
await navigator.clipboard.writeText(link); gap: '10px',
}}><InsertLinkIcon /> <Typography>Join Group Link</Typography></ButtonBase> }}
onClick={async () => {
const link = `qortal://use-group/action-join/groupid-${groupInfo?.groupId}`;
await navigator.clipboard.writeText(link);
}}
>
<InsertLinkIcon /> <Typography>Join Group Link</Typography>
</ButtonBase>
</Box> </Box>
<Spacer height="20px" /> <Spacer height="20px" />
{selectedGroup?.groupId && !isOwner && ( {selectedGroup?.groupId && !isOwner && (
<LoadingButton size="small" loading={isLoadingLeave} loadingPosition="start" <LoadingButton
variant="contained" onClick={handleLeaveGroup}> size="small"
Leave Group loading={isLoadingLeave}
</LoadingButton> loadingPosition="start"
)} variant="contained"
onClick={handleLeaveGroup}
>
Leave Group
</LoadingButton>
)}
</Card> </Card>
{value === 0 && ( {value === 0 && (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
padding: "25px", padding: '25px',
maxWidth: '750px' maxWidth: '750px',
}} }}
> >
<Button variant="contained" onClick={()=> getMembersWithNames(selectedGroup?.groupId)}>Load members with names</Button> <Button
variant="contained"
onClick={() => getMembersWithNames(selectedGroup?.groupId)}
>
Load members with names
</Button>
<Spacer height="10px" /> <Spacer height="10px" />
<ListOfMembers <ListOfMembers
members={membersWithNames || []} members={membersWithNames || []}
groupId={selectedGroup?.groupId} groupId={selectedGroup?.groupId}
setOpenSnack={setOpenSnack} setOpenSnack={setOpenSnack}
setInfoSnack={setInfoSnack} setInfoSnack={setInfoSnack}
isAdmin={isAdmin} isAdmin={isAdmin}
isOwner={isOwner} isOwner={isOwner}
@ -304,64 +329,87 @@ export const ManageMembers = ({
/> />
</Box> </Box>
)} )}
{value === 1 && ( {value === 1 && (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
padding: "25px", padding: '25px',
maxWidth: '750px' maxWidth: '750px',
}} }}
> >
<InviteMember show={show} groupId={selectedGroup?.groupId} setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} /> <InviteMember
show={show}
groupId={selectedGroup?.groupId}
setOpenSnack={setOpenSnack}
setInfoSnack={setInfoSnack}
/>
</Box> </Box>
)} )}
{value === 2 && ( {value === 2 && (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
padding: "25px", padding: '25px',
maxWidth: '750px' maxWidth: '750px',
}} }}
> >
<ListOfInvites show={show} groupId={selectedGroup?.groupId} setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} /> <ListOfInvites
show={show}
groupId={selectedGroup?.groupId}
setOpenSnack={setOpenSnack}
setInfoSnack={setInfoSnack}
/>
</Box> </Box>
)} )}
{value === 3 && ( {value === 3 && (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
padding: "25px", padding: '25px',
maxWidth: '750px' maxWidth: '750px',
}} }}
> >
<ListOfBans show={show} groupId={selectedGroup?.groupId} setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} /> <ListOfBans
show={show}
groupId={selectedGroup?.groupId}
setOpenSnack={setOpenSnack}
setInfoSnack={setInfoSnack}
/>
</Box> </Box>
)} )}
{value === 4 && ( {value === 4 && (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
padding: "25px", padding: '25px',
maxWidth: '750px' maxWidth: '750px',
}} }}
> >
<ListOfJoinRequests show={show} setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} groupId={selectedGroup?.groupId} /> <ListOfJoinRequests
show={show}
setOpenSnack={setOpenSnack}
setInfoSnack={setInfoSnack}
groupId={selectedGroup?.groupId}
/>
</Box> </Box>
)} )}
</Box> </Box>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} /> <CustomizedSnackbars
open={openSnack}
setOpen={setOpenSnack}
info={infoSnack}
setInfo={setInfoSnack}
/>
<LoadingSnackbar <LoadingSnackbar
open={isLoadingMembers} open={isLoadingMembers}
info={{ info={{
message: "Loading member list with names... please wait.", message: 'Loading member list with names... please wait.',
}} }}
/> />
</Dialog> </Dialog>
</React.Fragment> </React.Fragment>
); );
}; };

View File

@ -1,14 +1,12 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react';
import List from "@mui/material/List"; import List from '@mui/material/List';
import ListItem from "@mui/material/ListItem"; import ListItem from '@mui/material/ListItem';
import ListItemButton from "@mui/material/ListItemButton"; import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from "@mui/material/ListItemText"; import ListItemText from '@mui/material/ListItemText';
import moment from 'moment' import moment from 'moment';
import { Box, ButtonBase, Collapse, Typography } from "@mui/material"; import { Box, ButtonBase, Collapse, Typography, useTheme } from '@mui/material';
import { Spacer } from "../../common/Spacer"; import { getBaseApiReact, isMobile } from '../../App';
import { getBaseApiReact, isMobile } from "../../App";
import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
import MailIcon from '@mui/icons-material/Mail'; import MailIcon from '@mui/icons-material/Mail';
import MailOutlineIcon from '@mui/icons-material/MailOutline'; import MailOutlineIcon from '@mui/icons-material/MailOutline';
import { executeEvent } from '../../utils/events'; import { executeEvent } from '../../utils/events';
@ -18,262 +16,283 @@ import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import MarkEmailUnreadIcon from '@mui/icons-material/MarkEmailUnread'; import MarkEmailUnreadIcon from '@mui/icons-material/MarkEmailUnread';
export const isLessThanOneWeekOld = (timestamp) => { export const isLessThanOneWeekOld = (timestamp) => {
// Current time in milliseconds // Current time in milliseconds
const now = Date.now(); const now = Date.now();
// One week ago in milliseconds (7 days * 24 hours * 60 minutes * 60 seconds * 1000 milliseconds) // One week ago in milliseconds (7 days * 24 hours * 60 minutes * 60 seconds * 1000 milliseconds)
const oneWeekAgo = now - (7 * 24 * 60 * 60 * 1000); const oneWeekAgo = now - 7 * 24 * 60 * 60 * 1000;
// Check if the timestamp is newer than one week ago // Check if the timestamp is newer than one week ago
return timestamp > oneWeekAgo; return timestamp > oneWeekAgo;
}; };
export function formatEmailDate(timestamp: number) { export function formatEmailDate(timestamp: number) {
const date = moment(timestamp); const date = moment(timestamp);
const now = moment(); const now = moment();
if (date.isSame(now, 'day')) { if (date.isSame(now, 'day')) {
// If the email was received today, show the time // If the email was received today, show the time
return date.format('h:mm A'); return date.format('h:mm A');
} else if (date.isSame(now, 'year')) { } else if (date.isSame(now, 'year')) {
// If the email was received this year, show the month and day // If the email was received this year, show the month and day
return date.format('MMM D'); return date.format('MMM D');
} else { } else {
// For older emails, show the full date // For older emails, show the full date
return date.format('MMM D, YYYY'); return date.format('MMM D, YYYY');
} }
} }
export const QMailMessages = ({userName, userAddress}) => {
const [isExpanded, setIsExpanded] = useState(false)
const [mails, setMails] = useRecoilState(mailsAtom)
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom)
const [loading, setLoading] = useState(true)
const getMails = useCallback(async () => { export const QMailMessages = ({ userName, userAddress }) => {
try { const [isExpanded, setIsExpanded] = useState(false);
setLoading(true) const [mails, setMails] = useRecoilState(mailsAtom);
const query = `qortal_qmail_${userName.slice( const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(
0, qMailLastEnteredTimestampAtom
20 );
)}_${userAddress.slice(-6)}_mail_` const [loading, setLoading] = useState(true);
const response = await fetch(`${getBaseApiReact()}/arbitrary/resources/search?service=MAIL_PRIVATE&query=${query}&limit=10&includemetadata=false&offset=0&reverse=true&excludeblocked=true&mode=ALL`); const theme = useTheme();
const mailData = await response.json();
setMails(mailData);
} catch (error) {
console.error(error);
} finally {
setLoading(false)
} const getMails = useCallback(async () => {
}, []) try {
setLoading(true);
const query = `qortal_qmail_${userName.slice(
0,
20
)}_${userAddress.slice(-6)}_mail_`;
const response = await fetch(
`${getBaseApiReact()}/arbitrary/resources/search?service=MAIL_PRIVATE&query=${query}&limit=10&includemetadata=false&offset=0&reverse=true&excludeblocked=true&mode=ALL`
);
const mailData = await response.json();
const getTimestamp = async () => { setMails(mailData);
try { } catch (error) {
return new Promise((res, rej) => { console.error(error);
window.sendMessage("getEnteredQmailTimestamp") } finally {
.then((response) => { setLoading(false);
if (!response?.error) { }
if(response?.timestamp){ }, []);
setLastEnteredTimestamp(response?.timestamp)
} const getTimestamp = async () => {
try {
return new Promise((res, rej) => {
window
.sendMessage('getEnteredQmailTimestamp')
.then((response) => {
if (!response?.error) {
if (response?.timestamp) {
setLastEnteredTimestamp(response?.timestamp);
} }
rej(response.error); }
}) rej(response.error);
.catch((error) => { })
rej(error.message || "An error occurred"); .catch((error) => {
}); rej(error.message || 'An error occurred');
}); });
} catch (error) {} });
}; } catch (error) {
console.log(error);
useEffect(() => { }
getTimestamp() };
if(!userName || !userAddress) return
getMails();
const interval = setInterval(() => { useEffect(() => {
getTimestamp() getTimestamp();
getMails(); if (!userName || !userAddress) return;
}, 300000); getMails();
return () => clearInterval(interval);
}, [getMails, userName, userAddress]);
const anyUnread = useMemo(()=> { const interval = setInterval(() => {
let unread = false getTimestamp();
getMails();
mails.forEach((mail)=> { }, 300000);
if(!lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created) || (lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created) && lastEnteredTimestamp < mail?.created)){
unread = true return () => clearInterval(interval);
} }, [getMails, userName, userAddress]);
})
return unread const anyUnread = useMemo(() => {
}, [mails, lastEnteredTimestamp]) let unread = false;
mails.forEach((mail) => {
if (
(!lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created)) ||
(lastEnteredTimestamp &&
isLessThanOneWeekOld(mail?.created) &&
lastEnteredTimestamp < mail?.created)
) {
unread = true;
}
});
return unread;
}, [mails, lastEnteredTimestamp]);
return ( return (
<Box <Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<ButtonBase
sx={{ sx={{
width: "322px", width: '100%',
display: "flex", display: 'flex',
flexDirection: "row", flexDirection: 'column',
gap: '10px', alignItems: 'center',
padding: "0px 20px",
justifyContent: 'flex-start'
}} }}
onClick={()=> setIsExpanded((prev)=> !prev)}
> >
<Typography <ButtonBase
sx={{ sx={{
fontSize: "1rem", width: '322px',
display: 'flex',
flexDirection: 'row',
gap: '10px',
padding: '0px 20px',
justifyContent: 'flex-start',
}} }}
onClick={() => setIsExpanded((prev) => !prev)}
> >
Latest Q-Mails <Typography
</Typography> sx={{
<MarkEmailUnreadIcon sx={{ fontSize: '1rem',
color: anyUnread ? 'var(--unread)' : 'white' }}
}}/> >
{isExpanded ? <ExpandLessIcon sx={{ Latest Q-Mails
marginLeft: 'auto' </Typography>
}} /> : ( <MarkEmailUnreadIcon
<ExpandMoreIcon sx={{ sx={{
color: anyUnread ? 'var(--unread)' : 'white', color: anyUnread ? 'var(--unread)' : 'white',
marginLeft: 'auto' }}
}} /> />
)} {isExpanded ? (
</ButtonBase> <ExpandLessIcon
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box
className="scrollable-container"
sx={{
width: "322px",
height: isMobile ? "165px" : "250px",
display: "flex",
flexDirection: "column",
bgcolor: "background.paper",
padding: "20px",
borderRadius: "19px",
overflow: 'auto'
}}
>
{loading && mails.length === 0 && (
<Box
sx={{ sx={{
width: "100%", marginLeft: 'auto',
display: "flex",
justifyContent: "center",
}} }}
> />
<CustomLoader /> ) : (
</Box> <ExpandMoreIcon
sx={{
color: anyUnread ? 'var(--unread)' : 'white',
marginLeft: 'auto',
}}
/>
)} )}
{!loading && mails.length === 0 && ( </ButtonBase>
<Box
sx={{ <Collapse in={isExpanded} timeout="auto" unmountOnExit>
width: "100%", <Box
display: "flex", className="scrollable-container"
justifyContent: "center", sx={{
alignItems: 'center', bgcolor: theme.palette.background.paper,
height: '100%', borderRadius: '19px',
display: 'flex',
}} flexDirection: 'column',
> height: isMobile ? '165px' : '250px',
<Typography overflow: 'auto',
padding: '20px',
width: '322px',
}}
>
{loading && mails.length === 0 && (
<Box
sx={{ sx={{
fontSize: "11px", width: '100%',
fontWeight: 400, display: 'flex',
color: 'rgba(255, 255, 255, 0.2)' justifyContent: 'center',
}} }}
> >
Nothing to display <CustomLoader />
</Typography> </Box>
</Box> )}
)} {!loading && mails.length === 0 && (
<Box
<List sx={{ width: "100%", maxWidth: 360 }}> sx={{
{mails?.map((mail)=> { width: '100%',
return ( display: 'flex',
<ListItem justifyContent: 'center',
alignItems: 'center',
disablePadding height: '100%',
sx={{ }}
marginBottom: '20px' >
}} <Typography
onClick={()=> { sx={{
executeEvent("addTab", { data: { service: 'APP', name: 'q-mail' } }); fontSize: '11px',
executeEvent("open-apps-mode", { }); fontWeight: 400,
setLastEnteredTimestamp(Date.now()) color: theme.palette.primary,
}}
>
Nothing to display
</Typography>
</Box>
)}
<List sx={{ width: '100%', maxWidth: 360 }}>
{mails?.map((mail) => {
return (
<ListItem
disablePadding
sx={{
marginBottom: '20px',
}}
onClick={() => {
executeEvent('addTab', {
data: { service: 'APP', name: 'q-mail' },
});
executeEvent('open-apps-mode', {});
setLastEnteredTimestamp(Date.now());
}}
>
<ListItemButton
sx={{
padding: '0px',
}} }}
disableRipple
role={undefined}
dense
> >
<ListItemButton <ListItemText
sx={{ sx={{
padding: "0px", '& .MuiTypography-root': {
fontSize: '13px',
fontWeight: 400,
},
}}
primary={`From: ${mail?.name}`}
secondary={`${formatEmailDate(mail?.created)}`}
/>
<ListItemIcon
sx={{
justifyContent: 'flex-end',
}} }}
disableRipple
role={undefined}
dense
> >
<ListItemText {!lastEnteredTimestamp &&
sx={{ isLessThanOneWeekOld(mail?.created) ? (
"& .MuiTypography-root": { <MailIcon
fontSize: "13px", sx={{
fontWeight: 400, color: 'var(--unread)',
}, }}
}} />
primary={`From: ${mail?.name}`} ) : !lastEnteredTimestamp ? (
secondary={`${formatEmailDate(mail?.created)}`} <MailOutlineIcon
/> sx={{
<ListItemIcon color: 'white',
sx={{ }}
justifyContent: "flex-end", />
}} ) : lastEnteredTimestamp < mail?.created &&
> isLessThanOneWeekOld(mail?.created) ? (
{!lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created) ? ( <MailIcon
<MailIcon sx={{ sx={{
color: 'var(--unread)' color: 'var(--unread)',
}} /> }}
) : !lastEnteredTimestamp ? ( />
<MailOutlineIcon sx={{ ) : (
color: 'white' <MailOutlineIcon
}} /> sx={{
): (lastEnteredTimestamp < mail?.created) && isLessThanOneWeekOld(mail?.created) ? ( color: 'white',
<MailIcon sx={{ }}
color: 'var(--unread)' />
}} /> )}
) : ( </ListItemIcon>
<MailOutlineIcon sx={{ </ListItemButton>
color: 'white' </ListItem>
}} /> );
)
}
</ListItemIcon>
</ListItemButton>
</ListItem>
)
})} })}
</List>
</Box>
</List> </Collapse>
</Box> </Box>
</Collapse> );
</Box> };
)
}

View File

@ -1,37 +1,20 @@
import * as React from "react"; import * as React from 'react';
import Button from "@mui/material/Button"; import Dialog from '@mui/material/Dialog';
import Dialog from "@mui/material/Dialog"; import AppBar from '@mui/material/AppBar';
import ListItemText from "@mui/material/ListItemText"; import Toolbar from '@mui/material/Toolbar';
import ListItemButton from "@mui/material/ListItemButton"; import IconButton from '@mui/material/IconButton';
import List from "@mui/material/List"; import Typography from '@mui/material/Typography';
import Divider from "@mui/material/Divider"; import CloseIcon from '@mui/icons-material/Close';
import AppBar from "@mui/material/AppBar"; import Slide from '@mui/material/Slide';
import Toolbar from "@mui/material/Toolbar"; import { TransitionProps } from '@mui/material/transitions';
import IconButton from "@mui/material/IconButton"; import { Box, FormControlLabel, Switch, styled, useTheme } from '@mui/material';
import Typography from "@mui/material/Typography"; import { enabledDevModeAtom } from '../../atoms/global';
import CloseIcon from "@mui/icons-material/Close"; import { useRecoilState } from 'recoil';
import Slide from "@mui/material/Slide";
import { TransitionProps } from "@mui/material/transitions";
import ListOfMembers from "./ListOfMembers";
import { InviteMember } from "./InviteMember";
import { ListOfInvites } from "./ListOfInvites";
import { ListOfBans } from "./ListOfBans";
import { ListOfJoinRequests } from "./ListOfJoinRequests";
import { Box, FormControlLabel, Switch, Tab, Tabs, styled } from "@mui/material";
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
import { MyContext, isMobile } from "../../App";
import { getGroupMembers, getNames } from "./Group";
import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar";
import { getFee } from "../../background";
import { LoadingButton } from "@mui/lab";
import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
import { enabledDevModeAtom } from "../../atoms/global";
import { useRecoilState } from "recoil";
function a11yProps(index: number) { function a11yProps(index: number) {
return { return {
id: `simple-tab-${index}`, id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`, 'aria-controls': `simple-tabpanel-${index}`,
}; };
} }
@ -49,13 +32,13 @@ const LocalNodeSwitch = styled(Switch)(({ theme }) => ({
}, },
'&::before': { '&::before': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent( backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent(
theme.palette.getContrastText(theme.palette.primary.main), theme.palette.getContrastText(theme.palette.primary.main)
)}" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/></svg>')`, )}" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/></svg>')`,
left: 12, left: 12,
}, },
'&::after': { '&::after': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent( backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent(
theme.palette.getContrastText(theme.palette.primary.main), theme.palette.getContrastText(theme.palette.primary.main)
)}" d="M19,13H5V11H19V13Z" /></svg>')`, )}" d="M19,13H5V11H19V13Z" /></svg>')`,
right: 12, right: 12,
}, },
@ -77,35 +60,34 @@ const Transition = React.forwardRef(function Transition(
return <Slide direction="up" ref={ref} {...props} />; return <Slide direction="up" ref={ref} {...props} />;
}); });
export const Settings = ({ export const Settings = ({ address, open, setOpen }) => {
address,
open,
setOpen,
}) => {
const [checked, setChecked] = React.useState(false); const [checked, setChecked] = React.useState(false);
const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom) const [isEnabledDevMode, setIsEnabledDevMode] =
useRecoilState(enabledDevModeAtom);
const theme = useTheme();
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked); setChecked(event.target.checked);
window.sendMessage("addUserSettings", { window
keyValue: { .sendMessage('addUserSettings', {
key: 'disable-push-notifications', keyValue: {
value: event.target.checked, key: 'disable-push-notifications',
}, value: event.target.checked,
}) },
})
.then((response) => { .then((response) => {
if (response?.error) { if (response?.error) {
console.error("Error adding user settings:", response.error); console.error('Error adding user settings:', response.error);
} else { } else {
console.log("User settings added successfully"); console.log('User settings added successfully');
} }
}) })
.catch((error) => { .catch((error) => {
console.error("Failed to add user settings:", error.message || "An error occurred"); console.error(
'Failed to add user settings:',
error.message || 'An error occurred'
);
}); });
}; };
const handleClose = () => { const handleClose = () => {
@ -115,9 +97,10 @@ export const Settings = ({
const getUserSettings = async () => { const getUserSettings = async () => {
try { try {
return new Promise((res, rej) => { return new Promise((res, rej) => {
window.sendMessage("getUserSettings", { window
key: "disable-push-notifications", .sendMessage('getUserSettings', {
}) key: 'disable-push-notifications',
})
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
setChecked(response || false); setChecked(response || false);
@ -127,12 +110,11 @@ export const Settings = ({
rej(response.error); rej(response.error);
}) })
.catch((error) => { .catch((error) => {
rej(error.message || "An error occurred"); rej(error.message || 'An error occurred');
}); });
}); });
} catch (error) { } catch (error) {
console.log("error", error); console.log('error', error);
} }
}; };
@ -140,8 +122,6 @@ export const Settings = ({
getUserSettings(); getUserSettings();
}, []); }, []);
return ( return (
<React.Fragment> <React.Fragment>
<Dialog <Dialog
@ -150,11 +130,14 @@ export const Settings = ({
onClose={handleClose} onClose={handleClose}
TransitionComponent={Transition} TransitionComponent={Transition}
> >
<AppBar sx={{ position: "relative", bgcolor: "#232428" }}> <AppBar
sx={{ position: 'relative', bgcolor: theme.palette.background }}
>
<Toolbar> <Toolbar>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div"> <Typography sx={{ ml: 2, flex: 1 }} variant="h4" component="div">
General Settings General Settings
</Typography> </Typography>
<IconButton <IconButton
edge="start" edge="start"
color="inherit" color="inherit"
@ -165,21 +148,22 @@ export const Settings = ({
</IconButton> </IconButton>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<Box <Box
sx={{ sx={{
bgcolor: "#27282c", bgcolor: theme.palette.background,
flexGrow: 1, flexGrow: 1,
overflowY: "auto", overflowY: 'auto',
color: "white", color: theme.palette.text.primary,
padding: "20px", padding: '20px',
flexDirection: 'column', flexDirection: 'column',
display: 'flex', display: 'flex',
gap: '20px' gap: '20px',
}} }}
> >
<FormControlLabel <FormControlLabel
sx={{ sx={{
color: "white", color: theme.palette.text.primary,
}} }}
control={ control={
<LocalNodeSwitch checked={checked} onChange={handleChange} /> <LocalNodeSwitch checked={checked} onChange={handleChange} />
@ -188,17 +172,23 @@ export const Settings = ({
/> />
{window?.electronAPI && ( {window?.electronAPI && (
<FormControlLabel <FormControlLabel
sx={{ sx={{
color: "white", color: 'white',
}} }}
control={ control={
<LocalNodeSwitch checked={isEnabledDevMode} onChange={(e)=> { <LocalNodeSwitch
setIsEnabledDevMode(e.target.checked) checked={isEnabledDevMode}
localStorage.setItem('isEnabledDevMode', JSON.stringify(e.target.checked)) onChange={(e) => {
}} /> setIsEnabledDevMode(e.target.checked);
} localStorage.setItem(
label="Enable dev mode" 'isEnabledDevMode',
/> JSON.stringify(e.target.checked)
);
}}
/>
}
label="Enable dev mode"
/>
)} )}
</Box> </Box>
</Dialog> </Dialog>

View File

@ -1,23 +1,17 @@
import { Box, ButtonBase, Divider, Typography } from "@mui/material"; import { Box, ButtonBase, Divider, Typography } from '@mui/material';
import React, { import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
useCallback, import CloseIcon from '@mui/icons-material/Close';
useEffect, import AppViewerContainer from '../Apps/AppViewerContainer';
useMemo,
useRef,
useState,
} from "react";
import CloseIcon from "@mui/icons-material/Close";
import AppViewerContainer from "../Apps/AppViewerContainer";
import { import {
executeEvent, executeEvent,
subscribeToEvent, subscribeToEvent,
unsubscribeFromEvent, unsubscribeFromEvent,
} from "../../utils/events"; } from '../../utils/events';
import { useRecoilState } from "recoil"; import { useRecoilState } from 'recoil';
import { navigationControllerAtom } from "../../atoms/global"; import { navigationControllerAtom } from '../../atoms/global';
import { AppsNavBarLeft, AppsNavBarParent } from "../Apps/Apps-styles"; import { AppsNavBarLeft, AppsNavBarParent } from '../Apps/Apps-styles';
import NavBack from "../../assets/svgs/NavBack.svg"; import { NavBack } from '../../assets/Icons/NavBack.tsx';
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from '@mui/icons-material/Refresh';
export const WalletsAppWrapper = () => { export const WalletsAppWrapper = () => {
const iframeRef = useRef(null); const iframeRef = useRef(null);
@ -26,10 +20,10 @@ export const WalletsAppWrapper = () => {
navigationControllerAtom navigationControllerAtom
); );
const [selectedTab, setSelectedTab] = useState({ const [selectedTab, setSelectedTab] = useState({
tabId: "5558589", tabId: '5558589',
name: "Q-Wallets", name: 'Q-Wallets',
service: "APP", service: 'APP',
path: 'qortal?authOnMount=true' path: 'qortal?authOnMount=true',
}); });
const isDisableBackButton = useMemo(() => { const isDisableBackButton = useMemo(() => {
@ -48,64 +42,64 @@ export const WalletsAppWrapper = () => {
); );
useEffect(() => { useEffect(() => {
subscribeToEvent("openWalletsApp", openWalletsAppFunc); subscribeToEvent('openWalletsApp', openWalletsAppFunc);
return () => { return () => {
unsubscribeFromEvent("openWalletsApp", openWalletsAppFunc); unsubscribeFromEvent('openWalletsApp', openWalletsAppFunc);
}; };
}, [openWalletsAppFunc]); }, [openWalletsAppFunc]);
const handleClose = ()=> { const handleClose = () => {
setIsOpen(false); setIsOpen(false);
iframeRef.current = null iframeRef.current = null;
} };
return ( return (
<> <>
{isOpen && ( {isOpen && (
<Box <Box
sx={{ sx={{
position: "fixed", position: 'fixed',
height: "100vh", height: '100vh',
width: "100vw", width: '100vw',
backgroundColor: "#27282c", backgroundColor: '#27282c', // TODO: set color theme
zIndex: 100, zIndex: 100,
bottom: 0, bottom: 0,
right: 0, right: 0,
overflow: "hidden", overflow: 'hidden',
borderTopLeftRadius: "10px", borderTopLeftRadius: '10px',
borderTopRightRadius: "10px", borderTopRightRadius: '10px',
boxShadow: 4, boxShadow: 4,
}} }}
> >
<Box <Box
sx={{ sx={{
height: "100%", height: '100%',
width: "100%", width: '100%',
}} }}
> >
<Box <Box
sx={{ sx={{
height: "40px", height: '40px',
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
padding: "5px", padding: '5px',
justifyContent: "space-between", justifyContent: 'space-between',
}} }}
> >
<Typography>Q-Wallets</Typography> <Typography>Q-Wallets</Typography>
<ButtonBase <ButtonBase onClick={handleClose}>
onClick={handleClose}
>
<CloseIcon <CloseIcon
sx={{ sx={{
color: "white", color: 'white',
}} }}
/> />
</ButtonBase> </ButtonBase>
</Box> </Box>
<Divider /> <Divider />
<AppViewerContainer <AppViewerContainer
customHeight="calc(100% - 40px - 60px)" customHeight="calc(100% - 40px - 60px)"
app={selectedTab} app={selectedTab}
@ -114,9 +108,11 @@ export const WalletsAppWrapper = () => {
skipAuth={true} skipAuth={true}
/> />
<AppsNavBarParent> <AppsNavBarParent>
<AppsNavBarLeft sx={{ <AppsNavBarLeft
gap: '25px' sx={{
}}> gap: '25px',
}}
>
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {}); executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {});
@ -124,31 +120,30 @@ export const WalletsAppWrapper = () => {
disabled={isDisableBackButton} disabled={isDisableBackButton}
sx={{ sx={{
opacity: !isDisableBackButton ? 1 : 0.1, opacity: !isDisableBackButton ? 1 : 0.1,
cursor: !isDisableBackButton ? "pointer" : "default", cursor: !isDisableBackButton ? 'pointer' : 'default',
}} }}
> >
<img src={NavBack} /> <NavBack />
</ButtonBase> </ButtonBase>
<ButtonBase onClick={() => { <ButtonBase
if (selectedTab?.refreshFunc) { onClick={() => {
selectedTab.refreshFunc(selectedTab?.tabId); if (selectedTab?.refreshFunc) {
selectedTab.refreshFunc(selectedTab?.tabId);
} else { } else {
executeEvent("refreshApp", { executeEvent('refreshApp', {
tabId: selectedTab?.tabId, tabId: selectedTab?.tabId,
}); });
} }
}}
>
}}> <RefreshIcon
<RefreshIcon height={20}
height={20} sx={{
sx={{ color: 'rgba(250, 250, 250, 0.5)',
color: "rgba(250, 250, 250, 0.5)", height: '30px',
height: '30px', width: 'auto',
width: 'auto' }}
}} />
/>
</ButtonBase> </ButtonBase>
</AppsNavBarLeft> </AppsNavBarLeft>
</AppsNavBarParent> </AppsNavBarParent>

View File

@ -1,22 +1,29 @@
import React, { useContext, useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from 'react';
import Logo2 from "../assets/svgs/Logo2.svg"; import Logo2 from '../assets/svgs/Logo2.svg';
import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from "../App"; import { MyContext, getArbitraryEndpointReact, getBaseApiReact } from '../App';
import { Avatar, Box, Button, ButtonBase, Popover, Typography } from "@mui/material"; import {
import { Spacer } from "../common/Spacer"; Avatar,
import ImageUploader from "../common/ImageUploader"; Box,
import { getFee } from "../background"; Button,
import { fileToBase64 } from "../utils/fileReading"; ButtonBase,
import { LoadingButton } from "@mui/lab"; Popover,
Typography,
} from '@mui/material';
import { Spacer } from '../common/Spacer';
import ImageUploader from '../common/ImageUploader';
import { getFee } from '../background';
import { fileToBase64 } from '../utils/fileReading';
import { LoadingButton } from '@mui/lab';
import ErrorIcon from '@mui/icons-material/Error'; import ErrorIcon from '@mui/icons-material/Error';
export const MainAvatar = ({ myName, balance, setOpenSnack, setInfoSnack }) => { export const MainAvatar = ({ myName, balance, setOpenSnack, setInfoSnack }) => {
const [hasAvatar, setHasAvatar] = useState(false); const [hasAvatar, setHasAvatar] = useState(false);
const [avatarFile, setAvatarFile] = useState(null); const [avatarFile, setAvatarFile] = useState(null);
const [tempAvatar, setTempAvatar] = useState(null) const [tempAvatar, setTempAvatar] = useState(null);
const { show } = useContext(MyContext); const { show } = useContext(MyContext);
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false);
// Handle child element click to open Popover // Handle child element click to open Popover
const handleChildClick = (event) => { const handleChildClick = (event) => {
event.stopPropagation(); // Prevent parent onClick from firing event.stopPropagation(); // Prevent parent onClick from firing
@ -37,93 +44,105 @@ const [isLoading, setIsLoading] = useState(false)
const identifier = `qortal_avatar`; const identifier = `qortal_avatar`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=THUMBNAIL&identifier=${identifier}&limit=1&name=${myName}&includemetadata=false&prefix=true`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=THUMBNAIL&identifier=${identifier}&limit=1&name=${myName}&includemetadata=false&prefix=true`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
const responseData = await response.json(); const responseData = await response.json();
if (responseData?.length > 0) { if (responseData?.length > 0) {
setHasAvatar(true); setHasAvatar(true);
} }
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
useEffect(() => { useEffect(() => {
if (!myName) return; if (!myName) return;
checkIfAvatarExists(); checkIfAvatarExists();
}, [myName]); }, [myName]);
const publishAvatar = async () => {
const publishAvatar = async ()=> {
try { try {
const fee = await getFee('ARBITRARY') const fee = await getFee('ARBITRARY');
if(+balance < +fee.fee) throw new Error(`Publishing an Avatar requires ${fee.fee}`) if (+balance < +fee.fee)
await show({ throw new Error(`Publishing an Avatar requires ${fee.fee}`);
message: "Would you like to publish an avatar?" , await show({
publishFee: fee.fee + ' QORT' message: 'Would you like to publish an avatar?',
}) publishFee: fee.fee + ' QORT',
setIsLoading(true); });
const avatarBase64 = await fileToBase64(avatarFile) setIsLoading(true);
await new Promise((res, rej) => { const avatarBase64 = await fileToBase64(avatarFile);
window.sendMessage("publishOnQDN", { await new Promise((res, rej) => {
data: avatarBase64, window
identifier: "qortal_avatar", .sendMessage('publishOnQDN', {
service: "THUMBNAIL", data: avatarBase64,
}) identifier: 'qortal_avatar',
.then((response) => { service: 'THUMBNAIL',
if (!response?.error) { })
res(response); .then((response) => {
return; if (!response?.error) {
} res(response);
rej(response.error); return;
}) }
.catch((error) => { rej(response.error);
rej(error.message || "An error occurred"); })
}); .catch((error) => {
rej(error.message || 'An error occurred');
}); });
setAvatarFile(null); });
setTempAvatar(`data:image/webp;base64,${avatarBase64}`) setAvatarFile(null);
handleClose() setTempAvatar(`data:image/webp;base64,${avatarBase64}`);
handleClose();
} catch (error) { } catch (error) {
if (error?.message) { if (error?.message) {
setOpenSnack(true) setOpenSnack(true);
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: error?.message, message: error?.message,
}); });
} }
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
} };
if(tempAvatar){ if (tempAvatar) {
return ( return (
<> <>
<Avatar <Avatar
sx={{
height: '138px',
width: '138px',
}}
src={tempAvatar}
alt={myName}
>
{myName?.charAt(0)}
</Avatar>
<ButtonBase onClick={handleChildClick}>
<Typography
sx={{ sx={{
height: "138px", fontSize: '12px',
width: "138px", opacity: 0.5,
}} }}
src={tempAvatar}
alt={myName}
> >
{myName?.charAt(0)} change avatar
</Avatar> </Typography>
<ButtonBase onClick={handleChildClick}> </ButtonBase>
<Typography <PopoverComp
sx={{ myName={myName}
fontSize: "12px", avatarFile={avatarFile}
opacity: 0.5, setAvatarFile={setAvatarFile}
}} id={id}
> open={open}
change avatar anchorEl={anchorEl}
</Typography> handleClose={handleClose}
</ButtonBase> publishAvatar={publishAvatar}
<PopoverComp myName={myName} avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} /> isLoading={isLoading}
</> />
); </>
);
} }
if (hasAvatar) { if (hasAvatar) {
@ -131,8 +150,8 @@ const [isLoading, setIsLoading] = useState(false)
<> <>
<Avatar <Avatar
sx={{ sx={{
height: "138px", height: '138px',
width: "138px", width: '138px',
}} }}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${myName}/qortal_avatar?async=true`} src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${myName}/qortal_avatar?async=true`}
alt={myName} alt={myName}
@ -142,14 +161,24 @@ const [isLoading, setIsLoading] = useState(false)
<ButtonBase onClick={handleChildClick}> <ButtonBase onClick={handleChildClick}>
<Typography <Typography
sx={{ sx={{
fontSize: "12px", fontSize: '12px',
opacity: 0.5, opacity: 0.5,
}} }}
> >
change avatar change avatar
</Typography> </Typography>
</ButtonBase> </ButtonBase>
<PopoverComp myName={myName} avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} /> <PopoverComp
myName={myName}
avatarFile={avatarFile}
setAvatarFile={setAvatarFile}
id={id}
open={open}
anchorEl={anchorEl}
handleClose={handleClose}
publishAvatar={publishAvatar}
isLoading={isLoading}
/>
</> </>
); );
} }
@ -160,42 +189,61 @@ const [isLoading, setIsLoading] = useState(false)
<ButtonBase onClick={handleChildClick}> <ButtonBase onClick={handleChildClick}>
<Typography <Typography
sx={{ sx={{
fontSize: "12px", fontSize: '12px',
opacity: 0.5, opacity: 0.5,
}} }}
> >
set avatar set avatar
</Typography> </Typography>
</ButtonBase> </ButtonBase>
<PopoverComp myName={myName} avatarFile={avatarFile} setAvatarFile={setAvatarFile} id={id} open={open} anchorEl={anchorEl} handleClose={handleClose} publishAvatar={publishAvatar} isLoading={isLoading} /> <PopoverComp
myName={myName}
avatarFile={avatarFile}
setAvatarFile={setAvatarFile}
id={id}
open={open}
anchorEl={anchorEl}
handleClose={handleClose}
publishAvatar={publishAvatar}
isLoading={isLoading}
/>
</> </>
); );
}; };
const PopoverComp = ({
const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose, publishAvatar, isLoading, myName}) => { avatarFile,
return ( setAvatarFile,
<Popover id,
open,
anchorEl,
handleClose,
publishAvatar,
isLoading,
myName,
}) => {
return (
<Popover
id={id} id={id}
open={open} open={open}
anchorEl={anchorEl} anchorEl={anchorEl}
onClose={handleClose} // Close popover on click outside onClose={handleClose} // Close popover on click outside
anchorOrigin={{ anchorOrigin={{
vertical: "bottom", vertical: 'bottom',
horizontal: "center", horizontal: 'center',
}} }}
transformOrigin={{ transformOrigin={{
vertical: "top", vertical: 'top',
horizontal: "center", horizontal: 'center',
}} }}
> >
<Box sx={{ p: 2, display: "flex", flexDirection: "column", gap: 1 }}> <Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography <Typography
sx={{ sx={{
fontSize: "12px", fontSize: '12px',
}} }}
> >
(500 KB max. for GIFS){" "} (500 KB max. for GIFS){' '}
</Typography> </Typography>
<ImageUploader onPick={(file) => setAvatarFile(file)}> <ImageUploader onPick={(file) => setAvatarFile(file)}>
<Button variant="contained">Choose Image</Button> <Button variant="contained">Choose Image</Button>
@ -203,23 +251,34 @@ const PopoverComp = ({avatarFile, setAvatarFile, id, open, anchorEl, handleClose
{avatarFile?.name} {avatarFile?.name}
<Spacer height="25px" /> <Spacer height="25px" />
{!myName && ( {!myName && (
<Box sx={{ <Box
sx={{
display: 'flex', display: 'flex',
gap: '5px', gap: '5px',
alignItems: 'center' alignItems: 'center',
}}> }}
<ErrorIcon sx={{ >
color: 'white' <ErrorIcon
}} /> sx={{
<Typography>A registered name is required to set an avatar</Typography> color: 'white',
}}
/>
<Typography>
A registered name is required to set an avatar
</Typography>
</Box> </Box>
)} )}
<Spacer height="25px" /> <Spacer height="25px" />
<LoadingButton loading={isLoading} disabled={!avatarFile || !myName} onClick={publishAvatar} variant="contained"> <LoadingButton
loading={isLoading}
disabled={!avatarFile || !myName}
onClick={publishAvatar}
variant="contained"
>
Publish avatar Publish avatar
</LoadingButton> </LoadingButton>
</Box> </Box>
</Popover> </Popover>
) );
}; };

View File

@ -9,31 +9,20 @@ import {
DialogTitle, DialogTitle,
Divider, Divider,
IconButton, IconButton,
InputBase,
InputLabel,
Snackbar, Snackbar,
Typography, Typography,
useTheme,
} from '@mui/material'; } from '@mui/material';
import React, { import { useCallback, useEffect, useMemo, useState } from 'react';
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { MyContext, getBaseApiReact } from '../../App'; import { getBaseApiReact } from '../../App';
import { import {
executeEvent, executeEvent,
subscribeToEvent, subscribeToEvent,
unsubscribeFromEvent, unsubscribeFromEvent,
} from '../../utils/events'; } from '../../utils/events';
import { getFee, getNameOrAddress } from '../../background'; import { getFee, getNameOrAddress } from '../../background';
import CopyToClipboard from 'react-copy-to-clipboard';
import { AddressBox } from '../../styles/App-styles';
import { Spacer } from '../../common/Spacer'; import { Spacer } from '../../common/Spacer';
import Copy from '../../assets/svgs/Copy.svg';
import { Loader } from '../Loader';
import { FidgetSpinner } from 'react-loader-spinner'; import { FidgetSpinner } from 'react-loader-spinner';
import { useModal } from '../../common/useModal'; import { useModal } from '../../common/useModal';
@ -56,15 +45,18 @@ export const Minting = ({
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { show: showKey, message } = useModal(); const { show: showKey, message } = useModal();
const { isShow: isShowNext, onOk, show: showNext } = useModal(); const { isShow: isShowNext, onOk, show: showNext } = useModal();
const theme = useTheme();
const [info, setInfo] = useState(null); const [info, setInfo] = useState(null);
const [names, setNames] = useState({}); const [names, setNames] = useState({});
const [accountInfos, setAccountInfos] = useState({}); const [accountInfos, setAccountInfos] = useState({});
const [showWaitDialog, setShowWaitDialog] = useState(false); const [showWaitDialog, setShowWaitDialog] = useState(false);
const isPartOfMintingGroup = useMemo(() => { const isPartOfMintingGroup = useMemo(() => {
if (groups?.length === 0) return false; if (groups?.length === 0) return false;
return !!groups?.find((item) => item?.groupId?.toString() === '694'); return !!groups?.find((item) => item?.groupId?.toString() === '694');
}, [groups]); }, [groups]);
const getMintingAccounts = useCallback(async () => { const getMintingAccounts = useCallback(async () => {
try { try {
const url = `${getBaseApiReact()}/admin/mintingaccounts`; const url = `${getBaseApiReact()}/admin/mintingaccounts`;
@ -74,7 +66,9 @@ export const Minting = ({
} }
const data = await response.json(); const data = await response.json();
setMintingAccounts(data); setMintingAccounts(data);
} catch (error) {} } catch (error) {
console.log(error);
}
}, []); }, []);
const accountIsMinting = useMemo(() => { const accountIsMinting = useMemo(() => {
@ -105,7 +99,7 @@ export const Minting = ({
}); });
} }
} catch (error) { } catch (error) {
// error console.log(error);
} }
}; };
@ -131,6 +125,7 @@ export const Minting = ({
setAccountInfo(data); setAccountInfo(data);
} }
} catch (error) { } catch (error) {
console.log(error);
} finally { } finally {
if (!others) { if (!others) {
setIsLoading(false); setIsLoading(false);
@ -199,7 +194,9 @@ export const Minting = ({
const data = await response.json(); const data = await response.json();
setRewardShares(data); setRewardShares(data);
return data; return data;
} catch (error) {} } catch (error) {
console.log(error);
}
}, []); }, []);
const addMintingAccount = useCallback(async (val) => { const addMintingAccount = useCallback(async (val) => {
@ -250,7 +247,6 @@ export const Minting = ({
window window
.sendMessage( .sendMessage(
'ADMIN_ACTION', 'ADMIN_ACTION',
{ {
type: 'removemintingaccount', type: 'removemintingaccount',
value: val, value: val,
@ -354,7 +350,6 @@ export const Minting = ({
if (findRewardShare) { if (findRewardShare) {
return true; // Exit early if found return true; // Exit early if found
} }
await sleep(pollingInterval); // Wait before the next poll await sleep(pollingInterval); // Wait before the next poll
} }
@ -507,7 +502,7 @@ export const Minting = ({
const _blocksNeed = () => { const _blocksNeed = () => {
if (accountInfo?.level === 0) { if (accountInfo?.level === 0) {
return 7200; return 7200; // TODO manage these magic numbers in a proper location
} else if (accountInfo?.level === 1) { } else if (accountInfo?.level === 1) {
return 72000; return 72000;
} else if (accountInfo?.level === 2) { } else if (accountInfo?.level === 2) {
@ -558,11 +553,11 @@ export const Minting = ({
fullScreen fullScreen
sx={{ sx={{
'& .MuiDialog-paper': { '& .MuiDialog-paper': {
height: '100vh',
margin: 0, margin: 0,
maxWidth: '100%', maxWidth: '100%',
width: '100%',
height: '100vh',
overflow: 'hidden', // Prevent scrollbars overflow: 'hidden', // Prevent scrollbars
width: '100%',
}, },
}} }}
> >
@ -579,6 +574,7 @@ export const Minting = ({
> >
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
<DialogContent <DialogContent
sx={{ sx={{
position: 'relative', position: 'relative',
@ -587,37 +583,40 @@ export const Minting = ({
{isLoading && ( {isLoading && (
<Box <Box
sx={{ sx={{
position: 'absolute', alignItems: 'center',
top: 0,
left: 0,
right: 0,
bottom: 0, bottom: 0,
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', left: 0,
position: 'absolute',
right: 0,
top: 0,
}} }}
> >
<FidgetSpinner <FidgetSpinner
visible={true}
height="80"
width="80"
ariaLabel="fidget-spinner-loading" ariaLabel="fidget-spinner-loading"
wrapperStyle={{}} height="80"
visible={true}
width="80"
wrapperClass="fidget-spinner-wrapper" wrapperClass="fidget-spinner-wrapper"
wrapperStyle={{}}
/> />
</Box> </Box>
)} )}
<Card <Card
sx={{ sx={{
backgroundColor: 'var(--bg-2)', backgroundColor: theme.palette.background.default,
padding: '10px', padding: '10px',
}} }}
> >
<Typography>Account: {handleNames(accountInfo?.address)}</Typography> <Typography>Account: {handleNames(accountInfo?.address)}</Typography>
<Typography>Level: {accountInfo?.level}</Typography> <Typography>Level: {accountInfo?.level}</Typography>
<Typography> <Typography>
blocks remaining until next level: {_levelUpBlocks()} blocks remaining until next level: {_levelUpBlocks()}
</Typography> </Typography>
<Typography> <Typography>
This node is minting: {nodeInfos?.isMintingPossible?.toString()} This node is minting: {nodeInfos?.isMintingPossible?.toString()}
</Typography> </Typography>
@ -626,11 +625,11 @@ export const Minting = ({
{isPartOfMintingGroup && !accountIsMinting && ( {isPartOfMintingGroup && !accountIsMinting && (
<Box <Box
sx={{ sx={{
display: 'flex',
gap: '5px',
flexDirection: 'column',
width: '100%',
alignItems: 'center', alignItems: 'center',
display: 'flex',
flexDirection: 'column',
gap: '5px',
width: '100%',
}} }}
> >
<Button <Button
@ -670,7 +669,7 @@ export const Minting = ({
)} )}
<Card <Card
sx={{ sx={{
backgroundColor: 'var(--bg-2)', backgroundColor: theme.palette.background.default,
padding: '10px', padding: '10px',
}} }}
> >
@ -705,14 +704,14 @@ export const Minting = ({
size="small" size="small"
sx={{ sx={{
backgroundColor: 'var(--danger)', backgroundColor: 'var(--danger)',
color: 'black', color: theme.palette.text.primary,
fontWeight: 'bold', fontWeight: 'bold',
opacity: 0.7,
maxWidth: '90%', maxWidth: '90%',
opacity: 0.7,
width: '200px', width: '200px',
'&:hover': { '&:hover': {
backgroundColor: 'var(--danger)', backgroundColor: 'var(--danger)',
color: 'black', color: theme.palette.text.primary,
opacity: 1, opacity: 1,
}, },
}} }}
@ -723,7 +722,9 @@ export const Minting = ({
> >
Remove minting account Remove minting account
</Button> </Button>
<Divider /> <Divider />
<Spacer height="10px" /> <Spacer height="10px" />
</Box> </Box>
))} ))}
@ -740,7 +741,7 @@ export const Minting = ({
{!isPartOfMintingGroup && ( {!isPartOfMintingGroup && (
<Card <Card
sx={{ sx={{
backgroundColor: 'var(--bg-2)', backgroundColor: theme.palette.background.default,
padding: '10px', padding: '10px',
}} }}
> >
@ -764,7 +765,7 @@ export const Minting = ({
size="small" size="small"
sx={{ sx={{
backgroundColor: 'var(--green)', backgroundColor: 'var(--green)',
color: 'black', color: theme.palette.text.primary,
fontWeight: 'bold', fontWeight: 'bold',
opacity: 0.7, opacity: 0.7,
@ -798,6 +799,7 @@ export const Minting = ({
<DialogTitle id="alert-dialog-title"> <DialogTitle id="alert-dialog-title">
{isShowNext ? 'Confirmed' : 'Please Wait'} {isShowNext ? 'Confirmed' : 'Please Wait'}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
{!isShowNext && ( {!isShowNext && (
<Typography> <Typography>

View File

@ -1,12 +1,15 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import QMailLogo from '../assets/QMailLogo.png'; import EmailIcon from '@mui/icons-material/Email';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { mailsAtom, qMailLastEnteredTimestampAtom } from '../atoms/global'; import { mailsAtom, qMailLastEnteredTimestampAtom } from '../atoms/global';
import { isLessThanOneWeekOld } from './Group/QMailMessages'; import { isLessThanOneWeekOld } from './Group/QMailMessages';
import { ButtonBase, Tooltip } from '@mui/material'; import { ButtonBase, Tooltip, useTheme } from '@mui/material';
import { executeEvent } from '../utils/events'; import { executeEvent } from '../utils/events';
import { Mail } from '@mui/icons-material';
export const QMailStatus = () => { export const QMailStatus = () => {
const theme = useTheme();
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState( const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(
qMailLastEnteredTimestampAtom qMailLastEnteredTimestampAtom
); );
@ -24,6 +27,7 @@ export const QMailStatus = () => {
return true; return true;
return false; return false;
}, [lastEnteredTimestamp, mails]); }, [lastEnteredTimestamp, mails]);
return ( return (
<ButtonBase <ButtonBase
onClick={() => { onClick={() => {
@ -38,21 +42,27 @@ export const QMailStatus = () => {
{hasNewMail && ( {hasNewMail && (
<div <div
style={{ style={{
position: 'absolute',
zIndex: 1,
top: '-7px',
right: '-7px',
backgroundColor: 'var(--unread)', backgroundColor: 'var(--unread)',
height: '15px',
width: '15px',
borderRadius: '50%', borderRadius: '50%',
height: '15px',
outline: '1px solid white', outline: '1px solid white',
position: 'absolute',
right: '-7px',
top: '-7px',
width: '15px',
zIndex: 1,
}} }}
/> />
)} )}
<Tooltip <Tooltip
title={ title={
<span style={{ color: 'white', fontSize: '14px', fontWeight: 700 }}> <span
style={{
color: theme.palette.text.primary,
fontSize: '14px',
fontWeight: 700,
}}
>
Q-MAIL Q-MAIL
</span> </span>
} }
@ -62,18 +72,18 @@ export const QMailStatus = () => {
slotProps={{ slotProps={{
tooltip: { tooltip: {
sx: { sx: {
color: '#ffffff', color: theme.palette.text.primary,
backgroundColor: '#444444', backgroundColor: theme.palette.background.default,
}, },
}, },
arrow: { arrow: {
sx: { sx: {
color: '#444444', color: theme.palette.text.primary,
}, },
}, },
}} }}
> >
<img style={{ width: '24px', height: 'auto' }} src={QMailLogo} /> <Mail />
</Tooltip> </Tooltip>
</ButtonBase> </ButtonBase>
); );

View File

@ -1,5 +1,5 @@
import { Box, CircularProgress } from '@mui/material'; import { Box, CircularProgress, useTheme } from '@mui/material';
import React, { useEffect, useState } from 'react'; import { useState } from 'react';
import { import {
CustomButton, CustomButton,
CustomInput, CustomInput,
@ -13,6 +13,7 @@ import { ErrorText } from './ErrorText/ErrorText';
import { getFee } from '../background'; import { getFee } from '../background';
export const QortPayment = ({ balance, show, onSuccess, defaultPaymentTo }) => { export const QortPayment = ({ balance, show, onSuccess, defaultPaymentTo }) => {
const theme = useTheme();
const [paymentTo, setPaymentTo] = useState<string>(defaultPaymentTo); const [paymentTo, setPaymentTo] = useState<string>(defaultPaymentTo);
const [paymentAmount, setPaymentAmount] = useState<number>(0); const [paymentAmount, setPaymentAmount] = useState<number>(0);
const [paymentPassword, setPaymentPassword] = useState<string>(''); const [paymentPassword, setPaymentPassword] = useState<string>('');
@ -42,7 +43,9 @@ export const QortPayment = ({ balance, show, onSuccess, defaultPaymentTo }) => {
message: `Would you like to transfer ${Number(paymentAmount)} QORT?`, message: `Would you like to transfer ${Number(paymentAmount)} QORT?`,
paymentFee: fee.fee + ' QORT', paymentFee: fee.fee + ' QORT',
}); });
setIsLoadingSendCoin(true); setIsLoadingSendCoin(true);
window window
.sendMessage('sendCoin', { .sendMessage('sendCoin', {
amount: Number(paymentAmount), amount: Number(paymentAmount),
@ -62,65 +65,76 @@ export const QortPayment = ({ balance, show, onSuccess, defaultPaymentTo }) => {
setIsLoadingSendCoin(false); setIsLoadingSendCoin(false);
}); });
} catch (error) { } catch (error) {
// error console.log(error);
} }
}; };
return ( return (
<> <>
<Box <Box
sx={{ sx={{
alignItems: 'flex-start',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'flex-start',
}} }}
> >
<TextP <TextP
sx={{ sx={{
textAlign: 'start',
lineHeight: '24px',
fontSize: '20px', fontSize: '20px',
fontWeight: 600, fontWeight: 600,
lineHeight: '24px',
textAlign: 'start',
}} }}
> >
Transfer QORT Transfer QORT
</TextP> </TextP>
<Spacer height="35px" /> <Spacer height="35px" />
<TextP <TextP
sx={{ sx={{
textAlign: 'start', color: theme.palette.text.primary,
lineHeight: '16px',
fontSize: '20px', fontSize: '20px',
fontWeight: 600, fontWeight: 600,
color: 'rgba(255, 255, 255, 0.5)', lineHeight: '16px',
textAlign: 'start',
}} }}
> >
Balance: Balance:
</TextP> </TextP>
<TextP <TextP
sx={{ sx={{
textAlign: 'start',
lineHeight: '24px',
fontSize: '20px', fontSize: '20px',
fontWeight: 700, fontWeight: 700,
lineHeight: '24px',
textAlign: 'start',
}} }}
> >
{balance?.toFixed(2)} QORT {balance?.toFixed(2)} QORT
</TextP> </TextP>
</Box> </Box>
<Spacer height="35px" /> <Spacer height="35px" />
<Box> <Box>
<CustomLabel htmlFor="standard-adornment-name">To</CustomLabel> <CustomLabel htmlFor="standard-adornment-name">To</CustomLabel>
<Spacer height="5px" /> <Spacer height="5px" />
<CustomInput <CustomInput
id="standard-adornment-name" id="standard-adornment-name"
value={paymentTo} value={paymentTo}
onChange={(e) => setPaymentTo(e.target.value)} onChange={(e) => setPaymentTo(e.target.value)}
autoComplete="off" autoComplete="off"
/> />
<Spacer height="6px" /> <Spacer height="6px" />
<CustomLabel htmlFor="standard-adornment-amount">Amount</CustomLabel> <CustomLabel htmlFor="standard-adornment-amount">Amount</CustomLabel>
<Spacer height="5px" /> <Spacer height="5px" />
<BoundedNumericTextField <BoundedNumericTextField
value={paymentAmount} value={paymentAmount}
minValue={0} minValue={0}
@ -130,11 +144,15 @@ export const QortPayment = ({ balance, show, onSuccess, defaultPaymentTo }) => {
allowNegatives={false} allowNegatives={false}
afterChange={(e: string) => setPaymentAmount(+e)} afterChange={(e: string) => setPaymentAmount(+e)}
/> />
<Spacer height="6px" /> <Spacer height="6px" />
<CustomLabel htmlFor="standard-adornment-password"> <CustomLabel htmlFor="standard-adornment-password">
Confirm Wallet Password Confirm Wallet Password
</CustomLabel> </CustomLabel>
<Spacer height="5px" /> <Spacer height="5px" />
<PasswordField <PasswordField
id="standard-adornment-password" id="standard-adornment-password"
value={paymentPassword} value={paymentPassword}
@ -142,10 +160,14 @@ export const QortPayment = ({ balance, show, onSuccess, defaultPaymentTo }) => {
autoComplete="off" autoComplete="off"
/> />
</Box> </Box>
<Spacer height="10px" /> <Spacer height="10px" />
<ErrorText>{sendPaymentError}</ErrorText> <ErrorText>{sendPaymentError}</ErrorText>
{/* <Typography>{sendPaymentSuccess}</Typography> */} {/* <Typography>{sendPaymentSuccess}</Typography> */}
<Spacer height="25px" /> <Spacer height="25px" />
<CustomButton <CustomButton
sx={{ sx={{
cursor: isLoadingSendCoin ? 'default' : 'pointer', cursor: isLoadingSendCoin ? 'default' : 'pointer',
@ -159,7 +181,7 @@ export const QortPayment = ({ balance, show, onSuccess, defaultPaymentTo }) => {
<CircularProgress <CircularProgress
size={16} size={16}
sx={{ sx={{
color: 'white', color: theme.palette.text.primary,
}} }}
/> />
)} )}

View File

@ -1,6 +1,6 @@
import React, { useContext, useEffect, useMemo, useState } from "react"; import React, { useContext, useEffect, useMemo, useState } from 'react';
import { useRecoilState, useSetRecoilState } from "recoil"; import { useRecoilState, useSetRecoilState } from 'recoil';
import isEqual from "lodash/isEqual"; // Import deep comparison utility import isEqual from 'lodash/isEqual'; // Import deep comparison utility
import { import {
canSaveSettingToQdnAtom, canSaveSettingToQdnAtom,
hasSettingsChangedAtom, hasSettingsChangedAtom,
@ -9,35 +9,35 @@ import {
settingsLocalLastUpdatedAtom, settingsLocalLastUpdatedAtom,
settingsQDNLastUpdatedAtom, settingsQDNLastUpdatedAtom,
sortablePinnedAppsAtom, sortablePinnedAppsAtom,
} from "../../atoms/global"; } from '../../atoms/global';
import { Box, Button, ButtonBase, Popover, Typography } from "@mui/material"; import { Box, Button, ButtonBase, Popover, Typography } from '@mui/material';
import { objectToBase64 } from "../../qdn/encryption/group-encryption"; import { objectToBase64 } from '../../qdn/encryption/group-encryption';
import { MyContext } from "../../App"; import { MyContext } from '../../App';
import { getFee } from "../../background"; import { getFee } from '../../background';
import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { SaveIcon } from "../../assets/svgs/SaveIcon"; import { SaveIcon } from '../../assets/Icons/SaveIcon';
import { IconWrapper } from "../Desktop/DesktopFooter"; import { IconWrapper } from '../Desktop/DesktopFooter';
import { Spacer } from "../../common/Spacer"; import { Spacer } from '../../common/Spacer';
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from '@mui/lab';
import { saveToLocalStorage } from "../Apps/AppsNavBar"; import { saveToLocalStorage } from '../Apps/AppsNavBar';
import { decryptData, encryptData } from "../../qortalRequests/get"; import { decryptData, encryptData } from '../../qortalRequests/get';
import { saveFileToDiskGeneric } from "../../utils/generateWallet/generateWallet"; import { saveFileToDiskGeneric } from '../../utils/generateWallet/generateWallet';
import { import {
base64ToUint8Array, base64ToUint8Array,
uint8ArrayToObject, uint8ArrayToObject,
} from "../../backgroundFunctions/encryption"; } from '../../backgroundFunctions/encryption';
export const handleImportClick = async () => { export const handleImportClick = async () => {
const fileInput = document.createElement("input"); const fileInput = document.createElement('input');
fileInput.type = "file"; fileInput.type = 'file';
fileInput.accept = ".base64,.txt"; fileInput.accept = '.base64,.txt';
// Create a promise to handle file selection and reading synchronously // Create a promise to handle file selection and reading synchronously
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
fileInput.onchange = () => { fileInput.onchange = () => {
const file = fileInput.files[0]; const file = fileInput.files[0];
if (!file) { if (!file) {
reject(new Error("No file selected")); reject(new Error('No file selected'));
return; return;
} }
@ -46,7 +46,7 @@ export const handleImportClick = async () => {
resolve(e.target.result); // Resolve with the file content resolve(e.target.result); // Resolve with the file content
}; };
reader.onerror = () => { reader.onerror = () => {
reject(new Error("Error reading file")); reject(new Error('Error reading file'));
}; };
reader.readAsText(file); // Read the file as text (Base64 string) reader.readAsText(file); // Read the file as text (Base64 string)
@ -124,7 +124,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
const encryptData = await new Promise((res, rej) => { const encryptData = await new Promise((res, rej) => {
window window
.sendMessage( .sendMessage(
"ENCRYPT_DATA", 'ENCRYPT_DATA',
{ {
data64, data64,
}, },
@ -139,23 +139,23 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
} }
}) })
.catch((error) => { .catch((error) => {
console.error("Failed qortalRequest", error); console.error('Failed qortalRequest', error);
}); });
}); });
if (encryptData && !encryptData?.error) { if (encryptData && !encryptData?.error) {
const fee = await getFee("ARBITRARY"); const fee = await getFee('ARBITRARY');
await show({ await show({
message: message:
"Would you like to publish your settings to QDN (encrypted) ?", 'Would you like to publish your settings to QDN (encrypted) ?',
publishFee: fee.fee + " QORT", publishFee: fee.fee + ' QORT',
}); });
const response = await new Promise((res, rej) => { const response = await new Promise((res, rej) => {
window window
.sendMessage("publishOnQDN", { .sendMessage('publishOnQDN', {
data: encryptData, data: encryptData,
identifier: "ext_saved_settings", identifier: 'ext_saved_settings',
service: "DOCUMENT_PRIVATE", service: 'DOCUMENT_PRIVATE',
}) })
.then((response) => { .then((response) => {
if (!response?.error) { if (!response?.error) {
@ -165,15 +165,15 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
rej(response.error); rej(response.error);
}) })
.catch((error) => { .catch((error) => {
rej(error.message || "An error occurred"); rej(error.message || 'An error occurred');
}); });
}); });
if (response?.identifier) { if (response?.identifier) {
setOldPinnedApps(pinnedApps); setOldPinnedApps(pinnedApps);
setSettingsQdnLastUpdated(Date.now()); setSettingsQdnLastUpdated(Date.now());
setInfoSnack({ setInfoSnack({
type: "success", type: 'success',
message: "Sucessfully published to QDN", message: 'Sucessfully published to QDN',
}); });
setOpenSnack(true); setOpenSnack(true);
setAnchorEl(null); setAnchorEl(null);
@ -181,8 +181,8 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
} }
} catch (error) { } catch (error) {
setInfoSnack({ setInfoSnack({
type: "error", type: 'error',
message: error?.message || "Unable to save to QDN", message: error?.message || 'Unable to save to QDN',
}); });
setOpenSnack(true); setOpenSnack(true);
} finally { } finally {
@ -196,7 +196,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
const revertChanges = () => { const revertChanges = () => {
setPinnedApps(oldPinnedApps); setPinnedApps(oldPinnedApps);
saveToLocalStorage("ext_saved_settings", "sortablePinnedApps", null); saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', null);
setAnchorEl(null); setAnchorEl(null);
}; };
@ -218,11 +218,11 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
selected={false} selected={false}
> >
<SaveIcon <SaveIcon
color={hasChanged && !isLoading ? "#5EB049" : undefined} color={hasChanged && !isLoading ? '#5EB049' : undefined}
/> />
</IconWrapper> </IconWrapper>
) : ( ) : (
<SaveIcon color={hasChanged && !isLoading ? "#5EB049" : undefined} /> <SaveIcon color={hasChanged && !isLoading ? '#5EB049' : undefined} />
)} )}
</ButtonBase> </ButtonBase>
<Popover <Popover
@ -230,41 +230,41 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
anchorEl={anchorEl} anchorEl={anchorEl}
onClose={() => setAnchorEl(null)} // Close popover on click outside onClose={() => setAnchorEl(null)} // Close popover on click outside
anchorOrigin={{ anchorOrigin={{
vertical: "bottom", vertical: 'bottom',
horizontal: "center", horizontal: 'center',
}} }}
transformOrigin={{ transformOrigin={{
vertical: "top", vertical: 'top',
horizontal: "center", horizontal: 'center',
}} }}
sx={{ sx={{
width: "300px", width: '300px',
maxWidth: "90%", maxWidth: '90%',
maxHeight: "80%", maxHeight: '80%',
overflow: "auto", overflow: 'auto',
}} }}
> >
{isUsingImportExportSettings && ( {isUsingImportExportSettings && (
<Box <Box
sx={{ sx={{
padding: "15px", padding: '15px',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: 1, gap: 1,
width: "100%", width: '100%',
}} }}
> >
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: '14px',
}} }}
> >
You are using the export/import way of saving settings. You are using the export/import way of saving settings.
@ -274,8 +274,8 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
size="small" size="small"
onClick={() => { onClick={() => {
saveToLocalStorage( saveToLocalStorage(
"ext_saved_settings_import_export", 'ext_saved_settings_import_export',
"sortablePinnedApps", 'sortablePinnedApps',
null, null,
true true
); );
@ -283,13 +283,13 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
}} }}
variant="contained" variant="contained"
sx={{ sx={{
backgroundColor: "var(--danger)", backgroundColor: 'var(--danger)',
color: "black", color: 'black',
fontWeight: "bold", fontWeight: 'bold',
opacity: 0.7, opacity: 0.7,
"&:hover": { '&:hover': {
backgroundColor: "var(--danger)", backgroundColor: 'var(--danger)',
color: "black", color: 'black',
opacity: 1, opacity: 1,
}, },
}} }}
@ -302,25 +302,25 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
{!isUsingImportExportSettings && ( {!isUsingImportExportSettings && (
<Box <Box
sx={{ sx={{
padding: "15px", padding: '15px',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: 1, gap: 1,
width: "100%", width: '100%',
}} }}
> >
{!myName ? ( {!myName ? (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: '14px',
}} }}
> >
You need a registered Qortal name to save your pinned apps to You need a registered Qortal name to save your pinned apps to
@ -332,15 +332,15 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
{hasChanged && ( {hasChanged && (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: '14px',
}} }}
> >
You have unsaved changes to your pinned apps. Save them to You have unsaved changes to your pinned apps. Save them to
@ -349,13 +349,13 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
<Spacer height="10px" /> <Spacer height="10px" />
<LoadingButton <LoadingButton
sx={{ sx={{
backgroundColor: "var(--green)", backgroundColor: 'var(--green)',
color: "black", color: 'black',
opacity: 0.7, opacity: 0.7,
fontWeight: "bold", fontWeight: 'bold',
"&:hover": { '&:hover': {
backgroundColor: "var(--green)", backgroundColor: 'var(--green)',
color: "black", color: 'black',
opacity: 1, opacity: 1,
}, },
}} }}
@ -372,7 +372,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
<> <>
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: '14px',
}} }}
> >
Don't like your current local changes? Would you Don't like your current local changes? Would you
@ -385,13 +385,13 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
onClick={revertChanges} onClick={revertChanges}
variant="contained" variant="contained"
sx={{ sx={{
backgroundColor: "var(--danger)", backgroundColor: 'var(--danger)',
color: "black", color: 'black',
fontWeight: "bold", fontWeight: 'bold',
opacity: 0.7, opacity: 0.7,
"&:hover": { '&:hover': {
backgroundColor: "var(--danger)", backgroundColor: 'var(--danger)',
color: "black", color: 'black',
opacity: 1, opacity: 1,
}, },
}} }}
@ -405,7 +405,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
<> <>
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: '14px',
}} }}
> >
Don't like your current local changes? Would you Don't like your current local changes? Would you
@ -428,15 +428,15 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
isUsingImportExportSettings !== true && ( isUsingImportExportSettings !== true && (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: '14px',
}} }}
> >
The app was unable to download your existing QDN-saved The app was unable to download your existing QDN-saved
@ -449,13 +449,13 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
onClick={saveToQdn} onClick={saveToQdn}
variant="contained" variant="contained"
sx={{ sx={{
backgroundColor: "var(--danger)", backgroundColor: 'var(--danger)',
color: "black", color: 'black',
fontWeight: "bold", fontWeight: 'bold',
opacity: 0.7, opacity: 0.7,
"&:hover": { '&:hover': {
backgroundColor: "var(--danger)", backgroundColor: 'var(--danger)',
color: "black", color: 'black',
opacity: 1, opacity: 1,
}, },
}} }}
@ -467,15 +467,15 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
{!hasChanged && ( {!hasChanged && (
<Box <Box
sx={{ sx={{
width: "100%", width: '100%',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
alignItems: "center", alignItems: 'center',
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: '14px',
}} }}
> >
You currently do not have any changes to your pinned apps You currently do not have any changes to your pinned apps
@ -488,19 +488,19 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
)} )}
<Box <Box
sx={{ sx={{
padding: "15px", padding: '15px',
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
gap: 1, gap: 1,
width: "100%", width: '100%',
}} }}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: 'flex',
gap: "10px", gap: '10px',
justifyContent: "flex-end", justifyContent: 'flex-end',
width: "100%", width: '100%',
}} }}
> >
<ButtonBase <ButtonBase
@ -517,8 +517,8 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
); );
if (Array.isArray(responseData)) { if (Array.isArray(responseData)) {
saveToLocalStorage( saveToLocalStorage(
"ext_saved_settings_import_export", 'ext_saved_settings_import_export',
"sortablePinnedApps", 'sortablePinnedApps',
responseData, responseData,
{ {
isUsingImportExport: true, isUsingImportExport: true,
@ -529,7 +529,7 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
setIsUsingImportExportSettings(true); setIsUsingImportExportSettings(true);
} }
} catch (error) { } catch (error) {
console.log("error", error); console.log('error', error);
} }
}} }}
> >
@ -544,14 +544,14 @@ export const Save = ({ isDesktop, disableWidth, myName }) => {
data64, data64,
}); });
const blob = new Blob([encryptedData], { const blob = new Blob([encryptedData], {
type: "text/plain", type: 'text/plain',
}); });
const timestamp = new Date().toISOString().replace(/:/g, "-"); // Safe timestamp for filenames const timestamp = new Date().toISOString().replace(/:/g, '-'); // Safe timestamp for filenames
const filename = `qortal-new-ui-backup-settings-${timestamp}.txt`; const filename = `qortal-new-ui-backup-settings-${timestamp}.txt`;
await saveFileToDiskGeneric(blob, filename); await saveFileToDiskGeneric(blob, filename);
} catch (error) { } catch (error) {
console.log("error", error); console.log('error', error);
} }
}} }}
> >

View File

@ -1,31 +1,36 @@
import * as React from 'react'; import * as React from 'react';
import Button from '@mui/material/Button';
import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar'; import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar';
import Alert from '@mui/material/Alert'; import Alert from '@mui/material/Alert';
export const CustomizedSnackbars = ({open, setOpen, info, setInfo, duration}) => { export const CustomizedSnackbars = ({
open,
setOpen,
info,
setInfo,
duration,
}) => {
const handleClose = ( const handleClose = (
event?: React.SyntheticEvent | Event, event?: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason, reason?: SnackbarCloseReason
) => { ) => {
if (reason === 'clickaway') { if (reason === 'clickaway') {
return; return;
} }
setOpen(false); setOpen(false);
setInfo(null) setInfo(null);
}; };
if(!open) return null if (!open) return null;
return ( return (
<div> <div>
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={info?.duration === null ? null : (duration || 6000)} onClose={handleClose}> <Snackbar
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
open={open}
autoHideDuration={info?.duration === null ? null : duration || 6000}
onClose={handleClose}
>
<Alert <Alert
onClose={handleClose} onClose={handleClose}
severity={info?.type} severity={info?.type}
variant="filled" variant="filled"
@ -36,4 +41,4 @@ export const CustomizedSnackbars = ({open, setOpen, info, setInfo, duration}) =
</Snackbar> </Snackbar>
</div> </div>
); );
} };

View File

@ -5,19 +5,21 @@ import {
ListItemText, ListItemText,
Collapse, Collapse,
IconButton, IconButton,
} from "@mui/material"; useTheme,
import React, { useContext, useEffect, useRef } from "react"; } from '@mui/material';
import PendingIcon from "@mui/icons-material/Pending"; import React, { useContext, useEffect, useRef } from 'react';
import TaskAltIcon from "@mui/icons-material/TaskAlt"; import PendingIcon from '@mui/icons-material/Pending';
import ExpandLess from "@mui/icons-material/ExpandLess"; import TaskAltIcon from '@mui/icons-material/TaskAlt';
import ExpandMore from "@mui/icons-material/ExpandMore"; import ExpandLess from '@mui/icons-material/ExpandLess';
import { MyContext, getBaseApiReact, isMobile } from "../../App"; import ExpandMore from '@mui/icons-material/ExpandMore';
import { executeEvent } from "../../utils/events"; import { MyContext, getBaseApiReact, isMobile } from '../../App';
import { executeEvent } from '../../utils/events';
export const TaskManager = ({ getUserInfo }) => { export const TaskManager = ({ getUserInfo }) => {
const { txList, setTxList, memberGroups } = useContext(MyContext); const { txList, setTxList, memberGroups } = useContext(MyContext);
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const intervals = useRef({}); const intervals = useRef({});
const theme = useTheme();
const handleClick = () => { const handleClick = () => {
setOpen((prev) => !prev); setOpen((prev) => !prev);
@ -58,7 +60,9 @@ export const TaskManager = ({ getUserInfo }) => {
} }
clearInterval(intervals.current[signature]); clearInterval(intervals.current[signature]);
} }
} catch (error) {} } catch (error) {
console.log(error);
}
stop = false; stop = false;
} }
}; };
@ -71,7 +75,7 @@ export const TaskManager = ({ getUserInfo }) => {
let previousData = [...prev]; let previousData = [...prev];
memberGroups.forEach((group) => { memberGroups.forEach((group) => {
const findGroup = txList.findIndex( const findGroup = txList.findIndex(
(tx) => tx?.type === "joined-group" && tx?.groupId === group.groupId (tx) => tx?.type === 'joined-group' && tx?.groupId === group.groupId
); );
if (findGroup !== -1 && !previousData[findGroup]?.done) { if (findGroup !== -1 && !previousData[findGroup]?.done) {
previousData[findGroup].done = true; previousData[findGroup].done = true;
@ -81,7 +85,7 @@ export const TaskManager = ({ getUserInfo }) => {
memberGroups.forEach((group) => { memberGroups.forEach((group) => {
const findGroup = txList.findIndex( const findGroup = txList.findIndex(
(tx) => (tx) =>
tx?.type === "created-group" && tx?.groupName === group.groupName tx?.type === 'created-group' && tx?.groupName === group.groupName
); );
if (findGroup !== -1 && !previousData[findGroup]?.done) { if (findGroup !== -1 && !previousData[findGroup]?.done) {
previousData[findGroup].done = true; previousData[findGroup].done = true;
@ -90,49 +94,52 @@ export const TaskManager = ({ getUserInfo }) => {
prev.forEach((tx, index) => { prev.forEach((tx, index) => {
if ( if (
tx?.type === "leave-group" && tx?.type === 'leave-group' &&
memberGroups.findIndex((group) => tx?.groupId === group.groupId) === -1 memberGroups.findIndex((group) => tx?.groupId === group.groupId) ===
-1
) { ) {
previousData[index].done = true; previousData[index].done = true;
} }
}); });
return previousData; return previousData;
}); });
}, [memberGroups, getUserInfo]); }, [memberGroups, getUserInfo]);
useEffect(()=> { useEffect(() => {
txList.forEach((tx) => {
txList.forEach((tx) => { if (
if ( [
["created-common-secret", "joined-group-request", "join-request-accept"].includes( 'created-common-secret',
tx?.type 'joined-group-request',
) && 'join-request-accept',
tx?.signature && ].includes(tx?.type) &&
!tx.done tx?.signature &&
) { !tx.done
if (!intervals.current[tx.signature]) { ) {
getStatus({ signature: tx.signature }); if (!intervals.current[tx.signature]) {
} getStatus({ signature: tx.signature });
} }
if (tx?.type === "register-name" && tx?.signature && !tx.done) { }
if (!intervals.current[tx.signature]) { if (tx?.type === 'register-name' && tx?.signature && !tx.done) {
getStatus({ signature: tx.signature }, getUserInfo); if (!intervals.current[tx.signature]) {
} getStatus({ signature: tx.signature }, getUserInfo);
} }
if((tx?.type === "remove-rewardShare" || tx?.type === "add-rewardShare") && tx?.signature && !tx.done){ }
if (!intervals.current[tx.signature]) { if (
const sendEventForRewardShare = ()=> { (tx?.type === 'remove-rewardShare' || tx?.type === 'add-rewardShare') &&
executeEvent('refresh-rewardshare-list', {}) tx?.signature &&
} !tx.done
getStatus({ signature: tx.signature }, sendEventForRewardShare); ) {
} if (!intervals.current[tx.signature]) {
const sendEventForRewardShare = () => {
executeEvent('refresh-rewardshare-list', {});
};
getStatus({ signature: tx.signature }, sendEventForRewardShare);
} }
}); }
});
}, [txList]) }, [txList]);
if (isMobile || txList?.length === 0 || txList.every((item) => item?.done)) if (isMobile || txList?.length === 0 || txList.every((item) => item?.done))
return null; return null;
@ -143,43 +150,48 @@ export const TaskManager = ({ getUserInfo }) => {
<IconButton <IconButton
onClick={handleClick} onClick={handleClick}
sx={{ sx={{
// position: "fixed", bgcolor: theme.palette.primary.main,
// bottom: 16, color: theme.palette.text.primary,
// right: 16, ':hover': { bgcolor: theme.palette.primary },
bgcolor: "primary.main",
color: "white",
":hover": { bgcolor: "primary.dark" },
}} }}
> >
{txList.some((item) => !item.done) ? <PendingIcon /> : <TaskAltIcon />} {txList.some((item) => !item.done) ? (
<PendingIcon />
) : (
<TaskAltIcon />
)}
</IconButton> </IconButton>
)} )}
{open && ( {open && (
<List <List
sx={{ sx={{
position: "fixed", bgcolor: theme.palette.background.paper,
bottom: 16, bottom: 16,
right: 16,
width: "300px",
maxHeight: "400px",
bgcolor: "background.paper",
boxShadow: 4, boxShadow: 4,
overflow: "auto", maxHeight: '400px',
overflow: 'auto',
padding: '0px',
position: 'fixed',
right: 16,
width: '300px',
zIndex: 10, zIndex: 10,
padding: '0px'
}} }}
component="nav" component="nav"
> >
<ListItemButton onClick={handleClick}> <ListItemButton onClick={handleClick}>
<ListItemIcon> <ListItemIcon>
{txList.some((item) => !item.done) ? ( {txList.some((item) => !item.done) ? (
<PendingIcon sx={{ <PendingIcon
color:'white' sx={{
}} /> color: theme.palette.primary,
}}
/>
) : ( ) : (
<TaskAltIcon sx={{ <TaskAltIcon
color:'white' sx={{
}} /> color: theme.palette.primary,
}}
/>
)} )}
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Ongoing Transactions" /> <ListItemText primary="Ongoing Transactions" />

View File

@ -1,6 +1,7 @@
import { createContext, useContext, useState, useMemo } from 'react'; import { createContext, useContext, useState, useMemo } from 'react';
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles'; import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import { darkTheme, lightTheme } from '../../styles/theme'; import { darkTheme } from '../../styles/theme-dark';
import { lightTheme } from '../../styles/theme-light';
const ThemeContext = createContext({ const ThemeContext = createContext({
themeMode: 'light', themeMode: 'light',

Some files were not shown because too many files have changed in this diff Show More