Merge branch 'develop' into feature/asset-qortalrequests

This commit is contained in:
2025-05-01 12:31:21 +03:00
312 changed files with 38734 additions and 35336 deletions

View File

@@ -1,57 +1,57 @@
import React, { useState } from "react";
import QRCode from "react-qr-code";
import { TextP } from "../App-styles";
import { Box, Typography } from "@mui/material";
import React, { useState } from 'react';
import QRCode from 'react-qr-code';
import { TextP } from '../styles/App-styles';
import { Box, Typography } from '@mui/material';
export const AddressQRCode = ({ targetAddress }) => {
const [open, setOpen] = useState(false);
return (
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
flexDirection: "column",
marginTop: '10px'
display: 'flex',
gap: '10px',
alignItems: 'center',
flexDirection: 'column',
marginTop: '10px',
}}
>
<Typography
sx={{
cursor: "pointer",
fontSize: "14px",
cursor: 'pointer',
fontSize: '14px',
}}
onClick={() => {
setOpen((prev)=> !prev);
setOpen((prev) => !prev);
}}
>
{open ? 'Hide QR code' :'See QR code'}
{open ? 'Hide QR code' : 'See QR code'}
</Typography>
{open && (
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
justifyContent: "center",
width: "100%",
display: 'flex',
gap: '10px',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
width: "100%",
alignItems: "center",
flexDirection: "column",
marginTop: "20px",
display: 'flex',
gap: '10px',
width: '100%',
alignItems: 'center',
flexDirection: 'column',
marginTop: '20px',
}}
>
<TextP
sx={{
textAlign: "center",
textAlign: 'center',
lineHeight: 1.2,
fontSize: "16px",
fontSize: '16px',
fontWeight: 500,
}}
>

View File

@@ -1,15 +1,13 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from 'react';
import {
AppCircle,
AppCircleContainer,
AppCircleLabel,
AppDownloadButton,
AppDownloadButtonText,
AppInfoAppName,
AppInfoSnippetContainer,
AppInfoSnippetLeft,
AppInfoSnippetMiddle,
AppInfoSnippetRight,
AppInfoUserName,
AppsCategoryInfo,
AppsCategoryInfoLabel,
@@ -17,193 +15,228 @@ import {
AppsCategoryInfoValue,
AppsInfoDescription,
AppsLibraryContainer,
AppsParent,
AppsWidthLimiter,
} from "./Apps-styles";
import { Avatar, Box, ButtonBase, InputBase } from "@mui/material";
import { Add } from "@mui/icons-material";
import { getBaseApiReact, isMobile } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
} from './Apps-styles';
import { Avatar, Box, useTheme } from '@mui/material';
import { getBaseApiReact } from '../../App';
import LogoSelected from '../../assets/svgs/LogoSelected.svg';
import { Spacer } from '../../common/Spacer';
import { executeEvent } from '../../utils/events';
import { AppRating } from './AppRating';
import {
settingsLocalLastUpdatedAtom,
sortablePinnedAppsAtom,
} from '../../atoms/global';
import { saveToLocalStorage } from './AppsNavBarDesktop';
import { Spacer } from "../../common/Spacer";
import { executeEvent } from "../../utils/events";
import { AppRating } from "./AppRating";
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global";
import { saveToLocalStorage } from "./AppsNavBar";
import { useRecoilState, useSetRecoilState } from "recoil";
import { useAtom, useSetAtom } from 'jotai';
export const AppInfo = ({ app, myName }) => {
const isInstalled = app?.status?.status === "READY";
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom);
const isInstalled = app?.status?.status === 'READY';
const [sortablePinnedApps, setSortablePinnedApps] = useAtom(
sortablePinnedAppsAtom
);
const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === app?.name && item?.service === app?.service)
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
const theme = useTheme();
const isSelectedAppPinned = !!sortablePinnedApps?.find(
(item) => item?.name === app?.name && item?.service === app?.service
);
const setSettingsLocalLastUpdated = useSetAtom(settingsLocalLastUpdatedAtom);
return (
<AppsLibraryContainer
sx={{
height: !isMobile && "100%",
justifyContent: !isMobile && "flex-start",
alignItems: isMobile && 'center'
height: '100%',
justifyContent: 'flex-start',
}}
>
<Box sx={{
display: 'flex',
flexDirection: 'column',
maxWidth: "500px",
width: '90%'
}}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
maxWidth: '500px',
width: '90%',
}}
>
<Spacer height="30px" />
{!isMobile && <Spacer height="30px" />}
<AppsWidthLimiter>
<AppInfoSnippetContainer>
<AppInfoSnippetLeft
sx={{
flexGrow: 1,
gap: "18px",
}}
>
<AppCircleContainer
<AppsWidthLimiter>
<AppInfoSnippetContainer>
<AppInfoSnippetLeft
sx={{
width: "auto",
flexGrow: 1,
gap: '18px',
}}
>
<AppCircle
<AppCircleContainer
sx={{
border: "none",
height: "100px",
width: "100px",
width: 'auto',
}}
>
<Avatar
<AppCircle
sx={{
height: "43px",
width: "43px",
"& img": {
objectFit: "fill",
},
border: 'none',
height: '100px',
width: '100px',
}}
alt={app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: "43px",
height: "auto",
<Avatar
sx={{
height: '43px',
width: '43px',
'& img': {
objectFit: 'fill',
},
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
</AppCircleContainer>
<AppInfoSnippetMiddle>
<AppInfoAppName>
{app?.metadata?.title || app?.name}
</AppInfoAppName>
<Spacer height="6px" />
<AppInfoUserName>{app?.name}</AppInfoUserName>
<Spacer height="3px" />
</AppInfoSnippetMiddle>
</AppInfoSnippetLeft>
<AppInfoSnippetRight></AppInfoSnippetRight>
</AppInfoSnippetContainer>
<Spacer height="11px" />
<Box sx={{
width: '100%',
display: 'flex',
alignItems: 'center',
gap: '20px'
}}>
<AppDownloadButton
onClick={() => {
setSortablePinnedApps((prev) => {
let updatedApps;
if (isSelectedAppPinned) {
// Remove the selected app if it is pinned
updatedApps = prev.filter(
(item) => !(item?.name === app?.name && item?.service === app?.service)
alt={app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: '43px',
height: 'auto',
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
</AppCircleContainer>
<AppInfoSnippetMiddle>
<AppInfoAppName>
{app?.metadata?.title || app?.name}
</AppInfoAppName>
<Spacer height="6px" />
<AppInfoUserName>{app?.name}</AppInfoUserName>
<Spacer height="3px" />
</AppInfoSnippetMiddle>
</AppInfoSnippetLeft>
</AppInfoSnippetContainer>
<Spacer height="11px" />
<Box
sx={{
width: '100%',
display: 'flex',
alignItems: 'center',
gap: '20px',
}}
>
<AppDownloadButton
onClick={() => {
setSortablePinnedApps((prev) => {
let updatedApps;
if (isSelectedAppPinned) {
// Remove the selected app if it is pinned
updatedApps = prev.filter(
(item) =>
!(
item?.name === app?.name &&
item?.service === app?.service
)
);
} else {
// Add the selected app if it is not pinned
updatedApps = [
...prev,
{
name: app?.name,
service: app?.service,
},
];
}
saveToLocalStorage(
'ext_saved_settings',
'sortablePinnedApps',
updatedApps
);
} else {
// Add the selected app if it is not pinned
updatedApps = [...prev, {
name: app?.name,
service: app?.service,
}];
}
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps)
return updatedApps;
});
setSettingsLocalLastUpdated(Date.now())
}}
sx={{
backgroundColor: "#359ff7ff",
width: "100%",
maxWidth: "320px",
height: "29px",
opacity: isSelectedAppPinned ? 0.6 : 1
}}
>
<AppDownloadButtonText>
{!isMobile ? (
<>
{isSelectedAppPinned ? 'Unpin from dashboard' : 'Pin to dashboard'}
</>
) : (
<>
{isSelectedAppPinned ? 'Unpin' : 'Pin'}
</>
)}
</AppDownloadButtonText>
</AppDownloadButton>
<AppDownloadButton
onClick={() => {
executeEvent("addTab", {
data: app,
});
}}
sx={{
backgroundColor: isInstalled ? "#0091E1" : "#247C0E",
width: "100%",
maxWidth: "320px",
height: "29px",
}}
>
<AppDownloadButtonText>
{isInstalled ? "Open" : "Download"}
</AppDownloadButtonText>
</AppDownloadButton>
</Box>
</AppsWidthLimiter>
<Spacer height="20px" />
<AppsWidthLimiter>
<AppsCategoryInfo>
<AppRating ratingCountPosition="top" myName={myName} app={app} />
<Spacer width="16px" />
<Spacer height="40px" width="1px" backgroundColor="white" />
<Spacer width="16px" />
<AppsCategoryInfoSub>
<AppsCategoryInfoLabel>Category:</AppsCategoryInfoLabel>
<Spacer height="4px" />
<AppsCategoryInfoValue>
{app?.metadata?.categoryName || "none"}
</AppsCategoryInfoValue>
</AppsCategoryInfoSub>
</AppsCategoryInfo>
<Spacer height="30px" />
<AppInfoAppName>About this Q-App</AppInfoAppName>
</AppsWidthLimiter>
<Spacer height="20px" />
<AppsInfoDescription>
{app?.metadata?.description || "No description"}
</AppsInfoDescription>
return updatedApps;
});
setSettingsLocalLastUpdated(Date.now());
}}
sx={{
backgroundColor: theme.palette.background.paper,
height: '29px',
maxWidth: '320px',
opacity: isSelectedAppPinned ? 0.6 : 1,
width: '100%',
}}
>
<AppDownloadButtonText>
{isSelectedAppPinned
? 'Unpin from dashboard'
: 'Pin to dashboard'}
</AppDownloadButtonText>
</AppDownloadButton>
<AppDownloadButton
onClick={() => {
executeEvent('addTab', {
data: app,
});
}}
sx={{
backgroundColor: isInstalled
? theme.palette.primary.main
: theme.palette.background.paper,
height: '29px',
maxWidth: '320px',
width: '100%',
}}
>
<AppDownloadButtonText>
{isInstalled ? 'Open' : 'Download'}
</AppDownloadButtonText>
</AppDownloadButton>
</Box>
</AppsWidthLimiter>
<Spacer height="20px" />
<AppsWidthLimiter>
<AppsCategoryInfo>
<AppRating ratingCountPosition="top" myName={myName} app={app} />
<Spacer width="16px" />
<Spacer
backgroundColor={theme.palette.background.paper}
height="40px"
width="1px"
/>
<Spacer width="16px" />
<AppsCategoryInfoSub>
<AppsCategoryInfoLabel>Category:</AppsCategoryInfoLabel>
<Spacer height="4px" />
<AppsCategoryInfoValue>
{app?.metadata?.categoryName || 'none'}
</AppsCategoryInfoValue>
</AppsCategoryInfoSub>
</AppsCategoryInfo>
<Spacer height="30px" />
<AppInfoAppName>About this Q-App</AppInfoAppName>
</AppsWidthLimiter>
<Spacer height="20px" />
<AppsInfoDescription>
{app?.metadata?.description || 'No description'}
</AppsInfoDescription>
</Box>
</AppsLibraryContainer>
);

View File

@@ -1,4 +1,4 @@
import React from "react";
import React from 'react';
import {
AppCircle,
AppCircleContainer,
@@ -10,148 +10,185 @@ import {
AppInfoSnippetMiddle,
AppInfoSnippetRight,
AppInfoUserName,
} from "./Apps-styles";
import { Avatar, ButtonBase } from "@mui/material";
import { getBaseApiReact, isMobile } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
} from './Apps-styles';
import { Avatar, ButtonBase, useTheme } from '@mui/material';
import { getBaseApiReact } from '../../App';
import LogoSelected from '../../assets/svgs/LogoSelected.svg';
import { Spacer } from '../../common/Spacer';
import { executeEvent } from '../../utils/events';
import { AppRating } from './AppRating';
import {
settingsLocalLastUpdatedAtom,
sortablePinnedAppsAtom,
} from '../../atoms/global';
import { saveToLocalStorage } from './AppsNavBarDesktop';
import { useAtom, useSetAtom } from 'jotai';
import { Spacer } from "../../common/Spacer";
import { executeEvent } from "../../utils/events";
import { AppRating } from "./AppRating";
import { useRecoilState, useSetRecoilState } from "recoil";
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global";
import { saveToLocalStorage } from "./AppsNavBar";
export const AppInfoSnippet = ({
app,
myName,
isFromCategory,
parentStyles = {},
}) => {
const isInstalled = app?.status?.status === 'READY';
const [sortablePinnedApps, setSortablePinnedApps] = useAtom(
sortablePinnedAppsAtom
);
const setSettingsLocalLastUpdated = useSetAtom(settingsLocalLastUpdatedAtom);
export const AppInfoSnippet = ({ app, myName, isFromCategory, parentStyles = {} }) => {
const isSelectedAppPinned = !!sortablePinnedApps?.find(
(item) => item?.name === app?.name && item?.service === app?.service
);
const isInstalled = app?.status?.status === 'READY'
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom);
const theme = useTheme();
const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === app?.name && item?.service === app?.service)
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
return (
<AppInfoSnippetContainer sx={{
...parentStyles
}}>
<AppInfoSnippetContainer
sx={{
...parentStyles,
}}
>
<AppInfoSnippetLeft>
<ButtonBase
sx={{
height: "80px",
width: "60px",
}}
onClick={()=> {
if(isFromCategory){
executeEvent("selectedAppInfoCategory", {
<ButtonBase
sx={{
height: '80px',
width: '60px',
}}
onClick={() => {
if (isFromCategory) {
executeEvent('selectedAppInfoCategory', {
data: app,
});
return;
}
executeEvent('selectedAppInfo', {
data: app,
});
return
}
executeEvent("selectedAppInfo", {
data: app,
});
}}
>
<AppCircleContainer>
<AppCircle
sx={{
border: "none",
}}
>
<AppCircleContainer>
<AppCircle
sx={{
border: 'none',
}}
>
<Avatar
sx={{
height: '42px',
width: '42px',
'& img': {
objectFit: 'fill',
},
}}
alt={app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: '31px',
height: 'auto',
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
</AppCircleContainer>
</ButtonBase>
<AppInfoSnippetMiddle>
<ButtonBase
onClick={() => {
if (isFromCategory) {
executeEvent('selectedAppInfoCategory', {
data: app,
});
return;
}
executeEvent('selectedAppInfo', {
data: app,
});
}}
>
<Avatar
sx={{
height: "42px",
width: "42px",
'& img': {
objectFit: 'fill',
}
}}
alt={app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: "31px",
height: "auto",
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
</AppCircleContainer>
</ButtonBase>
<AppInfoSnippetMiddle>
<ButtonBase onClick={()=> {
if(isFromCategory){
executeEvent("selectedAppInfoCategory", {
<AppInfoAppName>{app?.metadata?.title || app?.name}</AppInfoAppName>
</ButtonBase>
<Spacer height="6px" />
<AppInfoUserName>{app?.name}</AppInfoUserName>
<Spacer height="3px" />
<AppRating app={app} myName={myName} />
</AppInfoSnippetMiddle>
</AppInfoSnippetLeft>
<AppInfoSnippetRight
sx={{
gap: '10px',
}}
>
<AppDownloadButton
onClick={() => {
setSortablePinnedApps((prev) => {
let updatedApps;
if (isSelectedAppPinned) {
// Remove the selected app if it is pinned
updatedApps = prev.filter(
(item) =>
!(
item?.name === app?.name && item?.service === app?.service
)
);
} else {
// Add the selected app if it is not pinned
updatedApps = [
...prev,
{
name: app?.name,
service: app?.service,
},
];
}
saveToLocalStorage(
'ext_saved_settings',
'sortablePinnedApps',
updatedApps
);
return updatedApps;
});
setSettingsLocalLastUpdated(Date.now());
}}
sx={{
backgroundColor: theme.palette.background.paper,
opacity: isSelectedAppPinned ? 0.6 : 1,
}}
>
<AppDownloadButtonText>
{' '}
{isSelectedAppPinned ? 'Unpin' : 'Pin'}
</AppDownloadButtonText>
</AppDownloadButton>
<AppDownloadButton
onClick={() => {
executeEvent('addTab', {
data: app,
});
return
}
executeEvent("selectedAppInfo", {
data: app,
});
}}>
<AppInfoAppName >
{app?.metadata?.title || app?.name}
</AppInfoAppName>
</ButtonBase>
<Spacer height="6px" />
<AppInfoUserName>
{ app?.name}
</AppInfoUserName>
<Spacer height="3px" />
<AppRating app={app} myName={myName} />
</AppInfoSnippetMiddle>
</AppInfoSnippetLeft>
<AppInfoSnippetRight sx={{
gap: '10px'
}}>
{!isMobile && (
<AppDownloadButton onClick={()=> {
setSortablePinnedApps((prev) => {
let updatedApps;
if (isSelectedAppPinned) {
// Remove the selected app if it is pinned
updatedApps = prev.filter(
(item) => !(item?.name === app?.name && item?.service === app?.service)
);
} else {
// Add the selected app if it is not pinned
updatedApps = [...prev, {
name: app?.name,
service: app?.service,
}];
}
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps)
return updatedApps;
});
setSettingsLocalLastUpdated(Date.now())
}} sx={{
backgroundColor: '#359ff7ff',
opacity: isSelectedAppPinned ? 0.6 : 1
}}>
<AppDownloadButtonText> {isSelectedAppPinned ? 'Unpin' : 'Pin'}</AppDownloadButtonText>
</AppDownloadButton>
)}
<AppDownloadButton onClick={()=> {
executeEvent("addTab", {
data: app
})
}} sx={{
backgroundColor: isInstalled ? '#0091E1' : '#247C0E',
}}>
<AppDownloadButtonText>{isInstalled ? 'Open' : 'Download'}</AppDownloadButtonText>
}}
sx={{
backgroundColor: isInstalled
? theme.palette.primary.main
: theme.palette.background.paper,
}}
>
<AppDownloadButtonText>
{isInstalled ? 'Open' : 'Download'}
</AppDownloadButtonText>
</AppDownloadButton>
</AppInfoSnippetRight>
</AppInfoSnippetContainer>

View File

@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useMemo, useState } from "react";
import React, { useContext, useEffect, useState } from 'react';
import {
AppCircle,
AppCircleContainer,
@@ -19,90 +19,80 @@ import {
PublishQAppCTAButton,
PublishQAppChoseFile,
PublishQAppInfo,
} from "./Apps-styles";
} from './Apps-styles';
import {
Avatar,
Box,
ButtonBase,
InputBase,
InputLabel,
MenuItem,
Select,
} from "@mui/material";
import {
Select as BaseSelect,
SelectProps,
selectClasses,
SelectRootSlotProps,
} from "@mui/base/Select";
import { Option as BaseOption, optionClasses } from "@mui/base/Option";
import { styled } from "@mui/system";
import UnfoldMoreRoundedIcon from "@mui/icons-material/UnfoldMoreRounded";
import { Add } from "@mui/icons-material";
import { MyContext, getBaseApiReact, isMobile } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { Spacer } from "../../common/Spacer";
import { executeEvent } from "../../utils/events";
import { useDropzone } from "react-dropzone";
import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar";
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
import { getFee } from "../../background";
import { fileToBase64 } from "../../utils/fileReading";
useTheme,
} from '@mui/material';
import { styled } from '@mui/system';
import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded';
import { Add } from '@mui/icons-material';
import { MyContext, getBaseApiReact } from '../../App';
import LogoSelected from '../../assets/svgs/LogoSelected.svg';
import { Spacer } from '../../common/Spacer';
import { executeEvent } from '../../utils/events';
import { useDropzone } from 'react-dropzone';
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { getFee } from '../../background';
import { fileToBase64 } from '../../utils/fileReading';
const CustomSelect = styled(Select)({
border: "0.5px solid var(--50-white, #FFFFFF80)",
padding: "0px 15px",
borderRadius: "5px",
height: "36px",
width: "100%",
maxWidth: "450px",
"& .MuiSelect-select": {
padding: "0px",
border: '0.5px solid var(--50-white, #FFFFFF80)',
padding: '0px 15px',
borderRadius: '5px',
height: '36px',
width: '100%',
maxWidth: '450px',
'& .MuiSelect-select': {
padding: '0px',
},
"&:hover": {
borderColor: "none", // Border color on hover
'&:hover': {
borderColor: 'none', // Border color on hover
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: "none", // Border color when focused
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: 'none', // Border color when focused
},
"&.Mui-disabled": {
'&.Mui-disabled': {
opacity: 0.5, // Lower opacity when disabled
},
"& .MuiSvgIcon-root": {
color: "var(--50-white, #FFFFFF80)",
'& .MuiSvgIcon-root': {
color: 'var(--50-white, #FFFFFF80)',
},
});
const CustomMenuItem = styled(MenuItem)({
backgroundColor: "#1f1f1f", // Background for dropdown items
color: "#ccc",
"&:hover": {
backgroundColor: "#333", // Darker background on hover
},
// backgroundColor: '#1f1f1f', // Background for dropdown items
// color: '#ccc',
// '&:hover': {
// backgroundColor: '#333', // Darker background on hover
// },
});
export const AppPublish = ({ names, categories }) => {
const [name, setName] = useState("");
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [category, setCategory] = useState("");
const [appType, setAppType] = useState("APP");
const [name, setName] = useState('');
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [category, setCategory] = useState('');
const [appType, setAppType] = useState('APP');
const [file, setFile] = useState(null);
const { show } = useContext(MyContext);
const [tag1, setTag1] = useState("");
const [tag2, setTag2] = useState("");
const [tag3, setTag3] = useState("");
const [tag4, setTag4] = useState("");
const [tag5, setTag5] = useState("");
const theme = useTheme();
const [tag1, setTag1] = useState('');
const [tag2, setTag2] = useState('');
const [tag3, setTag3] = useState('');
const [tag4, setTag4] = useState('');
const [tag5, setTag5] = useState('');
const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null);
const [isLoading, setIsLoading] = useState("");
const maxFileSize = appType === "APP" ? 50 * 1024 * 1024 : 400 * 1024 * 1024; // 50MB or 400MB
const [isLoading, setIsLoading] = useState('');
const maxFileSize = appType === 'APP' ? 50 * 1024 * 1024 : 400 * 1024 * 1024; // 50MB or 400MB
const { getRootProps, getInputProps } = useDropzone({
accept: {
"application/zip": [".zip"], // Only accept zip files
'application/zip': ['.zip'], // Only accept zip files
},
maxSize: maxFileSize, // Set the max size based on appType
multiple: false, // Disable multiple file uploads
@@ -114,7 +104,7 @@ export const AppPublish = ({ names, categories }) => {
onDropRejected: (fileRejections) => {
fileRejections.forEach(({ file, errors }) => {
errors.forEach((error) => {
if (error.code === "file-too-large") {
if (error.code === 'file-too-large') {
console.error(
`File ${file.name} is too large. Max size allowed is ${
maxFileSize / (1024 * 1024)
@@ -128,13 +118,13 @@ export const AppPublish = ({ names, categories }) => {
const getQapp = React.useCallback(async (name, appType) => {
try {
setIsLoading("Loading app information");
setIsLoading('Loading app information');
const url = `${getBaseApiReact()}/arbitrary/resources/search?service=${appType}&mode=ALL&name=${name}&includemetadata=true`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
if (!response?.ok) return;
@@ -142,18 +132,18 @@ export const AppPublish = ({ names, categories }) => {
if (responseData?.length > 0) {
const myApp = responseData[0];
setTitle(myApp?.metadata?.title || "");
setDescription(myApp?.metadata?.description || "");
setCategory(myApp?.metadata?.category || "");
setTag1(myApp?.metadata?.tags[0] || "");
setTag2(myApp?.metadata?.tags[1] || "");
setTag3(myApp?.metadata?.tags[2] || "");
setTag4(myApp?.metadata?.tags[3] || "");
setTag5(myApp?.metadata?.tags[4] || "");
setTitle(myApp?.metadata?.title || '');
setDescription(myApp?.metadata?.description || '');
setCategory(myApp?.metadata?.category || '');
setTag1(myApp?.metadata?.tags[0] || '');
setTag2(myApp?.metadata?.tags[1] || '');
setTag3(myApp?.metadata?.tags[2] || '');
setTag4(myApp?.metadata?.tags[3] || '');
setTag5(myApp?.metadata?.tags[4] || '');
}
} catch (error) {
} finally {
setIsLoading("");
setIsLoading('');
}
}, []);
@@ -173,12 +163,12 @@ export const AppPublish = ({ names, categories }) => {
file,
};
const requiredFields = [
"name",
"title",
"description",
"category",
"appType",
"file",
'name',
'title',
'description',
'category',
'appType',
'file',
];
const missingFields: string[] = [];
@@ -188,32 +178,33 @@ export const AppPublish = ({ names, categories }) => {
}
});
if (missingFields.length > 0) {
const missingFieldsString = missingFields.join(", ");
const missingFieldsString = missingFields.join(', ');
const errorMsg = `Missing fields: ${missingFieldsString}`;
throw new Error(errorMsg);
}
const fee = await getFee("ARBITRARY");
const fee = await getFee('ARBITRARY');
await show({
message: "Would you like to publish this app?",
publishFee: fee.fee + " QORT",
message: 'Would you like to publish this app?',
publishFee: fee.fee + ' QORT',
});
setIsLoading("Publishing... Please wait.");
setIsLoading('Publishing... Please wait.');
const fileBase64 = await fileToBase64(file);
await new Promise((res, rej) => {
window.sendMessage("publishOnQDN", {
data: fileBase64,
service: appType,
title,
description,
category,
tag1,
tag2,
tag3,
tag4,
tag5,
uploadType: "zip",
})
window
.sendMessage('publishOnQDN', {
data: fileBase64,
service: appType,
title,
description,
category,
tag1,
tag2,
tag3,
tag4,
tag5,
uploadType: 'zip',
})
.then((response) => {
if (!response?.error) {
res(response);
@@ -222,14 +213,13 @@ export const AppPublish = ({ names, categories }) => {
rej(response.error);
})
.catch((error) => {
rej(error.message || "An error occurred");
rej(error.message || 'An error occurred');
});
});
setInfoSnack({
type: "success",
type: 'success',
message:
"Successfully published. Please wait a couple minutes for the network to propogate the changes.",
'Successfully published. Please wait a couple minutes for the network to propogate the changes.',
});
setOpenSnack(true);
const dataObj = {
@@ -242,35 +232,47 @@ export const AppPublish = ({ names, categories }) => {
},
created: Date.now(),
};
executeEvent("addTab", {
executeEvent('addTab', {
data: dataObj,
});
} catch (error) {
setInfoSnack({
type: "error",
message: error?.message || "Unable to publish app",
type: 'error',
message: error?.message || 'Unable to publish app',
});
setOpenSnack(true);
} finally {
setIsLoading("");
setIsLoading('');
}
};
return (
<AppsLibraryContainer sx={{
height: !isMobile ? '100%' : 'auto',
paddingTop: !isMobile && '30px',
alignItems: !isMobile && 'center'
}}>
<AppsWidthLimiter sx={{
width: !isMobile ? 'auto' : '90%'
}}>
<AppsLibraryContainer
sx={{
alignItems: 'center',
height: '100%',
paddingTop: '30px',
}}
>
<AppsWidthLimiter
sx={{
width: 'auto',
}}
>
<AppLibrarySubTitle>Create Apps!</AppLibrarySubTitle>
<Spacer height="18px" />
<PublishQAppInfo>
Note: Currently, only one App and Website is allowed per Name.
</PublishQAppInfo>
<Spacer height="18px" />
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>Name/App</InputLabel>
<InputLabel sx={{ fontSize: '14px', marginBottom: '2px' }}>
Name/App
</InputLabel>
<CustomSelect
placeholder="Select Name/App"
displayEmpty
@@ -280,19 +282,24 @@ export const AppPublish = ({ names, categories }) => {
<CustomMenuItem value="">
<em
style={{
color: "var(--50-white, #FFFFFF80)",
color: theme.palette.text.secondary,
}}
>
Select Name/App
</em>{" "}
</em>
{/* This is the placeholder item */}
</CustomMenuItem>
{names.map((name) => {
return <CustomMenuItem value={name}>{name}</CustomMenuItem>;
})}
</CustomSelect>
<Spacer height="15px" />
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>App service type</InputLabel>
<InputLabel sx={{ fontSize: '14px', marginBottom: '2px' }}>
App service type
</InputLabel>
<CustomSelect
placeholder="SERVICE TYPE"
displayEmpty
@@ -302,59 +309,72 @@ export const AppPublish = ({ names, categories }) => {
<CustomMenuItem value="">
<em
style={{
color: "var(--50-white, #FFFFFF80)",
color: theme.palette.text.secondary,
}}
>
Select App Type
</em>{" "}
{/* This is the placeholder item */}
</em>
</CustomMenuItem>
<CustomMenuItem value={"APP"}>App</CustomMenuItem>
<CustomMenuItem value={"WEBSITE"}>Website</CustomMenuItem>
<CustomMenuItem value={'APP'}>App</CustomMenuItem>
<CustomMenuItem value={'WEBSITE'}>Website</CustomMenuItem>
</CustomSelect>
<Spacer height="15px" />
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>Title</InputLabel>
<InputLabel sx={{ fontSize: '14px', marginBottom: '2px' }}>
Title
</InputLabel>
<InputBase
value={title}
onChange={(e) => setTitle(e.target.value)}
sx={{
border: "0.5px solid var(--50-white, #FFFFFF80)",
padding: "0px 15px",
borderRadius: "5px",
height: "36px",
width: "100%",
maxWidth: "450px",
border: `0.5px solid ${theme.palette.action.disabled}`,
padding: '0px 15px',
borderRadius: '5px',
height: '36px',
width: '100%',
maxWidth: '450px',
}}
placeholder="Title"
inputProps={{
"aria-label": "Title",
fontSize: "14px",
fontWeight: 400,
}}
/>
<Spacer height="15px" />
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>Description</InputLabel>
<InputBase
value={description}
onChange={(e) => setDescription(e.target.value)}
sx={{
border: "0.5px solid var(--50-white, #FFFFFF80)",
padding: "0px 15px",
borderRadius: "5px",
height: "36px",
width: "100%",
maxWidth: "450px",
}}
placeholder="Description"
inputProps={{
"aria-label": "Description",
fontSize: "14px",
'aria-label': 'Title',
fontSize: '14px',
fontWeight: 400,
}}
/>
<Spacer height="15px" />
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>Category</InputLabel>
<InputLabel sx={{ fontSize: '14px', marginBottom: '2px' }}>
Description
</InputLabel>
<InputBase
value={description}
onChange={(e) => setDescription(e.target.value)}
sx={{
border: `0.5px solid ${theme.palette.action.disabled}`,
padding: '0px 15px',
borderRadius: '5px',
height: '36px',
width: '100%',
maxWidth: '450px',
}}
placeholder="Description"
inputProps={{
'aria-label': 'Description',
fontSize: '14px',
fontWeight: 400,
}}
/>
<Spacer height="15px" />
<InputLabel sx={{ fontSize: '14px', marginBottom: '2px' }}>
Category
</InputLabel>
<CustomSelect
displayEmpty
placeholder="Select Category"
@@ -364,12 +384,11 @@ export const AppPublish = ({ names, categories }) => {
<CustomMenuItem value="">
<em
style={{
color: "var(--50-white, #FFFFFF80)",
color: theme.palette.text.secondary,
}}
>
Select Category
</em>{" "}
{/* This is the placeholder item */}
</em>
</CustomMenuItem>
{categories?.map((category) => {
return (
@@ -379,23 +398,28 @@ export const AppPublish = ({ names, categories }) => {
);
})}
</CustomSelect>
<Spacer height="15px" />
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>Tags</InputLabel>
<InputLabel sx={{ fontSize: '14px', marginBottom: '2px' }}>
Tags
</InputLabel>
<AppPublishTagsContainer>
<InputBase
value={tag1}
onChange={(e) => setTag1(e.target.value)}
sx={{
border: "0.5px solid var(--50-white, #FFFFFF80)",
padding: "0px 15px",
borderRadius: "5px",
height: "36px",
width: "100px",
border: `0.5px solid ${theme.palette.action.disabled}`,
padding: '0px 15px',
borderRadius: '5px',
height: '36px',
width: '100px',
}}
placeholder="Tag 1"
inputProps={{
"aria-label": "Tag 1",
fontSize: "14px",
'aria-label': 'Tag 1',
fontSize: '14px',
fontWeight: 400,
}}
/>
@@ -403,16 +427,16 @@ export const AppPublish = ({ names, categories }) => {
value={tag2}
onChange={(e) => setTag2(e.target.value)}
sx={{
border: "0.5px solid var(--50-white, #FFFFFF80)",
padding: "0px 15px",
borderRadius: "5px",
height: "36px",
width: "100px",
border: `0.5px solid ${theme.palette.action.disabled}`,
padding: '0px 15px',
borderRadius: '5px',
height: '36px',
width: '100px',
}}
placeholder="Tag 2"
inputProps={{
"aria-label": "Tag 2",
fontSize: "14px",
'aria-label': 'Tag 2',
fontSize: '14px',
fontWeight: 400,
}}
/>
@@ -420,16 +444,16 @@ export const AppPublish = ({ names, categories }) => {
value={tag3}
onChange={(e) => setTag3(e.target.value)}
sx={{
border: "0.5px solid var(--50-white, #FFFFFF80)",
padding: "0px 15px",
borderRadius: "5px",
height: "36px",
width: "100px",
border: `0.5px solid ${theme.palette.action.disabled}`,
padding: '0px 15px',
borderRadius: '5px',
height: '36px',
width: '100px',
}}
placeholder="Tag 3"
inputProps={{
"aria-label": "Tag 3",
fontSize: "14px",
'aria-label': 'Tag 3',
fontSize: '14px',
fontWeight: 400,
}}
/>
@@ -437,16 +461,16 @@ export const AppPublish = ({ names, categories }) => {
value={tag4}
onChange={(e) => setTag4(e.target.value)}
sx={{
border: "0.5px solid var(--50-white, #FFFFFF80)",
padding: "0px 15px",
borderRadius: "5px",
height: "36px",
width: "100px",
border: `0.5px solid ${theme.palette.action.disabled}`,
padding: '0px 15px',
borderRadius: '5px',
height: '36px',
width: '100px',
}}
placeholder="Tag 4"
inputProps={{
"aria-label": "Tag 4",
fontSize: "14px",
'aria-label': 'Tag 4',
fontSize: '14px',
fontWeight: 400,
}}
/>
@@ -454,27 +478,31 @@ export const AppPublish = ({ names, categories }) => {
value={tag5}
onChange={(e) => setTag5(e.target.value)}
sx={{
border: "0.5px solid var(--50-white, #FFFFFF80)",
padding: "0px 15px",
borderRadius: "5px",
height: "36px",
width: "100px",
border: `0.5px solid ${theme.palette.action.disabled}`,
padding: '0px 15px',
borderRadius: '5px',
height: '36px',
width: '100px',
}}
placeholder="Tag 5"
inputProps={{
"aria-label": "Tag 5",
fontSize: "14px",
'aria-label': 'Tag 5',
fontSize: '14px',
fontWeight: 400,
}}
/>
</AppPublishTagsContainer>
<Spacer height="30px" />
<PublishQAppInfo>
Select .zip file containing static content:{" "}
Select .zip file containing static content:{' '}
</PublishQAppInfo>
<Spacer height="10px" />
<PublishQAppInfo>{`(${
appType === "APP" ? "50mb" : "400mb"
appType === 'APP' ? '50mb' : '400mb'
} MB maximum)`}</PublishQAppInfo>
{file && (
<>
@@ -484,21 +512,25 @@ export const AppPublish = ({ names, categories }) => {
)}
<Spacer height="18px" />
<PublishQAppChoseFile {...getRootProps()}>
{" "}
{' '}
<input {...getInputProps()} />
Choose File
</PublishQAppChoseFile>
<Spacer height="35px" />
<PublishQAppCTAButton
sx={{
alignSelf: "center",
alignSelf: 'center',
}}
onClick={publishApp}
>
Publish
</PublishQAppCTAButton>
</AppsWidthLimiter>
<LoadingSnackbar
open={!!isLoading}
info={{
@@ -512,7 +544,6 @@ export const AppPublish = ({ names, categories }) => {
info={infoSnack}
setInfo={setInfoSnack}
/>
</AppsLibraryContainer>
);
};

View File

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

View File

@@ -1,201 +1,251 @@
import React, { useContext, useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from 'react';
import { Box } from '@mui/material';
import { getBaseApiReact } from '../../App';
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
import { useFrame } from 'react-frame-component';
import { useQortalMessageListener } from './useQortalMessageListener';
import { useThemeContext } from '../Theme/ThemeContext';
import { Avatar, Box, } from "@mui/material";
import { Add } from "@mui/icons-material";
import { MyContext, getBaseApiReact, isMobile } from "../../App";
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
import { useFrame } from "react-frame-component";
import { useQortalMessageListener } from "./useQortalMessageListener";
export const AppViewer = React.forwardRef(({ app , hide, isDevMode, skipAuth}, iframeRef) => {
const { rootHeight } = useContext(MyContext);
// const iframeRef = useRef(null);
const { document, window: frameWindow } = useFrame();
const {path, history, changeCurrentIndex, resetHistory} = useQortalMessageListener(frameWindow, iframeRef, app?.tabId, isDevMode, app?.name, app?.service, skipAuth)
const [url, setUrl] = useState('')
useEffect(()=> {
if(app?.isPreview) return
if(isDevMode){
setUrl(app?.url)
return
}
let hasQueryParam = false
if(app?.path && app.path.includes('?')){
hasQueryParam = true
}
setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? `/${app?.path}` : ''}${hasQueryParam ? "&": "?" }theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}`)
}, [app?.service, app?.name, app?.identifier, app?.path, app?.isPreview])
useEffect(()=> {
if(app?.isPreview && app?.url){
resetHistory()
setUrl(app.url)
}
}, [app?.url, app?.isPreview])
const defaultUrl = useMemo(()=> {
return url
}, [url, isDevMode])
const refreshAppFunc = (e) => {
const {tabId} = e.detail
if(tabId === app?.tabId){
if(isDevMode){
resetHistory()
if(!app?.isPreview || app?.isPrivate){
setUrl(app?.url + `?time=${Date.now()}`)
}
return
}
const constructUrl = `${getBaseApiReact()}/render/${app?.service}/${app?.name}${path != null ? path : ''}?theme=dark&identifier=${app?.identifier != null ? app?.identifier : ''}&time=${new Date().getMilliseconds()}`
setUrl(constructUrl)
}
};
useEffect(() => {
subscribeToEvent("refreshApp", refreshAppFunc);
return () => {
unsubscribeFromEvent("refreshApp", refreshAppFunc);
};
}, [app, path, isDevMode]);
const removeTrailingSlash = (str) => str.replace(/\/$/, '');
const copyLinkFunc = (e) => {
const {tabId} = e.detail
if(tabId === app?.tabId){
let link = 'qortal://' + app?.service + '/' + app?.name
if(path && path.startsWith('/')){
link = link + removeTrailingSlash(path)
}
if(path && !path.startsWith('/')){
link = link + '/' + removeTrailingSlash(path)
}
navigator.clipboard.writeText(link)
.then(() => {
console.log("Path copied to clipboard:", path);
})
.catch((error) => {
console.error("Failed to copy path:", error);
});
}
};
useEffect(() => {
subscribeToEvent("copyLink", copyLinkFunc);
return () => {
unsubscribeFromEvent("copyLink", copyLinkFunc);
};
}, [app, path]);
// Function to navigate back in iframe
const navigateBackInIframe = async () => {
if (iframeRef.current && iframeRef.current.contentWindow && history?.currentIndex > 0) {
// Calculate the previous index and path
const previousPageIndex = history.currentIndex - 1;
const previousPath = history.customQDNHistoryPaths[previousPageIndex];
const targetOrigin = iframeRef.current ? new URL(iframeRef.current.src).origin : "*";
// Signal non-manual navigation
iframeRef.current.contentWindow.postMessage(
{ action: 'PERFORMING_NON_MANUAL', currentIndex: previousPageIndex },targetOrigin
);
// Update the current index locally
changeCurrentIndex(previousPageIndex);
// Create a navigation promise with a 200ms timeout
const navigationPromise = new Promise((resolve, reject) => {
function handleNavigationSuccess(event) {
if (event.data?.action === 'NAVIGATION_SUCCESS' && event.data.path === previousPath) {
frameWindow.removeEventListener('message', handleNavigationSuccess);
resolve();
}
}
frameWindow.addEventListener('message', handleNavigationSuccess);
// Timeout after 200ms if no response
setTimeout(() => {
window.removeEventListener('message', handleNavigationSuccess);
reject(new Error("Navigation timeout"));
}, 200);
const targetOrigin = iframeRef.current ? new URL(iframeRef.current.src).origin : "*";
// Send the navigation command after setting up the listener and timeout
iframeRef.current.contentWindow.postMessage(
{ action: 'NAVIGATE_TO_PATH', path: previousPath, requestedHandler: 'UI' }, targetOrigin
export const AppViewer = React.forwardRef(
({ app, hide, isDevMode, skipAuth }, iframeRef) => {
// const iframeRef = useRef(null);
const { window: frameWindow } = useFrame();
const { path, history, changeCurrentIndex, resetHistory } =
useQortalMessageListener(
frameWindow,
iframeRef,
app?.tabId,
isDevMode,
app?.name,
app?.service,
skipAuth
);
});
const [url, setUrl] = useState('');
const { themeMode } = useThemeContext();
// Execute navigation promise and handle timeout fallback
try {
await navigationPromise;
} catch (error) {
if(isDevMode){
setUrl(`${url}${previousPath != null ? previousPath : ''}?theme=dark&time=${new Date().getMilliseconds()}&isManualNavigation=false`)
return
}
setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${previousPath != null ? previousPath : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}&time=${new Date().getMilliseconds()}&isManualNavigation=false`)
// iframeRef.current.contentWindow.location.href = previousPath; // Fallback URL update
}
} else {
console.log('Iframe not accessible or does not have a content window.');
}
};
useEffect(() => {
if (app?.isPreview) return;
if (isDevMode) {
setUrl(app?.url);
return;
}
let hasQueryParam = false;
if (app?.path && app.path.includes('?')) {
hasQueryParam = true;
}
const navigateBackAppFunc = (e) => {
setUrl(
`${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? `/${app?.path}` : ''}${hasQueryParam ? '&' : '?'}theme=${themeMode}&identifier=${app?.identifier != null && app?.identifier != 'null' ? app?.identifier : ''}`
);
}, [app?.service, app?.name, app?.identifier, app?.path, app?.isPreview]);
navigateBackInIframe()
};
useEffect(() => {
if (app?.isPreview && app?.url) {
resetHistory();
setUrl(app.url);
}
}, [app?.url, app?.isPreview]);
const defaultUrl = useMemo(() => {
return url;
}, [url, isDevMode]);
useEffect(() => {
if(!app?.tabId) return
subscribeToEvent(`navigateBackApp-${app?.tabId}`, navigateBackAppFunc);
return () => {
unsubscribeFromEvent(`navigateBackApp-${app?.tabId}`, navigateBackAppFunc);
const refreshAppFunc = (e) => {
const { tabId } = e.detail;
if (tabId === app?.tabId) {
if (isDevMode) {
resetHistory();
if (!app?.isPreview || app?.isPrivate) {
setUrl(app?.url + `?time=${Date.now()}`);
}
return;
}
const constructUrl = `${getBaseApiReact()}/render/${app?.service}/${app?.name}${path != null ? path : ''}?theme=${themeMode}&identifier=${app?.identifier != null ? app?.identifier : ''}&time=${new Date().getMilliseconds()}`;
setUrl(constructUrl);
}
};
}, [app, history]);
useEffect(() => {
subscribeToEvent('refreshApp', refreshAppFunc);
// Function to navigate back in iframe
const navigateForwardInIframe = async () => {
return () => {
unsubscribeFromEvent('refreshApp', refreshAppFunc);
};
}, [app, path, isDevMode]);
if (iframeRef.current && iframeRef.current.contentWindow) {
const targetOrigin = iframeRef.current ? new URL(iframeRef.current.src).origin : "*";
iframeRef.current.contentWindow.postMessage(
{ action: 'NAVIGATE_FORWARD'},
useEffect(() => {
const iframe = iframeRef?.current;
if (!iframe) return;
try {
const targetOrigin = new URL(iframe.src).origin;
iframe.contentWindow?.postMessage(
{ action: 'THEME_CHANGED', theme: themeMode, requestedHandler: 'UI' },
targetOrigin
);
} else {
console.log('Iframe not accessible or does not have a content window.');
);
} catch (err) {
console.error('Failed to send theme change to iframe:', err);
}
}, [themeMode]);
const removeTrailingSlash = (str) => str.replace(/\/$/, '');
const copyLinkFunc = (e) => {
const { tabId } = e.detail;
if (tabId === app?.tabId) {
let link = 'qortal://' + app?.service + '/' + app?.name;
if (path && path.startsWith('/')) {
link = link + removeTrailingSlash(path);
}
if (path && !path.startsWith('/')) {
link = link + '/' + removeTrailingSlash(path);
}
navigator.clipboard
.writeText(link)
.then(() => {
console.log('Path copied to clipboard:', path);
})
.catch((error) => {
console.error('Failed to copy path:', error);
});
}
};
useEffect(() => {
subscribeToEvent('copyLink', copyLinkFunc);
return () => {
unsubscribeFromEvent('copyLink', copyLinkFunc);
};
}, [app, path]);
// Function to navigate back in iframe
const navigateBackInIframe = async () => {
if (
iframeRef.current &&
iframeRef.current.contentWindow &&
history?.currentIndex > 0
) {
// Calculate the previous index and path
const previousPageIndex = history.currentIndex - 1;
const previousPath = history.customQDNHistoryPaths[previousPageIndex];
const targetOrigin = iframeRef.current
? new URL(iframeRef.current.src).origin
: '*';
// Signal non-manual navigation
iframeRef.current.contentWindow.postMessage(
{ action: 'PERFORMING_NON_MANUAL', currentIndex: previousPageIndex },
targetOrigin
);
// Update the current index locally
changeCurrentIndex(previousPageIndex);
// Create a navigation promise with a 200ms timeout
const navigationPromise = new Promise((resolve, reject) => {
function handleNavigationSuccess(event) {
if (
event.data?.action === 'NAVIGATION_SUCCESS' &&
event.data.path === previousPath
) {
frameWindow.removeEventListener(
'message',
handleNavigationSuccess
);
resolve();
}
}
frameWindow.addEventListener('message', handleNavigationSuccess);
// Timeout after 200ms if no response
setTimeout(() => {
window.removeEventListener('message', handleNavigationSuccess);
reject(new Error('Navigation timeout'));
}, 200);
const targetOrigin = iframeRef.current
? new URL(iframeRef.current.src).origin
: '*';
// Send the navigation command after setting up the listener and timeout
iframeRef.current.contentWindow.postMessage(
{
action: 'NAVIGATE_TO_PATH',
path: previousPath,
requestedHandler: 'UI',
},
targetOrigin
);
});
// Execute navigation promise and handle timeout fallback
try {
await navigationPromise;
} catch (error) {
if (isDevMode) {
setUrl(
`${url}${previousPath != null ? previousPath : ''}?theme=${themeMode}&time=${new Date().getMilliseconds()}&isManualNavigation=false`
);
return;
}
setUrl(
`${getBaseApiReact()}/render/${app?.service}/${app?.name}${previousPath != null ? previousPath : ''}?theme=${themeMode}&identifier=${app?.identifier != null && app?.identifier != 'null' ? app?.identifier : ''}&time=${new Date().getMilliseconds()}&isManualNavigation=false`
);
// iframeRef.current.contentWindow.location.href = previousPath; // Fallback URL update
}
} else {
console.log('Iframe not accessible or does not have a content window.');
}
};
const navigateBackAppFunc = (e) => {
navigateBackInIframe();
};
useEffect(() => {
if (!app?.tabId) return;
subscribeToEvent(`navigateBackApp-${app?.tabId}`, navigateBackAppFunc);
return () => {
unsubscribeFromEvent(
`navigateBackApp-${app?.tabId}`,
navigateBackAppFunc
);
};
}, [app, history]);
// Function to navigate back in iframe
const navigateForwardInIframe = async () => {
if (iframeRef.current && iframeRef.current.contentWindow) {
const targetOrigin = iframeRef.current
? new URL(iframeRef.current.src).origin
: '*';
iframeRef.current.contentWindow.postMessage(
{ action: 'NAVIGATE_FORWARD' },
targetOrigin
);
} else {
console.log('Iframe not accessible or does not have a content window.');
}
};
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<iframe
ref={iframeRef}
style={{
height: '100vh',
border: 'none',
width: '100%',
}}
id="browser-iframe"
src={defaultUrl}
sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals"
allow="fullscreen; clipboard-read; clipboard-write"
></iframe>
</Box>
);
}
};
return (
<Box sx={{
display: 'flex',
flexDirection: 'column',
}}>
<iframe ref={iframeRef} style={{
height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px )`,
border: 'none',
width: '100%'
}} id="browser-iframe" src={defaultUrl} sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals"
allow="fullscreen; clipboard-read; clipboard-write">
</iframe>
</Box>
);
});
);

View File

@@ -1,26 +1,23 @@
import React, { useContext, } from 'react';
import React, { useContext } from 'react';
import { AppViewer } from './AppViewer';
import Frame from 'react-frame-component';
import { MyContext, isMobile } from '../../App';
import { MyContext } from '../../App';
const AppViewerContainer = React.forwardRef(({ app, isSelected, hide, isDevMode, customHeight, skipAuth }, ref) => {
const { rootHeight } = useContext(MyContext);
return (
<Frame
id={`browser-iframe-${app?.tabId}`}
head={
<>
<style>
{`
const AppViewerContainer = React.forwardRef(
({ app, isSelected, hide, isDevMode, customHeight, skipAuth }, ref) => {
return (
<Frame
id={`browser-iframe-${app?.tabId}`}
head={
<>
<style>
{`
body {
margin: 0;
padding: 0;
}
* {
-ms-overflow-style: none; /* IE and Edge */
msOverflowStyle: 'none', /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
*::-webkit-scrollbar {
@@ -28,24 +25,31 @@ const AppViewerContainer = React.forwardRef(({ app, isSelected, hide, isDevMode,
}
.frame-content {
overflow: hidden;
height: ${!isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`};
height: 100vh;
}
`}
</style>
</>
}
style={{
position: (!isSelected || hide) && 'fixed',
left: (!isSelected || hide) && '-200vw',
height: customHeight ? customHeight : !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`,
border: 'none',
width: '100%',
overflow: 'hidden',
}}
>
<AppViewer skipAuth={skipAuth} app={app} ref={ref} hide={!isSelected || hide} isDevMode={isDevMode} />
</Frame>
);
});
</style>
</>
}
style={{
border: 'none',
height: customHeight || '100vh',
left: (!isSelected || hide) && '-200vw',
overflow: 'hidden',
position: (!isSelected || hide) && 'fixed',
width: '100%',
}}
>
<AppViewer
skipAuth={skipAuth}
app={app}
ref={ref}
hide={!isSelected || hide}
isDevMode={isDevMode}
/>
</Frame>
);
}
);
export default AppViewerContainer;

View File

@@ -1,315 +1,397 @@
import {
AppBar,
Button,
Toolbar,
Typography,
Box,
TextField,
InputLabel,
ButtonBase,
} from "@mui/material";
import { styled } from "@mui/system";
export const AppsParent = styled(Box)(({ theme }) => ({
display: "flex",
width: "100%",
flexDirection: "column",
height: "100%",
alignItems: "center",
overflow: 'auto',
// For WebKit-based browsers (Chrome, Safari, etc.)
"::-webkit-scrollbar": {
width: "0px", // Set the width to 0 to hide the scrollbar
height: "0px", // Set the height to 0 for horizontal scrollbar
},
// For Firefox
scrollbarWidth: "none", // Hides the scrollbar in Firefox
// Optional for better cross-browser consistency
"-ms-overflow-style": "none" // Hides scrollbar in IE and Edge
}));
export const AppsContainer = styled(Box)(({ theme }) => ({
display: "flex",
width: "90%",
justifyContent: 'space-evenly',
gap: '24px',
flexWrap: 'wrap',
alignItems: 'flex-start',
alignSelf: 'center'
}));
export const AppsLibraryContainer = styled(Box)(({ theme }) => ({
display: "flex",
width: "100%",
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'center',
}));
export const AppsWidthLimiter = styled(Box)(({ theme }) => ({
display: "flex",
width: "90%",
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
}));
export const AppsSearchContainer = styled(Box)(({ theme }) => ({
display: "flex",
width: "90%",
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: '#434343',
borderRadius: '8px',
padding: '0px 10px',
height: '36px'
}));
export const AppsSearchLeft = styled(Box)(({ theme }) => ({
display: "flex",
width: "90%",
justifyContent: 'flex-start',
alignItems: 'center',
gap: '10px',
flexGrow: 1,
flexShrink: 0
}));
export const AppsSearchRight = styled(Box)(({ theme }) => ({
display: "flex",
width: "90%",
justifyContent: 'flex-end',
alignItems: 'center',
flexShrink: 1
}));
export const AppCircleContainer = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
gap: '5px',
alignItems: 'center',
width: '100%'
}));
export const Add = styled(Typography)(({ theme }) => ({
fontSize: '36px',
fontWeight: 500,
lineHeight: '43.57px',
textAlign: 'left'
}));
export const AppCircleLabel = styled(Typography)(({ theme }) => ({
fontSize: '14px',
fontWeight: 500,
lineHeight: 1.2,
// whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
import { Typography, Box, ButtonBase } from '@mui/material';
import { styled } from '@mui/system';
export const AppsParent = styled(Box)(({ theme }) => ({
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
height: '100%',
overflow: 'auto',
width: '100%',
// For WebKit-based browsers (Chrome, Safari, etc.)
'::-webkit-scrollbar': {
width: '0px', // Set the width to 0 to hide the scrollbar
height: '0px', // Set the height to 0 for horizontal scrollbar
},
// For Firefox
scrollbarWidth: 'none', // Hides the scrollbar in Firefox
// Optional for better cross-browser consistency
msOverflowStyle: 'none', // Hides scrollbar in IE and Edge
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}));
export const AppsContainer = styled(Box)(({ theme }) => ({
alignItems: 'flex-start',
alignSelf: 'center',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
flexWrap: 'wrap',
gap: '24px',
justifyContent: 'space-evenly',
width: '90%',
}));
export const AppsDesktopLibraryHeader = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
flexDirection: 'column',
flexShrink: 0,
width: '100%',
}));
export const AppsDesktopLibraryBody = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
width: '100%',
}));
export const AppsLibraryContainer = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.default,
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-start',
width: '100%',
}));
export const AppsWidthLimiter = styled(Box)(({ theme }) => ({
alignItems: 'flex-start',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-start',
width: '90%',
}));
export const AppsSearchContainer = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.paper,
borderRadius: '8px',
color: theme.palette.text.primary,
display: 'flex',
height: '36px',
justifyContent: 'space-between',
padding: '0px 10px',
width: '90%',
}));
export const AppsSearchLeft = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
display: 'flex',
flexGrow: 1,
flexShrink: 0,
gap: '10px',
justifyContent: 'flex-start',
width: '90%',
}));
export const AppsSearchRight = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
display: 'flex',
flexShrink: 1,
justifyContent: 'flex-end',
width: '90%',
}));
export const AppCircleContainer = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
flexDirection: 'column',
gap: '5px',
width: '100%',
}));
export const AppCircleLabel = styled(Typography)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: '-webkit-box',
fontSize: '14px',
fontWeight: 500,
lineHeight: 1.2,
overflow: 'hidden',
textOverflow: 'ellipsis',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: '2',
width: '120%',
'-webkit-line-clamp': '2',
'-webkit-box-orient': 'vertical',
'display': '-webkit-box',
}));
export const AppLibrarySubTitle = styled(Typography)(({ theme }) => ({
fontSize: '16px',
fontWeight: 500,
lineHeight: 1.2,
}));
export const AppCircle = styled(Box)(({ theme }) => ({
display: "flex",
width: "75px",
flexDirection: "column",
height: "75px",
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
backgroundColor: "var(--apps-circle)",
border: '1px solid #FFFFFF'
}));
}));
export const AppInfoSnippetContainer = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'space-between',
alignItems: 'center',
width: '100%'
}));
export const AppLibrarySubTitle = styled(Typography)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
fontSize: '16px',
fontWeight: 500,
lineHeight: 1.2,
}));
export const AppInfoSnippetLeft = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'flex-start',
alignItems: 'center',
gap: '12px'
}));
export const AppInfoSnippetRight = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'flex-end',
alignItems: 'center',
}));
export const AppCircle = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.surface,
borderColor:
theme.palette.mode === 'dark'
? 'rgb(209, 209, 209)'
: 'rgba(41, 41, 43, 1)',
borderRadius: '50%',
borderStyle: 'solid',
borderWidth: '1px',
color: theme.palette.text.primary,
display: 'flex',
flexDirection: 'column',
height: '75px',
justifyContent: 'center',
width: '75px',
}));
export const AppDownloadButton = styled(ButtonBase)(({ theme }) => ({
backgroundColor: "#247C0E",
width: '101px',
height: '29px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '25px',
alignSelf: 'center'
}));
export const AppInfoSnippetContainer = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
justifyContent: 'space-between',
width: '100%',
}));
export const AppDownloadButtonText = styled(Typography)(({ theme }) => ({
fontSize: '14px',
fontWeight: 500,
lineHeight: 1.2,
}));
export const AppInfoSnippetLeft = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
gap: '12px',
justifyContent: 'flex-start',
}));
export const AppPublishTagsContainer = styled(Box)(({theme})=> ({
gap: '10px',
flexWrap: 'wrap',
justifyContent: 'flex-start',
width: '100%',
display: 'flex'
}))
export const AppInfoSnippetRight = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
justifyContent: 'flex-end',
}));
export const AppDownloadButton = styled(ButtonBase)(({ theme }) => ({
alignItems: 'center',
alignSelf: 'center',
backgroundColor: theme.palette.background.default,
borderRadius: '25px',
color: theme.palette.text.primary,
display: 'flex',
height: '29px',
justifyContent: 'center',
width: '101px',
}));
export const AppInfoSnippetMiddle = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
justifyContent: 'center',
alignItems: 'flex-start',
}));
export const AppDownloadButtonText = styled(Typography)({
fontSize: '14px',
fontWeight: 500,
lineHeight: 1.2,
});
export const AppInfoAppName = styled(Typography)(({ theme }) => ({
fontSize: '16px',
fontWeight: 500,
lineHeight: 1.2,
textAlign: 'start'
}));
export const AppInfoUserName = styled(Typography)(({ theme }) => ({
fontSize: '13px',
fontWeight: 400,
lineHeight: 1.2,
color: '#8D8F93',
textAlign: 'start'
}));
export const AppPublishTagsContainer = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
flexWrap: 'wrap',
gap: '10px',
justifyContent: 'flex-start',
width: '100%',
}));
export const AppInfoSnippetMiddle = styled(Box)(({ theme }) => ({
alignItems: 'flex-start',
backgroundColor: theme.palette.background.default,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
color: theme.palette.text.primary,
}));
export const AppsNavBarParent = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
height: '60px',
backgroundColor: '#1F2023',
padding: '0px 10px',
position: "fixed",
bottom: 0,
zIndex: 1,
}));
export const AppInfoAppName = styled(Typography)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
fontSize: '16px',
fontWeight: 500,
lineHeight: 1.2,
textAlign: 'start',
}));
export const AppsNavBarLeft = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'flex-start',
alignItems: 'center',
flexGrow: 1
}));
export const AppsNavBarRight = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'flex-end',
alignItems: 'center',
}));
export const AppInfoUserName = styled(Typography)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
fontSize: '13px',
fontWeight: 400,
lineHeight: 1.2,
textAlign: 'start',
}));
export const TabParent = styled(Box)(({ theme }) => ({
height: '36px',
width: '36px',
backgroundColor: '#434343',
position: 'relative',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}));
export const AppsNavBarParent = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.default,
bottom: 0,
color: theme.palette.text.primary,
display: 'flex',
height: '60px',
justifyContent: 'space-between',
padding: '0px 10px',
position: 'fixed',
width: '100%',
zIndex: 1,
}));
export const PublishQAppCTAParent = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
backgroundColor: '#181C23'
}));
export const AppsNavBarLeft = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
flexGrow: 1,
justifyContent: 'flex-start',
}));
export const PublishQAppCTALeft = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'flex-start',
alignItems: 'center',
}));
export const PublishQAppCTARight = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'flex-end',
alignItems: 'center',
}));
export const AppsNavBarRight = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
justifyContent: 'flex-end',
}));
export const PublishQAppCTAButton = styled(ButtonBase)(({ theme }) => ({
width: '101px',
height: '29px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '25px',
border: '1px solid #FFFFFF'
}));
export const PublishQAppDotsBG = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'center',
alignItems: 'center',
width: '60px',
height: '60px',
backgroundColor: '#4BBCFE'
}));
export const PublishQAppInfo = styled(Typography)(({ theme }) => ({
fontSize: '10px',
fontWeight: 400,
lineHeight: 1.2,
fontStyle: 'italic'
}));
export const TabParent = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.default,
borderRadius: '50%',
color: theme.palette.text.primary,
display: 'flex',
height: '36px',
justifyContent: 'center',
position: 'relative',
width: '36px',
}));
export const PublishQAppChoseFile = styled(ButtonBase)(({ theme }) => ({
width: '101px',
height: '30px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '5px',
backgroundColor: '#0091E1',
color: 'white',
fontWeight: 600,
fontSize: '10px'
}));
export const PublishQAppCTAParent = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
display: 'flex',
justifyContent: 'space-between',
width: '100%',
}));
export const PublishQAppCTALeft = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
display: 'flex',
justifyContent: 'flex-start',
}));
export const AppsCategoryInfo = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: 'center',
width: '100%',
}));
export const PublishQAppCTARight = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
display: 'flex',
justifyContent: 'flex-end',
}));
export const AppsCategoryInfoSub = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: 'column',
}));
export const AppsCategoryInfoLabel = styled(Typography)(({ theme }) => ({
fontSize: '12px',
fontWeight: 700,
lineHeight: 1.2,
color: '#8D8F93',
}));
export const AppsCategoryInfoValue = styled(Typography)(({ theme }) => ({
fontSize: '12px',
fontWeight: 500,
lineHeight: 1.2,
color: '#8D8F93',
}));
export const AppsInfoDescription = styled(Typography)(({ theme }) => ({
fontSize: '13px',
fontWeight: 300,
lineHeight: 1.2,
width: '90%',
textAlign: 'start'
}));
export const PublishQAppCTAButton = styled(ButtonBase)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.default,
borderColor: theme.palette.text.primary,
borderRadius: '25px',
borderStyle: 'solid',
borderWidth: '1px',
color: theme.palette.text.primary,
display: 'flex',
height: '29px',
justifyContent: 'center',
width: '101px',
}));
export const PublishQAppDotsBG = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
display: 'flex',
height: '80px',
justifyContent: 'center',
width: '60px',
}));
export const PublishQAppInfo = styled(Typography)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
fontSize: '16px',
fontStyle: 'italic',
fontWeight: 400,
lineHeight: 1.2,
}));
export const PublishQAppChoseFile = styled(ButtonBase)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.paper,
borderRadius: '5px',
color: theme.palette.text.primary,
display: 'flex',
fontSize: '16px',
fontWeight: 600,
height: '40px',
justifyContent: 'center',
width: '120px',
'&:hover': {
backgroundColor: 'action.hover', // background on hover
},
}));
export const AppsCategoryInfo = styled(Box)(({ theme }) => ({
alignItems: 'center',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
width: '100%',
}));
export const AppsCategoryInfoSub = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
flexDirection: 'column',
}));
export const AppsCategoryInfoLabel = styled(Typography)(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
fontSize: '12px',
fontWeight: 700,
lineHeight: 1.2,
}));
export const AppsCategoryInfoValue = styled(Typography)(({ theme }) => ({
fontSize: '12px',
fontWeight: 500,
lineHeight: 1.2,
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}));
export const AppsInfoDescription = styled(Typography)(({ theme }) => ({
fontSize: '13px',
fontWeight: 300,
lineHeight: 1.2,
width: '90%',
textAlign: 'start',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}));

View File

@@ -1,326 +0,0 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { AppsHome } from "./AppsHome";
import { Spacer } from "../../common/Spacer";
import { getBaseApiReact } from "../../App";
import { AppInfo } from "./AppInfo";
import {
executeEvent,
subscribeToEvent,
unsubscribeFromEvent,
} from "../../utils/events";
import { AppsParent } from "./Apps-styles";
import AppViewerContainer from "./AppViewerContainer";
import ShortUniqueId from "short-unique-id";
import { AppPublish } from "./AppPublish";
import { AppsCategory } from "./AppsCategory";
import { AppsLibrary } from "./AppsLibrary";
const uid = new ShortUniqueId({ length: 8 });
export const Apps = ({ mode, setMode, show , myName}) => {
const [availableQapps, setAvailableQapps] = useState([]);
const [selectedAppInfo, setSelectedAppInfo] = useState(null);
const [selectedCategory, setSelectedCategory] = useState(null)
const [tabs, setTabs] = useState([]);
const [selectedTab, setSelectedTab] = useState(null);
const [isNewTabWindow, setIsNewTabWindow] = useState(false);
const [categories, setCategories] = useState([])
const iframeRefs = useRef({});
const myApp = useMemo(()=> {
return availableQapps.find((app)=> app.name === myName && app.service === 'APP')
}, [myName, availableQapps])
const myWebsite = useMemo(()=> {
return availableQapps.find((app)=> app.name === myName && app.service === 'WEBSITE')
}, [myName, availableQapps])
useEffect(() => {
setTimeout(() => {
executeEvent("setTabsToNav", {
data: {
tabs: tabs,
selectedTab: selectedTab,
isNewTabWindow: isNewTabWindow,
},
});
}, 100);
}, [show, tabs, selectedTab, isNewTabWindow]);
const getCategories = React.useCallback(async () => {
try {
const url = `${getBaseApiReact()}/arbitrary/categories`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response?.ok) return;
const responseData = await response.json();
setCategories(responseData);
} catch (error) {
} finally {
// dispatch(setIsLoadingGlobal(false))
}
}, []);
const getQapps = React.useCallback(async () => {
try {
let apps = [];
let websites = [];
// dispatch(setIsLoadingGlobal(true))
const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&limit=0&includestatus=true&includemetadata=true`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response?.ok) return;
const responseData = await response.json();
const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&limit=0&includestatus=true&includemetadata=true`;
const responseWebsites = await fetch(urlWebsites, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!responseWebsites?.ok) return;
const responseDataWebsites = await responseWebsites.json();
apps = responseData;
websites = responseDataWebsites;
const combine = [...apps, ...websites];
setAvailableQapps(combine);
} catch (error) {
} finally {
// dispatch(setIsLoadingGlobal(false))
}
}, []);
useEffect(() => {
getQapps();
getCategories()
}, [getQapps, getCategories]);
const selectedAppInfoFunc = (e) => {
const data = e.detail?.data;
setSelectedAppInfo(data);
setMode("appInfo");
};
useEffect(() => {
subscribeToEvent("selectedAppInfo", selectedAppInfoFunc);
return () => {
unsubscribeFromEvent("selectedAppInfo", selectedAppInfoFunc);
};
}, []);
const selectedAppInfoCategoryFunc = (e) => {
const data = e.detail?.data;
setSelectedAppInfo(data);
setMode("appInfo-from-category");
};
useEffect(() => {
subscribeToEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc);
return () => {
unsubscribeFromEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc);
};
}, []);
const selectedCategoryFunc = (e) => {
const data = e.detail?.data;
setSelectedCategory(data);
setMode("category");
};
useEffect(() => {
subscribeToEvent("selectedCategory", selectedCategoryFunc);
return () => {
unsubscribeFromEvent("selectedCategory", selectedCategoryFunc);
};
}, []);
const navigateBackFunc = (e) => {
if (['category', 'appInfo-from-category', 'appInfo', 'library', 'publish'].includes(mode)) {
// Handle the various modes as needed
if (mode === 'category') {
setMode('library');
setSelectedCategory(null);
} else if (mode === 'appInfo-from-category') {
setMode('category');
} else if (mode === 'appInfo') {
setMode('library');
} else if (mode === 'library') {
if (isNewTabWindow) {
setMode('viewer');
} else {
setMode('home');
}
} else if (mode === 'publish') {
setMode('library');
}
} else if(selectedTab?.tabId) {
executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {})
}
};
useEffect(() => {
subscribeToEvent("navigateBack", navigateBackFunc);
return () => {
unsubscribeFromEvent("navigateBack", navigateBackFunc);
};
}, [mode, selectedTab]);
const addTabFunc = (e) => {
const data = e.detail?.data;
const newTab = {
...data,
tabId: uid.rnd(),
};
setTabs((prev) => [...prev, newTab]);
setSelectedTab(newTab);
setMode("viewer");
setIsNewTabWindow(false);
};
useEffect(() => {
subscribeToEvent("addTab", addTabFunc);
return () => {
unsubscribeFromEvent("addTab", addTabFunc);
};
}, [tabs]);
const setSelectedTabFunc = (e) => {
const data = e.detail?.data;
setSelectedTab(data);
setTimeout(() => {
executeEvent("setTabsToNav", {
data: {
tabs: tabs,
selectedTab: data,
isNewTabWindow: isNewTabWindow,
},
});
}, 100);
setIsNewTabWindow(false);
};
useEffect(() => {
subscribeToEvent("setSelectedTab", setSelectedTabFunc);
return () => {
unsubscribeFromEvent("setSelectedTab", setSelectedTabFunc);
};
}, [tabs, isNewTabWindow]);
const removeTabFunc = (e) => {
const data = e.detail?.data;
const copyTabs = [...tabs].filter((tab) => tab?.tabId !== data?.tabId);
if (copyTabs?.length === 0) {
setMode("home");
} else {
setSelectedTab(copyTabs[0]);
}
setTabs(copyTabs);
setSelectedTab(copyTabs[0]);
setTimeout(() => {
executeEvent("setTabsToNav", {
data: {
tabs: copyTabs,
selectedTab: copyTabs[0],
},
});
}, 400);
};
useEffect(() => {
subscribeToEvent("removeTab", removeTabFunc);
return () => {
unsubscribeFromEvent("removeTab", removeTabFunc);
};
}, [tabs]);
const setNewTabWindowFunc = (e) => {
setIsNewTabWindow(true);
setSelectedTab(null)
};
useEffect(() => {
subscribeToEvent("newTabWindow", setNewTabWindowFunc);
return () => {
unsubscribeFromEvent("newTabWindow", setNewTabWindowFunc);
};
}, [tabs]);
return (
<AppsParent
sx={{
display: !show && "none",
}}
>
{mode !== "viewer" && !selectedTab && <Spacer height="30px" />}
{mode === "home" && (
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
)}
<AppsLibrary
isShow={mode === "library" && !selectedTab}
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) => {
if (!iframeRefs.current[tab.tabId]) {
iframeRefs.current[tab.tabId] = React.createRef();
}
return (
<AppViewerContainer
key={tab?.tabId}
hide={isNewTabWindow}
isSelected={tab?.tabId === selectedTab?.tabId}
app={tab}
ref={iframeRefs.current[tab.tabId]}
/>
);
})}
{isNewTabWindow && mode === "viewer" && (
<>
<Spacer height="30px" />
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
</>
)}
{mode !== "viewer" && !selectedTab && <Spacer height="180px" />}
</AppsParent>
);
};

View File

@@ -1,193 +0,0 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import {
AppCircle,
AppCircleContainer,
AppCircleLabel,
AppLibrarySubTitle,
AppsContainer,
AppsLibraryContainer,
AppsParent,
AppsSearchContainer,
AppsSearchLeft,
AppsSearchRight,
AppsWidthLimiter,
PublishQAppCTAButton,
PublishQAppCTALeft,
PublishQAppCTAParent,
PublishQAppCTARight,
PublishQAppDotsBG,
} from "./Apps-styles";
import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material";
import { Add } from "@mui/icons-material";
import { MyContext, getBaseApiReact } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import IconSearch from "../../assets/svgs/Search.svg";
import IconClearInput from "../../assets/svgs/ClearInput.svg";
import qappDevelopText from "../../assets/svgs/qappDevelopText.svg";
import qappDots from "../../assets/svgs/qappDots.svg";
import { Spacer } from "../../common/Spacer";
import { AppInfoSnippet } from "./AppInfoSnippet";
import { Virtuoso } from "react-virtuoso";
import { executeEvent } from "../../utils/events";
const officialAppList = [
"q-tube",
"q-blog",
"q-share",
"q-support",
"q-mail",
"q-fund",
"q-shop",
"q-trade",
"q-support",
"q-manager",
"q-wallets",
"q-search",
"q-nodecontrol"
];
const ScrollerStyled = styled('div')({
// Hide scrollbar for WebKit browsers (Chrome, Safari)
"::-webkit-scrollbar": {
width: "0px",
height: "0px",
},
// Hide scrollbar for Firefox
scrollbarWidth: "none",
// Hide scrollbar for IE and older Edge
"-ms-overflow-style": "none",
});
const StyledVirtuosoContainer = styled('div')({
position: 'relative',
width: '100%',
display: 'flex',
flexDirection: 'column',
// Hide scrollbar for WebKit browsers (Chrome, Safari)
"::-webkit-scrollbar": {
width: "0px",
height: "0px",
},
// Hide scrollbar for Firefox
scrollbarWidth: "none",
// Hide scrollbar for IE and older Edge
"-ms-overflow-style": "none",
});
export const AppsCategory = ({ availableQapps, myName, category, isShow }) => {
const [searchValue, setSearchValue] = useState("");
const virtuosoRef = useRef();
const { rootHeight } = useContext(MyContext);
const categoryList = useMemo(() => {
return availableQapps.filter(
(app) =>
app?.metadata?.category === category?.id
);
}, [availableQapps, category]);
const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value
// Debounce logic
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(searchValue);
}, 350);
// Cleanup timeout if searchValue changes before the timeout completes
return () => {
clearTimeout(handler);
};
}, [searchValue]); // Runs effect when searchValue changes
// Example: Perform search or other actions based on debouncedValue
const searchedList = useMemo(() => {
if (!debouncedValue) return categoryList
return categoryList.filter((app) =>
app.name.toLowerCase().includes(debouncedValue.toLowerCase())
);
}, [debouncedValue, categoryList]);
const rowRenderer = (index) => {
let app = searchedList[index];
return <AppInfoSnippet key={`${app?.service}-${app?.name}`} app={app} myName={myName} isFromCategory={true} />;
};
return (
<AppsLibraryContainer sx={{
display: !isShow && 'none'
}}>
<AppsWidthLimiter>
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "center",
}}
>
<AppsSearchContainer>
<AppsSearchLeft>
<img src={IconSearch} />
<InputBase
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
sx={{ ml: 1, flex: 1 }}
placeholder="Search for apps"
inputProps={{
"aria-label": "Search for apps",
fontSize: "16px",
fontWeight: 400,
}}
/>
</AppsSearchLeft>
<AppsSearchRight>
{searchValue && (
<ButtonBase
onClick={() => {
setSearchValue("");
}}
>
<img src={IconClearInput} />
</ButtonBase>
)}
</AppsSearchRight>
</AppsSearchContainer>
</Box>
</AppsWidthLimiter>
<Spacer height="25px" />
<AppsWidthLimiter>
<AppLibrarySubTitle>{`Category: ${category?.name}`}</AppLibrarySubTitle>
<Spacer height="25px" />
</AppsWidthLimiter>
<AppsWidthLimiter>
<StyledVirtuosoContainer sx={{
height: rootHeight
}}>
<Virtuoso
ref={virtuosoRef}
data={searchedList}
itemContent={rowRenderer}
atBottomThreshold={50}
followOutput="smooth"
components={{
Scroller: ScrollerStyled // Use the styled scroller component
}}
/>
</StyledVirtuosoContainer>
</AppsWidthLimiter>
</AppsLibraryContainer>
);
};

View File

@@ -1,90 +1,52 @@
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useEffect, useMemo, useRef, useState } from 'react';
import {
AppCircle,
AppCircleContainer,
AppCircleLabel,
AppLibrarySubTitle,
AppsContainer,
AppsDesktopLibraryBody,
AppsDesktopLibraryHeader,
AppsLibraryContainer,
AppsParent,
AppsSearchContainer,
AppsSearchLeft,
AppsSearchRight,
AppsWidthLimiter,
PublishQAppCTAButton,
PublishQAppCTALeft,
PublishQAppCTAParent,
PublishQAppCTARight,
PublishQAppDotsBG,
} from "./Apps-styles";
import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material";
import { Add } from "@mui/icons-material";
import { MyContext, getBaseApiReact } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import IconSearch from "../../assets/svgs/Search.svg";
import IconClearInput from "../../assets/svgs/ClearInput.svg";
import qappDevelopText from "../../assets/svgs/qappDevelopText.svg";
import qappDots from "../../assets/svgs/qappDots.svg";
} from './Apps-styles';
import { ButtonBase, InputBase, styled, useTheme } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import IconClearInput from '../../assets/svgs/ClearInput.svg';
import { Spacer } from '../../common/Spacer';
import { AppInfoSnippet } from './AppInfoSnippet';
import { Virtuoso } from 'react-virtuoso';
import { Spacer } from "../../common/Spacer";
import { AppInfoSnippet } from "./AppInfoSnippet";
import { Virtuoso } from "react-virtuoso";
import { executeEvent } from "../../utils/events";
import { AppsDesktopLibraryBody, AppsDesktopLibraryHeader } from "./AppsDesktop-styles";
const officialAppList = [
"q-tube",
"q-blog",
"q-share",
"q-support",
"q-mail",
"q-fund",
"q-shop",
"q-trade",
"q-support",
"q-manager",
"q-wallets",
"q-search",
"q-nodecontrol"
];
const ScrollerStyled = styled("div")({
const ScrollerStyled = styled('div')({
// Hide scrollbar for WebKit browsers (Chrome, Safari)
"::-webkit-scrollbar": {
width: "0px",
height: "0px",
'::-webkit-scrollbar': {
width: '0px',
height: '0px',
},
// Hide scrollbar for Firefox
scrollbarWidth: "none",
scrollbarWidth: 'none',
// Hide scrollbar for IE and older Edge
"-ms-overflow-style": "none",
msOverflowStyle: 'none',
});
const StyledVirtuosoContainer = styled("div")({
position: "relative",
width: "100%",
display: "flex",
flexDirection: "column",
const StyledVirtuosoContainer = styled('div')({
position: 'relative',
width: '100%',
display: 'flex',
flexDirection: 'column',
// Hide scrollbar for WebKit browsers (Chrome, Safari)
"::-webkit-scrollbar": {
width: "0px",
height: "0px",
'::-webkit-scrollbar': {
width: '0px',
height: '0px',
},
// Hide scrollbar for Firefox
scrollbarWidth: "none",
scrollbarWidth: 'none',
// Hide scrollbar for IE and older Edge
"-ms-overflow-style": "none",
msOverflowStyle: 'none',
});
export const AppsCategoryDesktop = ({
@@ -93,29 +55,28 @@ export const AppsCategoryDesktop = ({
category,
isShow,
}) => {
const [searchValue, setSearchValue] = useState("");
const [searchValue, setSearchValue] = useState('');
const virtuosoRef = useRef();
const { rootHeight } = useContext(MyContext);
const theme = useTheme();
const categoryList = useMemo(() => {
if(category?.id === 'all') return availableQapps
if (category?.id === 'all') return availableQapps;
return availableQapps.filter(
(app) => app?.metadata?.category === category?.id
);
}, [availableQapps, category]);
const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value
const [debouncedValue, setDebouncedValue] = useState(''); // Debounced value
// Debounce logic
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(searchValue);
}, 350);
setTimeout(() => {
virtuosoRef.current.scrollToIndex({
index: 0
});
if (virtuosoRef.current) {
virtuosoRef.current.scrollToIndex({ index: 0 });
}
}, 500);
// Cleanup timeout if searchValue changes before the timeout completes
return () => {
@@ -127,8 +88,13 @@ export const AppsCategoryDesktop = ({
const searchedList = useMemo(() => {
if (!debouncedValue) return categoryList;
return categoryList.filter((app) =>
app.name.toLowerCase().includes(debouncedValue.toLowerCase()) || (app?.metadata?.title && app?.metadata?.title?.toLowerCase().includes(debouncedValue.toLowerCase()))
return categoryList.filter(
(app) =>
app.name.toLowerCase().includes(debouncedValue.toLowerCase()) ||
(app?.metadata?.title &&
app?.metadata?.title
?.toLowerCase()
.includes(debouncedValue.toLowerCase()))
);
}, [debouncedValue, categoryList]);
@@ -141,7 +107,7 @@ export const AppsCategoryDesktop = ({
myName={myName}
isFromCategory={true}
parentStyles={{
padding: '0px 10px'
padding: '0px 10px',
}}
/>
);
@@ -150,46 +116,56 @@ export const AppsCategoryDesktop = ({
return (
<AppsLibraryContainer
sx={{
display: !isShow && "none",
padding: "0px",
height: "100vh",
overflow: "hidden",
paddingTop: "30px",
display: !isShow && 'none',
height: '100vh',
overflow: 'hidden',
padding: '0px',
paddingTop: '30px',
}}
>
<AppsDesktopLibraryHeader
sx={{
maxWidth: "1500px",
width: "90%",
maxWidth: '1200px',
width: '90%',
}}
>
<AppsWidthLimiter
sx={{
alignItems: "flex-end",
alignItems: 'flex-end',
}}
>
<AppsSearchContainer sx={{
width: "412px",
}}>
<AppsSearchContainer
sx={{
width: '412px',
}}
>
<AppsSearchLeft>
<img src={IconSearch} />
<SearchIcon />
<InputBase
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
sx={{ ml: 1, flex: 1 }}
sx={{
background: theme.palette.background.paper,
borderRadius: '6px',
flex: 1,
ml: 1,
paddingLeft: '12px',
}}
placeholder="Search for apps"
inputProps={{
"aria-label": "Search for apps",
fontSize: "16px",
'aria-label': 'Search for apps',
fontSize: '16px',
fontWeight: 400,
}}
/>
</AppsSearchLeft>
<AppsSearchRight>
{searchValue && (
<ButtonBase
onClick={() => {
setSearchValue("");
setSearchValue('');
}}
>
<img src={IconClearInput} />
@@ -199,23 +175,27 @@ export const AppsCategoryDesktop = ({
</AppsSearchContainer>
</AppsWidthLimiter>
</AppsDesktopLibraryHeader>
<AppsDesktopLibraryBody
sx={{
alignItems: 'center',
height: `calc(100vh - 36px)`,
overflow: "auto",
padding: "0px",
alignItems: "center",
overflow: 'auto',
padding: '0px',
width: '70%',
}}
>
<Spacer height="25px" />
<AppsWidthLimiter>
<AppLibrarySubTitle>{`Category: ${category?.name}`}</AppLibrarySubTitle>
<Spacer height="25px" />
</AppsWidthLimiter>
<AppsWidthLimiter>
<StyledVirtuosoContainer
sx={{
sx={{
height: `calc(100vh - 36px - 90px - 25px)`,
}}
>

View File

@@ -1,24 +0,0 @@
import {
AppBar,
Button,
Toolbar,
Typography,
Box,
TextField,
InputLabel,
ButtonBase,
} from "@mui/material";
import { styled } from "@mui/system";
export const AppsDesktopLibraryHeader = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: 'column',
flexShrink: 0,
width: '100%'
}));
export const AppsDesktopLibraryBody = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: 'column',
flexGrow: 1,
width: '100%'
}));

View File

@@ -1,65 +1,78 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { AppsHomeDesktop } from "./AppsHomeDesktop";
import { Spacer } from "../../common/Spacer";
import { GlobalContext, MyContext, getBaseApiReact } from "../../App";
import { AppInfo } from "./AppInfo";
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { AppsHomeDesktop } from './AppsHomeDesktop';
import { Spacer } from '../../common/Spacer';
import { MyContext, getBaseApiReact } from '../../App';
import { AppInfo } from './AppInfo';
import {
executeEvent,
subscribeToEvent,
unsubscribeFromEvent,
} from "../../utils/events";
import { AppsParent } from "./Apps-styles";
import AppViewerContainer from "./AppViewerContainer";
import ShortUniqueId from "short-unique-id";
import { AppPublish } from "./AppPublish";
import { AppsLibraryDesktop } from "./AppsLibraryDesktop";
import { AppsCategoryDesktop } from "./AppsCategoryDesktop";
import { AppsNavBarDesktop } from "./AppsNavBarDesktop";
import { Box, ButtonBase } from "@mui/material";
import { HomeIcon } from "../../assets/Icons/HomeIcon";
import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
import { Save } from "../Save/Save";
import { HubsIcon } from "../../assets/Icons/HubsIcon";
import { CoreSyncStatus } from "../CoreSyncStatus";
import { IconWrapper } from "../Desktop/DesktopFooter";
import AppIcon from "../../assets/svgs/AppIcon.svg";
import { useRecoilState } from "recoil";
import { enabledDevModeAtom } from "../../atoms/global";
import { AppsIcon } from "../../assets/Icons/AppsIcon";
} from '../../utils/events';
import { AppsParent } from './Apps-styles';
import AppViewerContainer from './AppViewerContainer';
import ShortUniqueId from 'short-unique-id';
import { AppPublish } from './AppPublish';
import { AppsLibraryDesktop } from './AppsLibraryDesktop';
import { AppsCategoryDesktop } from './AppsCategoryDesktop';
import { AppsNavBarDesktop } from './AppsNavBarDesktop';
import { Box, ButtonBase, useTheme } from '@mui/material';
import { HomeIcon } from '../../assets/Icons/HomeIcon';
import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
import { Save } from '../Save/Save';
import { IconWrapper } from '../Desktop/DesktopFooter';
import { enabledDevModeAtom } from '../../atoms/global';
import { AppsIcon } from '../../assets/Icons/AppsIcon';
import { CoreSyncStatus } from '../CoreSyncStatus';
import { MessagingIconFilled } from '../../assets/Icons/MessagingIconFilled';
import { useAtom } from 'jotai';
const uid = new ShortUniqueId({ length: 8 });
export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktopSideView, hasUnreadDirects, isDirects, isGroups, hasUnreadGroups, toggleSideViewGroups, toggleSideViewDirects, setDesktopViewMode, isApps, desktopViewMode}) => {
export const AppsDesktop = ({
mode,
setMode,
show,
myName,
goToHome,
hasUnreadDirects,
hasUnreadGroups,
setDesktopViewMode,
desktopViewMode,
}) => {
const [availableQapps, setAvailableQapps] = useState([]);
const [selectedAppInfo, setSelectedAppInfo] = useState(null);
const [selectedCategory, setSelectedCategory] = useState(null)
const [selectedCategory, setSelectedCategory] = useState(null);
const [tabs, setTabs] = useState([]);
const [selectedTab, setSelectedTab] = useState(null);
const [isNewTabWindow, setIsNewTabWindow] = useState(false);
const [categories, setCategories] = useState([])
const [categories, setCategories] = useState([]);
const iframeRefs = useRef({});
const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom)
const { showTutorial } = useContext(GlobalContext);
const [isEnabledDevMode, setIsEnabledDevMode] = useAtom(enabledDevModeAtom);
const myApp = useMemo(()=> {
return availableQapps.find((app)=> app.name === myName && app.service === 'APP')
}, [myName, availableQapps])
const myWebsite = useMemo(()=> {
return availableQapps.find((app)=> app.name === myName && app.service === 'WEBSITE')
}, [myName, availableQapps])
const { showTutorial } = useContext(MyContext);
const theme = useTheme();
const myApp = useMemo(() => {
return availableQapps.find(
(app) => app.name === myName && app.service === 'APP'
);
}, [myName, availableQapps]);
useEffect(()=> {
if(show){
showTutorial('qapps')
const myWebsite = useMemo(() => {
return availableQapps.find(
(app) => app.name === myName && app.service === 'WEBSITE'
);
}, [myName, availableQapps]);
useEffect(() => {
if (show) {
showTutorial('qapps');
}
}, [show])
}, [show]);
useEffect(() => {
setTimeout(() => {
executeEvent("setTabsToNav", {
executeEvent('setTabsToNav', {
data: {
tabs: tabs,
selectedTab: selectedTab,
@@ -74,17 +87,17 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
const url = `${getBaseApiReact()}/arbitrary/categories`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
if (!response?.ok) return;
const responseData = await response.json();
setCategories(responseData);
} catch (error) {
console.log(error);
} finally {
// dispatch(setIsLoadingGlobal(false))
}
@@ -98,9 +111,9 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&limit=0&includestatus=true&includemetadata=true`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
if (!response?.ok) return;
@@ -108,33 +121,37 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&limit=0&includestatus=true&includemetadata=true`;
const responseWebsites = await fetch(urlWebsites, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
if (!responseWebsites?.ok) return;
const responseDataWebsites = await responseWebsites.json();
apps = responseData;
websites = responseDataWebsites;
const combine = [...apps, ...websites];
setAvailableQapps(combine);
} catch (error) {
console.log(error);
} finally {
// dispatch(setIsLoadingGlobal(false))
}
}, []);
useEffect(() => {
getCategories()
getCategories();
}, [getCategories]);
useEffect(() => {
getQapps();
const interval = setInterval(() => {
getQapps();
}, 20 * 60 * 1000); // 20 minutes in milliseconds
const interval = setInterval(
() => {
getQapps();
},
20 * 60 * 1000
); // 20 minutes in milliseconds
return () => clearInterval(interval);
}, [getQapps]);
@@ -142,54 +159,58 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
const selectedAppInfoFunc = (e) => {
const data = e.detail?.data;
setSelectedAppInfo(data);
setMode("appInfo");
setMode('appInfo');
};
useEffect(() => {
subscribeToEvent("selectedAppInfo", selectedAppInfoFunc);
subscribeToEvent('selectedAppInfo', selectedAppInfoFunc);
return () => {
unsubscribeFromEvent("selectedAppInfo", selectedAppInfoFunc);
unsubscribeFromEvent('selectedAppInfo', selectedAppInfoFunc);
};
}, []);
const selectedAppInfoCategoryFunc = (e) => {
const data = e.detail?.data;
setSelectedAppInfo(data);
setMode("appInfo-from-category");
setMode('appInfo-from-category');
};
useEffect(() => {
subscribeToEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc);
subscribeToEvent('selectedAppInfoCategory', selectedAppInfoCategoryFunc);
return () => {
unsubscribeFromEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc);
unsubscribeFromEvent(
'selectedAppInfoCategory',
selectedAppInfoCategoryFunc
);
};
}, []);
const selectedCategoryFunc = (e) => {
const data = e.detail?.data;
setSelectedCategory(data);
setMode("category");
setMode('category');
};
useEffect(() => {
subscribeToEvent("selectedCategory", selectedCategoryFunc);
subscribeToEvent('selectedCategory', selectedCategoryFunc);
return () => {
unsubscribeFromEvent("selectedCategory", selectedCategoryFunc);
unsubscribeFromEvent('selectedCategory', selectedCategoryFunc);
};
}, []);
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
if (mode === 'category') {
setMode('library');
@@ -207,17 +228,16 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
} else if (mode === 'publish') {
setMode('library');
}
} else if(selectedTab?.tabId) {
executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {})
} else if (selectedTab?.tabId) {
executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {});
}
};
useEffect(() => {
subscribeToEvent("navigateBack", navigateBackFunc);
subscribeToEvent('navigateBack', navigateBackFunc);
return () => {
unsubscribeFromEvent("navigateBack", navigateBackFunc);
unsubscribeFromEvent('navigateBack', navigateBackFunc);
};
}, [mode, selectedTab]);
@@ -229,27 +249,25 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
};
setTabs((prev) => [...prev, newTab]);
setSelectedTab(newTab);
setMode("viewer");
setMode('viewer');
setIsNewTabWindow(false);
};
useEffect(() => {
subscribeToEvent("addTab", addTabFunc);
subscribeToEvent('addTab', addTabFunc);
return () => {
unsubscribeFromEvent("addTab", addTabFunc);
unsubscribeFromEvent('addTab', addTabFunc);
};
}, [tabs]);
const setSelectedTabFunc = (e) => {
const data = e.detail?.data;
if(e.detail?.isDevMode) return
if (e.detail?.isDevMode) return;
setSelectedTab(data);
setTimeout(() => {
executeEvent("setTabsToNav", {
executeEvent('setTabsToNav', {
data: {
tabs: tabs,
selectedTab: data,
@@ -259,13 +277,12 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
}, 100);
setIsNewTabWindow(false);
};
useEffect(() => {
subscribeToEvent("setSelectedTab", setSelectedTabFunc);
subscribeToEvent('setSelectedTab', setSelectedTabFunc);
return () => {
unsubscribeFromEvent("setSelectedTab", setSelectedTabFunc);
unsubscribeFromEvent('setSelectedTab', setSelectedTabFunc);
};
}, [tabs, isNewTabWindow]);
@@ -273,14 +290,14 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
const data = e.detail?.data;
const copyTabs = [...tabs].filter((tab) => tab?.tabId !== data?.tabId);
if (copyTabs?.length === 0) {
setMode("home");
setMode('home');
} else {
setSelectedTab(copyTabs[0]);
}
setTabs(copyTabs);
setSelectedTab(copyTabs[0]);
setTimeout(() => {
executeEvent("setTabsToNav", {
executeEvent('setTabsToNav', {
data: {
tabs: copyTabs,
selectedTab: copyTabs[0],
@@ -290,23 +307,23 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
};
useEffect(() => {
subscribeToEvent("removeTab", removeTabFunc);
subscribeToEvent('removeTab', removeTabFunc);
return () => {
unsubscribeFromEvent("removeTab", removeTabFunc);
unsubscribeFromEvent('removeTab', removeTabFunc);
};
}, [tabs]);
const setNewTabWindowFunc = (e) => {
setIsNewTabWindow(true);
setSelectedTab(null)
setSelectedTab(null);
};
useEffect(() => {
subscribeToEvent("newTabWindow", setNewTabWindowFunc);
subscribeToEvent('newTabWindow', setNewTabWindowFunc);
return () => {
unsubscribeFromEvent("newTabWindow", setNewTabWindowFunc);
unsubscribeFromEvent('newTabWindow', setNewTabWindowFunc);
};
}, [tabs]);
@@ -315,167 +332,153 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
sx={{
position: !show && 'fixed',
left: !show && '-200vw',
flexDirection: 'row'
flexDirection: 'row',
}}
>
<Box sx={{
width: '60px',
flexDirection: 'column',
height: '100vh',
alignItems: 'center',
display: 'flex',
gap: '25px'
}}>
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
gap: '25px',
height: '100vh',
width: '60px',
backgroundColor: theme.palette.background.surface,
borderRight: `1px solid ${theme.palette.border.subtle}`,
}}
>
<ButtonBase
sx={{
width: '70px',
height: '70px',
paddingTop: '23px',
}}
>
<CoreSyncStatus />
</ButtonBase>
<ButtonBase
sx={{
width: '60px',
height: '60px',
paddingTop: '23px'
}}
onClick={() => {
goToHome();
}}
>
<HomeIcon
height={34}
color={desktopViewMode === 'home' ? 'white': "rgba(250, 250, 250, 0.5)"}
/>
<HomeIcon height={34} color={theme.palette.text.secondary} />
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopViewMode('apps')
setDesktopViewMode('apps');
}}
>
<IconWrapper label="Apps" disableWidth>
<AppsIcon height={30} color={theme.palette.text.primary} />
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopViewMode('chat');
}}
>
<IconWrapper
color={isApps ? 'white' :"rgba(250, 250, 250, 0.5)"}
label="Apps"
disableWidth
>
<AppsIcon height={30} color={isApps ? 'white' :"rgba(250, 250, 250, 0.5)"} />
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopViewMode('chat')
}}
>
<IconWrapper
color={(hasUnreadDirects || hasUnreadGroups) ? "var(--unread)" : desktopViewMode === 'chat' ? 'white' :"rgba(250, 250, 250, 0.5)"}
color={
hasUnreadDirects || hasUnreadGroups
? theme.palette.other.unread
: desktopViewMode === 'chat'
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="Chat"
disableWidth
>
<MessagingIcon
<MessagingIconFilled
height={30}
color={
(hasUnreadDirects || hasUnreadGroups)
? "var(--unread)"
hasUnreadDirects || hasUnreadGroups
? theme.palette.other.unread
: desktopViewMode === 'chat'
? "white"
: "rgba(250, 250, 250, 0.5)"
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
</IconWrapper>
</ButtonBase>
{/* <ButtonBase
onClick={() => {
setDesktopSideView("directs");
toggleSideViewDirects()
}}
>
<MessagingIcon
height={30}
color={
hasUnreadDirects
? "var(--danger)"
: isDirects
? "white"
: "rgba(250, 250, 250, 0.5)"
}
/>
</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} />
{isEnabledDevMode && (
<ButtonBase
onClick={() => {
setDesktopViewMode('dev')
}}
>
<IconWrapper
color={desktopViewMode === 'dev' ? 'white' : "rgba(250, 250, 250, 0.5)"}
label="Dev"
disableWidth
>
<AppsIcon color={desktopViewMode === 'dev' ? 'white' : "rgba(250, 250, 250, 0.5)"} height={30} />
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopViewMode('dev');
}}
>
<IconWrapper label="Dev" disableWidth>
<AppsIcon height={30} />
</IconWrapper>
</ButtonBase>
)}
{mode !== 'home' && (
<AppsNavBarDesktop disableBack={isNewTabWindow && mode === 'viewer'} />
<AppsNavBarDesktop
disableBack={isNewTabWindow && mode === 'viewer'}
/>
)}
</Box>
</Box>
{mode === "home" && (
<Box sx={{
display: 'flex',
width: '100%',
flexDirection: 'column',
height: '100vh',
overflow: 'auto'
}}>
<Spacer height="30px" />
<AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
{mode === 'home' && (
<Box
sx={{
display: 'flex',
width: '100%',
flexDirection: 'column',
height: '100vh',
overflow: 'auto',
}}
>
<Spacer height="30px" />
<AppsHomeDesktop
myName={myName}
availableQapps={availableQapps}
setMode={setMode}
myApp={myApp}
myWebsite={myWebsite}
/>
</Box>
)}
<AppsLibraryDesktop
isShow={mode === "library" && !selectedTab}
availableQapps={availableQapps}
setMode={setMode}
myName={myName}
hasPublishApp={!!(myApp || myWebsite)}
categories={categories}
getQapps={getQapps}
/>
{mode === "appInfo" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />}
{mode === "appInfo-from-category" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />}
<AppsCategoryDesktop availableQapps={availableQapps} isShow={mode === 'category' && !selectedTab} category={selectedCategory} myName={myName} />
{mode === "publish" && !selectedTab && <AppPublish names={myName ? [myName] : []} categories={categories} />}
<AppsLibraryDesktop
availableQapps={availableQapps}
categories={categories}
getQapps={getQapps}
hasPublishApp={!!(myApp || myWebsite)}
isShow={mode === 'library' && !selectedTab}
myName={myName}
setMode={setMode}
/>
{mode === 'appInfo' && !selectedTab && (
<AppInfo app={selectedAppInfo} myName={myName} />
)}
{mode === 'appInfo-from-category' && !selectedTab && (
<AppInfo app={selectedAppInfo} myName={myName} />
)}
<AppsCategoryDesktop
availableQapps={availableQapps}
isShow={mode === 'category' && !selectedTab}
category={selectedCategory}
myName={myName}
/>
{mode === 'publish' && !selectedTab && (
<AppPublish names={myName ? [myName] : []} categories={categories} />
)}
{tabs.map((tab) => {
if (!iframeRefs.current[tab.tabId]) {
iframeRefs.current[tab.tabId] = React.createRef();
}
return (
<AppViewerContainer
key={tab?.tabId}
key={tab?.tabId}
hide={isNewTabWindow}
isSelected={tab?.tabId === selectedTab?.tabId}
app={tab}
@@ -485,18 +488,25 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
);
})}
{isNewTabWindow && mode === "viewer" && (
{isNewTabWindow && mode === 'viewer' && (
<>
<Box sx={{
display: 'flex',
width: '100%',
flexDirection: 'column',
height: '100vh',
overflow: 'auto'
}}>
<Spacer height="30px" />
<AppsHomeDesktop myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
<Box
sx={{
display: 'flex',
width: '100%',
flexDirection: 'column',
height: '100vh',
overflow: 'auto',
}}
>
<Spacer height="30px" />
<AppsHomeDesktop
myName={myName}
availableQapps={availableQapps}
setMode={setMode}
myApp={myApp}
myWebsite={myWebsite}
/>
</Box>
</>
)}

View File

@@ -1,46 +1,57 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { AppsDevModeHome } from "./AppsDevModeHome";
import { Spacer } from "../../common/Spacer";
import { MyContext, getBaseApiReact } from "../../App";
import { AppInfo } from "./AppInfo";
import React, { useEffect, useRef, useState } from 'react';
import { AppsDevModeHome } from './AppsDevModeHome';
import { Spacer } from '../../common/Spacer';
import {
executeEvent,
subscribeToEvent,
unsubscribeFromEvent,
} from "../../utils/events";
import { AppsParent } from "./Apps-styles";
import AppViewerContainer from "./AppViewerContainer";
import ShortUniqueId from "short-unique-id";
import { AppPublish } from "./AppPublish";
import { AppsLibraryDesktop } from "./AppsLibraryDesktop";
import { AppsCategoryDesktop } from "./AppsCategoryDesktop";
import { AppsNavBarDesktop } from "./AppsNavBarDesktop";
import { Box, ButtonBase } from "@mui/material";
import { HomeIcon } from "../../assets/Icons/HomeIcon";
import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
import { Save } from "../Save/Save";
import { HubsIcon } from "../../assets/Icons/HubsIcon";
import { AppsDevModeNavBar } from "./AppsDevModeNavBar";
import { CoreSyncStatus } from "../CoreSyncStatus";
import { AppsIcon } from "../../assets/Icons/AppsIcon";
import { IconWrapper } from "../Desktop/DesktopFooter";
} from '../../utils/events';
import { AppsParent } from './Apps-styles';
import AppViewerContainer from './AppViewerContainer';
import ShortUniqueId from 'short-unique-id';
import { Box, ButtonBase, useTheme } from '@mui/material';
import { HomeIcon } from '../../assets/Icons/HomeIcon';
import { Save } from '../Save/Save';
import { AppsDevModeNavBar } from './AppsDevModeNavBar';
import { AppsIcon } from '../../assets/Icons/AppsIcon';
import { IconWrapper } from '../Desktop/DesktopFooter';
import { CoreSyncStatus } from '../CoreSyncStatus';
import { MessagingIconFilled } from '../../assets/Icons/MessagingIconFilled';
const uid = new ShortUniqueId({ length: 8 });
export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktopSideView, hasUnreadDirects, isDirects, isGroups, hasUnreadGroups, toggleSideViewGroups, toggleSideViewDirects, setDesktopViewMode, desktopViewMode, isApps}) => {
export const AppsDevMode = ({
mode,
setMode,
show,
myName,
goToHome,
setDesktopSideView,
hasUnreadDirects,
isDirects,
isGroups,
hasUnreadGroups,
toggleSideViewGroups,
toggleSideViewDirects,
setDesktopViewMode,
desktopViewMode,
isApps,
}) => {
const [availableQapps, setAvailableQapps] = useState([]);
const [selectedAppInfo, setSelectedAppInfo] = useState(null);
const [selectedCategory, setSelectedCategory] = useState(null)
const [selectedCategory, setSelectedCategory] = useState(null);
const [tabs, setTabs] = useState([]);
const [selectedTab, setSelectedTab] = useState(null);
const [isNewTabWindow, setIsNewTabWindow] = useState(false);
const [categories, setCategories] = useState([])
const [categories, setCategories] = useState([]);
const iframeRefs = useRef({});
const theme = useTheme();
useEffect(() => {
setTimeout(() => {
executeEvent("appsDevModeSetTabsToNav", {
executeEvent('appsDevModeSetTabsToNav', {
data: {
tabs: tabs,
selectedTab: selectedTab,
@@ -50,17 +61,16 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
}, 100);
}, [show, tabs, selectedTab, isNewTabWindow]);
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
if (mode === 'category') {
setMode('library');
@@ -78,17 +88,16 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
} else if (mode === 'publish') {
setMode('library');
}
} else if(selectedTab?.tabId) {
executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {})
} else if (selectedTab?.tabId) {
executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {});
}
};
useEffect(() => {
subscribeToEvent("devModeNavigateBack", navigateBackFunc);
subscribeToEvent('devModeNavigateBack', navigateBackFunc);
return () => {
unsubscribeFromEvent("devModeNavigateBack", navigateBackFunc);
unsubscribeFromEvent('devModeNavigateBack', navigateBackFunc);
};
}, [mode, selectedTab]);
@@ -100,58 +109,52 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
};
setTabs((prev) => [...prev, newTab]);
setSelectedTab(newTab);
setMode("viewer");
setMode('viewer');
setIsNewTabWindow(false);
};
useEffect(() => {
subscribeToEvent("appsDevModeAddTab", addTabFunc);
subscribeToEvent('appsDevModeAddTab', addTabFunc);
return () => {
unsubscribeFromEvent("appsDevModeAddTab", addTabFunc);
unsubscribeFromEvent('appsDevModeAddTab', addTabFunc);
};
}, [tabs]);
const updateTabFunc = (e) => {
const data = e.detail?.data;
if(!data.tabId) return
const findIndexTab = tabs.findIndex((tab)=> tab?.tabId === data?.tabId)
if(findIndexTab === -1) return
const copyTabs = [...tabs]
const newTab ={
if (!data.tabId) return;
const findIndexTab = tabs.findIndex((tab) => tab?.tabId === data?.tabId);
if (findIndexTab === -1) return;
const copyTabs = [...tabs];
const newTab = {
...copyTabs[findIndexTab],
url: data.url
url: data.url,
};
copyTabs[findIndexTab] = newTab;
}
copyTabs[findIndexTab] = newTab
setTabs(copyTabs);
setSelectedTab(newTab);
setMode("viewer");
setMode('viewer');
setIsNewTabWindow(false);
};
useEffect(() => {
subscribeToEvent("appsDevModeUpdateTab", updateTabFunc);
subscribeToEvent('appsDevModeUpdateTab', updateTabFunc);
return () => {
unsubscribeFromEvent("appsDevModeUpdateTab", updateTabFunc);
unsubscribeFromEvent('appsDevModeUpdateTab', updateTabFunc);
};
}, [tabs]);
const setSelectedTabFunc = (e) => {
const data = e.detail?.data;
if(!e.detail?.isDevMode) return
if (!e.detail?.isDevMode) return;
setSelectedTab(data);
setTimeout(() => {
executeEvent("appsDevModeSetTabsToNav", {
executeEvent('appsDevModeSetTabsToNav', {
data: {
tabs: tabs,
selectedTab: data,
@@ -161,13 +164,12 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
}, 100);
setIsNewTabWindow(false);
};
useEffect(() => {
subscribeToEvent("setSelectedTabDevMode", setSelectedTabFunc);
subscribeToEvent('setSelectedTabDevMode', setSelectedTabFunc);
return () => {
unsubscribeFromEvent("setSelectedTabDevMode", setSelectedTabFunc);
unsubscribeFromEvent('setSelectedTabDevMode', setSelectedTabFunc);
};
}, [tabs, isNewTabWindow]);
@@ -175,14 +177,14 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
const data = e.detail?.data;
const copyTabs = [...tabs].filter((tab) => tab?.tabId !== data?.tabId);
if (copyTabs?.length === 0) {
setMode("home");
setMode('home');
} else {
setSelectedTab(copyTabs[0]);
}
setTabs(copyTabs);
setSelectedTab(copyTabs[0]);
setTimeout(() => {
executeEvent("appsDevModeSetTabsToNav", {
executeEvent('appsDevModeSetTabsToNav', {
data: {
tabs: copyTabs,
selectedTab: copyTabs[0],
@@ -192,143 +194,178 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
};
useEffect(() => {
subscribeToEvent("removeTabDevMode", removeTabFunc);
subscribeToEvent('removeTabDevMode', removeTabFunc);
return () => {
unsubscribeFromEvent("removeTabDevMode", removeTabFunc);
unsubscribeFromEvent('removeTabDevMode', removeTabFunc);
};
}, [tabs]);
const setNewTabWindowFunc = (e) => {
setIsNewTabWindow(true);
setSelectedTab(null)
setSelectedTab(null);
};
useEffect(() => {
subscribeToEvent("devModeNewTabWindow", setNewTabWindowFunc);
subscribeToEvent('devModeNewTabWindow', setNewTabWindowFunc);
return () => {
unsubscribeFromEvent("devModeNewTabWindow", setNewTabWindowFunc);
unsubscribeFromEvent('devModeNewTabWindow', setNewTabWindowFunc);
};
}, [tabs]);
return (
<AppsParent
sx={{
flexDirection: 'row' ,
flexDirection: 'row',
position: !show && 'fixed',
left: !show && '-200vw',
}}
>
<Box sx={{
width: '60px',
flexDirection: 'column',
height: '100vh',
alignItems: 'center',
display: 'flex',
gap: '25px'
}}>
<Box
sx={{
width: '60px',
flexDirection: 'column',
height: '100vh',
alignItems: 'center',
display: 'flex',
gap: '25px',
}}
>
<ButtonBase
sx={{
width: '70px',
height: '70px',
paddingTop: '23px',
}}
>
<CoreSyncStatus />
</ButtonBase>
<ButtonBase
sx={{
width: '60px',
height: '60px',
paddingTop: '23px'
}}
onClick={() => {
goToHome();
}}
>
<HomeIcon
height={34}
color={desktopViewMode === 'home' ? 'white': "rgba(250, 250, 250, 0.5)"}
/>
<HomeIcon
height={34}
color={
desktopViewMode === 'home'
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopViewMode('apps')
setDesktopViewMode('apps');
}}
>
<IconWrapper
color={isApps ? 'white' :"rgba(250, 250, 250, 0.5)"}
color={
isApps ? theme.palette.text.primary : theme.palette.text.secondary
}
label="Apps"
disableWidth
>
<AppsIcon height={30} color={isApps ? 'white' :"rgba(250, 250, 250, 0.5)"} />
<AppsIcon
height={30}
color={
isApps
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopViewMode('chat')
setDesktopViewMode('chat');
}}
>
<IconWrapper
color={(hasUnreadDirects || hasUnreadGroups) ? "var(--unread)" : desktopViewMode === 'chat' ? 'white' :"rgba(250, 250, 250, 0.5)"}
<IconWrapper
color={
hasUnreadDirects || hasUnreadGroups
? theme.palette.other.unread
: desktopViewMode === 'chat'
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="Chat"
disableWidth
>
<MessagingIcon
<MessagingIconFilled
height={30}
color={
(hasUnreadDirects || hasUnreadGroups)
? "var(--unread)"
hasUnreadDirects || hasUnreadGroups
? theme.palette.other.unread
: desktopViewMode === 'chat'
? "white"
: "rgba(250, 250, 250, 0.5)"
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
</IconWrapper>
</ButtonBase>
<Save isDesktop disableWidth myName={myName} />
<ButtonBase
onClick={() => {
setDesktopViewMode('dev')
}}
>
<IconWrapper
color={desktopViewMode === 'dev' ? 'white' : "rgba(250, 250, 250, 0.5)"}
label="Dev"
disableWidth
>
<AppsIcon color={desktopViewMode === 'dev' ? 'white' : "rgba(250, 250, 250, 0.5)"} height={30} />
</IconWrapper>
</ButtonBase>
{mode !== 'home' && (
<AppsDevModeNavBar />
onClick={() => {
setDesktopViewMode('dev');
}}
>
<IconWrapper
color={
desktopViewMode === 'dev'
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="Dev"
disableWidth
>
<AppsIcon
color={
desktopViewMode === 'dev'
? theme.palette.text.primary
: theme.palette.text.secondary
}
height={30}
/>
</IconWrapper>
</ButtonBase>
{mode !== 'home' && <AppsDevModeNavBar />}
</Box>
)}
</Box>
{mode === "home" && (
<Box sx={{
display: 'flex',
width: '100%',
flexDirection: 'column',
height: '100vh',
overflow: 'auto'
}}>
<Spacer height="30px" />
<AppsDevModeHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={null} myWebsite={null} />
{mode === 'home' && (
<Box
sx={{
display: 'flex',
width: '100%',
flexDirection: 'column',
height: '100vh',
overflow: 'auto',
}}
>
<Spacer height="30px" />
<AppsDevModeHome
myName={myName}
availableQapps={availableQapps}
setMode={setMode}
myApp={null}
myWebsite={null}
/>
</Box>
)}
{tabs.map((tab) => {
if (!iframeRefs.current[tab.tabId]) {
iframeRefs.current[tab.tabId] = React.createRef();
}
return (
<AppViewerContainer
key={tab?.tabId}
key={tab?.tabId}
hide={isNewTabWindow}
isSelected={tab?.tabId === selectedTab?.tabId}
app={tab}
@@ -338,18 +375,25 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
);
})}
{isNewTabWindow && mode === "viewer" && (
{isNewTabWindow && mode === 'viewer' && (
<>
<Box sx={{
display: 'flex',
width: '100%',
flexDirection: 'column',
height: '100vh',
overflow: 'auto'
}}>
<Spacer height="30px" />
<AppsDevModeHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={null} myWebsite={null} />
<Box
sx={{
display: 'flex',
width: '100%',
flexDirection: 'column',
height: '100vh',
overflow: 'auto',
}}
>
<Spacer height="30px" />
<AppsDevModeHome
myName={myName}
availableQapps={availableQapps}
setMode={setMode}
myApp={null}
myWebsite={null}
/>
</Box>
</>
)}

View File

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

View File

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

View File

@@ -1,22 +1,21 @@
import React from "react";
import { TabParent } from "./Apps-styles";
import NavCloseTab from "../../assets/svgs/NavCloseTab.svg";
import { getBaseApiReact } from "../../App";
import { Avatar, ButtonBase } from "@mui/material";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { executeEvent } from "../../utils/events";
import { TabParent } from './Apps-styles';
import { NavCloseTab } from '../../assets/Icons/NavCloseTab.tsx';
import { getBaseApiReact } from '../../App';
import { Avatar, ButtonBase } from '@mui/material';
import LogoSelected from '../../assets/svgs/LogoSelected.svg';
import { executeEvent } from '../../utils/events';
export const AppsDevModeTabComponent = ({ isSelected, app }) => {
return (
<ButtonBase
onClick={() => {
if (isSelected) {
executeEvent("removeTabDevMode", {
executeEvent('removeTabDevMode', {
data: app,
});
return;
}
executeEvent("setSelectedTabDevMode", {
executeEvent('setSelectedTabDevMode', {
data: app,
isDevMode: true,
});
@@ -24,39 +23,40 @@ export const AppsDevModeTabComponent = ({ isSelected, app }) => {
>
<TabParent
sx={{
border: isSelected && "1px solid #FFFFFF",
border: isSelected && '1px solid #FFFFFF',
}}
>
{isSelected && (
<img
<NavCloseTab
style={{
position: "absolute",
top: "-5px",
right: "-5px",
position: 'absolute',
top: '-5px',
right: '-5px',
zIndex: 1,
}}
src={NavCloseTab}
/>
)}
<Avatar
sx={{
height: "28px",
width: "28px",
height: '28px',
width: '28px',
}}
alt=""
src={``}
>
<img
style={{
width: "28px",
height: "auto",
width: '28px',
height: 'auto',
}}
src={ app?.customIcon ? app?.customIcon :
app?.service
? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`
: LogoSelected
src={
app?.customIcon
? app?.customIcon
: app?.service
? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`
: LogoSelected
}
alt="center-icon"
/>

View File

@@ -1,57 +0,0 @@
import React, { useMemo, useState } from "react";
import {
AppCircle,
AppCircleContainer,
AppCircleLabel,
AppLibrarySubTitle,
AppsContainer,
AppsParent,
} from "./Apps-styles";
import { Avatar, ButtonBase } from "@mui/material";
import { Add } from "@mui/icons-material";
import { getBaseApiReact, isMobile } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { executeEvent } from "../../utils/events";
import { SortablePinnedApps } from "./SortablePinnedApps";
import { Spacer } from "../../common/Spacer";
export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps }) => {
return (
<>
<AppsContainer
sx={{
justifyContent: "flex-start",
}}
>
<AppLibrarySubTitle
>
Apps Dashboard
</AppLibrarySubTitle>
</AppsContainer>
<Spacer height="20px" />
<AppsContainer>
<ButtonBase
onClick={() => {
setMode("library");
}}
>
<AppCircleContainer sx={{
gap: !isMobile ? "10px" : "5px",
}}>
<AppCircle>
<Add>+</Add>
</AppCircle>
<AppCircleLabel>Library</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
<SortablePinnedApps availableQapps={availableQapps} myWebsite={myWebsite} myApp={myApp} />
</AppsContainer>
</>
);
};

View File

@@ -1,143 +1,154 @@
import React, { useMemo, useState } from "react";
import { useState } from 'react';
import {
AppCircle,
AppCircleContainer,
AppCircleLabel,
AppLibrarySubTitle,
AppsContainer,
AppsParent,
} from "./Apps-styles";
import { Avatar, Box, ButtonBase, Input } from "@mui/material";
import { Add } from "@mui/icons-material";
import { getBaseApiReact, isMobile } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { executeEvent } from "../../utils/events";
import { Spacer } from "../../common/Spacer";
import { SortablePinnedApps } from "./SortablePinnedApps";
import { extractComponents } from "../Chat/MessageDisplay";
} from './Apps-styles';
import { Box, ButtonBase, Input, useTheme } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import { executeEvent } from '../../utils/events';
import { Spacer } from '../../common/Spacer';
import { SortablePinnedApps } from './SortablePinnedApps';
import { extractComponents } from '../Chat/MessageDisplay';
import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
import { AppsPrivate } from "./AppsPrivate";
import { AppsPrivate } from './AppsPrivate';
import ThemeSelector from '../Theme/ThemeSelector';
import LanguageSelector from '../Language/LanguageSelector';
export const AppsHomeDesktop = ({
setMode,
myApp,
myWebsite,
availableQapps,
myName
myName,
}) => {
const [qortalUrl, setQortalUrl] = useState('')
const [qortalUrl, setQortalUrl] = useState('');
const theme = useTheme();
const openQortalUrl = ()=> {
const openQortalUrl = () => {
try {
if(!qortalUrl) return
if (!qortalUrl) return;
const res = extractComponents(qortalUrl);
if (res) {
const { service, name, identifier, path } = res;
executeEvent("addTab", { data: { service, name, identifier, path } });
executeEvent("open-apps-mode", { });
setQortalUrl('qortal://')
executeEvent('addTab', { data: { service, name, identifier, path } });
executeEvent('open-apps-mode', {});
setQortalUrl('qortal://');
}
} catch (error) {
console.log(error);
}
}
};
return (
<>
<AppsContainer
sx={{
justifyContent: "flex-start",
}}
>
<AppLibrarySubTitle
sx={{
fontSize: "30px",
}}
>
Apps Dashboard
</AppLibrarySubTitle>
</AppsContainer>
<Spacer height="20px" />
<AppsContainer
sx={{
justifyContent: "flex-start",
justifyContent: 'flex-start',
}}
>
<Box sx={{
display: 'flex',
gap: '20px',
alignItems: 'center',
backgroundColor: '#1f2023',
padding: '7px',
borderRadius: '20px',
width: '100%',
maxWidth: '500px'
}}>
<Input
id="standard-adornment-name"
value={qortalUrl}
onChange={(e) => {
setQortalUrl(e.target.value)
}}
disableUnderline
autoComplete='off'
autoCorrect='off'
placeholder="qortal://"
<AppLibrarySubTitle
sx={{
fontSize: '30px',
}}
>
Apps Dashboard
</AppLibrarySubTitle>
</AppsContainer>
<Spacer height="20px" />
<AppsContainer
sx={{
justifyContent: 'flex-start',
}}
>
<Box
sx={{
display: 'flex',
gap: '20px',
alignItems: 'center',
backgroundColor: theme.palette.background.paper,
padding: '7px',
borderRadius: '20px',
width: '100%',
maxWidth: '500px',
}}
>
<Input
id="standard-adornment-name"
value={qortalUrl}
onChange={(e) => {
setQortalUrl(e.target.value);
}}
disableUnderline
autoComplete="off"
autoCorrect="off"
placeholder="qortal://"
sx={{
width: '100%',
color: theme.palette.text.primary,
'& .MuiInput-input::placeholder': {
color: theme.palette.text.secondary,
fontSize: '20px',
fontStyle: 'normal',
fontWeight: 400,
lineHeight: '120%', // 24px
letterSpacing: '0.15px',
opacity: 1,
},
'&:focus': {
outline: 'none',
},
// Add any additional styles for the input here
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && qortalUrl) {
openQortalUrl();
}
}}
/>
<ButtonBase onClick={() => openQortalUrl()}>
<ArrowOutwardIcon
sx={{
width: '100%',
color: 'white',
'& .MuiInput-input::placeholder': {
color: 'rgba(84, 84, 84, 0.70) !important',
fontSize: '20px',
fontStyle: 'normal',
fontWeight: 400,
lineHeight: '120%', // 24px
letterSpacing: '0.15px',
opacity: 1
},
'&:focus': {
outline: 'none',
},
// Add any additional styles for the input here
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && qortalUrl) {
openQortalUrl();
}
color: qortalUrl
? theme.palette.text.primary
: theme.palette.text.secondary,
}}
/>
<ButtonBase onClick={()=> openQortalUrl()}>
<ArrowOutwardIcon sx={{
color: qortalUrl ? 'white' : 'rgba(84, 84, 84, 0.70)'
}} />
</ButtonBase>
</Box>
</AppsContainer>
</ButtonBase>
</Box>
</AppsContainer>
<Spacer height="45px" />
<AppsContainer
sx={{
gap: "50px",
justifyContent: "flex-start",
gap: '50px',
justifyContent: 'flex-start',
}}
>
<ButtonBase
onClick={() => {
setMode("library");
setMode('library');
}}
>
<AppCircleContainer
sx={{
gap: !isMobile ? "10px" : "5px",
gap: '10px',
}}
>
<AppCircle>
<Add>+</Add>
<AddIcon />
</AppCircle>
<AppCircleLabel>Library</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
<AppsPrivate myName={myName} />
<AppsPrivate myName={myName} />
<SortablePinnedApps
isDesktop={true}
availableQapps={availableQapps}
@@ -145,6 +156,9 @@ export const AppsHomeDesktop = ({
myApp={myApp}
/>
</AppsContainer>
<LanguageSelector />
<ThemeSelector />
</>
);
};

View File

@@ -1,327 +0,0 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import {
AppCircle,
AppCircleContainer,
AppCircleLabel,
AppLibrarySubTitle,
AppsContainer,
AppsLibraryContainer,
AppsParent,
AppsSearchContainer,
AppsSearchLeft,
AppsSearchRight,
AppsWidthLimiter,
PublishQAppCTAButton,
PublishQAppCTALeft,
PublishQAppCTAParent,
PublishQAppCTARight,
PublishQAppDotsBG,
} from "./Apps-styles";
import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material";
import { Add } from "@mui/icons-material";
import { MyContext, getBaseApiReact } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import IconSearch from "../../assets/svgs/Search.svg";
import IconClearInput from "../../assets/svgs/ClearInput.svg";
import qappDevelopText from "../../assets/svgs/qappDevelopText.svg";
import qappDots from "../../assets/svgs/qappDots.svg";
import ReturnSVG from '../../assets/svgs/Return.svg'
import { Spacer } from "../../common/Spacer";
import { AppInfoSnippet } from "./AppInfoSnippet";
import { Virtuoso } from "react-virtuoso";
import { executeEvent } from "../../utils/events";
import { ComposeP, MailIconImg, ShowMessageReturnButton } from "../Group/Forum/Mail-styles";
const officialAppList = [
'q-tube',
'q-blog',
'q-share',
'q-support',
'q-mail',
'q-fund',
'q-shop',
'q-trade',
'q-support',
'q-manager',
'q-wallets',
'q-search',
"q-nodecontrol"
];
const ScrollerStyled = styled('div')({
// Hide scrollbar for WebKit browsers (Chrome, Safari)
"::-webkit-scrollbar": {
width: "0px",
height: "0px",
},
// Hide scrollbar for Firefox
scrollbarWidth: "none",
// Hide scrollbar for IE and older Edge
"-ms-overflow-style": "none",
});
const StyledVirtuosoContainer = styled('div')({
position: 'relative',
width: '100%',
display: 'flex',
flexDirection: 'column',
// Hide scrollbar for WebKit browsers (Chrome, Safari)
"::-webkit-scrollbar": {
width: "0px",
height: "0px",
},
// Hide scrollbar for Firefox
scrollbarWidth: "none",
// Hide scrollbar for IE and older Edge
"-ms-overflow-style": "none",
});
export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, isShow, categories={categories} }) => {
const [searchValue, setSearchValue] = useState("");
const virtuosoRef = useRef();
const { rootHeight } = useContext(MyContext);
const officialApps = useMemo(() => {
return availableQapps.filter(
(app) =>
app.service === "APP" &&
officialAppList.includes(app?.name?.toLowerCase())
);
}, [availableQapps]);
const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value
// Debounce logic
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(searchValue);
}, 350);
// Cleanup timeout if searchValue changes before the timeout completes
return () => {
clearTimeout(handler);
};
}, [searchValue]); // Runs effect when searchValue changes
// Example: Perform search or other actions based on debouncedValue
const searchedList = useMemo(() => {
if (!debouncedValue) return [];
return availableQapps.filter((app) =>
app.name.toLowerCase().includes(debouncedValue.toLowerCase())
);
}, [debouncedValue]);
const rowRenderer = (index) => {
let app = searchedList[index];
return <AppInfoSnippet key={`${app?.service}-${app?.name}`} app={app} myName={myName} />;
};
return (
<AppsLibraryContainer sx={{
display: !isShow && 'none'
}}>
<AppsWidthLimiter>
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "center",
}}
>
<AppsSearchContainer>
<AppsSearchLeft>
<img src={IconSearch} />
<InputBase
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
sx={{ ml: 1, flex: 1 }}
placeholder="Search for apps"
inputProps={{
"aria-label": "Search for apps",
fontSize: "16px",
fontWeight: 400,
}}
/>
</AppsSearchLeft>
<AppsSearchRight>
{searchValue && (
<ButtonBase
onClick={() => {
setSearchValue("");
}}
>
<img src={IconClearInput} />
</ButtonBase>
)}
</AppsSearchRight>
</AppsSearchContainer>
</Box>
</AppsWidthLimiter>
<Spacer height="25px" />
<ShowMessageReturnButton sx={{
padding: '2px'
}} onClick={() => {
executeEvent("navigateBack", {});
}}>
<MailIconImg src={ReturnSVG} />
<ComposeP>Return to Apps Dashboard</ComposeP>
</ShowMessageReturnButton>
<Spacer height="25px" />
{searchedList?.length > 0 ? (
<AppsWidthLimiter>
<StyledVirtuosoContainer sx={{
height: rootHeight
}}>
<Virtuoso
ref={virtuosoRef}
data={searchedList}
itemContent={rowRenderer}
atBottomThreshold={50}
followOutput="smooth"
components={{
Scroller: ScrollerStyled // Use the styled scroller component
}}
/>
</StyledVirtuosoContainer>
</AppsWidthLimiter>
) : (
<>
<AppsWidthLimiter>
<AppLibrarySubTitle>Official Apps</AppLibrarySubTitle>
<Spacer height="18px" />
<AppsContainer>
{officialApps?.map((qapp) => {
return (
<ButtonBase
sx={{
height: "80px",
width: "60px",
}}
onClick={()=> {
// executeEvent("addTab", {
// data: qapp
// })
executeEvent("selectedAppInfo", {
data: qapp,
});
}}
>
<AppCircleContainer>
<AppCircle
sx={{
border: "none",
}}
>
<Avatar
sx={{
height: "31px",
width: "31px",
}}
alt={qapp?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
qapp?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: "31px",
height: "auto",
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
<AppCircleLabel>
{qapp?.metadata?.title || qapp?.name}
</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
);
})}
</AppsContainer>
<Spacer height="30px" />
<AppLibrarySubTitle>{hasPublishApp ? 'Update Apps!' : 'Create Apps!'}</AppLibrarySubTitle>
<Spacer height="18px" />
</AppsWidthLimiter>
<PublishQAppCTAParent>
<PublishQAppCTALeft>
<PublishQAppDotsBG>
<img src={qappDots} />
</PublishQAppDotsBG>
<Spacer width="29px" />
<img src={qappDevelopText} />
</PublishQAppCTALeft>
<PublishQAppCTARight onClick={()=> {
setMode('publish')
}}>
<PublishQAppCTAButton>
{hasPublishApp ? 'Update' : 'Publish'}
</PublishQAppCTAButton>
<Spacer width="20px" />
</PublishQAppCTARight>
</PublishQAppCTAParent>
<AppsWidthLimiter>
<Spacer height="18px" />
<AppLibrarySubTitle>Categories</AppLibrarySubTitle>
<Spacer height="18px" />
<AppsWidthLimiter sx={{
flexDirection: 'row',
overflowX: 'auto',
width: '100%',
gap: '5px',
"::-webkit-scrollbar": {
width: "0px",
height: "0px",
},
// Hide scrollbar for Firefox
scrollbarWidth: "none",
// Hide scrollbar for IE and older Edge
"-ms-overflow-style": "none",
}}>
{categories?.map((category)=> {
return (
<ButtonBase key={category?.id} onClick={()=> {
executeEvent('selectedCategory', {
data: category
})
}}>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
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'
}}>
{category?.name}
</Box>
</ButtonBase>
)
})}
</AppsWidthLimiter>
</AppsWidthLimiter>
</>
)}
</AppsLibraryContainer>
);
};

View File

@@ -1,19 +1,13 @@
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useEffect, useMemo, useRef, useState } from 'react';
import {
AppCircle,
AppCircleContainer,
AppCircleLabel,
AppLibrarySubTitle,
AppsContainer,
AppsDesktopLibraryBody,
AppsDesktopLibraryHeader,
AppsLibraryContainer,
AppsParent,
AppsSearchContainer,
AppsSearchLeft,
AppsSearchRight,
@@ -23,30 +17,31 @@ import {
PublishQAppCTAParent,
PublishQAppCTARight,
PublishQAppDotsBG,
} from "./Apps-styles";
import { Avatar, Box, ButtonBase, InputBase, Typography, styled } from "@mui/material";
import { Add } from "@mui/icons-material";
import { MyContext, getBaseApiReact } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import IconSearch from "../../assets/svgs/Search.svg";
import IconClearInput from "../../assets/svgs/ClearInput.svg";
import qappDevelopText from "../../assets/svgs/qappDevelopText.svg";
import qappLibraryText from "../../assets/svgs/qappLibraryText.svg";
import RefreshIcon from "@mui/icons-material/Refresh";
import qappDots from "../../assets/svgs/qappDots.svg";
import { Spacer } from "../../common/Spacer";
import { AppInfoSnippet } from "./AppInfoSnippet";
import { Virtuoso } from "react-virtuoso";
import { executeEvent } from "../../utils/events";
} from './Apps-styles';
import {
AppsDesktopLibraryBody,
AppsDesktopLibraryHeader,
} from "./AppsDesktop-styles";
import { AppsNavBarDesktop } from "./AppsNavBarDesktop";
import ReturnSVG from '../../assets/svgs/Return.svg'
import { ComposeP, MailIconImg, ShowMessageReturnButton } from "../Group/Forum/Mail-styles";
Avatar,
Box,
ButtonBase,
InputBase,
Typography,
styled,
useTheme,
} from '@mui/material';
import { getBaseApiReact } from '../../App';
import LogoSelected from '../../assets/svgs/LogoSelected.svg';
import SearchIcon from '@mui/icons-material/Search';
import IconClearInput from '../../assets/svgs/ClearInput.svg';
import { QappDevelopText } from '../../assets/Icons/QappDevelopText.tsx';
import { QappLibraryText } from '../../assets/Icons/QappLibraryText.tsx';
import RefreshIcon from '@mui/icons-material/Refresh';
import AppsIcon from '@mui/icons-material/Apps';
import { Spacer } from '../../common/Spacer';
import { AppInfoSnippet } from './AppInfoSnippet';
import { Virtuoso } from 'react-virtuoso';
import { executeEvent } from '../../utils/events';
import { ComposeP, ShowMessageReturnButton } from '../Group/Forum/Mail-styles';
import { ReturnIcon } from '../../assets/Icons/ReturnIcon.tsx';
const officialAppList = [
'q-tube',
'q-blog',
@@ -61,40 +56,40 @@ const officialAppList = [
'q-mintership',
'q-wallets',
'q-search',
"q-nodecontrol"
'q-nodecontrol',
];
const ScrollerStyled = styled("div")({
const ScrollerStyled = styled('div')({
// Hide scrollbar for WebKit browsers (Chrome, Safari)
"::-webkit-scrollbar": {
width: "0px",
height: "0px",
'::-webkit-scrollbar': {
width: '0px',
height: '0px',
},
// Hide scrollbar for Firefox
scrollbarWidth: "none",
scrollbarWidth: 'none',
// Hide scrollbar for IE and older Edge
"-ms-overflow-style": "none",
msOverflowStyle: 'none',
});
const StyledVirtuosoContainer = styled("div")({
position: "relative",
width: "100%",
display: "flex",
flexDirection: "column",
const StyledVirtuosoContainer = styled('div')({
position: 'relative',
width: '100%',
display: 'flex',
flexDirection: 'column',
// Hide scrollbar for WebKit browsers (Chrome, Safari)
"::-webkit-scrollbar": {
width: "0px",
height: "0px",
'::-webkit-scrollbar': {
width: '0px',
height: '0px',
},
// Hide scrollbar for Firefox
scrollbarWidth: "none",
scrollbarWidth: 'none',
// Hide scrollbar for IE and older Edge
"-ms-overflow-style": "none",
msOverflowStyle: 'none',
});
export const AppsLibraryDesktop = ({
@@ -104,20 +99,21 @@ export const AppsLibraryDesktop = ({
hasPublishApp,
isShow,
categories,
getQapps
getQapps,
}) => {
const [searchValue, setSearchValue] = useState("");
const [searchValue, setSearchValue] = useState('');
const virtuosoRef = useRef();
const theme = useTheme();
const officialApps = useMemo(() => {
return availableQapps.filter(
(app) =>
app.service === "APP" &&
app.service === 'APP' &&
officialAppList.includes(app?.name?.toLowerCase())
);
}, [availableQapps]);
const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value
const [debouncedValue, setDebouncedValue] = useState(''); // Debounced value
// Debounce logic
useEffect(() => {
@@ -125,9 +121,9 @@ export const AppsLibraryDesktop = ({
setDebouncedValue(searchValue);
}, 350);
setTimeout(() => {
virtuosoRef.current.scrollToIndex({
index: 0
});
if (virtuosoRef.current) {
virtuosoRef.current.scrollToIndex({ index: 0 });
}
}, 500);
// Cleanup timeout if searchValue changes before the timeout completes
return () => {
@@ -139,8 +135,13 @@ export const AppsLibraryDesktop = ({
const searchedList = useMemo(() => {
if (!debouncedValue) return [];
return availableQapps.filter((app) =>
app.name.toLowerCase().includes(debouncedValue.toLowerCase()) || (app?.metadata?.title && app?.metadata?.title?.toLowerCase().includes(debouncedValue.toLowerCase()))
return availableQapps.filter(
(app) =>
app.name.toLowerCase().includes(debouncedValue.toLowerCase()) ||
(app?.metadata?.title &&
app?.metadata?.title
?.toLowerCase()
.includes(debouncedValue.toLowerCase()))
);
}, [debouncedValue]);
@@ -152,7 +153,7 @@ export const AppsLibraryDesktop = ({
app={app}
myName={myName}
parentStyles={{
padding: '0px 10px'
padding: '0px 10px',
}}
/>
);
@@ -161,111 +162,125 @@ export const AppsLibraryDesktop = ({
return (
<AppsLibraryContainer
sx={{
display: !isShow && "none",
padding: "0px",
height: "100vh",
overflow: "hidden",
paddingTop: '30px'
display: !isShow && 'none',
padding: '0px',
height: '100vh',
overflow: 'hidden',
paddingTop: '30px',
}}
>
<AppsDesktopLibraryHeader
sx={{
maxWidth: "1500px",
width: "90%",
maxWidth: '1500px',
width: '90%',
}}
>
<AppsWidthLimiter>
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "space-between",
alignItems: 'center',
display: 'flex',
justifyContent: 'space-between',
width: '100%',
}}
>
<img src={qappLibraryText} />
<Box sx={{
display: 'flex',
gap: '20px',
alignItems: 'center'
}}>
<AppsSearchContainer
<QappLibraryText />
<Box
sx={{
width: "412px",
alignItems: 'center',
display: 'flex',
gap: '20px',
}}
>
<AppsSearchLeft>
<img src={IconSearch} />
<InputBase
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
sx={{ ml: 1, flex: 1 }}
placeholder="Search for apps"
inputProps={{
"aria-label": "Search for apps",
fontSize: "16px",
fontWeight: 400,
<AppsSearchContainer
sx={{
width: '412px',
}}
>
<AppsSearchLeft>
<SearchIcon />
<InputBase
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
sx={{
background: theme.palette.background.paper,
borderRadius: '6px',
flex: 1,
ml: 1,
paddingLeft: '12px',
}}
placeholder="Search for apps"
inputProps={{
'aria-label': 'Search for apps',
fontSize: '16px',
fontWeight: 400,
}}
/>
</AppsSearchLeft>
<AppsSearchRight>
{searchValue && (
<ButtonBase
onClick={() => {
setSearchValue('');
}}
>
<img src={IconClearInput} />
</ButtonBase>
)}
</AppsSearchRight>
</AppsSearchContainer>
<ButtonBase
onClick={(e) => {
getQapps();
}}
>
<RefreshIcon
sx={{
width: '40px',
height: 'auto',
}}
/>
</AppsSearchLeft>
<AppsSearchRight>
{searchValue && (
<ButtonBase
onClick={() => {
setSearchValue("");
}}
>
<img src={IconClearInput} />
</ButtonBase>
)}
</AppsSearchRight>
</AppsSearchContainer>
<ButtonBase
onClick={(e) => {
getQapps()
}}
>
<RefreshIcon
sx={{
color: "rgba(250, 250, 250, 0.5)",
width: '40px',
height: 'auto'
}}
/>
</ButtonBase>
</ButtonBase>
</Box>
</Box>
</AppsWidthLimiter>
</AppsDesktopLibraryHeader>
<AppsDesktopLibraryBody
sx={{
alignItems: 'center',
height: `calc(100vh - 36px)`,
overflow: "auto",
padding: "0px",
alignItems: "center",
overflow: 'auto',
padding: '0px',
}}
>
<AppsDesktopLibraryBody
sx={{
height: `calc(100vh - 36px)`,
flexGrow: "unset",
maxWidth: "1500px",
width: "90%",
flexGrow: 'unset',
maxWidth: '1500px',
width: '90%',
}}
>
<Spacer height="70px" />
<ShowMessageReturnButton sx={{
padding: '2px'
}} onClick={() => {
executeEvent("navigateBack", {});
}}>
<MailIconImg src={ReturnSVG} />
<ComposeP>Return to Apps Dashboard</ComposeP>
</ShowMessageReturnButton>
<Spacer height="20px" />
<ShowMessageReturnButton
sx={{
padding: '2px',
}}
onClick={() => {
executeEvent('navigateBack', {});
}}
>
<ReturnIcon />
<ComposeP>Return to Apps Dashboard</ComposeP>
</ShowMessageReturnButton>
<Spacer height="20px" />
{searchedList?.length > 0 ? (
<AppsWidthLimiter>
<StyledVirtuosoContainer
@@ -293,46 +308,46 @@ export const AppsLibraryDesktop = ({
<>
<AppLibrarySubTitle
sx={{
fontSize: "30px",
fontSize: '30px',
}}
>
Official Apps
</AppLibrarySubTitle>
<Spacer height="45px" />
<AppsContainer sx={{
gap: '50px',
justifyContent: 'flex-start'
}}>
<AppsContainer
sx={{
gap: '15px',
justifyContent: 'center',
}}
>
{officialApps?.map((qapp) => {
return (
<ButtonBase
sx={{
// height: "80px",
width: "80px",
width: '80px',
}}
onClick={() => {
// executeEvent("addTab", {
// data: qapp
// })
executeEvent("selectedAppInfo", {
executeEvent('selectedAppInfo', {
data: qapp,
});
}}
>
<AppCircleContainer
sx={{
gap: "10px",
gap: '10px',
}}
>
<AppCircle
sx={{
border: "none",
border: 'none',
}}
>
<Avatar
sx={{
height: "42px",
width: "42px",
height: '42px',
width: '42px',
}}
alt={qapp?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
@@ -341,14 +356,15 @@ export const AppsLibraryDesktop = ({
>
<img
style={{
width: "31px",
height: "auto",
width: '31px',
height: 'auto',
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
<AppCircleLabel>
{qapp?.metadata?.title || qapp?.name}
</AppCircleLabel>
@@ -357,122 +373,141 @@ export const AppsLibraryDesktop = ({
);
})}
</AppsContainer>
<Spacer height="80px" />
<Box
sx={{
width: "100%",
gap: "250px",
display: "flex",
width: '100%',
gap: '250px',
display: 'flex',
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
display: 'flex',
flexDirection: 'column',
}}
>
<AppLibrarySubTitle
sx={{
fontSize: "30px",
width: "100%",
textAlign: "start",
fontSize: '30px',
width: '100%',
textAlign: 'start',
}}
>
{hasPublishApp ? "Update Apps!" : "Create Apps!"}
{hasPublishApp ? 'Update your app' : 'Publish your app'}
</AppLibrarySubTitle>
<Spacer height="18px" />
<PublishQAppCTAParent
sx={{
gap: "25px",
gap: '25px',
}}
>
<PublishQAppCTALeft>
<PublishQAppDotsBG>
<img src={qappDots} />
<AppsIcon fontSize="large" />
</PublishQAppDotsBG>
<Spacer width="29px" />
<img src={qappDevelopText} />
<QappDevelopText />
</PublishQAppCTALeft>
<PublishQAppCTARight
onClick={() => {
setMode("publish");
setMode('publish');
}}
>
<PublishQAppCTAButton>
{hasPublishApp ? "Update" : "Publish"}
{hasPublishApp ? 'Update' : 'Publish'}
</PublishQAppCTAButton>
<Spacer width="20px" />
</PublishQAppCTARight>
</PublishQAppCTAParent>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
display: 'flex',
flexDirection: 'column',
}}
>
<AppLibrarySubTitle
sx={{
fontSize: "30px",
fontSize: '30px',
}}
>
Categories
</AppLibrarySubTitle>
<Spacer height="18px" />
<Box
sx={{
width: "100%",
display: "flex",
gap: "20px",
flexWrap: "wrap",
display: 'flex',
flexWrap: 'wrap',
gap: '20px',
width: '100%',
}}
>
<ButtonBase
onClick={() => {
executeEvent("selectedCategory", {
data: {
id: 'all',
name: 'All'
},
});
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "60px",
padding: "0px 24px",
border: "4px solid #10242F",
borderRadius: "6px",
boxShadow: "2px 4px 0px 0px #000000",
}}
>
All
</Box>
</ButtonBase>
<ButtonBase
onClick={() => {
executeEvent('selectedCategory', {
data: {
id: 'all',
name: 'All',
},
});
}}
>
<Box
sx={{
alignItems: 'center',
borderColor: theme.palette.background.paper,
borderRadius: '6px',
borderStyle: 'solid',
borderWidth: '4px',
display: 'flex',
height: '50px',
justifyContent: 'center',
padding: '0px 20px',
'&:hover': {
backgroundColor: 'action.hover', // background on hover
},
}}
>
All
</Box>
</ButtonBase>
{categories?.map((category) => {
return (
<ButtonBase
key={category?.id}
onClick={() => {
executeEvent("selectedCategory", {
executeEvent('selectedCategory', {
data: category,
});
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "60px",
padding: "0px 24px",
border: "4px solid #10242F",
borderRadius: "6px",
boxShadow: "2px 4px 0px 0px #000000",
alignItems: 'center',
borderColor: theme.palette.background.paper,
borderRadius: '6px',
borderStyle: 'solid',
borderWidth: '4px',
display: 'flex',
height: '50px',
justifyContent: 'center',
padding: '0px 20px',
'&:hover': {
backgroundColor: 'action.hover', // background on hover
},
}}
>
{category?.name}

View File

@@ -1,353 +0,0 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
AppsNavBarLeft,
AppsNavBarParent,
AppsNavBarRight,
} from "./Apps-styles";
import NavBack from "../../assets/svgs/NavBack.svg";
import NavAdd from "../../assets/svgs/NavAdd.svg";
import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg";
import {
ButtonBase,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Tab,
Tabs,
} from "@mui/material";
import {
executeEvent,
subscribeToEvent,
unsubscribeFromEvent,
} from "../../utils/events";
import TabComponent from "./TabComponent";
import PushPinIcon from "@mui/icons-material/PushPin";
import RefreshIcon from "@mui/icons-material/Refresh";
import { useRecoilState, useSetRecoilState } from "recoil";
import {
navigationControllerAtom,
settingsLocalLastUpdatedAtom,
sortablePinnedAppsAtom,
} from "../../atoms/global";
export function saveToLocalStorage(key, subKey, newValue, otherRootData = {}, deleteWholeKey) {
try {
if(deleteWholeKey){
localStorage.setItem(key, null);
return
}
// Fetch existing data
const existingData = localStorage.getItem(key);
let combinedData = {};
if (existingData) {
// Parse the existing data
const parsedData = JSON.parse(existingData);
// Merge with the new data under the subKey
combinedData = {
...parsedData,
...otherRootData,
timestamp: Date.now(), // Update the root timestamp
[subKey]: newValue, // Assuming the data is an array
};
} else {
// If no existing data, just use the new data under the subKey
combinedData = {
...otherRootData,
timestamp: Date.now(), // Set the initial root timestamp
[subKey]: newValue,
};
}
// Save combined data back to localStorage
const serializedValue = JSON.stringify(combinedData);
localStorage.setItem(key, serializedValue);
} catch (error) {
console.error("Error saving to localStorage:", error);
}
}
export const AppsNavBar = () => {
const [tabs, setTabs] = useState([]);
const [selectedTab, setSelectedTab] = useState(null);
const [isNewTabWindow, setIsNewTabWindow] = useState(false);
const tabsRef = useRef(null);
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(
sortablePinnedAppsAtom
);
const [navigationController, setNavigationController] = useRecoilState(navigationControllerAtom)
const isDisableBackButton = useMemo(()=> {
if(selectedTab && navigationController[selectedTab?.tabId]?.hasBack) return false
if(selectedTab && !navigationController[selectedTab?.tabId]?.hasBack) return true
return false
}, [navigationController, selectedTab])
const setSettingsLocalLastUpdated = useSetRecoilState(
settingsLocalLastUpdatedAtom
);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
useEffect(() => {
// Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added)
if (tabsRef.current) {
const tabElements = tabsRef.current.querySelectorAll(".MuiTab-root");
if (tabElements.length > 0) {
const lastTab = tabElements[tabElements.length - 1];
lastTab.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "end",
});
}
}
}, [tabs.length]); // Dependency on the number of tabs
const setTabsToNav = (e) => {
const { tabs, selectedTab, isNewTabWindow } = e.detail?.data;
setTabs([...tabs]);
setSelectedTab(!selectedTab ? null : { ...selectedTab });
setIsNewTabWindow(isNewTabWindow);
};
useEffect(() => {
subscribeToEvent("setTabsToNav", setTabsToNav);
return () => {
unsubscribeFromEvent("setTabsToNav", setTabsToNav);
};
}, []);
const isSelectedAppPinned = !!sortablePinnedApps?.find(
(item) =>
item?.name === selectedTab?.name && item?.service === selectedTab?.service
);
return (
<AppsNavBarParent>
<AppsNavBarLeft>
<ButtonBase
onClick={() => {
executeEvent("navigateBack", selectedTab?.tabId);
}}
disabled={isDisableBackButton}
sx={{
opacity: !isDisableBackButton ? 1 : 0.1,
cursor: !isDisableBackButton ? 'pointer': 'default'
}}
>
<img src={NavBack} />
</ButtonBase>
<Tabs
ref={tabsRef}
aria-label="basic tabs example"
variant="scrollable" // Make tabs scrollable
scrollButtons={false}
sx={{
"& .MuiTabs-indicator": {
backgroundColor: "white",
},
maxWidth: `calc(100vw - 150px)`, // Ensure the tabs container fits within the available space
overflow: "hidden", // Prevents overflow on small screens
}}
>
{tabs?.map((tab) => (
<Tab
key={tab.tabId}
label={
<TabComponent
isSelected={
tab?.tabId === selectedTab?.tabId && !isNewTabWindow
}
app={tab}
/>
} // Pass custom component
sx={{
"&.Mui-selected": {
color: "white",
},
padding: "0px",
margin: "0px",
minWidth: "0px",
width: "50px",
}}
/>
))}
</Tabs>
</AppsNavBarLeft>
{selectedTab && (
<AppsNavBarRight
sx={{
gap: "10px",
}}
>
<ButtonBase
onClick={() => {
setSelectedTab(null);
executeEvent("newTabWindow", {});
}}
>
<img
style={{
height: "40px",
width: "40px",
}}
src={NavAdd}
/>
</ButtonBase>
<ButtonBase
onClick={(e) => {
if (!selectedTab) return;
handleClick(e);
}}
>
<img
style={{
height: "34px",
width: "34px",
}}
src={NavMoreMenu}
/>
</ButtonBase>
</AppsNavBarRight>
)}
<Menu
id="navbar-more-mobile"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
"aria-labelledby": "basic-button",
}}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
slotProps={{
paper: {
sx: {
backgroundColor: "var(--bg-primary)",
color: "#fff",
width: "148px",
borderRadius: "5px",
},
},
}}
sx={{
marginTop: "10px",
}}
>
<MenuItem
onClick={() => {
if (!selectedTab) return;
setSortablePinnedApps((prev) => {
let updatedApps;
if (isSelectedAppPinned) {
// Remove the selected app if it is pinned
updatedApps = prev.filter(
(item) =>
!(
item?.name === selectedTab?.name &&
item?.service === selectedTab?.service
)
);
} else {
// Add the selected app if it is not pinned
updatedApps = [
...prev,
{
name: selectedTab?.name,
service: selectedTab?.service,
},
];
}
saveToLocalStorage(
"ext_saved_settings",
"sortablePinnedApps",
updatedApps
);
return updatedApps;
});
setSettingsLocalLastUpdated(Date.now());
handleClose();
}}
>
<ListItemIcon
sx={{
minWidth: "24px !important",
marginRight: "5px",
}}
>
<PushPinIcon
height={20}
sx={{
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)",
}}
/>
</ListItemIcon>
<ListItemText
sx={{
"& .MuiTypography-root": {
fontSize: "12px",
fontWeight: 600,
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)",
},
}}
primary={`${isSelectedAppPinned ? "Unpin app" : "Pin app"}`}
/>
</MenuItem>
<MenuItem
onClick={() => {
executeEvent("refreshApp", {
tabId: selectedTab?.tabId,
});
handleClose();
}}
>
<ListItemIcon
sx={{
minWidth: "24px !important",
marginRight: "5px",
}}
>
<RefreshIcon
height={20}
sx={{
color: "rgba(250, 250, 250, 0.5)",
}}
/>
</ListItemIcon>
<ListItemText
sx={{
"& .MuiTypography-root": {
fontSize: "12px",
fontWeight: 600,
color: "rgba(250, 250, 250, 0.5)",
},
}}
primary="Refresh"
/>
</MenuItem>
</Menu>
</AppsNavBarParent>
);
};

View File

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

View File

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

View File

@@ -1,205 +1,249 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useMemo } from 'react';
import { DndContext, closestCenter } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable';
import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
} from '@dnd-kit/sortable';
import {
KeyboardSensor,
PointerSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
import { Avatar, ButtonBase } from '@mui/material';
import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles';
import { getBaseApiReact, MyContext } from '../../App';
import { getBaseApiReact } from '../../App';
import { executeEvent } from '../../utils/events';
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { saveToLocalStorage } from './AppsNavBar';
import {
settingsLocalLastUpdatedAtom,
sortablePinnedAppsAtom,
} from '../../atoms/global';
import { saveToLocalStorage } from './AppsNavBarDesktop';
import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps';
import LockIcon from "@mui/icons-material/Lock";
import LockIcon from '@mui/icons-material/Lock';
import { useHandlePrivateApps } from './useHandlePrivateApps';
import { useAtom, useSetAtom } from 'jotai';
const SortableItem = ({ id, name, app, isDesktop }) => {
const {openApp} = useHandlePrivateApps()
const { openApp } = useHandlePrivateApps();
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
padding: '10px',
border: '1px solid #ccc',
marginBottom: '5px',
borderRadius: '4px',
backgroundColor: '#f9f9f9',
cursor: 'grab',
color: 'black'
};
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
return (
<ContextMenuPinnedApps app={app} isMine={!!app?.isMine}>
<ButtonBase
ref={setNodeRef} {...attributes} {...listeners}
sx={{
width: "80px",
transform: CSS.Transform.toString(transform),
transition,
}}
onClick={async ()=> {
if(app?.isPrivate){
try {
await openApp(app?.privateAppProperties)
} catch (error) {
console.error(error)
}
} else {
executeEvent("addTab", {
data: app
})
}
}}
>
<AppCircleContainer sx={{
border: "none",
gap: isDesktop ? '10px': '5px'
}}>
<AppCircle
sx={{
border: "none",
}}
>
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
<LockIcon
sx={{
height: "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>
);
};
const style = {
backgroundColor: '#f9f9f9',
border: '1px solid #ccc',
borderRadius: '4px',
color: 'black',
cursor: 'grab',
marginBottom: '5px',
padding: '10px',
transform: CSS.Transform.toString(transform),
transition,
};
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 };
return (
<ContextMenuPinnedApps app={app} isMine={!!app?.isMine}>
<ButtonBase
ref={setNodeRef}
{...attributes}
{...listeners}
sx={{
width: '80px',
transform: CSS.Transform.toString(transform),
transition,
}}
onClick={async () => {
if (app?.isPrivate) {
try {
await openApp(app?.privateAppProperties);
} catch (error) {
console.error(error);
}
} else {
// If not in the list, add it with `isMine: true` at the beginning
pinnedList.unshift({ ...appToCheck, isMine: true });
executeEvent('addTab', {
data: app,
});
}
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>
);
}}
>
<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 = [],
}) => {
const [pinnedApps, setPinnedApps] = useAtom(sortablePinnedAppsAtom);
const setSettingsLocalLastUpdated = useSetAtom(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,71 +1,80 @@
import React from 'react'
import { TabParent } from './Apps-styles'
import NavCloseTab from "../../assets/svgs/NavCloseTab.svg";
import { TabParent } from './Apps-styles';
import { NavCloseTab } from '../../assets/Icons/NavCloseTab.tsx';
import { getBaseApiReact } from '../../App';
import { Avatar, ButtonBase } from '@mui/material';
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { Avatar, ButtonBase, useTheme } from '@mui/material';
import LogoSelected from '../../assets/svgs/LogoSelected.svg';
import { executeEvent } from '../../utils/events';
import LockIcon from "@mui/icons-material/Lock";
import LockIcon from '@mui/icons-material/Lock';
const TabComponent = ({ isSelected, app }) => {
const theme = useTheme();
const TabComponent = ({isSelected, app}) => {
return (
<ButtonBase onClick={()=> {
if(isSelected){
executeEvent('removeTab', {
data: app
})
return
<ButtonBase
onClick={() => {
if (isSelected) {
executeEvent('removeTab', {
data: app,
});
return;
}
executeEvent('setSelectedTab', {
data: app
})
}}>
<TabParent sx={{
border: isSelected && '1px solid #FFFFFF'
}}>
data: app,
});
}}
>
<TabParent
sx={{
borderStyle: isSelected && 'solid',
borderWidth: isSelected && '1px',
borderColor: isSelected && theme.palette.text.primary,
}}
>
{isSelected && (
<img style={
{
position: 'absolute',
top: '-5px',
right: '-5px',
zIndex: 1
}
} src={NavCloseTab}/>
) }
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
<NavCloseTab
style={{
position: 'absolute',
top: '-5px',
right: '-5px',
zIndex: 1,
}}
/>
)}
{app?.isPrivate && !app?.privateAppProperties?.logo ? (
<LockIcon
sx={{
height: "28px",
width: "28px",
height: '28px',
width: '28px',
}}
/>
) : (
<Avatar
sx={{
height: "28px",
width: "28px",
height: '28px',
width: '28px',
}}
alt={app?.name}
src={app?.privateAppProperties?.logo ? app?.privateAppProperties?.logo :`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
src={
app?.privateAppProperties?.logo
? app?.privateAppProperties?.logo
: `${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`
}
>
<img
style={{
width: "28px",
height: "auto",
width: '28px',
height: 'auto',
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
)}
</TabParent>
</TabParent>
</ButtonBase>
)
}
);
};
export default TabComponent
export default TabComponent;

View File

@@ -1,49 +1,44 @@
import React, { useContext, useState } from "react";
import { executeEvent } from "../../utils/events";
import { getBaseApiReact, MyContext } from "../../App";
import { createEndpoint } from "../../background";
import { useRecoilState, useSetRecoilState } from "recoil";
import React, { useContext, useState } from 'react';
import { executeEvent } from '../../utils/events';
import { getBaseApiReact, MyContext } from '../../App';
import { createEndpoint } from '../../background';
import {
settingsLocalLastUpdatedAtom,
sortablePinnedAppsAtom,
} from "../../atoms/global";
import { saveToLocalStorage } from "./AppsNavBarDesktop";
import { base64ToBlobUrl } from "../../utils/fileReading";
import { base64ToUint8Array } from "../../qdn/encryption/group-encryption";
import { uint8ArrayToObject } from "../../backgroundFunctions/encryption";
} from '../../atoms/global';
import { saveToLocalStorage } from './AppsNavBarDesktop';
import { base64ToBlobUrl } from '../../utils/fileReading';
import { base64ToUint8Array } from '../../qdn/encryption/group-encryption';
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
import { useAtom, useSetAtom } from 'jotai';
export const useHandlePrivateApps = () => {
const [status, setStatus] = useState("");
const [status, setStatus] = useState('');
const {
openSnackGlobal,
setOpenSnackGlobal,
infoSnackCustom,
setInfoSnackCustom,
} = useContext(MyContext);
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(
sortablePinnedAppsAtom
);
const setSettingsLocalLastUpdated = useSetRecoilState(
settingsLocalLastUpdatedAtom
);
const setSortablePinnedApps = useSetAtom(sortablePinnedAppsAtom);
const setSettingsLocalLastUpdated = useSetAtom(settingsLocalLastUpdatedAtom);
const openApp = async (
privateAppProperties,
addToPinnedApps,
setLoadingStatePrivateApp
) => {
try {
if(setLoadingStatePrivateApp){
if (setLoadingStatePrivateApp) {
setLoadingStatePrivateApp(`Downloading and decrypting private app.`);
}
setOpenSnackGlobal(true);
setInfoSnackCustom({
type: "info",
message: "Fetching app data",
duration: null
type: 'info',
message: 'Fetching app data',
duration: null,
});
const urlData = `${getBaseApiReact()}/arbitrary/${
privateAppProperties?.service
@@ -53,32 +48,30 @@ export const useHandlePrivateApps = () => {
let data;
try {
const responseData = await fetch(urlData, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
if(!responseData?.ok){
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp("Error! Unable to download private app.");
if (!responseData?.ok) {
if (setLoadingStatePrivateApp) {
setLoadingStatePrivateApp('Error! Unable to download private app.');
}
throw new Error("Unable to fetch app");
}
throw new Error('Unable to fetch app');
}
data = await responseData.text();
if (data?.error) {
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp("Error! Unable to download private app.");
if (setLoadingStatePrivateApp) {
setLoadingStatePrivateApp('Error! Unable to download private app.');
}
throw new Error("Unable to fetch app");
throw new Error('Unable to fetch app');
}
} catch (error) {
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp("Error! Unable to download private app.");
if (setLoadingStatePrivateApp) {
setLoadingStatePrivateApp('Error! Unable to download private app.');
}
throw error;
}
@@ -87,7 +80,7 @@ export const useHandlePrivateApps = () => {
// eslint-disable-next-line no-useless-catch
try {
decryptedData = await window.sendMessage(
"DECRYPT_QORTAL_GROUP_DATA",
'DECRYPT_QORTAL_GROUP_DATA',
{
base64: data,
@@ -95,16 +88,14 @@ export const useHandlePrivateApps = () => {
}
);
if (decryptedData?.error) {
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp("Error! Unable to decrypt private app.");
if (setLoadingStatePrivateApp) {
setLoadingStatePrivateApp('Error! Unable to decrypt private app.');
}
throw new Error(decryptedData?.error);
}
} catch (error) {
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp("Error! Unable to decrypt private app.");
if (setLoadingStatePrivateApp) {
setLoadingStatePrivateApp('Error! Unable to decrypt private app.');
}
throw error;
}
@@ -112,19 +103,19 @@ export const useHandlePrivateApps = () => {
try {
const convertToUint = base64ToUint8Array(decryptedData);
const UintToObject = uint8ArrayToObject(convertToUint);
if (decryptedData) {
setInfoSnackCustom({
type: "info",
message: "Building app",
type: 'info',
message: 'Building app',
});
const endpoint = await createEndpoint(
`/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true`
);
const response = await fetch(endpoint, {
method: "POST",
method: 'POST',
headers: {
"Content-Type": "text/plain",
'Content-Type': 'text/plain',
},
body: UintToObject?.app,
});
@@ -135,7 +126,7 @@ export const useHandlePrivateApps = () => {
);
const res = await fetch(checkIfPreviewLinkStillWorksUrl);
if (res.ok) {
executeEvent("refreshApp", {
executeEvent('refreshApp', {
tabId: tabId,
});
} else {
@@ -143,51 +134,50 @@ export const useHandlePrivateApps = () => {
`/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true`
);
const response = await fetch(endpoint, {
method: "POST",
method: 'POST',
headers: {
"Content-Type": "text/plain",
'Content-Type': 'text/plain',
},
body: UintToObject?.app,
});
const previewPath = await response.text();
executeEvent("updateAppUrl", {
executeEvent('updateAppUrl', {
tabId: tabId,
url: await createEndpoint(previewPath),
});
setTimeout(() => {
executeEvent("refreshApp", {
executeEvent('refreshApp', {
tabId: tabId,
});
}, 300);
}
};
const appName = UintToObject?.name;
const logo = UintToObject?.logo
? `data:image/png;base64,${UintToObject?.logo}`
: null;
const dataBody = {
url: await createEndpoint(previewPath),
isPreview: true,
isPrivate: true,
privateAppProperties: { ...privateAppProperties, logo, appName },
filePath: "",
filePath: '',
refreshFunc: (tabId) => {
refreshfunc(tabId, privateAppProperties);
},
};
executeEvent("addTab", {
executeEvent('addTab', {
data: dataBody,
});
setInfoSnackCustom({
type: "success",
message: "Opened",
type: 'success',
message: 'Opened',
});
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp(``);
if (setLoadingStatePrivateApp) {
setLoadingStatePrivateApp(``);
}
if (addToPinnedApps) {
setSortablePinnedApps((prev) => {
@@ -203,10 +193,10 @@ export const useHandlePrivateApps = () => {
},
},
];
saveToLocalStorage(
"ext_saved_settings",
"sortablePinnedApps",
'ext_saved_settings',
'sortablePinnedApps',
updatedApps
);
return updatedApps;
@@ -215,20 +205,19 @@ export const useHandlePrivateApps = () => {
}
}
} catch (error) {
if(setLoadingStatePrivateApp){
setLoadingStatePrivateApp(`Error! ${error?.message || 'Unable to build private app.'}`);
if (setLoadingStatePrivateApp) {
setLoadingStatePrivateApp(
`Error! ${error?.message || 'Unable to build private app.'}`
);
}
throw error
throw error;
}
} catch (error) {
setInfoSnackCustom({
type: 'error',
message: error?.message || 'Unable to fetch app',
});
}
catch (error) {
setInfoSnackCustom({
type: "error",
message: error?.message || "Unable to fetch app",
});
}
};
return {
openApp,

View File

@@ -1,15 +1,12 @@
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { executeEvent } from '../../utils/events';
import { useSetRecoilState } from 'recoil';
import { navigationControllerAtom } from '../../atoms/global';
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
import { saveFile } from '../../qortalRequests/get';
import { mimeToExtensionMap } from '../../utils/memeTypes';
import { MyContext } from '../../App';
import FileSaver from 'file-saver';
import { useSetAtom } from 'jotai';
export const saveFileInChunks = async (
blob: Blob,
@@ -45,7 +42,6 @@ export const saveFileInChunks = async (
// Map MIME type to file extension
const mimeTypeToExtension = (mimeType: string): string => {
return mimeToExtensionMap[mimeType] || existingExtension || ''; // Use existing extension if MIME type not found
};
@@ -76,21 +72,18 @@ export const saveFileInChunks = async (
offset += chunkSize;
isFirstChunk = false;
}
} catch (error) {
console.error('Error saving file in chunks:', error);
}
};
// Helper function to convert a Blob to a Base64 string
const blobToBase64 = (blob: Blob): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const base64data = reader.result?.toString().split(",")[1];
resolve(base64data || "");
const base64data = reader.result?.toString().split(',')[1];
resolve(base64data || '');
};
reader.onerror = reject;
reader.readAsDataURL(blob);
@@ -98,81 +91,81 @@ const blobToBase64 = (blob: Blob): Promise<string> => {
};
class Semaphore {
constructor(count) {
this.count = count
this.waiting = []
}
acquire() {
return new Promise(resolve => {
if (this.count > 0) {
this.count--
resolve()
} else {
this.waiting.push(resolve)
}
})
}
release() {
if (this.waiting.length > 0) {
const resolve = this.waiting.shift()
resolve()
} else {
this.count++
}
}
}
let semaphore = new Semaphore(1)
let reader = new FileReader()
const fileToBase64 = (file) => new Promise(async (resolve, reject) => {
if (!reader) {
reader = new FileReader()
}
await semaphore.acquire()
reader.readAsDataURL(file)
reader.onload = () => {
const dataUrl = reader.result
if (typeof dataUrl === "string") {
const base64String = dataUrl.split(',')[1]
reader.onload = null
reader.onerror = null
resolve(base64String)
} else {
reader.onload = null
reader.onerror = null
reject(new Error('Invalid data URL'))
}
semaphore.release()
}
reader.onerror = (error) => {
reader.onload = null
reader.onerror = null
reject(error)
semaphore.release()
}
})
export function openIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open("fileStorageDB", 1);
request.onupgradeneeded = function (event) {
const db = event.target.result;
if (!db.objectStoreNames.contains("files")) {
db.createObjectStore("files", { keyPath: "id" });
}
};
request.onsuccess = function (event) {
resolve(event.target.result);
};
request.onerror = function () {
reject("Error opening IndexedDB");
};
constructor(count) {
this.count = count;
this.waiting = [];
}
acquire() {
return new Promise((resolve) => {
if (this.count > 0) {
this.count--;
resolve();
} else {
this.waiting.push(resolve);
}
});
}
release() {
if (this.waiting.length > 0) {
const resolve = this.waiting.shift();
resolve();
} else {
this.count++;
}
}
}
let semaphore = new Semaphore(1);
let reader = new FileReader();
const fileToBase64 = (file) =>
new Promise(async (resolve, reject) => {
if (!reader) {
reader = new FileReader();
}
await semaphore.acquire();
reader.readAsDataURL(file);
reader.onload = () => {
const dataUrl = reader.result;
if (typeof dataUrl === 'string') {
const base64String = dataUrl.split(',')[1];
reader.onload = null;
reader.onerror = null;
resolve(base64String);
} else {
reader.onload = null;
reader.onerror = null;
reject(new Error('Invalid data URL'));
}
semaphore.release();
};
reader.onerror = (error) => {
reader.onload = null;
reader.onerror = null;
reject(error);
semaphore.release();
};
});
export function openIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('fileStorageDB', 1);
request.onupgradeneeded = function (event) {
const db = event.target.result;
if (!db.objectStoreNames.contains('files')) {
db.createObjectStore('files', { keyPath: 'id' });
}
};
request.onsuccess = function (event) {
resolve(event.target.result);
};
request.onerror = function () {
reject('Error opening IndexedDB');
};
});
}
export const listOfAllQortalRequests = [
'GET_USER_ACCOUNT',
@@ -209,7 +202,7 @@ export const listOfAllQortalRequests = [
'DECRYPT_QORTAL_GROUP_DATA',
'DECRYPT_DATA_WITH_SHARING_KEY',
'DELETE_HOSTED_DATA',
'GET_HOSTED_DATA',
'GET_HOSTED_DATA',
'PUBLISH_MULTIPLE_QDN_RESOURCES',
'PUBLISH_QDN_RESOURCE',
'ENCRYPT_DATA',
@@ -261,8 +254,8 @@ export const listOfAllQortalRequests = [
'CANCEL_SELL_NAME',
'BUY_NAME',
'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA',
'TRANSFER_ASSET'
]
'TRANSFER_ASSET',
];
export const UIQortalRequests = [
'GET_USER_ACCOUNT',
@@ -323,316 +316,327 @@ export const UIQortalRequests = [
'CANCEL_SELL_NAME',
'BUY_NAME',
'MULTI_ASSET_PAYMENT_WITH_PRIVATE_DATA',
'TRANSFER_ASSET'
'TRANSFER_ASSET',
];
async function retrieveFileFromIndexedDB(fileId) {
const db = await openIndexedDB();
const transaction = db.transaction(['files'], 'readwrite');
const objectStore = transaction.objectStore('files');
return new Promise((resolve, reject) => {
const getRequest = objectStore.get(fileId);
async function retrieveFileFromIndexedDB(fileId) {
getRequest.onsuccess = function (event) {
if (getRequest.result) {
// File found, resolve it and delete from IndexedDB
const file = getRequest.result.data;
objectStore.delete(fileId);
resolve(file);
} else {
reject('File not found in IndexedDB');
}
};
getRequest.onerror = function () {
reject('Error retrieving file from IndexedDB');
};
});
}
async function deleteQortalFilesFromIndexedDB() {
try {
const db = await openIndexedDB();
const transaction = db.transaction(["files"], "readwrite");
const objectStore = transaction.objectStore("files");
return new Promise((resolve, reject) => {
const getRequest = objectStore.get(fileId);
getRequest.onsuccess = function (event) {
if (getRequest.result) {
// File found, resolve it and delete from IndexedDB
const file = getRequest.result.data;
objectStore.delete(fileId);
resolve(file);
} else {
reject("File not found in IndexedDB");
const transaction = db.transaction(['files'], 'readwrite');
const objectStore = transaction.objectStore('files');
// Create a request to get all keys
const getAllKeysRequest = objectStore.getAllKeys();
getAllKeysRequest.onsuccess = function (event) {
const keys = event.target.result;
// Iterate through keys to find and delete those containing '_qortalfile'
for (let key of keys) {
if (key.includes('_qortalfile')) {
const deleteRequest = objectStore.delete(key);
deleteRequest.onsuccess = function () {
console.log(
`File with key '${key}' has been deleted from IndexedDB`
);
};
deleteRequest.onerror = function () {
console.error(
`Failed to delete file with key '${key}' from IndexedDB`
);
};
}
};
getRequest.onerror = function () {
reject("Error retrieving file from IndexedDB");
};
});
}
async function deleteQortalFilesFromIndexedDB() {
try {
const db = await openIndexedDB();
const transaction = db.transaction(["files"], "readwrite");
const objectStore = transaction.objectStore("files");
// Create a request to get all keys
const getAllKeysRequest = objectStore.getAllKeys();
getAllKeysRequest.onsuccess = function (event) {
const keys = event.target.result;
// Iterate through keys to find and delete those containing '_qortalfile'
for (let key of keys) {
if (key.includes("_qortalfile")) {
const deleteRequest = objectStore.delete(key);
deleteRequest.onsuccess = function () {
console.log(`File with key '${key}' has been deleted from IndexedDB`);
};
deleteRequest.onerror = function () {
console.error(`Failed to delete file with key '${key}' from IndexedDB`);
};
}
}
};
getAllKeysRequest.onerror = function () {
console.error("Failed to retrieve keys from IndexedDB");
};
transaction.oncomplete = function () {
console.log("Transaction complete for deleting files from IndexedDB");
};
transaction.onerror = function () {
console.error("Error occurred during transaction for deleting files");
};
} catch (error) {
console.error("Error opening IndexedDB:", error);
}
}
};
getAllKeysRequest.onerror = function () {
console.error('Failed to retrieve keys from IndexedDB');
};
transaction.oncomplete = function () {
console.log('Transaction complete for deleting files from IndexedDB');
};
transaction.onerror = function () {
console.error('Error occurred during transaction for deleting files');
};
} catch (error) {
console.error('Error opening IndexedDB:', error);
}
}
export const showSaveFilePicker = async (
data,
{ openSnackGlobal, setOpenSnackGlobal, infoSnackCustom, setInfoSnackCustom }
) => {
try {
const { filename, mimeType, blob, fileHandleOptions } = data;
export const showSaveFilePicker = async (data, {openSnackGlobal,
setOpenSnackGlobal,
infoSnackCustom,
setInfoSnackCustom}) => {
try {
const { filename, mimeType, blob, fileHandleOptions } = data;
setInfoSnackCustom({
type: "info",
message:
"Saving file...",
});
setOpenSnackGlobal(true);
FileSaver.saveAs(blob, filename)
setInfoSnackCustom({
type: "success",
message:
"Saving file success!",
setInfoSnackCustom({
type: 'info',
message: 'Saving file...',
});
setOpenSnackGlobal(true);
} catch (error) {
setInfoSnackCustom({
type: "error",
message:
error?.message ? `Error saving file: ${error?.message}` : 'Error saving file',
});
setOpenSnackGlobal(true);
console.error("Error saving file:", error);
FileSaver.saveAs(blob, filename);
setInfoSnackCustom({
type: 'success',
message: 'Saving file success!',
});
setOpenSnackGlobal(true);
} catch (error) {
setInfoSnackCustom({
type: 'error',
message: error?.message
? `Error saving file: ${error?.message}`
: 'Error saving file',
});
setOpenSnackGlobal(true);
console.error('Error saving file:', error);
}
};
declare var cordova: any;
async function storeFilesInIndexedDB(obj) {
// First delete any existing files in IndexedDB with '_qortalfile' in their ID
await deleteQortalFilesFromIndexedDB();
// Open the IndexedDB
const db = await openIndexedDB();
const transaction = db.transaction(['files'], 'readwrite');
const objectStore = transaction.objectStore('files');
// Handle the obj.file if it exists and is a File instance
if (obj.file) {
const fileId = Date.now() + 'objFile_qortalfile';
// Store the file in IndexedDB
const fileData = {
id: fileId,
data: obj.file,
};
objectStore.put(fileData);
// Replace the file object with the file ID in the original object
obj.fileId = fileId;
delete obj.file;
}
if (obj.blob) {
const fileId = Date.now() + 'objFile_qortalfile';
// Store the file in IndexedDB
const fileData = {
id: fileId,
data: obj.blob,
};
objectStore.put(fileData);
// Replace the file object with the file ID in the original object
let blobObj = {
type: obj.blob?.type,
};
obj.fileId = fileId;
delete obj.blob;
obj.blob = blobObj;
}
// Iterate through resources to find files and save them to IndexedDB
for (let resource of obj?.resources || []) {
if (resource.file) {
const fileId = resource.identifier + Date.now() + '_qortalfile';
// Store the file in IndexedDB
const fileData = {
id: fileId,
data: resource.file,
};
objectStore.put(fileData);
// Replace the file object with the file ID in the original object
resource.fileId = fileId;
delete resource.file;
}
}
// Set transaction completion handlers
transaction.oncomplete = function () {
console.log('Files saved successfully to IndexedDB');
};
declare var cordova: any;
transaction.onerror = function () {
console.error('Error saving files to IndexedDB');
};
return obj; // Updated object with references to stored files
}
async function storeFilesInIndexedDB(obj) {
// First delete any existing files in IndexedDB with '_qortalfile' in their ID
await deleteQortalFilesFromIndexedDB();
// Open the IndexedDB
const db = await openIndexedDB();
const transaction = db.transaction(["files"], "readwrite");
const objectStore = transaction.objectStore("files");
// Handle the obj.file if it exists and is a File instance
if (obj.file) {
const fileId = Date.now() + "objFile_qortalfile";
// Store the file in IndexedDB
const fileData = {
id: fileId,
data: obj.file,
};
objectStore.put(fileData);
// Replace the file object with the file ID in the original object
obj.fileId = fileId;
delete obj.file;
}
if (obj.blob) {
const fileId = Date.now() + "objFile_qortalfile";
// Store the file in IndexedDB
const fileData = {
id: fileId,
data: obj.blob,
};
objectStore.put(fileData);
// Replace the file object with the file ID in the original object
let blobObj = {
type: obj.blob?.type
}
obj.fileId = fileId;
delete obj.blob;
obj.blob = blobObj
}
// Iterate through resources to find files and save them to IndexedDB
for (let resource of (obj?.resources || [])) {
if (resource.file) {
const fileId = resource.identifier + Date.now() + "_qortalfile";
// Store the file in IndexedDB
const fileData = {
id: fileId,
data: resource.file,
};
objectStore.put(fileData);
// Replace the file object with the file ID in the original object
resource.fileId = fileId;
delete resource.file;
}
}
// Set transaction completion handlers
transaction.oncomplete = function () {
console.log("Files saved successfully to IndexedDB");
};
transaction.onerror = function () {
console.error("Error saving files to IndexedDB");
};
return obj; // Updated object with references to stored files
}
export const useQortalMessageListener = (frameWindow, iframeRef, tabId, isDevMode, appName, appService, skipAuth) => {
const [path, setPath] = useState('')
export const useQortalMessageListener = (
frameWindow,
iframeRef,
tabId,
isDevMode,
appName,
appService,
skipAuth
) => {
const [path, setPath] = useState('');
const [history, setHistory] = useState({
customQDNHistoryPaths: [],
currentIndex: -1,
isDOMContentLoaded: false
})
const setHasSettingsChangedAtom = useSetRecoilState(navigationControllerAtom);
const { openSnackGlobal,
currentIndex: -1,
isDOMContentLoaded: false,
});
const setHasSettingsChangedAtom = useSetAtom(navigationControllerAtom);
const {
openSnackGlobal,
setOpenSnackGlobal,
infoSnackCustom,
setInfoSnackCustom } = useContext(MyContext);
setInfoSnackCustom,
} = useContext(MyContext);
useEffect(()=> {
if(tabId && !isNaN(history?.currentIndex)){
setHasSettingsChangedAtom((prev)=> {
useEffect(() => {
if (tabId && !isNaN(history?.currentIndex)) {
setHasSettingsChangedAtom((prev) => {
return {
...prev,
[tabId]: {
hasBack: history?.currentIndex > 0,
}
}
})
},
};
});
}
}, [history?.currentIndex, tabId])
}, [history?.currentIndex, tabId]);
const changeCurrentIndex = useCallback((value)=> {
setHistory((prev)=> {
const changeCurrentIndex = useCallback((value) => {
setHistory((prev) => {
return {
...prev,
currentIndex: value
}
})
}, [])
currentIndex: value,
};
});
}, []);
const resetHistory = useCallback(()=> {
const resetHistory = useCallback(() => {
setHistory({
customQDNHistoryPaths: [],
currentIndex: -1,
isManualNavigation: true,
isDOMContentLoaded: false
})
}, [])
currentIndex: -1,
isManualNavigation: true,
isDOMContentLoaded: false,
});
}, []);
useEffect(() => {
const listener = async (event) => {
if (event?.data?.requestedHandler !== 'UI') return;
const sendMessageToRuntime = (message, eventPort) => {
window.sendMessage(message.action, message.payload, 300000, message.isExtension, {
name: appName, service: appService
}, skipAuth)
.then((response) => {
if (response.error) {
eventPort.postMessage({
result: null,
error: {
error: response?.error,
message: typeof response?.error === 'string' ? response?.error : typeof response?.message === 'string' ? response?.message : 'An error has occurred'
},
});
} else {
eventPort.postMessage({
result: response,
error: null,
});
}
})
.catch((error) => {
console.error("Failed qortalRequest", error);
});
window
.sendMessage(
message.action,
message.payload,
300000,
message.isExtension,
{
name: appName,
service: appService,
},
skipAuth
)
.then((response) => {
if (response.error) {
eventPort.postMessage({
result: null,
error: {
error: response?.error,
message:
typeof response?.error === 'string'
? response?.error
: typeof response?.message === 'string'
? response?.message
: 'An error has occurred',
},
});
} else {
eventPort.postMessage({
result: response,
error: null,
});
}
})
.catch((error) => {
console.error('Failed qortalRequest', error);
});
};
// Check if action is included in the predefined list of UI requests
if (UIQortalRequests.includes(event.data.action)) {
sendMessageToRuntime(
{ action: event.data.action, type: 'qortalRequest', payload: event.data, isExtension: true },
{
action: event.data.action,
type: 'qortalRequest',
payload: event.data,
isExtension: true,
},
event.ports[0]
);
} else if(event?.data?.action === 'SAVE_FILE'
){
} else if (event?.data?.action === 'SAVE_FILE') {
try {
const res = await saveFile( event.data, null, true, {
openSnackGlobal,
setOpenSnackGlobal,
infoSnackCustom,
setInfoSnackCustom
const res = await saveFile(event.data, null, true, {
openSnackGlobal,
setOpenSnackGlobal,
infoSnackCustom,
setInfoSnackCustom,
});
} catch (error) {
}
} catch (error) {}
} else if (
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
event?.data?.action === 'PUBLISH_QDN_RESOURCE' ||
event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'ENCRYPT_DATA_WITH_SHARING_KEY' || event?.data?.action === 'ENCRYPT_QORTAL_GROUP_DATA'
event?.data?.action === 'ENCRYPT_DATA' ||
event?.data?.action === 'ENCRYPT_DATA_WITH_SHARING_KEY' ||
event?.data?.action === 'ENCRYPT_QORTAL_GROUP_DATA'
) {
const data = event.data;
if (data) {
sendMessageToRuntime(
{ action: event.data.action, type: 'qortalRequest', payload: data, isExtension: true },
{
action: event.data.action,
type: 'qortalRequest',
payload: data,
isExtension: true,
},
event.ports[0]
);
} else {
@@ -641,52 +645,73 @@ isDOMContentLoaded: false
error: 'Failed to prepare data for publishing',
});
}
} else if(event?.data?.action === 'LINK_TO_QDN_RESOURCE' ||
event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){
const pathUrl = event?.data?.path != null ? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path : null
setPath(pathUrl)
if(appName?.toLowerCase() === 'q-mail'){
window.sendMessage("addEnteredQmailTimestamp").catch((error) => {
} else if (
event?.data?.action === 'LINK_TO_QDN_RESOURCE' ||
event?.data?.action === 'QDN_RESOURCE_DISPLAYED'
) {
const pathUrl =
event?.data?.path != null
? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path
: null;
setPath(pathUrl);
if (appName?.toLowerCase() === 'q-mail') {
window.sendMessage('addEnteredQmailTimestamp').catch((error) => {
// error
});
} else if(appName?.toLowerCase() === 'q-wallets'){
executeEvent('setLastEnteredTimestampPaymentEvent', {})
} else if (appName?.toLowerCase() === 'q-wallets') {
executeEvent('setLastEnteredTimestampPaymentEvent', {});
}
} else if(event?.data?.action === 'NAVIGATION_HISTORY'){
if(event?.data?.payload?.isDOMContentLoaded){
setHistory((prev)=> {
const copyPrev = {...prev}
if((copyPrev?.customQDNHistoryPaths || []).at(-1) === (event?.data?.payload?.customQDNHistoryPaths || []).at(-1)) {
} else if (event?.data?.action === 'NAVIGATION_HISTORY') {
if (event?.data?.payload?.isDOMContentLoaded) {
setHistory((prev) => {
const copyPrev = { ...prev };
if (
(copyPrev?.customQDNHistoryPaths || []).at(-1) ===
(event?.data?.payload?.customQDNHistoryPaths || []).at(-1)
) {
return {
...prev,
currentIndex: prev.customQDNHistoryPaths.length - 1 === -1 ? 0 : prev.customQDNHistoryPaths.length - 1
}
currentIndex:
prev.customQDNHistoryPaths.length - 1 === -1
? 0
: prev.customQDNHistoryPaths.length - 1,
};
}
const copyHistory = {...prev}
const paths = [...(copyHistory?.customQDNHistoryPaths.slice(0, copyHistory.currentIndex + 1) || []), ...(event?.data?.payload?.customQDNHistoryPaths || [])]
const copyHistory = { ...prev };
const paths = [
...(copyHistory?.customQDNHistoryPaths.slice(
0,
copyHistory.currentIndex + 1
) || []),
...(event?.data?.payload?.customQDNHistoryPaths || []),
];
return {
...prev,
customQDNHistoryPaths: paths,
currentIndex: paths.length - 1
}
})
currentIndex: paths.length - 1,
};
});
} else {
setHistory(event?.data?.payload)
setHistory(event?.data?.payload);
}
} else if(event?.data?.action === 'SET_TAB' && !isDevMode){
executeEvent("addTab", {
data: event?.data?.payload
})
const targetOrigin = iframeRef.current ? new URL(iframeRef.current.src).origin : "*";
} else if (event?.data?.action === 'SET_TAB' && !isDevMode) {
executeEvent('addTab', {
data: event?.data?.payload,
});
const targetOrigin = iframeRef.current
? new URL(iframeRef.current.src).origin
: '*';
iframeRef.current.contentWindow.postMessage(
{ action: 'SET_TAB_SUCCESS', requestedHandler: 'UI',payload: {
name: event?.data?.payload?.name
} }, targetOrigin
{
action: 'SET_TAB_SUCCESS',
requestedHandler: 'UI',
payload: {
name: event?.data?.payload?.name,
},
},
targetOrigin
);
}
}
};
// Add the listener for messages coming from the frameWindow
@@ -696,12 +721,7 @@ isDOMContentLoaded: false
return () => {
frameWindow.removeEventListener('message', listener);
};
}, [isDevMode, appName, appService]); // Empty dependency array to run once when the component mounts
return {path, history, resetHistory, changeCurrentIndex}
return { path, history, resetHistory, changeCurrentIndex };
};

View File

@@ -0,0 +1,263 @@
import {
Box,
Checkbox,
FormControlLabel,
Typography,
useTheme,
} from '@mui/material';
import { Spacer } from '../../common/Spacer';
import { Return } from '../../assets/Icons/Return';
import { CustomButton, CustomLabel, TextP } from '../../styles/App-styles';
import { PasswordField } from '../PasswordField/PasswordField';
import { ErrorText } from '../ErrorText/ErrorText';
import Logo1Dark from '../../assets/svgs/Logo1Dark.svg';
import { useTranslation } from 'react-i18next';
import { saveFileToDisk } from '../../utils/generateWallet/generateWallet';
import { useState } from 'react';
import { decryptStoredWallet } from '../../utils/decryptWallet';
import PhraseWallet from '../../utils/generateWallet/phrase-wallet';
import { crypto, walletVersion } from '../../constants/decryptWallet';
export const DownloadWallet = ({
returnToMain,
setIsLoading,
showInfo,
rawWallet,
setWalletToBeDownloaded,
walletToBeDownloaded,
}) => {
const [walletToBeDownloadedPassword, setWalletToBeDownloadedPassword] =
useState<string>('');
const [newPassword, setNewPassword] = useState<string>('');
const [keepCurrentPassword, setKeepCurrentPassword] = useState<boolean>(true);
const theme = useTheme();
const [walletToBeDownloadedError, setWalletToBeDownloadedError] =
useState<string>('');
const { t } = useTranslation(['auth']);
const saveFileToDiskFunc = async () => {
try {
await saveFileToDisk(
walletToBeDownloaded.wallet,
walletToBeDownloaded.qortAddress
);
} catch (error: any) {
setWalletToBeDownloadedError(error?.message);
}
};
const saveWalletFunc = async (password: string, newPassword) => {
let wallet = structuredClone(rawWallet);
const res = await decryptStoredWallet(password, wallet);
const wallet2 = new PhraseWallet(res, wallet?.version || walletVersion);
const passwordToUse = newPassword || password;
wallet = await wallet2.generateSaveWalletData(
passwordToUse,
crypto.kdfThreads,
() => {}
);
setWalletToBeDownloaded({
wallet,
qortAddress: rawWallet.address0,
});
return {
wallet,
qortAddress: rawWallet.address0,
};
};
const confirmPasswordToDownload = async () => {
try {
setWalletToBeDownloadedError('');
if (!keepCurrentPassword && !newPassword) {
setWalletToBeDownloadedError(
t('auth:wallet.error.missing_new_password', {
postProcess: 'capitalize',
})
);
return;
}
if (!walletToBeDownloadedPassword) {
setWalletToBeDownloadedError(
t('auth:wallet.error.missing_password', { postProcess: 'capitalize' })
);
return;
}
setIsLoading(true);
await new Promise<void>((res) => {
setTimeout(() => {
res();
}, 250);
});
const newPasswordForWallet = !keepCurrentPassword ? newPassword : null;
const res = await saveWalletFunc(
walletToBeDownloadedPassword,
newPasswordForWallet
);
} catch (error: any) {
setWalletToBeDownloadedError(error?.message);
} finally {
setIsLoading(false);
}
};
return (
<>
<Spacer height="22px" />
<Box
sx={{
boxSizing: 'border-box',
display: 'flex',
justifyContent: 'flex-start',
maxWidth: '700px',
paddingLeft: '22px',
width: '100%',
}}
>
<Return
style={{
cursor: 'pointer',
height: '24px',
width: 'auto',
}}
onClick={returnToMain}
/>
</Box>
<Spacer height="10px" />
<div
className="image-container"
style={{
width: '136px',
height: '154px',
}}
>
<img src={Logo1Dark} className="base-image" />
</div>
<Spacer height="35px" />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
}}
>
<TextP
sx={{
textAlign: 'start',
lineHeight: '24px',
fontSize: '20px',
fontWeight: 600,
}}
>
{t('auth:download_account', { postProcess: 'capitalize' })}
</TextP>
</Box>
<Spacer height="35px" />
{!walletToBeDownloaded && (
<>
<CustomLabel htmlFor="standard-adornment-password">
{t('auth:wallet.password_confirmation', {
postProcess: 'capitalize',
})}
</CustomLabel>
<Spacer height="5px" />
<PasswordField
id="standard-adornment-password"
value={walletToBeDownloadedPassword}
onChange={(e) => setWalletToBeDownloadedPassword(e.target.value)}
/>
<Spacer height="20px" />
<FormControlLabel
sx={{
margin: 0,
}}
control={
<Checkbox
onChange={(e) => setKeepCurrentPassword(e.target.checked)}
checked={keepCurrentPassword}
edge="start"
tabIndex={-1}
disableRipple
sx={{
'&.Mui-checked': {
color: theme.palette.text.secondary,
},
'& .MuiSvgIcon-root': {
color: theme.palette.text.secondary,
},
}}
/>
}
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Typography sx={{ fontSize: '14px' }}>
{t('auth:wallet.keep_password', {
postProcess: 'capitalize',
})}
</Typography>
</Box>
}
/>
<Spacer height="20px" />
{!keepCurrentPassword && (
<>
<CustomLabel htmlFor="standard-adornment-password">
{t('auth:wallet.new_password', {
postProcess: 'capitalize',
})}
</CustomLabel>
<Spacer height="5px" />
<PasswordField
id="standard-adornment-password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
<Spacer height="20px" />
</>
)}
<CustomButton onClick={confirmPasswordToDownload}>
{t('auth:password_confirmation', {
postProcess: 'capitalize',
})}
</CustomButton>
<ErrorText>{walletToBeDownloadedError}</ErrorText>
</>
)}
{walletToBeDownloaded && (
<>
<CustomButton
onClick={async () => {
await saveFileToDiskFunc();
await showInfo({
message: t('auth:keep_secure', {
postProcess: 'capitalize',
}),
});
}}
>
{t('auth:download_account', {
postProcess: 'capitalize',
})}
</CustomButton>
</>
)}
</>
);
};

View File

@@ -1,154 +1,145 @@
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react';
import {
Avatar,
Box,
Button,
ButtonBase,
Collapse,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Input,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemIcon,
ListItemText,
List,
MenuItem,
Popover,
Select,
TextField,
Typography,
} from "@mui/material";
import { Label } from './Group/AddGroup';
Box,
Button,
ButtonBase,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
ListItem,
ListItemIcon,
ListItemText,
List,
Typography,
useTheme,
} from '@mui/material';
import { Spacer } from '../common/Spacer';
import { LoadingButton } from '@mui/lab';
import { getBaseApiReact, MyContext } from '../App';
import { getFee } from '../background';
import qTradeLogo from "../assets/Icons/q-trade-logo.webp";
import qTradeLogo from '../assets/Icons/q-trade-logo.webp';
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../utils/events';
import { BarSpinner } from '../common/Spinners/BarSpinner/BarSpinner';
import {
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 [isOpen, setIsOpen] = useState(false)
const theme = useTheme();
const openBuyQortInfoFunc = useCallback((e) => {
setIsOpen(true)
}, [ setIsOpen]);
useEffect(() => {
subscribeToEvent("openBuyQortInfo", openBuyQortInfoFunc);
return () => {
unsubscribeFromEvent("openBuyQortInfo", openBuyQortInfoFunc);
};
}, [openBuyQortInfoFunc]);
useEffect(() => {
subscribeToEvent('openBuyQortInfo', openBuyQortInfoFunc);
return () => {
unsubscribeFromEvent('openBuyQortInfo', openBuyQortInfoFunc);
};
}, [openBuyQortInfoFunc]);
return (
<Dialog
open={isOpen}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Get QORT"}
</DialogTitle>
<DialogContent>
<Box
sx={{
width: "400px",
maxWidth: '90vw',
height: "400px",
maxHeight: '90vh',
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
}}
>
<Typography>Get QORT using Qortal's crosschain trade portal</Typography>
<ButtonBase
sx={{
"&:hover": { backgroundColor: "secondary.main" },
transition: "all 0.1s ease-in-out",
padding: "5px",
borderRadius: "5px",
gap: "5px",
}}
onClick={async () => {
executeEvent("addTab", {
data: { service: "APP", name: "q-trade" },
});
executeEvent("open-apps-mode", {});
setIsOpen(false)
}}
>
<img
style={{
borderRadius: "50%",
height: '30px'
}}
src={qTradeLogo}
/>
<Typography
sx={{
fontSize: "1rem",
}}
>
Trade QORT
</Typography>
</ButtonBase>
<Spacer height='40px' />
<Typography sx={{
textDecoration: 'underline'
}}>Benefits of having QORT</Typography>
<List
sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}
aria-label="contacts"
open={isOpen}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<ListItem disablePadding>
<ListItemIcon>
<RadioButtonCheckedIcon sx={{
color: 'white'
}} />
</ListItemIcon>
<ListItemText primary="Create transactions on the Qortal Blockchain" />
</ListItem>
<ListItem disablePadding>
<ListItemIcon>
<RadioButtonCheckedIcon sx={{
color: 'white'
}} />
</ListItemIcon>
<ListItemText primary="Having at least 4 QORT in your balance allows you to send chat messages at near instant speed." />
</ListItem>
</List>
</Box>
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={() => {
setIsOpen(false)
<DialogTitle id="alert-dialog-title">{'Get QORT'}</DialogTitle>
<DialogContent>
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
gap: '10px',
height: '350px',
maxHeight: '80vh',
maxWidth: '90vw',
padding: '10px',
width: '400px',
}}
>
<Typography>
Get QORT using Qortal's crosschain trade portal
</Typography>
<ButtonBase
sx={{
'&:hover': { backgroundColor: theme.palette.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',
}}
>
Close
</Button>
</DialogActions>
</Dialog>
)
}
Trade QORT
</Typography>
</ButtonBase>
<Spacer height="15px" />
<Typography
sx={{
textDecoration: 'underline',
}}
>
Benefits of having QORT
</Typography>
<List
sx={{
maxWidth: 360,
width: '100%',
}}
aria-label="contacts"
>
<ListItem disablePadding>
<ListItemIcon>
<RadioButtonCheckedIcon />
</ListItemIcon>
<ListItemText primary="Create transactions on the Qortal Blockchain" />
</ListItem>
<ListItem disablePadding>
<ListItemIcon>
<RadioButtonCheckedIcon />
</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, {
useCallback,
useContext,
useEffect,
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";
import { useContext, useEffect, useState } from 'react';
import { MyContext } from '../../App';
import { Box, Typography } from '@mui/material';
import { AdminSpaceInner } from './AdminSpaceInner';
export const AdminSpace = ({
selectedGroup,
@@ -26,11 +12,12 @@ export const AdminSpace = ({
isAdmin,
myAddress,
hide,
defaultThread,
defaultThread,
setDefaultThread,
setIsForceShowCreationKeyPopup
setIsForceShowCreationKeyPopup,
balance,
isOwner,
}) => {
const { rootHeight } = useContext(MyContext);
const [isMoved, setIsMoved] = useState(false);
useEffect(() => {
if (hide) {
@@ -42,26 +29,40 @@ export const AdminSpace = ({
return (
<div
style={{
// reference to change height
height: isMobile ? `calc(${rootHeight} - 127px` : "calc(100vh - 70px)",
display: "flex",
flexDirection: "column",
width: "100%",
opacity: hide ? 0 : 1,
visibility: hide && 'hidden',
position: hide ? 'fixed' : 'relative',
left: hide && '-1000px'
}}
>
{!isAdmin && <Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
paddingTop: '25px'
}}><Typography>Sorry, this space is only for Admins.</Typography></Box>}
{isAdmin && <AdminSpaceInner setIsForceShowCreationKeyPopup={setIsForceShowCreationKeyPopup} adminsWithNames={adminsWithNames} selectedGroup={selectedGroup} />}
</div>
style={{
display: 'flex',
flexDirection: 'column',
height: 'calc(100vh - 70px)',
left: hide && '-1000px',
opacity: hide ? 0 : 1,
position: hide ? 'fixed' : 'relative',
visibility: hide && 'hidden',
width: '100%',
overflow: 'auto',
}}
>
{!isAdmin && (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
paddingTop: '25px',
width: '100%',
}}
>
<Typography>Sorry, this space is only for Admins.</Typography>
</Box>
)}
{isAdmin && (
<AdminSpaceInner
setIsForceShowCreationKeyPopup={setIsForceShowCreationKeyPopup}
adminsWithNames={adminsWithNames}
selectedGroup={selectedGroup}
balance={balance}
userInfo={userInfo}
isOwner={isOwner}
/>
)}
</div>
);
};

View File

@@ -1,39 +1,43 @@
import React, { useCallback, useContext, useEffect, useState } from "react";
import { useCallback, useContext, useEffect, useState } from 'react';
import {
MyContext,
getArbitraryEndpointReact,
getBaseApiReact,
} from "../../App";
import { Box, Button, Typography } from "@mui/material";
} from '../../App';
import { Box, Button, Typography } from '@mui/material';
import {
decryptResource,
getPublishesFromAdmins,
validateSecretKey,
} from "../Group/Group";
import { getFee } from "../../background";
import { base64ToUint8Array } from "../../qdn/encryption/group-encryption";
import { uint8ArrayToObject } from "../../backgroundFunctions/encryption";
import { formatTimestampForum } from "../../utils/time";
import { Spacer } from "../../common/Spacer";
} from '../Group/Group';
import { getFee } from '../../background';
import { base64ToUint8Array } from '../../qdn/encryption/group-encryption';
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
import { formatTimestampForum } from '../../utils/time';
import { Spacer } from '../../common/Spacer';
import { GroupAvatar } from '../GroupAvatar';
export const getPublishesFromAdminsAdminSpace = async (
admins: string[],
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 response = await fetch(url);
if (!response.ok) {
throw new Error("network error");
throw new Error('network error');
}
const adminData = await response.json();
const filterId = adminData.filter(
(data: any) => data.identifier === `admins-symmetric-qchat-group-${groupId}`
);
if (filterId?.length === 0) {
return false;
}
const sortedData = filterId.sort((a: any, b: any) => {
// Get the most recent date for both a and b
const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
@@ -50,6 +54,9 @@ export const AdminSpaceInner = ({
selectedGroup,
adminsWithNames,
setIsForceShowCreationKeyPopup,
balance,
userInfo,
isOwner,
}) => {
const [adminGroupSecretKey, setAdminGroupSecretKey] = useState(null);
const [isFetchingAdminGroupSecretKey, setIsFetchingAdminGroupSecretKey] =
@@ -63,7 +70,7 @@ export const AdminSpaceInner = ({
const [groupSecretKeyPublishDetails, setGroupSecretKeyPublishDetails] =
useState(null);
const [isLoadingPublishKey, setIsLoadingPublishKey] = useState(false);
const { show, setTxList, setInfoSnackCustom, setOpenSnackGlobal } =
const { show, setInfoSnackCustom, setOpenSnackGlobal } =
useContext(MyContext);
const getAdminGroupSecretKey = useCallback(async () => {
@@ -87,10 +94,11 @@ export const AdminSpaceInner = ({
const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
if (!validateSecretKey(decryptedKeyToObject))
throw new Error("SecretKey is not valid");
throw new Error('SecretKey is not valid');
setAdminGroupSecretKey(decryptedKeyToObject);
setAdminGroupSecretKeyPublishDetails(getLatestPublish);
} catch (error) {
console.log(error);
} finally {
setIsFetchingAdminGroupSecretKey(false);
}
@@ -106,6 +114,7 @@ export const AdminSpaceInner = ({
if (getLatestPublish === false) setGroupSecretKeyPublishDetails(false);
setGroupSecretKeyPublishDetails(getLatestPublish);
} catch (error) {
console.log(error);
} finally {
setIsFetchingGroupSecretKey(false);
}
@@ -113,15 +122,17 @@ export const AdminSpaceInner = ({
const createCommonSecretForAdmins = async () => {
try {
const fee = await getFee("ARBITRARY");
const fee = await getFee('ARBITRARY');
await show({
message: "Would you like to perform an ARBITRARY transaction?",
publishFee: fee.fee + " QORT",
message: 'Would you like to perform an ARBITRARY transaction?',
publishFee: fee.fee + ' QORT',
});
setIsLoadingPublishKey(true);
window
.sendMessage("encryptAndPublishSymmetricKeyGroupChatForAdmins", {
.sendMessage('encryptAndPublishSymmetricKeyGroupChatForAdmins', {
groupId: selectedGroup,
previousData: adminGroupSecretKey,
admins: adminsWithNames,
@@ -129,27 +140,29 @@ export const AdminSpaceInner = ({
.then((response) => {
if (!response?.error) {
setInfoSnackCustom({
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.",
'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);
return;
}
setInfoSnackCustom({
type: "error",
message: response?.error || "unable to re-encrypt secret key",
type: 'error',
message: response?.error || 'unable to re-encrypt secret key',
});
setOpenSnackGlobal(true);
})
.catch((error) => {
setInfoSnackCustom({
type: "error",
message: error?.message || "unable to re-encrypt secret key",
type: 'error',
message: error?.message || 'unable to re-encrypt secret key',
});
setOpenSnackGlobal(true);
});
} catch (error) {}
} catch (error) {
console.log(error);
}
};
useEffect(() => {
@@ -159,27 +172,32 @@ export const AdminSpaceInner = ({
return (
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
padding: "10px",
alignItems: 'center'
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
padding: '10px',
width: '100%',
}}
>
<Typography sx={{
fontSize: '14px'
}}>Reminder: After publishing the key, it will take a couple of minutes for it to appear. Please just wait.</Typography>
<Typography
sx={{
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" />
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "20px",
width: "300px",
maxWidth: "90%",
padding: '10px',
border: '1px solid gray',
borderRadius: '6px'
borderRadius: '6px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
maxWidth: '90%',
padding: '10px',
width: '300px',
}}
>
{isFetchingGroupSecretKey && (
@@ -191,33 +209,47 @@ export const AdminSpaceInner = ({
)}
{groupSecretKeyPublishDetails && (
<Typography>
Last encryption date:{" "}
Last encryption date:{' '}
{formatTimestampForum(
groupSecretKeyPublishDetails?.updated ||
groupSecretKeyPublishDetails?.created
)}{" "}
)}{' '}
{` by ${groupSecretKeyPublishDetails?.name}`}
</Typography>
)}
<Button disabled={isFetchingGroupSecretKey} onClick={()=> setIsForceShowCreationKeyPopup(true)} variant="contained">
<Button
disabled={isFetchingGroupSecretKey}
onClick={() => setIsForceShowCreationKeyPopup(true)}
variant="contained"
>
Publish group secret key
</Button>
<Spacer height="20px" />
<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>
<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>
<Spacer height="25px" />
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "20px",
width: "300px",
maxWidth: "90%",
padding: '10px',
border: '1px solid gray',
borderRadius: '6px'
borderRadius: '6px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
maxWidth: '90%',
padding: '10px',
width: '300px',
}}
>
{isFetchingAdminGroupSecretKey && (
@@ -228,21 +260,58 @@ export const AdminSpaceInner = ({
)}
{adminGroupSecretKeyPublishDetails && (
<Typography>
Last encryption date:{" "}
Last encryption date:{' '}
{formatTimestampForum(
adminGroupSecretKeyPublishDetails?.updated ||
adminGroupSecretKeyPublishDetails?.created
)}
</Typography>
)}
<Button disabled={isFetchingAdminGroupSecretKey} onClick={createCommonSecretForAdmins} variant="contained">
<Button
disabled={isFetchingAdminGroupSecretKey}
onClick={createCommonSecretForAdmins}
variant="contained"
>
Publish admin secret key
</Button>
<Spacer height="20px" />
<Typography sx={{
fontSize: '14px'
}}>This key is to encrypt ADMIN related content. Only admins would see content encrypted with it.</Typography>
<Typography
sx={{
fontSize: '14px',
}}
>
This key is to encrypt ADMIN related content. Only admins would see
content encrypted with it.
</Typography>
</Box>
<Spacer height="25px" />
{isOwner && (
<Box
sx={{
border: '1px solid gray',
borderRadius: '6px',
display: 'flex',
flexDirection: 'column',
gap: '20px',
maxWidth: '90%',
padding: '10px',
width: '300px',
alignItems: 'center',
}}
>
<Typography>Group Avatar</Typography>
<GroupAvatar
setOpenSnack={setOpenSnackGlobal}
setInfoSnack={setInfoSnackCustom}
myName={userInfo?.name}
balance={balance}
groupId={selectedGroup}
/>
</Box>
)}
</Box>
);
};

View File

@@ -1,18 +1,31 @@
import React, { useMemo, useRef, useState } from "react";
import TipTap from "./TipTap";
import { AuthenticatedContainerInnerTop, CustomButton } from "../../App-styles";
import { Box, CircularProgress } from "@mui/material";
import { objectToBase64 } from "../../qdn/encryption/group-encryption";
import ShortUniqueId from "short-unique-id";
import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar";
import { getBaseApi, getFee } from "../../background";
import { decryptPublishes, getTempPublish, handleUnencryptedPublishes, saveTempPublish } from "./GroupAnnouncements";
import { AnnouncementList } from "./AnnouncementList";
import { Spacer } from "../../common/Spacer";
import React, { useMemo, useRef, useState } from 'react';
import TipTap from './TipTap';
import {
AuthenticatedContainerInnerTop,
CustomButton,
} from '../../styles/App-styles';
import { Box, CircularProgress, useTheme } from '@mui/material';
import { objectToBase64 } from '../../qdn/encryption/group-encryption';
import ShortUniqueId from 'short-unique-id';
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
import { getBaseApi, getFee } from '../../background';
import {
decryptPublishes,
getTempPublish,
handleUnencryptedPublishes,
saveTempPublish,
} from './GroupAnnouncements';
import { AnnouncementList } from './AnnouncementList';
import { Spacer } from '../../common/Spacer';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { getArbitraryEndpointReact, getBaseApiReact, isMobile, pauseAllQueues, resumeAllQueues } from "../../App";
import {
getArbitraryEndpointReact,
getBaseApiReact,
pauseAllQueues,
resumeAllQueues,
} from '../../App';
const tempKey = 'accouncement-comment'
const tempKey = 'accouncement-comment';
const uid = new ShortUniqueId({ length: 8 });
export const AnnouncementDiscussion = ({
@@ -23,43 +36,40 @@ export const AnnouncementDiscussion = ({
setSelectedAnnouncement,
show,
myName,
isPrivate
isPrivate,
}) => {
const theme = useTheme();
const [isSending, setIsSending] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isFocusedParent, setIsFocusedParent] = useState(false);
const [comments, setComments] = useState([])
const [tempPublishedList, setTempPublishedList] = useState([])
const firstMountRef = useRef(false)
const [data, setData] = useState({})
const [comments, setComments] = useState([]);
const [tempPublishedList, setTempPublishedList] = useState([]);
const firstMountRef = useRef(false);
const [data, setData] = useState({});
const editorRef = useRef(null);
const setEditorRef = (editorInstance) => {
editorRef.current = editorInstance;
};
const clearEditorContent = () => {
if (editorRef.current) {
editorRef.current.chain().focus().clearContent().run();
if(isMobile){
setTimeout(() => {
editorRef.current?.chain().blur().run();
setIsFocusedParent(false)
}, 200);
}
}
};
const getData = async ({ identifier, name }, isPrivate) => {
try {
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64`
);
if(!res?.ok) return
if (!res?.ok) return;
const data = await res.text();
const response = isPrivate === false ? handleUnencryptedPublishes([data]) : await decryptPublishes([{ data }], secretKey);
const response =
isPrivate === false
? handleUnencryptedPublishes([data])
: await decryptPublishes([{ data }], secretKey);
const messageData = response[0];
setData((prev) => {
return {
@@ -67,19 +77,21 @@ export const AnnouncementDiscussion = ({
[`${identifier}-${name}`]: messageData,
};
});
} catch (error) {}
} catch (error) {
console.log(error);
}
};
const publishAnc = async ({ encryptedData, identifier }: any) => {
try {
if (!selectedAnnouncement) return;
return new Promise((res, rej) => {
window.sendMessage("publishGroupEncryptedResource", {
encryptedData,
identifier,
})
window
.sendMessage('publishGroupEncryptedResource', {
encryptedData,
identifier,
})
.then((response) => {
if (!response?.error) {
res(response);
@@ -88,63 +100,64 @@ export const AnnouncementDiscussion = ({
rej(response.error);
})
.catch((error) => {
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 () => {
try {
const getTempAnnouncements = await getTempPublish()
if(getTempAnnouncements[tempKey]){
let tempData = []
Object.keys(getTempAnnouncements[tempKey] || {}).map((key)=> {
const value = getTempAnnouncements[tempKey][key]
if(value.data?.announcementId === selectedAnnouncement.identifier){
tempData.push(value.data)
const getTempAnnouncements = await getTempPublish();
if (getTempAnnouncements[tempKey]) {
let tempData = [];
Object.keys(getTempAnnouncements[tempKey] || {}).map((key) => {
const value = getTempAnnouncements[tempKey][key];
if (value.data?.announcementId === selectedAnnouncement.identifier) {
tempData.push(value.data);
}
});
setTempPublishedList(tempData);
}
})
setTempPublishedList(tempData)
}
} catch (error) {
console.log(error);
}
}
};
const publishComment = async () => {
try {
pauseAllQueues()
const fee = await getFee('ARBITRARY')
pauseAllQueues();
const fee = await getFee('ARBITRARY');
await show({
message: "Would you like to perform a ARBITRARY transaction?" ,
publishFee: fee.fee + ' QORT'
})
message: 'Would you like to perform a ARBITRARY transaction?',
publishFee: fee.fee + ' QORT',
});
if (isSending) return;
if (editorRef.current) {
const htmlContent = editorRef.current.getHTML();
if (!htmlContent?.trim() || htmlContent?.trim() === "<p></p>") return;
if (!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') return;
setIsSending(true);
const message = {
version: 1,
extra: {},
message: htmlContent,
};
const secretKeyObject = isPrivate === false ? null : await getSecretKey(false, true);
const message64: any = await objectToBase64(message);
const encryptSingle = isPrivate === false ? message64 : await encryptChatMessage(
message64,
secretKeyObject
);
const secretKeyObject =
isPrivate === false ? null : await getSecretKey(false, true);
const message64: any = await objectToBase64(message);
const encryptSingle =
isPrivate === false
? message64
: await encryptChatMessage(message64, secretKeyObject);
const randomUid = uid.rnd();
const identifier = `cm-${selectedAnnouncement.identifier}-${randomUid}`;
const res = await publishAnc({
encryptedData: encryptSingle,
identifier
identifier,
});
const dataToSaveToStorage = {
@@ -153,18 +166,18 @@ export const AnnouncementDiscussion = ({
service: 'DOCUMENT',
tempData: message,
created: Date.now(),
announcementId: selectedAnnouncement.identifier
}
await saveTempPublish({data: dataToSaveToStorage, key: tempKey})
setTempData()
announcementId: selectedAnnouncement.identifier,
};
await saveTempPublish({ data: dataToSaveToStorage, key: tempKey });
setTempData();
clearEditorContent();
}
// send chat message
} catch (error) {
console.error(error);
} finally {
resumeAllQueues()
resumeAllQueues();
setIsSending(false);
}
};
@@ -172,7 +185,6 @@ export const AnnouncementDiscussion = ({
const getComments = React.useCallback(
async (selectedAnnouncement, isPrivate) => {
try {
setIsLoading(true);
const offset = 0;
@@ -181,19 +193,20 @@ export const AnnouncementDiscussion = ({
const identifier = `cm-${selectedAnnouncement.identifier}`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
setTempData()
setTempData();
setComments(responseData);
setIsLoading(false);
for (const data of responseData) {
getData({ name: data.name, identifier: data.identifier }, isPrivate);
}
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
@@ -203,119 +216,122 @@ export const AnnouncementDiscussion = ({
[secretKey]
);
const loadMore = async()=> {
const loadMore = async () => {
try {
setIsLoading(true);
const offset = comments.length
const offset = comments.length;
const identifier = `cm-${selectedAnnouncement.identifier}`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseData = await response.json();
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
setComments((prev)=> [...prev, ...responseData]);
setIsLoading(false);
for (const data of responseData) {
getData({ name: data.name, identifier: data.identifier }, isPrivate);
}
setComments((prev) => [...prev, ...responseData]);
setIsLoading(false);
for (const data of responseData) {
getData({ name: data.name, identifier: data.identifier }, isPrivate);
}
} catch (error) {
console.log(error);
}
}
};
const combinedListTempAndReal = useMemo(() => {
// Combine the two lists
const combined = [...tempPublishedList, ...comments];
// Remove duplicates based on the "identifier"
const uniqueItems = new Map();
combined.forEach(item => {
uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence
combined.forEach((item) => {
uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence
});
// Convert the map back to an array and sort by "created" timestamp in descending order
const sortedList = Array.from(uniqueItems.values()).sort((a, b) => b.created - a.created);
const sortedList = Array.from(uniqueItems.values()).sort(
(a, b) => b.created - a.created
);
return sortedList;
}, [tempPublishedList, comments]);
React.useEffect(() => {
if(!secretKey && isPrivate) return
if (!secretKey && isPrivate) return;
if (selectedAnnouncement && !firstMountRef.current && isPrivate !== null) {
getComments(selectedAnnouncement, isPrivate);
firstMountRef.current = true
firstMountRef.current = true;
}
}, [selectedAnnouncement, secretKey, isPrivate]);
return (
<div
style={{
height: isMobile ? '100%' : "100%",
display: "flex",
flexDirection: "column",
width: "100%",
display: 'flex',
flexDirection: 'column',
height: '100%',
width: '100%',
}}
>
<div style={{
position: "relative",
width: "100%",
display: "flex",
flexDirection: "column",
flexShrink: 0,
}}>
<div
style={{
display: 'flex',
flexDirection: 'column',
flexShrink: 0,
position: 'relative',
width: '100%',
}}
>
<AuthenticatedContainerInnerTop
sx={{
height: '20px',
}}
>
<ArrowBackIcon
onClick={() => setSelectedAnnouncement(null)}
sx={{
cursor: 'pointer',
}}
/>
</AuthenticatedContainerInnerTop>
<AuthenticatedContainerInnerTop sx={{
height: '20px'
}}>
<ArrowBackIcon onClick={()=> setSelectedAnnouncement(null)} sx={{
cursor: 'pointer'
}} />
</AuthenticatedContainerInnerTop>
<Spacer height="20px" />
</div>
<AnnouncementList
announcementData={data}
initialMessages={combinedListTempAndReal}
setSelectedAnnouncement={()=> {}}
setSelectedAnnouncement={() => {}}
disableComment
showLoadMore={comments.length > 0 && comments.length % 20 === 0}
loadMore={loadMore}
myName={myName}
/>
<div
style={{
// position: 'fixed',
// bottom: '0px',
backgroundColor: "#232428",
minHeight: isMobile ? "0px" : "150px",
maxHeight: isMobile ? "auto" : "400px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
width: "100%",
boxSizing: "border-box",
padding: isMobile ? "10px": "20px",
position: isFocusedParent ? 'fixed' : 'relative',
backgroundColor: theme.palette.background.default,
bottom: isFocusedParent ? '0px' : 'unset',
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
flexShrink: 0,
maxHeight: '400px',
minHeight: '150px',
overflow: 'hidden',
padding: '20px',
position: isFocusedParent ? 'fixed' : 'relative',
top: isFocusedParent ? '0px' : 'unset',
width: '100%',
zIndex: isFocusedParent ? 5 : 'unset',
flexShrink:0,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
// height: '100%',
flexGrow: isMobile && 1,
overflow: "auto",
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
overflow: 'auto',
}}
>
<TipTap
@@ -323,79 +339,78 @@ export const AnnouncementDiscussion = ({
onEnter={publishComment}
disableEnter
maxHeightOffset="60px"
isFocusedParent={isFocusedParent} setIsFocusedParent={setIsFocusedParent}
isFocusedParent={isFocusedParent}
setIsFocusedParent={setIsFocusedParent}
/>
</div>
<Box sx={{
display: 'flex',
width: '100&',
gap: '10px',
justifyContent: 'center',
flexShrink: 0,
position: 'relative',
}}>
{isFocusedParent && (
<CustomButton
onClick={()=> {
if(isSending) return
setIsFocusedParent(false)
clearEditorContent()
// Unfocus the editor
}}
style={{
marginTop: 'auto',
alignSelf: 'center',
cursor: isSending ? 'default' : 'pointer',
flexShrink: 0,
padding: isMobile && '5px',
fontSize: isMobile && '14px',
background: 'red',
}}
>
{` Close`}
</CustomButton>
)}
<CustomButton
onClick={() => {
if (isSending) return;
publishComment();
}}
style={{
marginTop: "auto",
alignSelf: "center",
cursor: isSending ? "default" : "pointer",
background: isSending && "rgba(0, 0, 0, 0.8)",
<Box
sx={{
display: 'flex',
flexShrink: 0,
padding: isMobile && '5px',
fontSize: isMobile && '14px'
gap: '10px',
justifyContent: 'center',
position: 'relative',
width: '100&',
}}
>
{isSending && (
<CircularProgress
size={18}
sx={{
position: "absolute",
top: "50%",
left: "50%",
marginTop: "-12px",
marginLeft: "-12px",
color: "white",
{isFocusedParent && (
<CustomButton
onClick={() => {
if (isSending) return;
setIsFocusedParent(false);
clearEditorContent();
// Unfocus the editor
}}
/>
style={{
alignSelf: 'center',
background: 'red',
cursor: isSending ? 'default' : 'pointer',
flexShrink: 0,
fontSize: '14px',
marginTop: 'auto',
padding: '5px',
}}
>
{` Close`}
</CustomButton>
)}
{` Publish Comment`}
</CustomButton>
</Box>
<CustomButton
onClick={() => {
if (isSending) return;
publishComment();
}}
style={{
alignSelf: 'center',
background: theme.palette.background.default,
cursor: isSending ? 'default' : 'pointer',
flexShrink: 0,
fontSize: '14px',
marginTop: 'auto',
padding: '5px',
}}
>
{isSending && (
<CircularProgress
size={18}
sx={{
color: theme.palette.text.primary,
left: '50%',
marginLeft: '-12px',
marginTop: '-12px',
position: 'absolute',
top: '50%',
}}
/>
)}
{` Publish Comment`}
</CustomButton>
</Box>
</div>
<LoadingSnackbar
open={isLoading}
info={{
message: "Loading comments... please wait.",
message: 'Loading comments... please wait.',
}}
/>
</div>

View File

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

View File

@@ -1,13 +1,8 @@
import React, { useCallback, useState, useEffect, useRef } from "react";
import {
List,
AutoSizer,
CellMeasurerCache,
CellMeasurer,
} from "react-virtualized";
import { AnnouncementItem } from "./AnnouncementItem";
import { Box } from "@mui/material";
import { CustomButton } from "../../App-styles";
import { useState, useEffect, useRef } from 'react';
import { CellMeasurerCache } from 'react-virtualized';
import { AnnouncementItem } from './AnnouncementItem';
import { Box } from '@mui/material';
import { CustomButton } from '../../styles/App-styles';
const cache = new CellMeasurerCache({
fixedWidth: true,
@@ -21,9 +16,8 @@ export const AnnouncementList = ({
disableComment,
showLoadMore,
loadMore,
myName
myName,
}) => {
const listRef = useRef();
const [messages, setMessages] = useState(initialMessages);
@@ -35,39 +29,44 @@ export const AnnouncementList = ({
setMessages(initialMessages);
}, [initialMessages]);
return (
<div
style={{
position: "relative",
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
width: "100%",
display: "flex",
flexDirection: "column",
flexShrink: 1,
overflow: 'auto'
position: 'relative',
width: '100%',
overflow: 'auto',
}}
>
{messages.map((message) => {
const messageData = message?.tempData ? {
decryptedData: message?.tempData
} : announcementData[`${message.identifier}-${message.name}`];
const messageData = message?.tempData
? {
decryptedData: message?.tempData,
}
: announcementData[`${message.identifier}-${message.name}`];
return (
<div
key={message?.identifier}
style={{
marginBottom: "10px",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<AnnouncementItem myName={myName} disableComment={disableComment} setSelectedAnnouncement={setSelectedAnnouncement} message={message} messageData={messageData} />
</div>
<div
key={message?.identifier}
style={{
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
marginBottom: '10px',
width: '100%',
}}
>
<AnnouncementItem
myName={myName}
disableComment={disableComment}
setSelectedAnnouncement={setSelectedAnnouncement}
message={message}
messageData={messageData}
/>
</div>
);
})}
{/* <AutoSizer>
@@ -83,16 +82,20 @@ export const AnnouncementList = ({
/>
)}
</AutoSizer> */}
<Box sx={{
width: '100%',
marginTop: '25px',
display: 'flex',
justifyContent: 'center'
}}>
{showLoadMore && (
<CustomButton onClick={loadMore}>Load older announcements</CustomButton>
)}
</Box>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
marginTop: '25px',
width: '100%',
}}
>
{showLoadMore && (
<CustomButton onClick={loadMore}>
Load older announcements
</CustomButton>
)}
</Box>
</div>
);
};

View File

@@ -1,56 +0,0 @@
import React, { useState } from "react";
import InfiniteScroll from "react-infinite-scroller";
import {
MainContainer,
ChatContainer,
MessageList,
Message,
MessageInput,
Avatar
} from "@chatscope/chat-ui-kit-react";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
export const ChatContainerComp = ({messages}) => {
// const [messages, setMessages] = useState([
// { id: 1, text: "Hello! How are you?", sender: "Joe"},
// { id: 2, text: "I'm good, thank you!", sender: "Me" }
// ]);
// const loadMoreMessages = () => {
// // Simulate loading more messages (you could fetch these from an API)
// const moreMessages = [
// { id: 3, text: "What about you?", sender: "Joe", direction: "incoming" },
// { id: 4, text: "I'm great, thanks!", sender: "Me", direction: "outgoing" }
// ];
// setMessages((prevMessages) => [...moreMessages, ...prevMessages]);
// };
return (
<div style={{ height: "500px", width: "300px" }}>
<MainContainer>
<ChatContainer>
<MessageList>
{messages.map((msg) => (
<Message
key={msg.id}
model={{
message: msg.text,
sentTime: "just now",
sender: msg.senderName,
direction: 'incoming',
position: "single"
}}
>
{msg.direction === "incoming" && <Avatar name={msg.senderName} />}
</Message>
))}
</MessageList>
<MessageInput placeholder="Type a message..." />
</ChatContainer>
</MainContainer>
</div>
);
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,59 @@
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 { LoadingButton } from '@mui/lab';
import { MyContext, getArbitraryEndpointReact, getBaseApiReact, pauseAllQueues } from '../../App';
import {
MyContext,
getArbitraryEndpointReact,
getBaseApiReact,
pauseAllQueues,
} from '../../App';
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 { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
import { useSetAtom } from 'jotai';
import { txListAtom } from '../../atoms/global';
export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, secretKeyDetails, userInfo, noSecretKey, setHideCommonKeyPopup, setIsForceShowCreationKeyPopup, isForceShowCreationKeyPopup}) => {
const { show, setTxList } = useContext(MyContext);
export const CreateCommonSecret = ({
groupId,
secretKey,
isOwner,
myAddress,
secretKeyDetails,
userInfo,
noSecretKey,
setHideCommonKeyPopup,
setIsForceShowCreationKeyPopup,
isForceShowCreationKeyPopup,
}) => {
const { show } = useContext(MyContext);
const setTxList = useSetAtom(txListAtom);
const [openSnack, setOpenSnack] = React.useState(false);
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 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-${
groupId
}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`;
const response = await fetch(url);
if(!response.ok){
throw new Error('network error')
if (!response.ok) {
throw new Error('network error');
}
const adminData = await response.json();
const filterId = adminData.filter(
(data: any) =>
data.identifier === `symmetric-qchat-group-${groupId}`
(data: any) => data.identifier === `symmetric-qchat-group-${groupId}`
);
if (filterId?.length === 0) {
return false;
@@ -38,149 +62,182 @@ export const CreateCommonSecret = ({groupId, secretKey, isOwner, myAddress, sec
// Get the most recent date for both a and b
const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
const dateB = b.updated ? new Date(b.updated) : new Date(b.created);
// Sort by most recent
return dateB.getTime() - dateA.getTime();
});
return sortedData[0];
};
const getSecretKey = async (loadingGroupParam?: boolean, secretKeyToPublish?: boolean) => {
const getSecretKey = async (
loadingGroupParam?: boolean,
secretKeyToPublish?: boolean
) => {
try {
pauseAllQueues()
const {names} = await getGroupAdmins(groupId);
if(!names.length){
throw new Error('Network error')
pauseAllQueues();
const { names } = await getGroupAdmins(groupId);
if (!names.length) {
throw new Error('Network error');
}
const publish = await getPublishesFromAdmins(names);
if (publish === false) {
return false;
}
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
publish.identifier
}?encoding=base64&rebuild=true`
);
const data = await res.text();
const decryptedKey: any = await decryptResource(data);
const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
if (!validateSecretKey(decryptedKeyToObject))
throw new Error("SecretKey is not valid");
throw new Error('SecretKey is not valid');
if (decryptedKeyToObject) {
return decryptedKeyToObject;
} else {
}
} catch (error) {
} finally {
console.log(error);
}
};
const createCommonSecret = async ()=> {
try {
const fee = await getFee('ARBITRARY')
await show({
message: "Would you like to perform an ARBITRARY transaction?" ,
publishFee: fee.fee + ' QORT'
})
setIsLoading(true)
const createCommonSecret = async () => {
try {
const fee = await getFee('ARBITRARY');
await show({
message: 'Would you like to perform an ARBITRARY transaction?',
publishFee: fee.fee + ' QORT',
});
setIsLoading(true);
const secretKey2 = await getSecretKey()
if((!secretKey2 && secretKey2 !== false)) throw new Error('invalid secret key')
if (secretKey2 && !validateSecretKey(secretKey2)) throw new Error('invalid secret key')
const secretKey2 = await getSecretKey();
if (!secretKey2 && secretKey2 !== false)
throw new Error('invalid secret key');
if (secretKey2 && !validateSecretKey(secretKey2))
throw new Error('invalid secret key');
const secretKeyToSend = !secretKey2 ? null : secretKey2
window.sendMessage("encryptAndPublishSymmetricKeyGroupChat", {
groupId: groupId,
previousData: secretKeyToSend,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
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.",
});
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);
const secretKeyToSend = !secretKey2 ? null : secretKey2;
window
.sendMessage('encryptAndPublishSymmetricKeyGroupChat', {
groupId: groupId,
previousData: secretKeyToSend,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
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.',
});
} catch (error) {
}
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);
});
} catch (error) {
console.log(error);
}
};
return (
<Box sx={{
padding: '25px',
display: 'flex',
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={{
<Box
sx={{
background: theme.palette.background.default,
display: 'flex',
width: '100%',
justifyContent: 'flex-end'
}}>
<Button onClick={()=> {
setHideCommonKeyPopup(true)
setIsForceShowCreationKeyPopup(false)
}} size='small'>Hide</Button>
flexDirection: 'column',
gap: '25px',
maxWidth: '350px',
padding: '25px',
}}
>
<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>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} />
<CustomizedSnackbars
open={openSnack}
setOpen={setOpenSnack}
info={infoSnack}
setInfo={setInfoSnack}
/>
</Box>
)
}
);
};

View File

@@ -4,54 +4,49 @@ import React, {
useMemo,
useRef,
useState,
} from "react";
import { CreateCommonSecret } from "./CreateCommonSecret";
import { reusableGet } from "../../qdn/publish/pubish";
import { uint8ArrayToObject } from "../../backgroundFunctions/encryption";
} from 'react';
import { uint8ArrayToObject } from '../../backgroundFunctions/encryption';
import {
base64ToUint8Array,
objectToBase64,
} 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 Tiptap from "./TipTap";
import { AuthenticatedContainerInnerTop, CustomButton } from "../../App-styles";
import CircularProgress from "@mui/material/CircularProgress";
import { getBaseApi, getFee } from "../../background";
import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar";
import { Box, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer";
import ShortUniqueId from "short-unique-id";
import { AnnouncementList } from "./AnnouncementList";
const uid = new ShortUniqueId({ length: 8 });
import CampaignIcon from "@mui/icons-material/Campaign";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { AnnouncementDiscussion } from "./AnnouncementDiscussion";
} from '../../qdn/encryption/group-encryption';
import Tiptap from './TipTap';
import { CustomButton } from '../../styles/App-styles';
import CircularProgress from '@mui/material/CircularProgress';
import { getFee } from '../../background';
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
import { Box, Typography, useTheme } from '@mui/material';
import { Spacer } from '../../common/Spacer';
import ShortUniqueId from 'short-unique-id';
import { AnnouncementList } from './AnnouncementList';
import CampaignIcon from '@mui/icons-material/Campaign';
import { AnnouncementDiscussion } from './AnnouncementDiscussion';
import {
MyContext,
getArbitraryEndpointReact,
getBaseApiReact,
isMobile,
pauseAllQueues,
resumeAllQueues,
} from "../../App";
import { RequestQueueWithPromise } from "../../utils/queue/queue";
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
import { addDataPublishesFunc, getDataPublishesFunc } from "../Group/Group";
import { getRootHeight } from "../../utils/mobile/mobileUtils";
} from '../../App';
import { RequestQueueWithPromise } from '../../utils/queue/queue';
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { addDataPublishesFunc, getDataPublishesFunc } from '../Group/Group';
const uid = new ShortUniqueId({ length: 8 });
export const requestQueueCommentCount = new RequestQueueWithPromise(3);
export const requestQueuePublishedAccouncements = new RequestQueueWithPromise(
3
);
export const saveTempPublish = async ({ data, key }: any) => {
return new Promise((res, rej) => {
window.sendMessage("saveTempPublish", {
data,
key,
})
window
.sendMessage('saveTempPublish', {
data,
key,
})
.then((response) => {
if (!response?.error) {
res(response);
@@ -60,37 +55,37 @@ export const saveTempPublish = async ({ data, key }: any) => {
rej(response.error);
})
.catch((error) => {
rej(error.message || "An error occurred");
rej(error.message || 'An error occurred');
});
});
};
export const getTempPublish = async () => {
return new Promise((res, rej) => {
window.sendMessage("getTempPublish", {})
.then((response) => {
if (!response?.error) {
res(response);
return;
}
rej(response.error);
})
.catch((error) => {
rej(error.message || "An error occurred");
});
window
.sendMessage('getTempPublish', {})
.then((response) => {
if (!response?.error) {
res(response);
return;
}
rej(response.error);
})
.catch((error) => {
rej(error.message || 'An error occurred');
});
});
};
export const decryptPublishes = async (encryptedMessages: any[], secretKey) => {
try {
return await new Promise((res, rej) => {
window.sendMessage("decryptSingleForPublishes", {
data: encryptedMessages,
secretKeyObject: secretKey,
skipDecodeBase64: true,
})
window
.sendMessage('decryptSingleForPublishes', {
data: encryptedMessages,
secretKeyObject: secretKey,
skipDecodeBase64: true,
})
.then((response) => {
if (!response?.error) {
res(response);
@@ -99,27 +94,29 @@ export const decryptPublishes = async (encryptedMessages: any[], secretKey) => {
rej(response.error);
})
.catch((error) => {
rej(error.message || "An error occurred");
rej(error.message || 'An error occurred');
});
});
} catch (error) {}
} catch (error) {
console.log(error);
}
};
export const handleUnencryptedPublishes = (publishes) => {
let publishesData = []
publishes.forEach((pub)=> {
export const handleUnencryptedPublishes = (publishes) => {
let publishesData = [];
publishes.forEach((pub) => {
try {
const decryptToUnit8Array = base64ToUint8Array(pub);
const decodedData = uint8ArrayToObject(decryptToUnit8Array);
if(decodedData){
publishesData.push({decryptedData: decodedData})
if (decodedData) {
publishesData.push({ decryptedData: decodedData });
}
} catch (error) {
console.log(error);
}
})
return publishesData
});
return publishesData;
};
export const GroupAnnouncements = ({
selectedGroup,
secretKey,
@@ -130,7 +127,7 @@ export const GroupAnnouncements = ({
isAdmin,
hide,
myName,
isPrivate
isPrivate,
}) => {
const [messages, setMessages] = useState([]);
const [isSending, setIsSending] = useState(false);
@@ -141,7 +138,7 @@ export const GroupAnnouncements = ({
const [selectedAnnouncement, setSelectedAnnouncement] = useState(null);
const [isFocusedParent, setIsFocusedParent] = useState(false);
const { show, rootHeight } = React.useContext(MyContext);
const { show } = React.useContext(MyContext);
const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null);
const hasInitialized = useRef(false);
@@ -159,12 +156,15 @@ export const GroupAnnouncements = ({
useEffect(() => {
if (!selectedGroup) return;
(async () => {
const res = await getDataPublishesFunc(selectedGroup, "anc");
const res = await getDataPublishesFunc(selectedGroup, 'anc');
dataPublishes.current = res || {};
})();
}, [selectedGroup]);
const getAnnouncementData = async ({ identifier, name, resource }, isPrivate) => {
const getAnnouncementData = async (
{ identifier, name, resource },
isPrivate
) => {
try {
let data = dataPublishes.current[`${name}-${identifier}`];
if (
@@ -179,14 +179,17 @@ export const GroupAnnouncements = ({
});
if (!res?.ok) return;
data = await res.text();
await addDataPublishesFunc({ ...resource, data }, selectedGroup, "anc");
await addDataPublishesFunc({ ...resource, data }, selectedGroup, 'anc');
} else {
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];
if(!messageData) return
if (!messageData) return;
setAnnouncementData((prev) => {
return {
...prev,
@@ -194,12 +197,17 @@ export const GroupAnnouncements = ({
};
});
} catch (error) {
console.error("error", error);
console.error('error', error);
}
};
useEffect(() => {
if ((!secretKey && isPrivate) || hasInitializedWebsocket.current || isPrivate === null) return;
if (
(!secretKey && isPrivate) ||
hasInitializedWebsocket.current ||
isPrivate === null
)
return;
setIsLoading(true);
// initWebsocketMessageGroup()
hasInitializedWebsocket.current = true;
@@ -208,10 +216,11 @@ export const GroupAnnouncements = ({
const encryptChatMessage = async (data: string, secretKeyObject: any) => {
try {
return new Promise((res, rej) => {
window.sendMessage("encryptSingle", {
data,
secretKeyObject,
})
window
.sendMessage('encryptSingle', {
data,
secretKeyObject,
})
.then((response) => {
if (!response?.error) {
res(response);
@@ -220,19 +229,21 @@ export const GroupAnnouncements = ({
rej(response.error);
})
.catch((error) => {
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) => {
return new Promise((res, rej) => {
window.sendMessage("publishGroupEncryptedResource", {
encryptedData,
identifier,
})
window
.sendMessage('publishGroupEncryptedResource', {
encryptedData,
identifier,
})
.then((response) => {
if (!response?.error) {
res(response);
@@ -241,23 +252,14 @@ export const GroupAnnouncements = ({
rej(response.error);
})
.catch((error) => {
rej(error.message || "An error occurred");
rej(error.message || 'An error occurred');
});
});
};
const clearEditorContent = () => {
if (editorRef.current) {
editorRef.current.chain().focus().clearContent().run();
if (isMobile) {
setTimeout(() => {
editorRef.current?.chain().blur().run();
setIsFocusedParent(false);
setTimeout(() => {
triggerRerender();
}, 300);
}, 200);
}
}
};
@@ -266,39 +268,46 @@ export const GroupAnnouncements = ({
const getTempAnnouncements = await getTempPublish();
if (getTempAnnouncements?.announcement) {
let tempData = [];
Object.keys(getTempAnnouncements?.announcement || {}).filter((annKey)=> annKey?.startsWith(`grp-${selectedGroup}-anc`)).map((key) => {
const value = getTempAnnouncements?.announcement[key];
tempData.push(value.data);
});
Object.keys(getTempAnnouncements?.announcement || {})
.filter((annKey) => annKey?.startsWith(`grp-${selectedGroup}-anc`))
.map((key) => {
const value = getTempAnnouncements?.announcement[key];
tempData.push(value.data);
});
setTempPublishedList(tempData);
}
} catch (error) {}
} catch (error) {
console.log(error);
}
};
const publishAnnouncement = async () => {
try {
pauseAllQueues();
const fee = await getFee("ARBITRARY");
const fee = await getFee('ARBITRARY');
await show({
message: "Would you like to perform a ARBITRARY transaction?",
publishFee: fee.fee + " QORT",
message: 'Would you like to perform a ARBITRARY transaction?',
publishFee: fee.fee + ' QORT',
});
if (isSending) return;
if (editorRef.current) {
const htmlContent = editorRef.current.getHTML();
if (!htmlContent?.trim() || htmlContent?.trim() === "<p></p>") return;
if (!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') return;
setIsSending(true);
const message = {
version: 1,
extra: {},
message: htmlContent,
};
const secretKeyObject = isPrivate === false ? null : await getSecretKey(false, true);
const message64: any = await objectToBase64(message);
const encryptSingle = isPrivate === false ? message64 : await encryptChatMessage(
message64,
secretKeyObject
);
const secretKeyObject =
isPrivate === false ? null : await getSecretKey(false, true);
const message64: any = await objectToBase64(message);
const encryptSingle =
isPrivate === false
? message64
: await encryptChatMessage(message64, secretKeyObject);
const randomUid = uid.rnd();
const identifier = `grp-${selectedGroup}-anc-${randomUid}`;
const res = await publishAnc({
@@ -309,13 +318,13 @@ export const GroupAnnouncements = ({
const dataToSaveToStorage = {
name: myName,
identifier,
service: "DOCUMENT",
service: 'DOCUMENT',
tempData: message,
created: Date.now(),
};
await saveTempPublish({
data: dataToSaveToStorage,
key: "announcement",
key: 'announcement',
});
setTempData(selectedGroup);
clearEditorContent();
@@ -324,7 +333,7 @@ export const GroupAnnouncements = ({
} catch (error) {
if (!error) return;
setInfoSnack({
type: "error",
type: 'error',
message: error,
});
setOpenSnack(true);
@@ -343,9 +352,9 @@ export const GroupAnnouncements = ({
const identifier = `grp-${selectedGroup}-anc-`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
@@ -354,23 +363,30 @@ export const GroupAnnouncements = ({
setAnnouncements(responseData);
setIsLoading(false);
for (const data of responseData) {
getAnnouncementData({
name: data.name,
identifier: data.identifier,
resource: data,
}, isPrivate);
getAnnouncementData(
{
name: data.name,
identifier: data.identifier,
resource: data,
},
isPrivate
);
}
} catch (error) {
} finally {
// dispatch(setIsLoadingGlobal(false))
console.log(error);
}
},
[secretKey]
);
React.useEffect(() => {
if(!secretKey && isPrivate) return
if (selectedGroup && !hasInitialized.current && !hide && isPrivate !== null) {
if (!secretKey && isPrivate) return;
if (
selectedGroup &&
!hasInitialized.current &&
!hide &&
isPrivate !== null
) {
getAnnouncements(selectedGroup, isPrivate);
hasInitialized.current = true;
}
@@ -384,9 +400,9 @@ export const GroupAnnouncements = ({
const identifier = `grp-${selectedGroup}-anc-`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
@@ -394,21 +410,28 @@ export const GroupAnnouncements = ({
setAnnouncements((prev) => [...prev, ...responseData]);
setIsLoading(false);
for (const data of responseData) {
getAnnouncementData({ name: data.name, identifier: data.identifier }, isPrivate);
getAnnouncementData(
{ name: data.name, identifier: data.identifier },
isPrivate
);
}
} catch (error) {}
} catch (error) {
console.log(error);
}
};
const interval = useRef<any>(null);
const theme = useTheme();
const checkNewMessages = React.useCallback(async () => {
try {
const identifier = `grp-${selectedGroup}-anc-`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
@@ -416,11 +439,16 @@ export const GroupAnnouncements = ({
if (!latestMessage) {
for (const data of responseData) {
try {
getAnnouncementData({
name: data.name,
identifier: data.identifier,
}, isPrivate);
} catch (error) {}
getAnnouncementData(
{
name: data.name,
identifier: data.identifier,
},
isPrivate
);
} catch (error) {
console.log(error);
}
}
setAnnouncements(responseData);
return;
@@ -434,12 +462,17 @@ export const GroupAnnouncements = ({
for (const data of newArray) {
try {
getAnnouncementData({ name: data.name, identifier: data.identifier }, isPrivate);
} catch (error) {}
getAnnouncementData(
{ name: data.name, identifier: data.identifier },
isPrivate
);
} catch (error) {
console.log(error);
}
}
setAnnouncements((prev) => [...newArray, ...prev]);
} catch (error) {
} finally {
console.log(error);
}
}, [announcements, secretKey, selectedGroup]);
@@ -485,14 +518,13 @@ export const GroupAnnouncements = ({
return (
<div
style={{
// reference to change height
height: isMobile ? `calc(${rootHeight} - 127px` : "calc(100vh - 70px)",
display: "flex",
flexDirection: "column",
width: "100%",
visibility: hide && "hidden",
position: hide && "fixed",
left: hide && "-1000px",
display: 'flex',
flexDirection: 'column',
height: 'calc(100vh - 70px)',
left: hide && '-1000px',
position: hide && 'fixed',
visibility: hide && 'hidden',
width: '100%',
}}
>
<AnnouncementDiscussion
@@ -509,63 +541,60 @@ export const GroupAnnouncements = ({
);
}
return (
<div
style={{
// reference to change height
height: isMobile ? `calc(${rootHeight} - 127px` : "calc(100vh - 70px)",
display: "flex",
flexDirection: "column",
width: "100%",
visibility: hide && "hidden",
position: hide && "fixed",
left: hide && "-1000px",
display: 'flex',
flexDirection: 'column',
height: 'calc(100vh - 70px)',
left: hide && '-1000px',
position: hide && 'fixed',
visibility: hide && 'hidden',
width: '100%',
}}
>
<div
style={{
position: "relative",
width: "100%",
display: "flex",
flexDirection: "column",
display: 'flex',
flexDirection: 'column',
flexShrink: 0,
position: 'relative',
width: '100%',
}}
>
{!isMobile && (
<Box
<Box
sx={{
alignItems: 'center',
display: 'flex',
fontSize: '20px',
gap: '20px',
justifyContent: 'center',
padding: '25px',
width: '100%',
}}
>
<CampaignIcon
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
padding: isMobile ? "8px" : "25px",
fontSize: isMobile ? "16px" : "20px",
gap: "20px",
alignItems: "center",
fontSize: '30px',
}}
>
<CampaignIcon
sx={{
fontSize: isMobile ? "16px" : "30px",
}}
/>
Group Announcements
</Box>
)}
/>
Group Announcements
</Box>
<Spacer height={isMobile ? "0px" : "25px"} />
<Spacer height={'25px'} />
</div>
{!isLoading && combinedListTempAndReal?.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
display: 'flex',
justifyContent: 'center',
width: '100%',
}}
>
<Typography
sx={{
fontSize: "16px",
fontSize: '16px',
}}
>
No announcements
@@ -587,31 +616,28 @@ export const GroupAnnouncements = ({
{isAdmin && (
<div
style={{
// position: 'fixed',
// bottom: '0px',
backgroundColor: "#232428",
minHeight: isMobile ? "0px" : "150px",
maxHeight: isMobile ? "auto" : "400px",
display: "flex",
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",
backgroundColor: theme.palette.background.default,
bottom: isFocusedParent ? '0px' : 'unset',
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
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
style={{
display: "flex",
flexDirection: "column",
flexGrow: isMobile && 1,
overflow: "auto",
// height: '100%',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
overflow: 'auto',
}}
>
<Tiptap
@@ -623,14 +649,15 @@ export const GroupAnnouncements = ({
setIsFocusedParent={setIsFocusedParent}
/>
</div>
<Box
sx={{
display: "flex",
width: "100&",
gap: "10px",
justifyContent: "center",
display: 'flex',
flexShrink: 0,
position: "relative",
gap: '10px',
justifyContent: 'center',
position: 'relative',
width: '100&',
}}
>
{isFocusedParent && (
@@ -639,49 +666,52 @@ export const GroupAnnouncements = ({
if (isSending) return;
setIsFocusedParent(false);
clearEditorContent();
setTimeout(() => {
triggerRerender();
}, 300);
setTimeout(() => {
triggerRerender();
}, 300);
// Unfocus the editor
}}
style={{
marginTop: "auto",
alignSelf: "center",
cursor: isSending ? "default" : "pointer",
background: "var(--danger)",
alignSelf: 'center',
background: theme.palette.other.danger,
cursor: isSending ? 'default' : 'pointer',
flexShrink: 0,
padding: isMobile && "5px",
fontSize: isMobile && "14px",
fontSize: '14px',
marginTop: 'auto',
padding: '5px',
}}
>
{` Close`}
</CustomButton>
)}
<CustomButton
onClick={() => {
if (isSending) return;
publishAnnouncement();
}}
style={{
marginTop: "auto",
alignSelf: "center",
cursor: isSending ? "default" : "pointer",
background: isSending && "rgba(0, 0, 0, 0.8)",
alignSelf: 'center',
background: isSending
? theme.palette.background.default
: theme.palette.background.paper,
cursor: isSending ? 'default' : 'pointer',
flexShrink: 0,
padding: isMobile && "5px",
fontSize: isMobile && "14px",
fontSize: '14px',
marginTop: 'auto',
padding: '5px',
}}
>
{isSending && (
<CircularProgress
size={18}
sx={{
position: "absolute",
top: "50%",
left: "50%",
marginTop: "-12px",
marginLeft: "-12px",
color: "white",
color: theme.palette.text.primary,
left: '50%',
marginLeft: '-12px',
marginTop: '-12px',
position: 'absolute',
top: '50%',
}}
/>
)}
@@ -701,7 +731,7 @@ export const GroupAnnouncements = ({
<LoadingSnackbar
open={isLoading}
info={{
message: "Loading announcements... please wait.",
message: 'Loading announcements... please wait.',
}}
/>
</div>

View File

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

View File

@@ -1,69 +1,68 @@
import React, {
forwardRef, useEffect, useImperativeHandle,
useState,
} from 'react'
export default forwardRef((props, ref) => {
const [selectedIndex, setSelectedIndex] = useState(0)
const selectItem = index => {
const item = props.items[index]
if (item) {
props.command(item)
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
export default forwardRef((props, ref) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index) => {
const item = props.items[index];
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;
}
}
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') {
downHandler()
return true
}
if (event.key === 'Enter') {
enterHandler()
return true
}
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>
)
})
if (event.key === 'ArrowDown') {
downHandler();
return true;
}
if (event.key === 'Enter') {
enterHandler();
return true;
}
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 './styles.css';
import './chat.css';
import { executeEvent } from '../../utils/events';
import { Embed } from '../Embeds/Embed';
import { Box, useTheme } from '@mui/material';
export const extractComponents = (url) => {
if (!url || !url.startsWith("qortal://")) {
if (!url || !url.startsWith('qortal://')) {
return null;
}
// Skip links starting with "qortal://use-"
if (url.startsWith("qortal://use-")) {
if (url.startsWith('qortal://use-')) {
return null;
}
url = url.replace(/^(qortal\:\/\/)/, "");
if (url.includes("/")) {
let parts = url.split("/");
url = url.replace(/^(qortal\:\/\/)/, '');
if (url.includes('/')) {
let parts = url.split('/');
const service = parts[0].toUpperCase();
parts.shift();
const name = parts[0];
parts.shift();
let identifier;
const path = parts.join("/");
const path = parts.join('/');
return { service, name, identifier, path };
}
@@ -64,8 +65,7 @@ function processText(input) {
}
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;
const urlPattern = /(\bhttps?:\/\/[^\s<]+|\bwww\.[^\s<]+)/g;
textFormatted = text.replace(urlPattern, (url) => {
@@ -75,22 +75,68 @@ const linkify = (text) => {
return processText(textFormatted);
};
export const MessageDisplay = ({ htmlContent, isReply }) => {
const theme = useTheme();
const sanitizedContent = useMemo(()=> {
const sanitizedContent = useMemo(() => {
return DOMPurify.sanitize(linkify(htmlContent), {
ALLOWED_TAGS: [
'a', 'b', 'i', 'em', 'strong', 'p', 'br', 'div', 'span', 'img',
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 's', 'hr'
'a',
'b',
'i',
'em',
'strong',
'p',
'br',
'div',
'span',
'img',
'ul',
'ol',
'li',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'blockquote',
'code',
'pre',
'table',
'thead',
'tbody',
'tr',
'th',
'td',
's',
'hr',
],
ALLOWED_ATTR: [
'href', 'target', 'rel', 'class', 'src', 'alt', 'title',
'width', 'height', 'style', 'align', 'valign', 'colspan', 'rowspan', 'border', 'cellpadding', 'cellspacing', 'data-url'
'href',
'target',
'rel',
'class',
'src',
'alt',
'title',
'width',
'height',
'style',
'align',
'valign',
'colspan',
'rowspan',
'border',
'cellpadding',
'cellspacing',
'data-url',
],
}).replace(/<span[^>]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g, '');
}, [htmlContent])
}).replace(
/<span[^>]*data-url="qortal:\/\/use-embed\/[^"]*"[^>]*>.*?<\/span>/g,
''
);
}, [htmlContent]);
const handleClick = async (e) => {
e.preventDefault();
@@ -98,7 +144,7 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
const target = e.target;
if (target.tagName === 'A') {
const href = target.getAttribute('href');
if(window?.electronAPI){
if (window?.electronAPI) {
window.electronAPI.openExternal(href);
} else {
window.open(href, '_system');
@@ -106,32 +152,32 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
} else if (target.getAttribute('data-url')) {
const url = target.getAttribute('data-url');
let copyUrl = url
let copyUrl = url;
try {
copyUrl = copyUrl.replace(/^(qortal:\/\/)/, '')
if (copyUrl.startsWith('use-')) {
// Handle the new 'use' format
const parts = copyUrl.split('/')
const type = parts[0].split('-')[1] // e.g., 'group' from 'use-group'
parts.shift()
const action = parts.length > 0 ? parts[0].split('-')[1] : null // e.g., 'invite' from 'action-invite'
parts.shift()
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'
if(action === 'join'){
executeEvent("globalActionJoinGroup", { groupId: id});
return
try {
copyUrl = copyUrl.replace(/^(qortal:\/\/)/, '');
if (copyUrl.startsWith('use-')) {
// Handle the new 'use' format
const parts = copyUrl.split('/');
const type = parts[0].split('-')[1]; // e.g., 'group' from 'use-group'
parts.shift();
const action = parts.length > 0 ? parts[0].split('-')[1] : null; // e.g., 'invite' from 'action-invite'
parts.shift();
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'
if (action === 'join') {
executeEvent('globalActionJoinGroup', { groupId: id });
return;
}
}
} catch (error) {
//error
}
} catch (error) {
//error
}
const res = extractComponents(url);
if (res) {
const { service, name, identifier, path } = res;
executeEvent("addTab", { data: { service, name, identifier, path } });
executeEvent("open-apps-mode", { });
executeEvent('addTab', { data: { service, name, identifier, path } });
executeEvent('open-apps-mode', {});
}
}
};
@@ -141,19 +187,24 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
let embedData = null;
if (embedLink) {
embedData = embedLink[0]
embedData = embedLink[0];
}
return (
<>
{embedLink && (
<Embed embedLink={embedData} />
)}
<div
className={`tiptap ${isReply ? 'isReply' : ''}`}
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
onClick={handleClick}
/>
</>
<Box
sx={{
'--text-primary': theme.palette.text.primary,
'--text-secondary': theme.palette.text.secondary,
'--background-default': theme.palette.background.default,
'--background-secondary': theme.palette.background.paper,
}}
>
{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 { useTheme } from '@mui/material';
const ResizableImage = ({ node, updateAttributes, selected }) => {
const imgRef = useRef(null);
const theme = useTheme();
const startResizing = (e) => {
e.preventDefault();
@@ -40,18 +42,23 @@ const ResizableImage = ({ node, updateAttributes, selected }) => {
src={node.attrs.src}
alt={node.attrs.alt || ''}
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
/>
<div
style={{
backgroundColor: theme.palette.background.paper,
bottom: 0,
cursor: 'nwse-resize',
height: '10px',
position: 'absolute',
right: 0,
bottom: 0,
width: '10px',
height: '10px',
backgroundColor: 'gray',
cursor: 'nwse-resize',
zIndex: 1, // Ensure the resize handle is above other content
}}
onMouseDown={startResizing}

View File

@@ -1,42 +1,35 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { EditorProvider, useCurrentEditor, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { Color } from "@tiptap/extension-color";
import ListItem from "@tiptap/extension-list-item";
import TextStyle from "@tiptap/extension-text-style";
import Placeholder from "@tiptap/extension-placeholder";
import Image from "@tiptap/extension-image";
import IconButton from "@mui/material/IconButton";
import FormatBoldIcon from "@mui/icons-material/FormatBold";
import FormatItalicIcon from "@mui/icons-material/FormatItalic";
import StrikethroughSIcon from "@mui/icons-material/StrikethroughS";
import FormatClearIcon from "@mui/icons-material/FormatClear";
import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted";
import FormatListNumberedIcon from "@mui/icons-material/FormatListNumbered";
import CodeIcon from "@mui/icons-material/Code";
import ImageIcon from "@mui/icons-material/Image"; // Import Image icon
import FormatQuoteIcon from "@mui/icons-material/FormatQuote";
import HorizontalRuleIcon from "@mui/icons-material/HorizontalRule";
import UndoIcon from "@mui/icons-material/Undo";
import RedoIcon from "@mui/icons-material/Redo";
import FormatHeadingIcon from "@mui/icons-material/FormatSize";
import DeveloperModeIcon from "@mui/icons-material/DeveloperMode";
import Compressor from "compressorjs";
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { EditorProvider, useCurrentEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { Color } from '@tiptap/extension-color';
import ListItem from '@tiptap/extension-list-item';
import TextStyle from '@tiptap/extension-text-style';
import Placeholder from '@tiptap/extension-placeholder';
import IconButton from '@mui/material/IconButton';
import FormatBoldIcon from '@mui/icons-material/FormatBold';
import FormatItalicIcon from '@mui/icons-material/FormatItalic';
import StrikethroughSIcon from '@mui/icons-material/StrikethroughS';
import FormatClearIcon from '@mui/icons-material/FormatClear';
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
import CodeIcon from '@mui/icons-material/Code';
import ImageIcon from '@mui/icons-material/Image'; // Import Image icon
import FormatQuoteIcon from '@mui/icons-material/FormatQuote';
import HorizontalRuleIcon from '@mui/icons-material/HorizontalRule';
import UndoIcon from '@mui/icons-material/Undo';
import RedoIcon from '@mui/icons-material/Redo';
import FormatHeadingIcon from '@mui/icons-material/FormatSize';
import DeveloperModeIcon from '@mui/icons-material/DeveloperMode';
import Compressor from 'compressorjs';
import Mention from '@tiptap/extension-mention';
import ImageResize from "tiptap-extension-resize-image"; // Import the ResizeImage extension
import { isMobile } from "../../App";
import tippy from "tippy.js";
import "tippy.js/dist/tippy.css";
import Popover from '@mui/material/Popover';
import List from '@mui/material/List';
import ListItemMui from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
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";
import ImageResize from 'tiptap-extension-resize-image'; // Import the ResizeImage extension
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
import { ReactRenderer } from '@tiptap/react';
import MentionList from './MentionList.jsx';
import { isDisabledEditorEnterAtom } from '../../atoms/global.js';
import { Box, Checkbox, Typography, useTheme } from '@mui/material';
import { useAtom } from 'jotai';
function textMatcher(doc, from) {
const textBeforeCursor = doc.textBetween(0, from, ' ', ' ');
@@ -47,266 +40,298 @@ function textMatcher(doc, from) {
const query = match[0];
return { start, query };
}
const MenuBar = ({ setEditorRef, isChat, isDisabledEditorEnter, setIsDisabledEditorEnter }) => {
const { editor } = useCurrentEditor();
const fileInputRef = useRef(null);
if (!editor) {
return null;
}
const MenuBar = React.memo(
({
setEditorRef,
isChat,
isDisabledEditorEnter,
setIsDisabledEditorEnter,
}) => {
const { editor } = useCurrentEditor();
const fileInputRef = useRef(null);
const theme = useTheme();
useEffect(() => {
if (editor && setEditorRef) {
setEditorRef(editor);
useEffect(() => {
if (editor && setEditorRef) {
setEditorRef(editor);
}
}, [editor, setEditorRef]);
if (!editor) {
return null;
}
}, [editor, setEditorRef]);
const handleImageUpload = async (file) => {
let compressedFile;
await new Promise<void>((resolve) => {
new Compressor(file, {
quality: 0.6,
maxWidth: 1200,
mimeType: "image/webp",
success(result) {
compressedFile = new File([result], "image.webp", {
type: "image/webp",
});
resolve();
},
error(err) {
console.error("Image compression error:", err);
},
const handleImageUpload = async (file) => {
let compressedFile;
await new Promise<void>((resolve) => {
new Compressor(file, {
quality: 0.6,
maxWidth: 1200,
mimeType: 'image/webp',
success(result) {
compressedFile = new File([result], 'image.webp', {
type: 'image/webp',
});
resolve();
},
error(err) {
console.error('Image compression error:', err);
},
});
});
});
if (compressedFile) {
const reader = new FileReader();
reader.onload = () => {
const url = reader.result;
editor
.chain()
.focus()
.setImage({ src: url, style: "width: auto" })
.run();
fileInputRef.current.value = "";
};
reader.readAsDataURL(compressedFile);
}
};
if (compressedFile) {
const reader = new FileReader();
reader.onload = () => {
const url = reader.result;
editor
.chain()
.focus()
.setImage({ src: url, style: 'width: auto' })
.run();
fileInputRef.current.value = '';
};
reader.readAsDataURL(compressedFile);
}
};
const triggerImageUpload = () => {
fileInputRef.current.click(); // Trigger the file input click
};
const triggerImageUpload = () => {
fileInputRef.current.click(); // Trigger the file input click
};
const handlePaste = (event) => {
const items = event.clipboardData.items;
for (const item of items) {
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
event.preventDefault(); // Prevent the default paste behavior
handleImageUpload(file); // Call the image upload function
const handlePaste = (event) => {
const items = event.clipboardData.items;
for (const item of items) {
if (item.type.startsWith('image/')) {
const file = item.getAsFile();
if (file) {
event.preventDefault(); // Prevent the default paste behavior
handleImageUpload(file); // Call the image upload function
}
}
}
}
};
};
useEffect(() => {
if (editor) {
editor.view.dom.addEventListener("paste", handlePaste);
return () => {
editor.view.dom.removeEventListener("paste", handlePaste);
};
}
}, [editor]);
useEffect(() => {
if (editor) {
editor.view.dom.addEventListener('paste', handlePaste);
return () => {
editor.view.dom.removeEventListener('paste', handlePaste);
};
}
}, [editor]);
return (
<div className="control-group">
<div className="button-group" style={{
display: 'flex'
}}>
<IconButton
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
sx={{
color: editor.isActive("bold") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
return (
<div className="control-group">
<div
className="button-group"
style={{
display: 'flex',
}}
>
<FormatBoldIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()}
sx={{
color: editor.isActive("italic") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatItalicIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={!editor.can().chain().focus().toggleStrike().run()}
sx={{
color: editor.isActive("strike") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<StrikethroughSIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleCode().run()}
disabled={!editor.can().chain().focus().toggleCode().run()}
sx={{
color: editor.isActive("code") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<CodeIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().unsetAllMarks().run()}
sx={{
color:
editor.isActive("bold") ||
editor.isActive("italic") ||
editor.isActive("strike") ||
editor.isActive("code")
? "white"
: "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatClearIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleBulletList().run()}
sx={{
color: editor.isActive("bulletList") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatListBulletedIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleOrderedList().run()}
sx={{
color: editor.isActive("orderedList") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatListNumberedIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
sx={{
color: editor.isActive("codeBlock") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<DeveloperModeIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleBlockquote().run()}
sx={{
color: editor.isActive("blockquote") ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatQuoteIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().setHorizontalRule().run()}
disabled={!editor.can().chain().focus().setHorizontalRule().run()}
sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }}
>
<HorizontalRuleIcon />
</IconButton>
<IconButton
onClick={() =>
editor.chain().focus().toggleHeading({ level: 1 }).run()
}
sx={{
color: editor.isActive("heading", { level: 1 }) ? "white" : "gray",
padding: isMobile ? "5px" : "revert",
}}
>
<FormatHeadingIcon fontSize="small" />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().chain().focus().undo().run()}
sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }}
>
<UndoIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().chain().focus().redo().run()}
sx={{ color: "gray" }}
>
<RedoIcon />
</IconButton>
{isChat && (
<Box
sx={{
display: "flex",
alignItems: "center",
marginLeft: '5px',
cursor: 'pointer'
}}
onClick={()=> {
setIsDisabledEditorEnter(!isDisabledEditorEnter)
}}
>
<Checkbox
edge="start"
tabIndex={-1}
disableRipple
checked={isDisabledEditorEnter}
sx={{
"&.Mui-checked": {
color: "gray", // Customize the color when checked
},
"& .MuiSvgIcon-root": {
color: "gray",
},
}}
/>
<Typography
<IconButton
onClick={() => editor.chain().focus().toggleBold().run()}
disabled={!editor.can().chain().focus().toggleBold().run()}
sx={{
color: editor.isActive('bold')
? theme.palette.text.primary
: theme.palette.text.secondary,
padding: 'revert',
}}
>
<FormatBoldIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleItalic().run()}
disabled={!editor.can().chain().focus().toggleItalic().run()}
sx={{
color: editor.isActive('italic')
? theme.palette.text.primary
: theme.palette.text.secondary,
padding: 'revert',
}}
>
<FormatItalicIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleStrike().run()}
disabled={!editor.can().chain().focus().toggleStrike().run()}
sx={{
color: editor.isActive('strike')
? theme.palette.text.primary
: theme.palette.text.secondary,
padding: 'revert',
}}
>
<StrikethroughSIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleCode().run()}
disabled={!editor.can().chain().focus().toggleCode().run()}
sx={{
color: editor.isActive('code')
? theme.palette.text.primary
: theme.palette.text.secondary,
padding: 'revert',
}}
>
<CodeIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().unsetAllMarks().run()}
sx={{
color:
editor.isActive('bold') ||
editor.isActive('italic') ||
editor.isActive('strike') ||
editor.isActive('code')
? theme.palette.text.primary
: theme.palette.text.secondary,
padding: 'revert',
}}
>
<FormatClearIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleBulletList().run()}
sx={{
color: editor.isActive('bulletList')
? theme.palette.text.primary
: theme.palette.text.secondary,
padding: 'revert',
}}
>
<FormatListBulletedIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleOrderedList().run()}
sx={{
color: editor.isActive('orderedList')
? theme.palette.text.primary
: theme.palette.text.secondary,
padding: 'revert',
}}
>
<FormatListNumberedIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
sx={{
color: editor.isActive('codeBlock')
? theme.palette.text.primary
: theme.palette.text.secondary,
padding: 'revert',
}}
>
<DeveloperModeIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().toggleBlockquote().run()}
sx={{
color: editor.isActive('blockquote')
? theme.palette.text.primary
: theme.palette.text.secondary,
padding: 'revert',
}}
>
<FormatQuoteIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().setHorizontalRule().run()}
disabled={!editor.can().chain().focus().setHorizontalRule().run()}
sx={{ color: 'gray', padding: 'revert' }}
>
<HorizontalRuleIcon />
</IconButton>
<IconButton
onClick={() =>
editor.chain().focus().toggleHeading({ level: 1 }).run()
}
sx={{
color: editor.isActive('heading', { level: 1 })
? theme.palette.text.primary
: theme.palette.text.secondary,
padding: 'revert',
}}
>
<FormatHeadingIcon fontSize="small" />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().chain().focus().undo().run()}
sx={{ color: 'gray', padding: 'revert' }}
>
<UndoIcon />
</IconButton>
<IconButton
onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().chain().focus().redo().run()}
sx={{ color: 'gray' }}
>
<RedoIcon />
</IconButton>
{isChat && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
marginLeft: '5px',
cursor: 'pointer',
}}
onClick={() => {
setIsDisabledEditorEnter(!isDisabledEditorEnter);
}}
>
<Checkbox
edge="start"
tabIndex={-1}
disableRipple
checked={isDisabledEditorEnter}
sx={{
fontSize: "14px",
color: 'gray'
'&.Mui-checked': {
color: theme.palette.text.secondary,
},
'& .MuiSvgIcon-root': {
color: theme.palette.text.secondary,
},
}}
/>
<Typography
sx={{
fontSize: '14px',
color: theme.palette.text.primary,
}}
>
disable enter
</Typography>
</Box>
)}
{!isChat && (
<>
<IconButton
onClick={triggerImageUpload}
sx={{ color: "gray", padding: isMobile ? "5px" : "revert" }}
>
<ImageIcon />
</IconButton>
<input
type="file"
ref={fileInputRef}
style={{ display: "none" }}
onChange={(event) => handleImageUpload(event.target.files[0])}
accept="image/*"
/>
</>
)}
</Box>
)}
{!isChat && (
<>
<IconButton
onClick={triggerImageUpload}
sx={{
color: theme.palette.text.secondary,
padding: 'revert',
}}
>
<ImageIcon />
</IconButton>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
onChange={(event) => handleImageUpload(event.target.files[0])}
accept="image/*"
/>
</>
)}
</div>
</div>
</div>
);
};
);
}
);
const extensions = [
Color.configure({ types: [TextStyle.name, ListItem.name] }),
@@ -322,7 +347,7 @@ const extensions = [
},
}),
Placeholder.configure({
placeholder: "Start typing here...",
placeholder: 'Start typing here...',
}),
ImageResize,
];
@@ -340,18 +365,21 @@ export default ({
overrideMobile,
customEditorHeight,
membersWithNames,
enableMentions
enableMentions,
}) => {
const [isDisabledEditorEnter, setIsDisabledEditorEnter] = useRecoilState(isDisabledEditorEnterAtom)
const theme = useTheme();
const [isDisabledEditorEnter, setIsDisabledEditorEnter] = useAtom(
isDisabledEditorEnterAtom
);
const extensionsFiltered = isChat
? extensions.filter((item) => item?.name !== "image")
? extensions.filter((item) => item?.name !== 'image')
: extensions;
const editorRef = useRef(null);
const setEditorRefFunc = (editorInstance) => {
const setEditorRefFunc = useCallback((editorInstance) => {
editorRef.current = editorInstance;
setEditorRef(editorInstance);
};
}, []);
// const users = [
// { id: 1, label: 'Alice' },
@@ -359,40 +387,29 @@ export default ({
// { id: 3, label: 'Charlie' },
// ];
const users = useMemo(()=> {
return (membersWithNames || [])?.map((item)=> {
const users = useMemo(() => {
return (membersWithNames || [])?.map((item) => {
return {
id: item,
label: item
}
})
}, [membersWithNames])
label: item,
};
});
}, [membersWithNames]);
const usersRef = useRef([]);
useEffect(() => {
usersRef.current = users; // Keep users up-to-date
}, [users]);
const handleFocus = () => {
if (!isMobile) return;
setIsFocusedParent(true);
};
const handleBlur = () => {
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
}
};
const additionalExtensions = useMemo(()=> {
if(!enableMentions) return []
const additionalExtensions = useMemo(() => {
if (!enableMentions) return [];
return [
Mention.configure({
HTMLAttributes: {
@@ -409,122 +426,125 @@ export default ({
let popup; // Reference to the Tippy.js instance
let component;
return {
onStart: props => {
component = new ReactRenderer(MentionList, {
props,
editor: props.editor,
})
if (!props.clientRect) {
return
}
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
})
},
onUpdate(props) {
component.updateProps(props)
if (!props.clientRect) {
return
}
popup[0].setProps({
getReferenceClientRect: props.clientRect,
})
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup[0].hide()
return true
}
return component.ref?.onKeyDown(props)
},
onExit() {
popup[0].destroy()
component.destroy()
},
}
return {
onStart: (props) => {
component = new ReactRenderer(MentionList, {
props,
editor: props.editor,
});
if (!props.clientRect) {
return;
}
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
});
},
onUpdate(props) {
component.updateProps(props);
if (!props.clientRect) {
return;
}
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup[0].hide();
return true;
}
return component.ref?.onKeyDown(props);
},
onExit() {
popup[0].destroy();
component.destroy();
},
};
},
},
})
]
}, [enableMentions])
}),
];
}, [enableMentions]);
const handleSetIsDisabledEditorEnter = useCallback((val)=> {
setIsDisabledEditorEnter(val)
const handleSetIsDisabledEditorEnter = useCallback((val) => {
setIsDisabledEditorEnter(val);
localStorage.setItem('settings-disable-editor-enter', JSON.stringify(val));
}, [])
}, []);
return (
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
height: '100%'
}}>
<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
<Box
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%',
justifyContent: 'space-between',
'--text-primary': theme.palette.text.primary,
'--text-secondary': theme.palette.text.secondary,
'--background-default': theme.palette.background.default,
'--background-secondary': theme.palette.background.paper,
}}
onUpdate={({ editor }) => {
editor.on('focus', handleFocus); // Ensure focus is updated
editor.on('blur', handleBlur); // Ensure blur is updated
}}
editorProps={{
attributes: {
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();
>
<EditorProvider
slotBefore={
<MenuBar
setEditorRef={setEditorRefFunc}
isChat={isChat}
isDisabledEditorEnter={isDisabledEditorEnter}
setIsDisabledEditorEnter={handleSetIsDisabledEditorEnter}
/>
}
extensions={[...extensionsFiltered, ...additionalExtensions]}
content={content}
onCreate={({ editor }) => {
editor.on('blur', handleBlur); // Listen for blur event
}}
onUpdate={({ editor }) => {
editor.on('blur', handleBlur); // Ensure blur is updated
}}
editorProps={{
attributes: {
class: 'tiptap-prosemirror',
style: `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;
},
}}
/>
</div>
return false;
},
}}
/>
</Box>
);
};

View File

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

View File

@@ -1,35 +1,45 @@
import React, { useState, useRef, useMemo, useEffect } from 'react';
import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material';
import {
ListItemIcon,
Menu,
MenuItem,
Typography,
styled,
useTheme,
} from '@mui/material';
import MailOutlineIcon from '@mui/icons-material/MailOutline';
import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
import { executeEvent } from '../utils/events';
import { mutedGroupsAtom } from '../atoms/global';
import { useAtom } from 'jotai';
const CustomStyledMenu = styled(Menu)(({ theme }) => ({
'& .MuiPaper-root': {
backgroundColor: '#f9f9f9',
borderRadius: '12px',
padding: theme.spacing(1),
boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
'& .MuiPaper-root': {
// backgroundColor: '#f9f9f9',
borderRadius: '12px',
padding: theme.spacing(1),
boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
},
'& .MuiMenuItem-root': {
fontSize: '14px', // Smaller font size for the menu item text
// color: '#444',
transition: '0.3s background-color',
'&:hover': {
backgroundColor: theme.palette.action.hover, // Explicit hover state
},
'& .MuiMenuItem-root': {
fontSize: '14px', // Smaller font size for the menu item text
color: '#444',
transition: '0.3s background-color',
'&:hover': {
backgroundColor: '#f0f0f0', // Explicit hover state
},
},
}));
},
}));
export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups }) => {
export const ContextMenu = ({ children, groupId, getUserSettings }) => {
const [menuPosition, setMenuPosition] = useState(null);
const longPressTimeout = useRef(null);
const preventClick = useRef(false); // Flag to prevent click after long-press or right-click
const theme = useTheme();
const [mutedGroups] = useAtom(mutedGroupsAtom);
const isMuted = useMemo(()=> {
return mutedGroups.includes(groupId)
}, [mutedGroups, groupId])
const isMuted = useMemo(() => {
return mutedGroups.includes(groupId);
}, [mutedGroups, groupId]);
// Handle right-click (context menu) for desktop
const handleContextMenu = (event) => {
@@ -67,56 +77,52 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups })
}
};
const handleSetGroupMute = ()=> {
const handleSetGroupMute = () => {
try {
let value = [...mutedGroups]
if(isMuted){
value = value.filter((group)=> group !== groupId)
} else {
value.push(groupId)
}
window.sendMessage("addUserSettings", {
let value = [...mutedGroups];
if (isMuted) {
value = value.filter((group) => group !== groupId);
} else {
value.push(groupId);
}
window
.sendMessage('addUserSettings', {
keyValue: {
key: 'mutedGroups',
value,
},
})
.then((response) => {
if (response?.error) {
console.error("Error adding user settings:", response.error);
} else {
console.log("User settings added successfully");
}
})
.catch((error) => {
console.error("Failed to add user settings:", error.message || "An error occurred");
});
setTimeout(() => {
getUserSettings()
}, 400);
.then((response) => {
if (response?.error) {
console.error('Error adding user settings:', response.error);
} else {
console.log('User settings added successfully');
}
})
.catch((error) => {
console.error(
'Failed to add user settings:',
error.message || 'An error occurred'
);
});
} catch (error) {
}
}
setTimeout(() => {
getUserSettings();
}, 400);
} catch (error) {}
};
const handleClose = (e) => {
e.preventDefault();
e.stopPropagation();
e.stopPropagation();
setMenuPosition(null);
};
return (
<div
onContextMenu={handleContextMenu} // For desktop right-click
onTouchStart={handleTouchStart} // For mobile long-press start
onTouchEnd={handleTouchEnd} // For mobile long-press end
onContextMenu={handleContextMenu} // For desktop right-click
onTouchStart={handleTouchStart} // For mobile long-press start
onTouchEnd={handleTouchEnd} // For mobile long-press end
style={{ width: '100%', height: '100%' }}
>
{children}
@@ -131,35 +137,48 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups })
? { top: menuPosition.mouseY, left: menuPosition.mouseX }
: undefined
}
onClick={(e)=> {
e.stopPropagation();
}}
onClick={(e) => {
e.stopPropagation();
}}
>
<MenuItem onClick={(e) => {
handleClose(e)
executeEvent("markAsRead", {
groupId
});
}}>
<MenuItem
onClick={(e) => {
handleClose(e);
executeEvent('markAsRead', {
groupId,
});
}}
>
<ListItemIcon sx={{ minWidth: '32px' }}>
<MailOutlineIcon fontSize="small" />
<MailOutlineIcon
sx={{
color: theme.palette.text.primary,
}}
fontSize="small"
/>
</ListItemIcon>
<Typography variant="inherit" sx={{ fontSize: '14px' }}>
Mark As Read
</Typography>
</MenuItem>
<MenuItem onClick={(e) => {
handleClose(e)
handleSetGroupMute()
}}>
<MenuItem
onClick={(e) => {
handleClose(e);
handleSetGroupMute();
}}
>
<ListItemIcon sx={{ minWidth: '32px' }}>
<NotificationsOffIcon fontSize="small" sx={{
color: isMuted && 'red'
}} />
<NotificationsOffIcon
fontSize="small"
sx={{
color: isMuted ? 'red' : theme.palette.text.primary,
}}
/>
</ListItemIcon>
<Typography variant="inherit" sx={{ fontSize: '14px', color: isMuted && 'red' }}>
<Typography
variant="inherit"
sx={{ fontSize: '14px', color: isMuted && 'red' }}
>
{isMuted ? 'Unmute ' : 'Mute '}Push Notifications
</Typography>
</MenuItem>
@@ -167,5 +186,3 @@ export const ContextMenu = ({ children, groupId, getUserSettings, mutedGroups })
</div>
);
};

View File

@@ -1,152 +1,188 @@
import React, { useState, useRef } from 'react';
import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material';
import {
ListItemIcon,
Menu,
MenuItem,
Typography,
styled,
useTheme,
} from '@mui/material';
import PushPinIcon from '@mui/icons-material/PushPin';
import { saveToLocalStorage } from './Apps/AppsNavBar';
import { useRecoilState } from 'recoil';
import { saveToLocalStorage } from './Apps/AppsNavBarDesktop';
import { sortablePinnedAppsAtom } from '../atoms/global';
import { useSetAtom } from 'jotai';
const CustomStyledMenu = styled(Menu)(({ theme }) => ({
'& .MuiPaper-root': {
backgroundColor: '#f9f9f9',
borderRadius: '12px',
padding: theme.spacing(1),
boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
},
'& .MuiMenuItem-root': {
fontSize: '14px',
color: '#444',
transition: '0.3s background-color',
'&:hover': {
backgroundColor: '#f0f0f0',
},
'& .MuiPaper-root': {
borderRadius: '12px',
padding: theme.spacing(1),
boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
},
'& .MuiMenuItem-root': {
fontSize: '14px',
color: '#444',
transition: '0.3s background-color',
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
},
}));
export const ContextMenuPinnedApps = ({ children, app, isMine }) => {
const [menuPosition, setMenuPosition] = useState(null);
const longPressTimeout = useRef(null);
const maxHoldTimeout = useRef(null);
const preventClick = useRef(false);
const startTouchPosition = useRef({ x: 0, y: 0 }); // Track initial touch position
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom);
const [menuPosition, setMenuPosition] = useState(null);
const longPressTimeout = useRef(null);
const maxHoldTimeout = useRef(null);
const preventClick = useRef(false);
const startTouchPosition = useRef({ x: 0, y: 0 }); // Track initial touch position
const handleContextMenu = (event) => {
if(isMine) return
event.preventDefault();
event.stopPropagation();
preventClick.current = true;
setMenuPosition({
mouseX: event.clientX,
mouseY: event.clientY,
});
};
const setSortablePinnedApps = useSetAtom(sortablePinnedAppsAtom);
const handleTouchStart = (event) => {
if(isMine) return
const theme = useTheme();
const { clientX, clientY } = event.touches[0];
startTouchPosition.current = { x: clientX, y: clientY };
const handleContextMenu = (event) => {
if (isMine) return;
event.preventDefault();
event.stopPropagation();
preventClick.current = true;
setMenuPosition({
mouseX: event.clientX,
mouseY: event.clientY,
});
};
longPressTimeout.current = setTimeout(() => {
preventClick.current = true;
event.stopPropagation();
setMenuPosition({
mouseX: clientX,
mouseY: clientY,
const handleTouchStart = (event) => {
if (isMine) return;
const { clientX, clientY } = event.touches[0];
startTouchPosition.current = { x: clientX, y: clientY };
longPressTimeout.current = setTimeout(() => {
preventClick.current = true;
event.stopPropagation();
setMenuPosition({
mouseX: clientX,
mouseY: clientY,
});
}, 500);
// Set a maximum hold duration (e.g., 1.5 seconds)
maxHoldTimeout.current = setTimeout(() => {
clearTimeout(longPressTimeout.current);
}, 1500);
};
const handleTouchMove = (event) => {
if (isMine) return;
const { clientX, clientY } = event.touches[0];
const { x, y } = startTouchPosition.current;
// Determine if the touch has moved beyond a small threshold (e.g., 10px)
const movedEnough =
Math.abs(clientX - x) > 10 || Math.abs(clientY - y) > 10;
if (movedEnough) {
clearTimeout(longPressTimeout.current);
clearTimeout(maxHoldTimeout.current);
}
};
const handleTouchEnd = (event) => {
if (isMine) return;
clearTimeout(longPressTimeout.current);
clearTimeout(maxHoldTimeout.current);
if (preventClick.current) {
event.preventDefault();
event.stopPropagation();
preventClick.current = false;
}
};
const handleClose = (e) => {
if (isMine) return;
e.preventDefault();
e.stopPropagation();
setMenuPosition(null);
};
return (
<div
onContextMenu={handleContextMenu}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{ touchAction: 'none' }}
>
{children}
<CustomStyledMenu
disableAutoFocusItem
open={!!menuPosition}
onClose={handleClose}
anchorReference="anchorPosition"
anchorPosition={
menuPosition
? { top: menuPosition.mouseY, left: menuPosition.mouseX }
: undefined
}
onClick={(e) => {
e.stopPropagation();
}}
>
<MenuItem
onClick={(e) => {
handleClose(e);
setSortablePinnedApps((prev) => {
if (app?.isPrivate) {
const updatedApps = prev.filter(
(item) =>
!(
item?.privateAppProperties?.name ===
app?.privateAppProperties?.name &&
item?.privateAppProperties?.service ===
app?.privateAppProperties?.service &&
item?.privateAppProperties?.identifier ===
app?.privateAppProperties?.identifier
)
);
saveToLocalStorage(
'ext_saved_settings',
'sortablePinnedApps',
updatedApps
);
return updatedApps;
} else {
const updatedApps = prev.filter(
(item) =>
!(
item?.name === app?.name && item?.service === app?.service
)
);
saveToLocalStorage(
'ext_saved_settings',
'sortablePinnedApps',
updatedApps
);
return updatedApps;
}
});
}, 500);
// Set a maximum hold duration (e.g., 1.5 seconds)
maxHoldTimeout.current = setTimeout(() => {
clearTimeout(longPressTimeout.current);
}, 1500);
};
const handleTouchMove = (event) => {
if(isMine) return
const { clientX, clientY } = event.touches[0];
const { x, y } = startTouchPosition.current;
// Determine if the touch has moved beyond a small threshold (e.g., 10px)
const movedEnough = Math.abs(clientX - x) > 10 || Math.abs(clientY - y) > 10;
if (movedEnough) {
clearTimeout(longPressTimeout.current);
clearTimeout(maxHoldTimeout.current);
}
};
const handleTouchEnd = (event) => {
if(isMine) return
clearTimeout(longPressTimeout.current);
clearTimeout(maxHoldTimeout.current);
if (preventClick.current) {
event.preventDefault();
event.stopPropagation();
preventClick.current = false;
}
};
const handleClose = (e) => {
if(isMine) return
e.preventDefault();
e.stopPropagation();
setMenuPosition(null);
};
return (
<div
onContextMenu={handleContextMenu}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{ touchAction: 'none' }}
}}
>
{children}
<CustomStyledMenu
disableAutoFocusItem
open={!!menuPosition}
onClose={handleClose}
anchorReference="anchorPosition"
anchorPosition={
menuPosition
? { top: menuPosition.mouseY, left: menuPosition.mouseX }
: undefined
}
onClick={(e) => {
e.stopPropagation();
}}
>
<MenuItem onClick={(e) => {
handleClose(e);
setSortablePinnedApps((prev) => {
if(app?.isPrivate){
const updatedApps = prev.filter(
(item) => !(item?.privateAppProperties?.name === app?.privateAppProperties?.name && item?.privateAppProperties?.service === app?.privateAppProperties?.service && item?.privateAppProperties?.identifier === app?.privateAppProperties?.identifier)
);
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps);
return updatedApps;
} else {
const updatedApps = prev.filter(
(item) => !(item?.name === app?.name && item?.service === app?.service)
);
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps);
return updatedApps;
}
});
}}>
<ListItemIcon sx={{ minWidth: '32px' }}>
<PushPinIcon fontSize="small" />
</ListItemIcon>
<Typography variant="inherit" sx={{ fontSize: '14px' }}>
Unpin app
</Typography>
</MenuItem>
</CustomStyledMenu>
</div>
);
<ListItemIcon sx={{ minWidth: '32px' }}>
<PushPinIcon
sx={{
color: theme.palette.text.primary,
}}
fontSize="small"
/>
</ListItemIcon>
<Typography sx={{ fontSize: '14px' }} color="text.primary">
Unpin app
</Typography>
</MenuItem>
</CustomStyledMenu>
</div>
);
};

View File

@@ -1,59 +0,0 @@
.lineHeight {
line-height: 33%;
}
.tooltip {
display: inline-block;
position: relative;
text-align: left;
}
.tooltip .bottom {
min-width: 225px;
max-width: 250px;
top: 35px;
right: 0px;
/* transform: translate(-50%, 0); */
padding: 10px 10px;
color: var(--black);
background-color: var(--bg-2);
font-weight: normal;
font-size: 13px;
border-radius: 8px;
position: absolute;
z-index: 99999999;
box-sizing: border-box;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
border: 1px solid var(--black);
visibility: hidden;
opacity: 0;
transition: opacity 0.2s;
}
.tooltip:hover .bottom {
visibility: visible;
opacity: 1;
z-index: 100;
}
.tooltip .bottom i {
position: absolute;
bottom: 100%;
left: 50%;
margin-left: -12px;
width: 24px;
height: 12px;
overflow: hidden;
}
.tooltip .bottom i::after {
content: '';
position: absolute;
width: 12px;
height: 12px;
left: 50%;
transform: translate(-50%, 50%) rotate(45deg);
background-color: var(--white);
border: 1px solid var(--black);
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
}

View File

@@ -1,28 +1,34 @@
import React, { useEffect, useState } from 'react';
import syncedImg from '../assets/syncStatus/synced.png'
import syncedMintingImg from '../assets/syncStatus/synced_minting.png'
import syncingImg from '../assets/syncStatus/syncing.png'
import { useEffect, useState } from 'react';
import syncedImg from '../assets/syncStatus/synced.png';
import syncedMintingImg from '../assets/syncStatus/synced_minting.png';
import syncingImg from '../assets/syncStatus/syncing.png';
import { getBaseApiReact } from '../App';
import './CoreSyncStatus.css'
export const CoreSyncStatus = ({imageSize, position}) => {
import '../styles/CoreSyncStatus.css';
import { useTheme } from '@mui/material';
import { useTranslation } from 'react-i18next';
export const CoreSyncStatus = () => {
const [nodeInfos, setNodeInfos] = useState({});
const [coreInfos, setCoreInfos] = useState({});
const [isUsingGateway, setIsUsingGateway] = useState(false);
const { t } = useTranslation(['auth', 'core']);
const theme = useTheme();
useEffect(() => {
const getNodeInfos = async () => {
try {
setIsUsingGateway(!!getBaseApiReact()?.includes('ext-node.qortal.link'))
const url = `${getBaseApiReact()}/admin/status`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
setIsUsingGateway(
!!getBaseApiReact()?.includes('ext-node.qortal.link')
);
const url = `${getBaseApiReact()}/admin/status`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
setNodeInfos(data);
} catch (error) {
console.error('Request failed', error);
@@ -30,14 +36,12 @@ export const CoreSyncStatus = ({imageSize, position}) => {
};
const getCoreInfos = async () => {
try {
const url = `${getBaseApiReact()}/admin/info`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const data = await response.json();
@@ -59,55 +63,87 @@ export const CoreSyncStatus = ({imageSize, position}) => {
}, []);
const renderSyncStatusIcon = () => {
const { isSynchronizing = false, syncPercent = 0, isMintingPossible = false, height = 0, numberOfConnections = 0 } = nodeInfos;
const buildVersion = coreInfos?.buildVersion ? coreInfos?.buildVersion.substring(0, 12) : '';
const {
isSynchronizing = false,
syncPercent = 0,
isMintingPossible = false,
height = 0,
numberOfConnections = 0,
} = nodeInfos;
const buildVersion = coreInfos?.buildVersion
? coreInfos?.buildVersion.substring(0, 12)
: '';
let imagePath = syncingImg;
let message = `Synchronizing`
let message = t('core:status.synchronizing', { postProcess: 'capitalize' });
if (isMintingPossible && !isUsingGateway) {
imagePath = syncedMintingImg;
message = `${isSynchronizing ? 'Synchronizing' : 'Synchronized'} ${'(Minting)'}`
message = `${t(`core:message.status.${isSynchronizing ? 'synchronizing' : 'synchronized'}`, { postProcess: 'capitalize' })} ${t('core:message.status.minting')}`;
} else if (isSynchronizing === true && syncPercent === 99) {
imagePath = syncingImg
imagePath = syncingImg;
} else if (isSynchronizing && !isMintingPossible && syncPercent === 100) {
imagePath = syncingImg;
message = `Synchronizing ${isUsingGateway ? '' :'(Not Minting)'}`
message = `${t('core:message.status.synchronizing', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:message.status.not_minting') : ''}`;
} else if (!isSynchronizing && !isMintingPossible && syncPercent === 100) {
imagePath = syncedImg
message = `Synchronized ${isUsingGateway ? '' :'(Not Minting)'}`
imagePath = syncedImg;
message = `${t('core:message.status.synchronized', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:message.status.not_minting') : ''}`;
} else if (isSynchronizing && isMintingPossible && syncPercent === 100) {
imagePath = syncingImg;
message = `Synchronizing ${isUsingGateway ? '' :'(Minting)'}`
message = `${t('core:message.status.synchronizing', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:message.status.minting') : ''}`;
} else if (!isSynchronizing && isMintingPossible && syncPercent === 100) {
imagePath = syncedMintingImg;
message = `Synchronized ${isUsingGateway ? '' :'(Minting)'}`
message = `${t('core:message.status.synchronized', { postProcess: 'capitalize' })} ${!isUsingGateway ? t('core:message.status.minting') : ''}`;
}
return (
<div className="tooltip" style={{ display: 'inline' }}>
<span><img src={imagePath} style={{ height: 'auto', width: imageSize ? imageSize : '24px' }} alt="sync status" /></span>
<div className="bottom" style={{
right: position && 'unset',
left: position && '0px'
}}>
<h3>Core Information</h3>
<h4 className="lineHeight">Core Version: <span style={{ color: '#03a9f4' }}>{buildVersion}</span></h4>
<div
className="tooltip"
data-theme={theme.palette.mode}
style={{ display: 'inline' }}
>
<span>
<img
src={imagePath}
style={{ height: 'auto', width: '35px' }}
alt="sync status"
/>
</span>
<div
className="bottom"
style={{
right: 'unset',
left: '55px',
top: '10px',
}}
>
<h3>{t('core:core.information', { postProcess: 'capitalize' })}</h3>
<h4 className="lineHeight">
{t('core:core.version', { postProcess: 'capitalize' })}:{' '}
<span style={{ color: '#03a9f4' }}>{buildVersion}</span>
</h4>
<h4 className="lineHeight">{message}</h4>
<h4 className="lineHeight">Block Height: <span style={{ color: '#03a9f4' }}>{height || ''}</span></h4>
<h4 className="lineHeight">Connected Peers: <span style={{ color: '#03a9f4' }}>{numberOfConnections || ''}</span></h4>
<h4 className="lineHeight">Using public node: <span style={{ color: '#03a9f4' }}>{isUsingGateway?.toString()}</span></h4>
<i></i>
<h4 className="lineHeight">
{t('core:core.block_height', { postProcess: 'capitalize' })}:{' '}
<span style={{ color: '#03a9f4' }}>{height || ''}</span>
</h4>
<h4 className="lineHeight">
{t('core:core.peers', { postProcess: 'capitalize' })}:{' '}
<span style={{ color: '#03a9f4' }}>
{numberOfConnections || ''}
</span>
</h4>
<h4 className="lineHeight">
{t('auth:node.using_public', { postProcess: 'capitalize' })}:{' '}
<span style={{ color: '#03a9f4' }}>
{isUsingGateway?.toString()}
</span>
</h4>
</div>
</div>
);
};
return (
<div id="core-sync-status-id">
{renderSyncStatusIcon()}
</div>
);
return <div id="core-sync-status-id">{renderSyncStatusIcon()}</div>;
};

View File

@@ -1,47 +1,49 @@
import * as React from "react";
import {
BottomNavigation,
BottomNavigationAction,
ButtonBase,
Typography,
} from "@mui/material";
import { Home, Groups, Message, ShowChart } from "@mui/icons-material";
import Box from "@mui/material/Box";
import BottomLogo from "../../assets/svgs/BottomLogo5.svg";
import { CustomSvg } from "../../common/CustomSvg";
import { WalletIcon } from "../../assets/Icons/WalletIcon";
import { HubsIcon } from "../../assets/Icons/HubsIcon";
import { TradingIcon } from "../../assets/Icons/TradingIcon";
import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
import AppIcon from "../../assets/svgs/AppIcon.svg";
import { ButtonBase, Typography, useTheme } from '@mui/material';
import Box from '@mui/material/Box';
import { HubsIcon } from '../../assets/Icons/HubsIcon';
import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
import AppIcon from '../../assets/svgs/AppIcon.svg';
import { HomeIcon } from "../../assets/Icons/HomeIcon";
import { Save } from "../Save/Save";
import { useRecoilState } from "recoil";
import { enabledDevModeAtom } from "../../atoms/global";
import { HomeIcon } from '../../assets/Icons/HomeIcon';
import { Save } from '../Save/Save';
import { enabledDevModeAtom } from '../../atoms/global';
import { useAtom } from 'jotai';
export const IconWrapper = ({
children,
label,
color,
selected,
disableWidth,
customWidth,
}) => {
const theme = useTheme();
export const IconWrapper = ({ children, label, color, selected, disableWidth, customWidth }) => {
return (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
gap: "5px",
flexDirection: "column",
height: customWidth ? customWidth : disableWidth ? 'auto' : "89px",
width: customWidth? customWidth : disableWidth ? 'auto' : "89px",
borderRadius: "50%",
backgroundColor: selected ? "rgba(28, 29, 32, 1)" : "transparent",
alignItems: 'center',
backgroundColor: selected
? theme.palette.action.selected
: 'transparent',
borderRadius: '50%',
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}
<Typography
sx={{
fontFamily: "Inter",
fontSize: "12px",
color: color || theme.palette.text.primary,
fontFamily: 'Inter',
fontSize: '12px',
fontWeight: 500,
color: color,
}}
>
{label}
@@ -51,24 +53,7 @@ export const IconWrapper = ({ children, label, color, selected, disableWidth, cu
};
export const DesktopFooter = ({
selectedGroup,
groupSection,
isUnread,
goToAnnouncements,
isUnreadChat,
goToChat,
goToThreads,
setOpenManageMembers,
groupChatHasUnread,
groupsAnnHasUnread,
directChatHasUnread,
chatMode,
openDrawerGroups,
goToHome,
setIsOpenDrawerProfile,
mobileViewMode,
setMobileViewMode,
setMobileViewModeKeepOpen,
hasUnreadGroups,
hasUnreadDirects,
isHome,
@@ -77,32 +62,32 @@ export const DesktopFooter = ({
setDesktopSideView,
isApps,
setDesktopViewMode,
desktopViewMode,
hide,
setIsOpenSideViewDirects,
setIsOpenSideViewGroups
setIsOpenSideViewGroups,
}) => {
const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom)
const [isEnabledDevMode, setIsEnabledDevMode] = useAtom(enabledDevModeAtom);
if(hide) return
const theme = useTheme();
if (hide) return;
return (
<Box
sx={{
width: "100%",
position: "absolute",
alignItems: 'center',
bottom: 0,
display: "flex",
alignItems: "center",
height: "100px", // Footer height
display: 'flex',
height: '100px', // Footer height
justifyContent: 'center',
position: 'absolute',
width: '100%',
zIndex: 1,
justifyContent: "center",
}}
>
<Box
sx={{
display: "flex",
gap: "20px",
display: 'flex',
gap: '20px',
}}
>
<ButtonBase
@@ -110,96 +95,75 @@ export const DesktopFooter = ({
goToHome();
}}
>
<IconWrapper
color="rgba(250, 250, 250, 0.5)"
label="Home"
selected={isHome}
>
<HomeIcon
height={30}
color={isHome ? "white" : "rgba(250, 250, 250, 0.5)"}
/>
<IconWrapper label="Home" selected={isHome}>
<HomeIcon height={30} />
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopViewMode('apps')
setIsOpenSideViewDirects(false)
setIsOpenSideViewGroups(false)
setDesktopViewMode('apps');
setIsOpenSideViewDirects(false);
setIsOpenSideViewGroups(false);
}}
>
<IconWrapper
color="rgba(250, 250, 250, 0.5)"
label="Apps"
selected={isApps}
>
<img src={AppIcon} />
<IconWrapper label="Apps" selected={isApps}>
<img src={AppIcon} />
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopSideView("groups");
setDesktopSideView('groups');
}}
>
<IconWrapper
color="rgba(250, 250, 250, 0.5)"
label="Groups"
selected={isGroups}
>
<IconWrapper label="Groups" selected={isGroups}>
<HubsIcon
height={30}
color={
hasUnreadGroups
? "var(--danger)"
? theme.palette.other.danger
: isGroups
? "white"
: "rgba(250, 250, 250, 0.5)"
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopSideView("directs");
setDesktopSideView('directs');
}}
>
<IconWrapper
color="rgba(250, 250, 250, 0.5)"
label="Messaging"
selected={isDirects}
>
<IconWrapper label="Messaging" selected={isDirects}>
<MessagingIcon
height={30}
color={
hasUnreadDirects
? "var(--danger)"
? theme.palette.other.danger
: isDirects
? "white"
: "rgba(250, 250, 250, 0.5)"
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
</ButtonBase>
<Save isDesktop />
{isEnabledDevMode && (
<ButtonBase
onClick={() => {
setDesktopViewMode('dev')
setIsOpenSideViewDirects(false)
setIsOpenSideViewGroups(false)
}}
>
<IconWrapper
color="rgba(250, 250, 250, 0.5)"
label="Dev Mode"
selected={isApps}
onClick={() => {
setDesktopViewMode('dev');
setIsOpenSideViewDirects(false);
setIsOpenSideViewGroups(false);
}}
>
<img src={AppIcon} />
</IconWrapper>
</ButtonBase>
<IconWrapper label="Dev Mode" selected={isApps}>
<img src={AppIcon} />
</IconWrapper>
</ButtonBase>
)}
</Box>
</Box>
);

View File

@@ -1,47 +1,43 @@
import * as React from "react";
import {
BottomNavigation,
BottomNavigationAction,
ButtonBase,
Typography,
} from "@mui/material";
import { Home, Groups, Message, ShowChart } from "@mui/icons-material";
import Box from "@mui/material/Box";
import BottomLogo from "../../assets/svgs/BottomLogo5.svg";
import { CustomSvg } from "../../common/CustomSvg";
import { WalletIcon } from "../../assets/Icons/WalletIcon";
import { HubsIcon } from "../../assets/Icons/HubsIcon";
import { TradingIcon } from "../../assets/Icons/TradingIcon";
import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
import { HomeIcon } from "../../assets/Icons/HomeIcon";
import { NotificationIcon2 } from "../../assets/Icons/NotificationIcon2";
import { ChatIcon } from "../../assets/Icons/ChatIcon";
import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon";
import { MembersIcon } from "../../assets/Icons/MembersIcon";
import { AdminsIcon } from "../../assets/Icons/AdminsIcon";
import * as React from 'react';
import { ButtonBase, Typography, useTheme } from '@mui/material';
import Box from '@mui/material/Box';
import { NotificationIcon2 } from '../../assets/Icons/NotificationIcon2';
import { ChatIcon } from '../../assets/Icons/ChatIcon';
import { ThreadsIcon } from '../../assets/Icons/ThreadsIcon';
import { MembersIcon } from '../../assets/Icons/MembersIcon';
import { AdminsIcon } from '../../assets/Icons/AdminsIcon';
import LockIcon from '@mui/icons-material/Lock';
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
const IconWrapper = ({ children, label, color, selected, selectColor, customHeight }) => {
const IconWrapper = ({
children,
label,
color,
selected,
selectColor,
customHeight,
}) => {
return (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
gap: "5px",
flexDirection: "column",
height: customHeight ? customHeight : "65px",
width: customHeight ? customHeight : "65px",
borderRadius: "50%",
backgroundColor: selected ? selectColor || "rgba(28, 29, 32, 1)" : "transparent",
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '5px',
flexDirection: 'column',
height: customHeight ? customHeight : '65px',
width: customHeight ? customHeight : '65px',
borderRadius: '50%',
backgroundColor: selected
? selectColor || 'rgba(28, 29, 32, 1)'
: 'transparent',
}}
>
{children}
<Typography
sx={{
fontFamily: "Inter",
fontSize: "10px",
fontFamily: 'Inter',
fontSize: '10px',
fontWeight: 500,
color: color,
}}
@@ -83,63 +79,75 @@ export const DesktopHeader = ({
isChat,
isForum,
setGroupSection,
isPrivate
isPrivate,
}) => {
const [value, setValue] = React.useState(0);
const theme = useTheme();
return (
<Box
sx={{
width: "100%",
display: "flex",
alignItems: "center",
height: "70px", // Footer height
width: '100%',
display: 'flex',
alignItems: 'center',
height: '70px', // Footer height
zIndex: 1,
justifyContent: "space-between",
padding: "10px",
justifyContent: 'space-between',
padding: '10px',
}}
>
<Box sx={{
display: 'flex',
gap: '10px'
}}>
<Box
sx={{
display: 'flex',
gap: '10px',
}}
>
{isPrivate && (
<LockIcon sx={{
color: 'var(--green)'
}} />
<LockIcon
sx={{
color: theme.palette.other.positive,
}}
/>
)}
{isPrivate === false && (
<NoEncryptionGmailerrorredIcon sx={{
color: 'var(--danger)'
}} />
<NoEncryptionGmailerrorredIcon
sx={{
color: theme.palette.other.danger,
}}
/>
)}
<Typography
sx={{
fontSize: "16px",
fontSize: '16px',
fontWeight: 600,
}}
>
{selectedGroup?.groupId === '0' ? 'General' :selectedGroup?.groupName}
{selectedGroup?.groupId === '0'
? 'General'
: selectedGroup?.groupName}
</Typography>
</Box>
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
visibility: selectedGroup?.groupId === '0' ? 'hidden' : 'visibile'
display: 'flex',
gap: '20px',
alignItems: 'center',
visibility: selectedGroup?.groupId === '0' ? 'hidden' : 'visibile',
}}
>
<ButtonBase
onClick={() => {
goToAnnouncements()
goToAnnouncements();
}}
>
<IconWrapper
color={isAnnouncement ? "black" :"rgba(250, 250, 250, 0.5)"}
color={
isAnnouncement
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="ANN"
selected={isAnnouncement}
selectColor="#09b6e8"
selectColor={theme.palette.action.selected}
customHeight="55px"
>
<NotificationIcon2
@@ -147,10 +155,10 @@ export const DesktopHeader = ({
width={20}
color={
isUnread
? "var(--unread)"
? theme.palette.other.unread
: isAnnouncement
? "black"
: "rgba(250, 250, 250, 0.5)"
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
@@ -158,14 +166,16 @@ export const DesktopHeader = ({
<ButtonBase
onClick={() => {
goToChat()
goToChat();
}}
>
<IconWrapper
color={isChat ? "black" :"rgba(250, 250, 250, 0.5)"}
color={
isChat ? theme.palette.text.primary : theme.palette.text.secondary
}
label="Chat"
selected={isChat}
selectColor="#09b6e8"
selectColor={theme.palette.action.selected}
customHeight="55px"
>
<ChatIcon
@@ -173,10 +183,10 @@ export const DesktopHeader = ({
width={20}
color={
isUnreadChat
? "var(--unread)"
? theme.palette.other.unread
: isChat
? "black"
: "rgba(250, 250, 250, 0.5)"
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
@@ -184,15 +194,18 @@ export const DesktopHeader = ({
<ButtonBase
onClick={() => {
setGroupSection("forum");
setGroupSection('forum');
}}
>
<IconWrapper
color={isForum ? 'black' : "rgba(250, 250, 250, 0.5)"}
color={
isForum
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="Threads"
selected={isForum}
selectColor="#09b6e8"
selectColor={theme.palette.action.selected}
customHeight="55px"
>
<ThreadsIcon
@@ -200,20 +213,19 @@ export const DesktopHeader = ({
width={20}
color={
isForum
? "black"
: "rgba(250, 250, 250, 0.5)"
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setOpenManageMembers(true)
setOpenManageMembers(true);
}}
>
<IconWrapper
color="rgba(250, 250, 250, 0.5)"
color={theme.palette.text.secondary}
label="Members"
selected={false}
customHeight="55px"
@@ -221,34 +233,33 @@ export const DesktopHeader = ({
<MembersIcon
height={25}
width={20}
color={
isForum
? "white"
: "rgba(250, 250, 250, 0.5)"
}
color={theme.palette.text.secondary}
/>
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setGroupSection("adminSpace");
setGroupSection('adminSpace');
}}
>
<IconWrapper
color={groupSection === 'adminSpace' ? 'black' : "rgba(250, 250, 250, 0.5)"}
color={
groupSection === 'adminSpace'
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="Admins"
selected={groupSection === 'adminSpace'}
customHeight="55px"
selectColor="#09b6e8"
selectColor={theme.palette.action.selected}
>
<AdminsIcon
height={25}
width={20}
color={
groupSection === 'adminSpace'
? "black"
: "rgba(250, 250, 250, 0.5)"
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>

View File

@@ -1,120 +1,150 @@
import { Box, ButtonBase } from '@mui/material';
import React from 'react'
import { HomeIcon } from "../assets/Icons/HomeIcon";
import { MessagingIcon } from "../assets/Icons/MessagingIcon";
import { Save } from "./Save/Save";
import { HubsIcon } from "../assets/Icons/HubsIcon";
import { CoreSyncStatus } from "./CoreSyncStatus";
import { Box, ButtonBase, useTheme } from '@mui/material';
import { HomeIcon } from '../assets/Icons/HomeIcon';
import { Save } from './Save/Save';
import { IconWrapper } from './Desktop/DesktopFooter';
import AppIcon from "./../assets/svgs/AppIcon.svg";
import { useRecoilState } from 'recoil';
import { enabledDevModeAtom } from '../atoms/global';
import { AppsIcon } from '../assets/Icons/AppsIcon';
import ThemeSelector from './Theme/ThemeSelector';
import { CoreSyncStatus } from './CoreSyncStatus';
import LanguageSelector from './Language/LanguageSelector';
import { MessagingIconFilled } from '../assets/Icons/MessagingIconFilled';
import { useAtom } from 'jotai';
export const DesktopSideBar = ({goToHome, setDesktopSideView, toggleSideViewDirects, hasUnreadDirects, isDirects, toggleSideViewGroups,hasUnreadGroups, isGroups, isApps, setDesktopViewMode, desktopViewMode, myName }) => {
const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom)
export const DesktopSideBar = ({
goToHome,
setDesktopSideView,
toggleSideViewDirects,
hasUnreadDirects,
isDirects,
toggleSideViewGroups,
hasUnreadGroups,
isGroups,
isApps,
setDesktopViewMode,
desktopViewMode,
myName,
}) => {
const [isEnabledDevMode, setIsEnabledDevMode] = useAtom(enabledDevModeAtom);
const theme = useTheme();
return (
<Box sx={{
width: '60px',
flexDirection: 'column',
height: '100vh',
<Box
sx={{
alignItems: 'center',
display: 'flex',
gap: '25px'
}}>
<ButtonBase
sx={{
width: '60px',
height: '60px',
paddingTop: '23px'
}}
onClick={() => {
goToHome();
flexDirection: 'column',
gap: '25px',
height: '100vh',
width: '60px',
backgroundColor: theme.palette.background.surface,
borderRight: `1px solid ${theme.palette.border.subtle}`,
}}
>
<ButtonBase
sx={{
width: '70px',
height: '70px',
paddingTop: '23px',
}}
>
<CoreSyncStatus />
</ButtonBase>
}}
>
<HomeIcon
height={34}
color={desktopViewMode === 'home' ? 'white': "rgba(250, 250, 250, 0.5)"}
<ButtonBase
sx={{
width: '60px',
height: '60px',
}}
onClick={() => {
goToHome();
}}
>
<HomeIcon
height={34}
color={
desktopViewMode === 'home'
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</ButtonBase>
/>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopViewMode('apps')
// setIsOpenSideViewDirects(false)
// setIsOpenSideViewGroups(false)
}}
>
<IconWrapper
color={isApps ? 'white' : "rgba(250, 250, 250, 0.5)"}
label="Apps"
selected={isApps}
disableWidth
>
<AppsIcon color={isApps ? 'white' : "rgba(250, 250, 250, 0.5)"} height={30} />
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopViewMode('chat')
}}
>
<ButtonBase
onClick={() => {
setDesktopViewMode('apps');
}}
>
<IconWrapper
color={(hasUnreadDirects || hasUnreadGroups) ? "var(--unread)" : desktopViewMode === 'chat' ? 'white' :"rgba(250, 250, 250, 0.5)"}
label="Chat"
disableWidth
>
<MessagingIcon
height={30}
color={
(hasUnreadDirects || hasUnreadGroups)
? "var(--unread)"
: desktopViewMode === 'chat'
? "white"
: "rgba(250, 250, 250, 0.5)"
}
/>
</IconWrapper>
</ButtonBase>
{/* <ButtonBase
onClick={() => {
setDesktopSideView("groups");
toggleSideViewGroups()
}}
color={
isApps ? theme.palette.text.primary : theme.palette.text.secondary
}
label="Apps"
selected={isApps}
disableWidth
>
<HubsIcon
height={30}
color={
hasUnreadGroups
? "var(--danger)"
: isGroups
? "white"
: "rgba(250, 250, 250, 0.5)"
}
/>
</ButtonBase> */}
<Save isDesktop disableWidth myName={myName} />
{/* <CoreSyncStatus imageSize="30px" position="left" /> */}
{isEnabledDevMode && (
<ButtonBase
<AppsIcon
color={
isApps ? theme.palette.text.primary : theme.palette.text.secondary
}
height={30}
/>
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopViewMode('chat');
}}
>
<IconWrapper
color={
hasUnreadDirects || hasUnreadGroups
? theme.palette.other.unread
: desktopViewMode === 'chat'
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="Chat"
disableWidth
>
<MessagingIconFilled
height={30}
color={
hasUnreadDirects || hasUnreadGroups
? theme.palette.other.unread
: desktopViewMode === 'chat'
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
</ButtonBase>
<Save isDesktop disableWidth myName={myName} />
{isEnabledDevMode && (
<ButtonBase
onClick={() => {
setDesktopViewMode('dev')
setDesktopViewMode('dev');
}}
>
<IconWrapper
color={desktopViewMode === 'dev' ? 'white' : "rgba(250, 250, 250, 0.5)"}
color={
desktopViewMode === 'dev'
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="Dev"
disableWidth
>
<AppsIcon color={desktopViewMode === 'dev' ? 'white' : "rgba(250, 250, 250, 0.5)"} height={30} />
<AppsIcon height={30} color={theme.palette.text.secondary} />
</IconWrapper>
</ButtonBase>
)}
</Box>
)
}
)}
<LanguageSelector />
<ThemeSelector />
</Box>
);
};

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import { MyContext, getBaseApiReact } from "../../App";
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { MyContext, getBaseApiReact } from '../../App';
import {
Card,
CardContent,
@@ -15,285 +15,294 @@ import {
Dialog,
IconButton,
CircularProgress,
} from "@mui/material";
import { base64ToBlobUrl } from "../../utils/fileReading";
import { saveFileToDiskGeneric } from "../../utils/generateWallet/generateWallet";
useTheme,
} from '@mui/material';
import { base64ToBlobUrl } from '../../utils/fileReading';
import { saveFileToDiskGeneric } from '../../utils/generateWallet/generateWallet';
import AttachmentIcon from '@mui/icons-material/Attachment';
import RefreshIcon from "@mui/icons-material/Refresh";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import { CustomLoader } from "../../common/CustomLoader";
import { Spacer } from "../../common/Spacer";
import { FileAttachmentContainer, FileAttachmentFont } from "./Embed-styles";
import DownloadIcon from "@mui/icons-material/Download";
import RefreshIcon from '@mui/icons-material/Refresh';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { CustomLoader } from '../../common/CustomLoader';
import { Spacer } from '../../common/Spacer';
import { FileAttachmentContainer, FileAttachmentFont } from './Embed-styles';
import DownloadIcon from '@mui/icons-material/Download';
import SaveIcon from '@mui/icons-material/Save';
import { useSetRecoilState } from "recoil";
import { blobControllerAtom } from "../../atoms/global";
import { decodeIfEncoded } from "../../utils/decode";
import { decodeIfEncoded } from '../../utils/decode';
export const AttachmentCard = ({
resourceData,
resourceDetails,
owner,
refresh,
openExternal,
external,
isLoadingParent,
errorMsg,
encryptionType,
selectedGroupId
}) => {
resourceData,
resourceDetails,
owner,
refresh,
openExternal,
external,
isLoadingParent,
errorMsg,
encryptionType,
selectedGroupId,
}) => {
const [isOpen, setIsOpen] = useState(true);
const { downloadResource } = useContext(MyContext);
const theme = useTheme();
const [isOpen, setIsOpen] = useState(true);
const { downloadResource } = useContext(MyContext);
const saveToDisk = async ()=> {
const { name, service, identifier } = resourceData;
const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}`;
fetch(url)
.then(response => response.blob())
.then(async blob => {
await saveFileToDiskGeneric(blob, resourceData?.fileName)
})
.catch(error => {
console.error("Error fetching the video:", error);
});
}
const saveToDiskEncrypted = async ()=> {
let blobUrl
const saveToDisk = async () => {
const { name, service, identifier } = resourceData;
const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}`;
fetch(url)
.then((response) => response.blob())
.then(async (blob) => {
await saveFileToDiskGeneric(blob, resourceData?.fileName);
})
.catch((error) => {
console.error('Error fetching the video:', error);
});
};
const saveToDiskEncrypted = async () => {
let blobUrl;
try {
const { name, service, identifier, key } = resourceData;
const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?encoding=base64`;
const res = await fetch(url);
const data = await res.text();
let decryptedData;
try {
const { name, service, identifier,key } = resourceData;
const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?encoding=base64`;
const res = await fetch(url)
const data = await res.text();
let decryptedData
try {
if(key && encryptionType === 'private'){
decryptedData = await window.sendMessage(
"DECRYPT_DATA_WITH_SHARING_KEY",
{
encryptedData: data,
key: decodeURIComponent(key),
}
);
}
if(encryptionType === 'group'){
decryptedData = await window.sendMessage(
"DECRYPT_QORTAL_GROUP_DATA",
{
data64: data,
groupId: selectedGroupId,
}
);
}
} catch (error) {
throw new Error('Unable to decrypt')
if (key && encryptionType === 'private') {
decryptedData = await window.sendMessage(
'DECRYPT_DATA_WITH_SHARING_KEY',
{
encryptedData: data,
key: decodeURIComponent(key),
}
);
}
if (encryptionType === 'group') {
decryptedData = await window.sendMessage(
'DECRYPT_QORTAL_GROUP_DATA',
{
data64: data,
groupId: selectedGroupId,
}
);
}
if (!decryptedData || decryptedData?.error) throw new Error("Could not decrypt data");
blobUrl = base64ToBlobUrl(decryptedData, resourceData?.mimeType)
const response = await fetch(blobUrl);
const blob = await response.blob();
await saveFileToDiskGeneric(blob, resourceData?.fileName)
} catch (error) {
console.error(error)
} finally {
if(blobUrl){
URL.revokeObjectURL(blobUrl);
}
throw new Error('Unable to decrypt');
}
if (!decryptedData || decryptedData?.error)
throw new Error('Could not decrypt data');
blobUrl = base64ToBlobUrl(decryptedData, resourceData?.mimeType);
const response = await fetch(blobUrl);
const blob = await response.blob();
await saveFileToDiskGeneric(blob, resourceData?.fileName);
} catch (error) {
console.error(error);
} finally {
if (blobUrl) {
URL.revokeObjectURL(blobUrl);
}
}
return (
<Card
};
return (
<Card
sx={{
backgroundColor: theme.palette.background.default,
height: '250px',
// height: isOpen ? "auto" : "150px",
}}
>
<Box
sx={{
backgroundColor: "#1F2023",
height: "250px",
// height: isOpen ? "auto" : "150px",
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '16px 16px 0px 16px',
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "16px 16px 0px 16px",
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<Box
<AttachmentIcon
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
color: theme.palette.text.primary,
}}
>
<AttachmentIcon
/>
<Typography>ATTACHMENT embed</Typography>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<ButtonBase>
<RefreshIcon
onClick={refresh}
sx={{
color: "white",
fontSize: '24px',
color: theme.palette.text.primary,
}}
/>
<Typography>ATTACHMENT embed</Typography>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
</ButtonBase>
{external && (
<ButtonBase>
<RefreshIcon
onClick={refresh}
<OpenInNewIcon
onClick={openExternal}
sx={{
fontSize: "24px",
color: "white",
fontSize: '24px',
color: theme.palette.text.primary,
}}
/>
</ButtonBase>
{external && (
<ButtonBase>
<OpenInNewIcon
onClick={openExternal}
sx={{
fontSize: "24px",
color: "white",
}}
/>
</ButtonBase>
)}
</Box>
</Box>
<Box
sx={{
padding: "8px 16px 8px 16px",
}}
>
<Typography
sx={{
fontSize: "12px",
color: "white",
}}
>
Created by {decodeIfEncoded(owner)}
</Typography>
<Typography
sx={{
fontSize: "12px",
color: "cadetblue",
}}
>
{encryptionType === 'private' ? "ENCRYPTED" : encryptionType === 'group' ? 'GROUP ENCRYPTED' : "Not encrypted"}
</Typography>
</Box>
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} />
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
alignItems: "center",
}}
>
{isLoadingParent && isOpen && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{" "}
<CustomLoader />{" "}
</Box>
)}
{errorMsg && (
<Box
</Box>
</Box>
<Box
sx={{
padding: '8px 16px 8px 16px',
}}
>
<Typography
sx={{
fontSize: '12px',
}}
>
Created by {decodeIfEncoded(owner)}
</Typography>
<Typography
sx={{
fontSize: '12px',
}}
>
{encryptionType === 'private'
? 'ENCRYPTED'
: encryptionType === 'group'
? 'GROUP ENCRYPTED'
: 'Not encrypted'}
</Typography>
</Box>
<Divider sx={{ borderColor: 'rgb(255 255 255 / 10%)' }} />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
alignItems: 'center',
}}
>
{isLoadingParent && isOpen && (
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
}}
>
{' '}
<CustomLoader />{' '}
</Box>
)}
{errorMsg && (
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
}}
>
{' '}
<Typography
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
fontSize: '14px',
color: theme.palette.other.danger,
}}
>
{" "}
{errorMsg}
</Typography>{' '}
</Box>
)}
</Box>
<Box>
<CardContent>
{resourceData?.fileName && (
<>
<Typography
sx={{
fontSize: "14px",
color: "var(--danger)",
fontSize: '14px',
}}
>
{errorMsg}
</Typography>{" "}
</Box>
{resourceData?.fileName}
</Typography>
<Spacer height="10px" />
</>
)}
</Box>
<Box>
<CardContent>
{resourceData?.fileName && (
<ButtonBase
sx={{
width: '90%',
maxWidth: '400px',
}}
onClick={() => {
if (resourceDetails?.status?.status === 'READY') {
if (encryptionType) {
saveToDiskEncrypted();
return;
}
saveToDisk();
return;
}
downloadResource(resourceData);
}}
>
<FileAttachmentContainer>
<Typography>
{resourceDetails?.status?.status === 'DOWNLOADED'
? 'BUILDING'
: resourceDetails?.status?.status}
</Typography>
{!resourceDetails && (
<>
<Typography sx={{
fontSize: '14px'
}}>{resourceData?.fileName}</Typography>
<Spacer height="10px" />
<DownloadIcon />
<FileAttachmentFont>Download File</FileAttachmentFont>
</>
)}
<ButtonBase sx={{
width: '90%',
maxWidth: '400px'
}} onClick={()=> {
if(resourceDetails?.status?.status === 'READY'){
if(encryptionType){
saveToDiskEncrypted()
return
}
saveToDisk()
return
}
downloadResource(resourceData)
}}>
<FileAttachmentContainer >
<Typography>{resourceDetails?.status?.status === 'DOWNLOADED' ? 'BUILDING' : resourceDetails?.status?.status}</Typography>
{!resourceDetails && (
<>
<DownloadIcon />
<FileAttachmentFont>Download File</FileAttachmentFont>
</>
)}
{resourceDetails && resourceDetails?.status?.status !== 'READY' && resourceDetails?.status?.status !== 'FAILED_TO_DOWNLOAD' && (
<>
<CircularProgress sx={{
color: 'white'
}} size={20} />
<FileAttachmentFont>Downloading: {resourceDetails?.status?.percentLoaded || '0'}%</FileAttachmentFont>
</>
)}
{resourceDetails && resourceDetails?.status?.status === 'READY' && (
<>
<SaveIcon />
<FileAttachmentFont>Save to Disk</FileAttachmentFont>
</>
)}
</FileAttachmentContainer>
</ButtonBase>
</CardContent>
</Box>
</Card>
);
};
{resourceDetails &&
resourceDetails?.status?.status !== 'READY' &&
resourceDetails?.status?.status !== 'FAILED_TO_DOWNLOAD' && (
<>
<CircularProgress
size={20}
sx={{
color: theme.palette.text.primary,
}}
/>
<FileAttachmentFont>
Downloading:{' '}
{resourceDetails?.status?.percentLoaded || '0'}%
</FileAttachmentFont>
</>
)}
{resourceDetails &&
resourceDetails?.status?.status === 'READY' && (
<>
<SaveIcon />
<FileAttachmentFont>Save to Disk</FileAttachmentFont>
</>
)}
</FileAttachmentContainer>
</ButtonBase>
</CardContent>
</Box>
</Card>
);
};

View File

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

View File

@@ -1,265 +1,266 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState } from 'react';
import {
Card,
CardContent,
Typography,
Box,
ButtonBase,
Divider,
Dialog,
IconButton,
useTheme,
} from '@mui/material';
} from "@mui/material";
import RefreshIcon from "@mui/icons-material/Refresh";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import { CustomLoader } from "../../common/CustomLoader";
import ImageIcon from "@mui/icons-material/Image";
import CloseIcon from "@mui/icons-material/Close";
import { decodeIfEncoded } from "../../utils/decode";
import RefreshIcon from '@mui/icons-material/Refresh';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { CustomLoader } from '../../common/CustomLoader';
import ImageIcon from '@mui/icons-material/Image';
import CloseIcon from '@mui/icons-material/Close';
import { decodeIfEncoded } from '../../utils/decode';
export const ImageCard = ({
image,
fetchImage,
owner,
refresh,
openExternal,
external,
isLoadingParent,
errorMsg,
encryptionType,
}) => {
const [isOpen, setIsOpen] = useState(true);
const [height, setHeight] = useState('400px')
useEffect(() => {
if (isOpen) {
fetchImage();
}
}, [isOpen]);
// useEffect(()=> {
// if(errorMsg){
// setHeight('300px')
// }
// }, [errorMsg])
return (
<Card
image,
fetchImage,
owner,
refresh,
openExternal,
external,
isLoadingParent,
errorMsg,
encryptionType,
}) => {
const theme = useTheme();
const [isOpen, setIsOpen] = useState(true);
const [height, setHeight] = useState('400px');
useEffect(() => {
if (isOpen) {
fetchImage();
}
}, [isOpen]);
// useEffect(()=> {
// if(errorMsg){
// setHeight('300px')
// }
// }, [errorMsg])
return (
<Card
sx={{
backgroundColor: theme.palette.background.default,
height: height,
transition: 'height 0.6s ease-in-out',
}}
>
<Box
sx={{
backgroundColor: "#1F2023",
height: height,
transition: "height 0.6s ease-in-out",
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '16px 16px 0px 16px',
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "16px 16px 0px 16px",
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<Box
<ImageIcon
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
color: theme.palette.text.primary,
}}
>
<ImageIcon
/>
<Typography>IMAGE embed</Typography>
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<ButtonBase>
<RefreshIcon
onClick={refresh}
sx={{
color: "white",
fontSize: '24px',
color: theme.palette.text.primary,
}}
/>
<Typography>IMAGE embed</Typography>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
</ButtonBase>
{external && (
<ButtonBase>
<RefreshIcon
onClick={refresh}
<OpenInNewIcon
onClick={openExternal}
sx={{
fontSize: "24px",
color: "white",
fontSize: '24px',
color: theme.palette.text.primary,
}}
/>
</ButtonBase>
{external && (
<ButtonBase>
<OpenInNewIcon
onClick={openExternal}
sx={{
fontSize: "24px",
color: "white",
}}
/>
</ButtonBase>
)}
)}
</Box>
</Box>
<Box
sx={{
padding: '8px 16px 8px 16px',
}}
>
<Typography
sx={{
fontSize: '12px',
}}
>
Created by {decodeIfEncoded(owner)}
</Typography>
<Typography
sx={{
fontSize: '12px',
}}
>
{encryptionType === 'private'
? 'ENCRYPTED'
: encryptionType === 'group'
? 'GROUP ENCRYPTED'
: 'Not encrypted'}
</Typography>
</Box>
<Divider sx={{ borderColor: 'rgb(255 255 255 / 10%)' }} />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
width: '100%',
alignItems: 'center',
}}
>
{isLoadingParent && isOpen && (
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
}}
>
{' '}
<CustomLoader />{' '}
</Box>
</Box>
<Box
sx={{
padding: "8px 16px 8px 16px",
}}
>
<Typography
)}
{errorMsg && (
<Box
sx={{
fontSize: "12px",
color: "white",
width: '100%',
display: 'flex',
justifyContent: 'center',
}}
>
Created by {decodeIfEncoded(owner)}
</Typography>
<Typography
sx={{
fontSize: "12px",
color: "cadetblue",
}}
>
{encryptionType === 'private' ? "ENCRYPTED" : encryptionType === 'group' ? 'GROUP ENCRYPTED' : "Not encrypted"}
</Typography>
</Box>
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} />
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
alignItems: "center",
}}
>
{isLoadingParent && isOpen && (
<Box
{' '}
<Typography
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
fontSize: '14px',
color: theme.palette.other.danger,
}}
>
{" "}
<CustomLoader />{" "}
</Box>
)}
{errorMsg && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{" "}
<Typography
sx={{
fontSize: "14px",
color: "var(--danger)",
}}
>
{errorMsg}
</Typography>{" "}
</Box>
)}
</Box>
<Box>
<CardContent>
<ImageViewer src={image} />
</CardContent>
</Box>
</Card>
);
};
{errorMsg}
</Typography>{' '}
</Box>
)}
</Box>
export function ImageViewer({ src, alt = "" }) {
const [isFullscreen, setIsFullscreen] = useState(false);
const handleOpenFullscreen = () => setIsFullscreen(true);
const handleCloseFullscreen = () => setIsFullscreen(false);
return (
<>
{/* Image in container */}
<Box>
<CardContent>
<ImageViewer src={image} />
</CardContent>
</Box>
</Card>
);
};
export function ImageViewer({ src, alt = '' }) {
const [isFullscreen, setIsFullscreen] = useState(false);
const handleOpenFullscreen = () => setIsFullscreen(true);
const handleCloseFullscreen = () => setIsFullscreen(false);
const theme = useTheme();
return (
<>
{/* Image in container */}
<Box
sx={{
maxWidth: '100%', // Prevent horizontal overflow
display: 'flex',
justifyContent: 'center',
cursor: 'pointer',
}}
onClick={handleOpenFullscreen}
>
<img
src={src}
alt={alt}
style={{
maxWidth: '100%',
maxHeight: '450px', // Adjust max height for small containers
objectFit: 'contain', // Preserve aspect ratio
}}
/>
</Box>
{/* Fullscreen Viewer */}
<Dialog
open={isFullscreen}
onClose={handleCloseFullscreen}
maxWidth="lg"
fullWidth
fullScreen
sx={{
'& .MuiDialog-paper': {
margin: 0,
maxWidth: '100%',
width: '100%',
height: '100vh',
overflow: 'hidden', // Prevent scrollbars
},
}}
>
<Box
sx={{
maxWidth: "100%", // Prevent horizontal overflow
display: "flex",
justifyContent: "center",
cursor: "pointer",
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: theme.palette.background.paper, // Optional: dark background for fullscreen mode
}}
onClick={handleOpenFullscreen}
>
{/* Close Button */}
<IconButton
onClick={handleCloseFullscreen}
sx={{
position: 'absolute',
top: 8,
right: 8,
zIndex: 10,
color: theme.palette.text.primary,
}}
>
<CloseIcon />
</IconButton>
{/* Fullscreen Image */}
<img
src={src}
alt={alt}
style={{
maxWidth: "100%",
maxHeight: "450px", // Adjust max height for small containers
objectFit: "contain", // Preserve aspect ratio
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain', // Preserve aspect ratio
}}
/>
</Box>
{/* Fullscreen Viewer */}
<Dialog
open={isFullscreen}
onClose={handleCloseFullscreen}
maxWidth="lg"
fullWidth
fullScreen
sx={{
"& .MuiDialog-paper": {
margin: 0,
maxWidth: "100%",
width: "100%",
height: "100vh",
overflow: "hidden", // Prevent scrollbars
},
}}
>
<Box
sx={{
position: "relative",
width: "100%",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "#000", // Optional: dark background for fullscreen mode
}}
>
{/* Close Button */}
<IconButton
onClick={handleCloseFullscreen}
sx={{
position: "absolute",
top: 8,
right: 8,
zIndex: 10,
color: "white",
}}
>
<CloseIcon />
</IconButton>
{/* Fullscreen Image */}
<img
src={src}
alt={alt}
style={{
maxWidth: "100%",
maxHeight: "100%",
objectFit: "contain", // Preserve aspect ratio
}}
/>
</Box>
</Dialog>
</>
);
}
</Dialog>
</>
);
}

View File

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

View File

@@ -1,23 +1,26 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
import ReactDOM from 'react-dom'
import { Box, IconButton, Slider } from '@mui/material'
import { CircularProgress, Typography } from '@mui/material'
import { Key } from 'ts-key-enum'
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { Box, IconButton, Slider } from '@mui/material';
import { CircularProgress, Typography } from '@mui/material';
import { Key } from 'ts-key-enum';
import {
PlayArrow,
Pause,
VolumeUp,
Fullscreen,
PictureInPicture, VolumeOff, Calculate
} from '@mui/icons-material'
import { styled } from '@mui/system'
import { Refresh } from '@mui/icons-material'
PictureInPicture,
VolumeOff,
Calculate,
} from '@mui/icons-material';
import { styled } from '@mui/system';
import { Refresh } from '@mui/icons-material';
import { Menu, MenuItem } from '@mui/material'
import { MoreVert as MoreIcon } from '@mui/icons-material'
import { GlobalContext, getBaseApiReact } from '../../App'
import { resourceKeySelector } from '../../atoms/global'
import { useRecoilValue } from 'recoil'
import { Menu, MenuItem } from '@mui/material';
import { MoreVert as MoreIcon } from '@mui/icons-material';
import { MyContext, getBaseApiReact } from '../../App';
import { resourceKeySelector } from '../../atoms/global';
import { useAtomValue } from 'jotai';
const VideoContainer = styled(Box)`
position: relative;
display: flex;
@@ -28,14 +31,14 @@ const VideoContainer = styled(Box)`
height: 100%;
margin: 0px;
padding: 0px;
`
`;
const VideoElement = styled('video')`
width: 100%;
height: auto;
max-height: calc(100vh - 150px);
background: rgb(33, 33, 33);
`
`;
const ControlsContainer = styled(Box)`
position: absolute;
@@ -47,18 +50,18 @@ const ControlsContainer = styled(Box)`
right: 0;
padding: 8px;
background-color: rgba(0, 0, 0, 0.6);
`
`;
interface VideoPlayerProps {
src?: string
poster?: string
name?: string
identifier?: string
service?: string
autoplay?: boolean
from?: string | null
customStyle?: any
user?: string
src?: string;
poster?: string;
name?: string;
identifier?: string;
service?: string;
autoplay?: boolean;
from?: string | null;
customStyle?: any;
user?: string;
}
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
@@ -69,33 +72,32 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
autoplay = true,
from = null,
customStyle = {},
node
node,
}) => {
const keyIdentifier = useMemo(()=> {
if(name && identifier && service){
return `${service}-${name}-${identifier}`
const keyIdentifier = useMemo(() => {
if (name && identifier && service) {
return `${service}-${name}-${identifier}`;
} else {
return undefined
return undefined;
}
}, [service, name, identifier])
const download = useRecoilValue(resourceKeySelector(keyIdentifier));
const { downloadResource } = useContext(GlobalContext);
}, [service, name, identifier]);
const videoRef = useRef<HTMLVideoElement | null>(null)
const [playing, setPlaying] = useState(false)
const [volume, setVolume] = useState(1)
const [mutedVolume, setMutedVolume] = useState(1)
const [isMuted, setIsMuted] = useState(false)
const [progress, setProgress] = useState(0)
const [isLoading, setIsLoading] = useState(false)
const [canPlay, setCanPlay] = useState(false)
const [startPlay, setStartPlay] = useState(false)
const [isMobileView, setIsMobileView] = useState(false)
const [playbackRate, setPlaybackRate] = useState(1)
const [anchorEl, setAnchorEl] = useState(null)
const reDownload = useRef<boolean>(false)
const download = useAtomValue(resourceKeySelector(keyIdentifier));
const { downloadResource } = useContext(MyContext);
const videoRef = useRef<HTMLVideoElement | null>(null);
const [playing, setPlaying] = useState(false);
const [volume, setVolume] = useState(1);
const [mutedVolume, setMutedVolume] = useState(1);
const [isMuted, setIsMuted] = useState(false);
const [progress, setProgress] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [canPlay, setCanPlay] = useState(false);
const [startPlay, setStartPlay] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1);
const [anchorEl, setAnchorEl] = useState(null);
const reDownload = useRef<boolean>(false);
const resetVideoState = () => {
// Reset all states to their initial values
@@ -107,10 +109,9 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
setIsLoading(false);
setCanPlay(false);
setStartPlay(false);
setIsMobileView(false);
setPlaybackRate(1);
setAnchorEl(null);
// Reset refs to their initial values
if (videoRef.current) {
videoRef.current.pause(); // Ensure the video is paused
@@ -120,18 +121,19 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
};
const src = useMemo(() => {
if(name && identifier && service){
return `${node || getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}`
}
return ''
}, [service, name, identifier])
if (name && identifier && service) {
return `${node || getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}`;
}
return '';
}, [service, name, identifier]);
useEffect(() => {
resetVideoState();
}, [keyIdentifier]);
useEffect(()=> {
resetVideoState()
}, [keyIdentifier])
const resourceStatus = useMemo(() => {
return download?.status || {}
}, [download])
return download?.status || {};
}, [download]);
const minSpeed = 0.25;
const maxSpeed = 4.0;
@@ -139,306 +141,339 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
const updatePlaybackRate = (newSpeed: number) => {
if (videoRef.current) {
if (newSpeed > maxSpeed || newSpeed < minSpeed)
newSpeed = minSpeed
videoRef.current.playbackRate = newSpeed
setPlaybackRate(newSpeed)
if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed;
videoRef.current.playbackRate = newSpeed;
setPlaybackRate(newSpeed);
}
}
};
const increaseSpeed = (wrapOverflow = true) => {
const changedSpeed = playbackRate + speedChange
let newSpeed = wrapOverflow ? changedSpeed : Math.min(changedSpeed, maxSpeed)
const changedSpeed = playbackRate + speedChange;
let newSpeed = wrapOverflow
? changedSpeed
: Math.min(changedSpeed, maxSpeed);
if (videoRef.current) {
updatePlaybackRate(newSpeed);
}
}
};
const decreaseSpeed = () => {
if (videoRef.current) {
updatePlaybackRate(playbackRate - speedChange);
}
}
};
const togglePlay = async () => {
if (!videoRef.current) return
setStartPlay(true)
if (!videoRef.current) return;
setStartPlay(true);
if (!src || resourceStatus?.status !== 'READY') {
ReactDOM.flushSync(() => {
setIsLoading(true)
})
getSrc()
setIsLoading(true);
});
getSrc();
}
if (playing) {
videoRef.current.pause()
videoRef.current.pause();
} else {
videoRef.current.play()
videoRef.current.play();
}
setPlaying(!playing)
}
setPlaying(!playing);
};
const onVolumeChange = (_: any, value: number | number[]) => {
if (!videoRef.current) return
videoRef.current.volume = value as number
setVolume(value as number)
setIsMuted(false)
}
if (!videoRef.current) return;
videoRef.current.volume = value as number;
setVolume(value as number);
setIsMuted(false);
};
const onProgressChange = (_: any, value: number | number[]) => {
if (!videoRef.current) return
videoRef.current.currentTime = value as number
setProgress(value as number)
if (!videoRef.current) return;
videoRef.current.currentTime = value as number;
setProgress(value as number);
if (!playing) {
videoRef.current.play()
setPlaying(true)
videoRef.current.play();
setPlaying(true);
}
}
};
const handleEnded = () => {
setPlaying(false)
}
setPlaying(false);
};
const updateProgress = () => {
if (!videoRef.current) return
setProgress(videoRef.current.currentTime)
}
if (!videoRef.current) return;
setProgress(videoRef.current.currentTime);
};
const [isFullscreen, setIsFullscreen] = useState(false)
const [isFullscreen, setIsFullscreen] = useState(false);
const enterFullscreen = () => {
if (!videoRef.current) return
if (!videoRef.current) return;
if (videoRef.current.requestFullscreen) {
videoRef.current.requestFullscreen()
videoRef.current.requestFullscreen();
}
}
};
const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen()
document.exitFullscreen();
}
}
};
const toggleFullscreen = () => {
isFullscreen ? exitFullscreen() : enterFullscreen()
}
isFullscreen ? exitFullscreen() : enterFullscreen();
};
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement)
}
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange)
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange)
}
}, [])
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
}, []);
const handleCanPlay = () => {
setIsLoading(false)
setCanPlay(true)
}
setIsLoading(false);
setCanPlay(true);
};
const getSrc = React.useCallback(async () => {
if (!name || !identifier || !service) return
if (!name || !identifier || !service) return;
try {
downloadResource({
name,
service,
identifier
})
} catch (error) {
console.error(error)
identifier,
});
} catch (error) {
console.error(error);
}
}, [identifier, name, service])
}, [identifier, name, service]);
function formatTime(seconds: number): string {
seconds = Math.floor(seconds)
let minutes: number | string = Math.floor(seconds / 60)
let hours: number | string = Math.floor(minutes / 60)
seconds = Math.floor(seconds);
let minutes: number | string = Math.floor(seconds / 60);
let hours: number | string = Math.floor(minutes / 60);
let remainingSeconds: number | string = seconds % 60
let remainingMinutes: number | string = minutes % 60
let remainingSeconds: number | string = seconds % 60;
let remainingMinutes: number | string = minutes % 60;
if (remainingSeconds < 10) {
remainingSeconds = '0' + remainingSeconds
remainingSeconds = '0' + remainingSeconds;
}
if (remainingMinutes < 10) {
remainingMinutes = '0' + remainingMinutes
remainingMinutes = '0' + remainingMinutes;
}
if (hours === 0) {
hours = ''
}
else {
hours = hours + ':'
hours = '';
} else {
hours = hours + ':';
}
return hours + remainingMinutes + ':' + remainingSeconds
return hours + remainingMinutes + ':' + remainingSeconds;
}
const reloadVideo = () => {
if (!videoRef.current) return
const currentTime = videoRef.current.currentTime
videoRef.current.src = src
videoRef.current.load()
videoRef.current.currentTime = currentTime
if (!videoRef.current) return;
const currentTime = videoRef.current.currentTime;
videoRef.current.src = src;
videoRef.current.load();
videoRef.current.currentTime = currentTime;
if (playing) {
videoRef.current.play()
videoRef.current.play();
}
}
};
useEffect(() => {
if (
resourceStatus?.status === 'DOWNLOADED' &&
reDownload?.current === false
) {
getSrc()
reDownload.current = true
getSrc();
reDownload.current = true;
}
}, [getSrc, resourceStatus])
}, [getSrc, resourceStatus]);
const handleMenuOpen = (event: any) => {
setAnchorEl(event.currentTarget)
}
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null)
}
setAnchorEl(null);
};
useEffect(() => {
const videoWidth = videoRef?.current?.offsetWidth
if (videoWidth && videoWidth <= 600) {
setIsMobileView(true)
}
}, [canPlay])
const videoWidth = videoRef?.current?.offsetWidth;
}, [canPlay]);
const getDownloadProgress = (current: number, total: number) => {
const progress = current / total * 100;
return Number.isNaN(progress) ? '' : progress.toFixed(0) + '%'
}
const progress = (current / total) * 100;
return Number.isNaN(progress) ? '' : progress.toFixed(0) + '%';
};
const mute = () => {
setIsMuted(true)
setMutedVolume(volume)
setVolume(0)
if (videoRef.current) videoRef.current.volume = 0
}
setIsMuted(true);
setMutedVolume(volume);
setVolume(0);
if (videoRef.current) videoRef.current.volume = 0;
};
const unMute = () => {
setIsMuted(false)
setVolume(mutedVolume)
if (videoRef.current) videoRef.current.volume = mutedVolume
}
setIsMuted(false);
setVolume(mutedVolume);
if (videoRef.current) videoRef.current.volume = mutedVolume;
};
const toggleMute = () => {
isMuted ? unMute() : mute();
}
};
const changeVolume = (volumeChange: number) => {
if (videoRef.current) {
const minVolume = 0;
const maxVolume = 1;
let newVolume = volumeChange + volume;
let newVolume = volumeChange + volume
newVolume = Math.max(newVolume, minVolume);
newVolume = Math.min(newVolume, maxVolume);
newVolume = Math.max(newVolume, minVolume)
newVolume = Math.min(newVolume, maxVolume)
setIsMuted(false)
setMutedVolume(newVolume)
videoRef.current.volume = newVolume
setIsMuted(false);
setMutedVolume(newVolume);
videoRef.current.volume = newVolume;
setVolume(newVolume);
}
}
};
const setProgressRelative = (secondsChange: number) => {
if (videoRef.current) {
const currentTime = videoRef.current?.currentTime
const minTime = 0
const maxTime = videoRef.current?.duration || 100
const currentTime = videoRef.current?.currentTime;
const minTime = 0;
const maxTime = videoRef.current?.duration || 100;
let newTime = currentTime + secondsChange;
newTime = Math.max(newTime, minTime)
newTime = Math.min(newTime, maxTime)
newTime = Math.max(newTime, minTime);
newTime = Math.min(newTime, maxTime);
videoRef.current.currentTime = newTime;
setProgress(newTime);
}
}
};
const setProgressAbsolute = (videoPercent: number) => {
if (videoRef.current) {
videoPercent = Math.min(videoPercent, 100)
videoPercent = Math.max(videoPercent, 0)
const finalTime = videoRef.current?.duration * videoPercent / 100
videoRef.current.currentTime = finalTime
videoPercent = Math.min(videoPercent, 100);
videoPercent = Math.max(videoPercent, 0);
const finalTime = (videoRef.current?.duration * videoPercent) / 100;
videoRef.current.currentTime = finalTime;
setProgress(finalTime);
}
}
};
const keyboardShortcutsDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
e.preventDefault()
e.preventDefault();
switch (e.key) {
case Key.Add: increaseSpeed(false); break;
case '+': increaseSpeed(false); break;
case '>': increaseSpeed(false); break;
case Key.Add:
increaseSpeed(false);
break;
case '+':
increaseSpeed(false);
break;
case '>':
increaseSpeed(false);
break;
case Key.Subtract: decreaseSpeed(); break;
case '-': decreaseSpeed(); break;
case '<': decreaseSpeed(); break;
case Key.Subtract:
decreaseSpeed();
break;
case '-':
decreaseSpeed();
break;
case '<':
decreaseSpeed();
break;
case Key.ArrowLeft: {
if (e.shiftKey) setProgressRelative(-300);
else if (e.ctrlKey) setProgressRelative(-60);
else if (e.altKey) setProgressRelative(-10);
else setProgressRelative(-5);
} break;
case Key.ArrowLeft:
{
if (e.shiftKey) setProgressRelative(-300);
else if (e.ctrlKey) setProgressRelative(-60);
else if (e.altKey) setProgressRelative(-10);
else setProgressRelative(-5);
}
break;
case Key.ArrowRight: {
if (e.shiftKey) setProgressRelative(300);
else if (e.ctrlKey) setProgressRelative(60);
else if (e.altKey) setProgressRelative(10);
else setProgressRelative(5);
} break;
case Key.ArrowRight:
{
if (e.shiftKey) setProgressRelative(300);
else if (e.ctrlKey) setProgressRelative(60);
else if (e.altKey) setProgressRelative(10);
else setProgressRelative(5);
}
break;
case Key.ArrowDown: changeVolume(-0.05); break;
case Key.ArrowUp: changeVolume(0.05); break;
case Key.ArrowDown:
changeVolume(-0.05);
break;
case Key.ArrowUp:
changeVolume(0.05);
break;
}
}
};
const keyboardShortcutsUp = (e: React.KeyboardEvent<HTMLDivElement>) => {
e.preventDefault()
e.preventDefault();
switch (e.key) {
case ' ': togglePlay(); break;
case 'm': toggleMute(); break;
case ' ':
togglePlay();
break;
case 'm':
toggleMute();
break;
case 'f': enterFullscreen(); break;
case Key.Escape: exitFullscreen(); break;
case 'f':
enterFullscreen();
break;
case Key.Escape:
exitFullscreen();
break;
case '0': setProgressAbsolute(0); break;
case '1': setProgressAbsolute(10); break;
case '2': setProgressAbsolute(20); break;
case '3': setProgressAbsolute(30); break;
case '4': setProgressAbsolute(40); break;
case '5': setProgressAbsolute(50); break;
case '6': setProgressAbsolute(60); break;
case '7': setProgressAbsolute(70); break;
case '8': setProgressAbsolute(80); break;
case '9': setProgressAbsolute(90); break;
case '0':
setProgressAbsolute(0);
break;
case '1':
setProgressAbsolute(10);
break;
case '2':
setProgressAbsolute(20);
break;
case '3':
setProgressAbsolute(30);
break;
case '4':
setProgressAbsolute(40);
break;
case '5':
setProgressAbsolute(50);
break;
case '6':
setProgressAbsolute(60);
break;
case '7':
setProgressAbsolute(70);
break;
case '8':
setProgressAbsolute(80);
break;
case '9':
setProgressAbsolute(90);
break;
}
}
};
return (
<VideoContainer
@@ -451,7 +486,6 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
height: '100%',
}}
>
{isLoading && (
<Box
position="absolute"
@@ -467,40 +501,44 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
sx={{
display: 'flex',
flexDirection: 'column',
gap: '10px'
gap: '10px',
}}
>
<CircularProgress color="secondary" />
<Typography
variant="subtitle2"
component="div"
sx={{
color: 'white',
fontSize: '15px',
textAlign: 'center'
}}
>
{resourceStatus?.status === 'REFETCHING' ? (
<>
<>
{getDownloadProgress(resourceStatus?.localChunkCount, resourceStatus?.totalChunkCount)}
</>
<> Refetching data in 25 seconds</>
</>
) : resourceStatus?.status === 'DOWNLOADED' ? (
<>Download Completed: building tutorial video...</>
) : resourceStatus?.status !== 'READY' ? (
<Typography
variant="subtitle2"
component="div"
sx={{
color: 'white',
fontSize: '15px',
textAlign: 'center',
}}
>
{resourceStatus?.status === 'REFETCHING' ? (
<>
<>
{getDownloadProgress(resourceStatus?.localChunkCount || 0, resourceStatus?.totalChunkCount || 100)}
{getDownloadProgress(
resourceStatus?.localChunkCount,
resourceStatus?.totalChunkCount
)}
</>
) : (
<>Fetching tutorial from the Qortal Network...</>
)}
</Typography>
<> Refetching data in 25 seconds</>
</>
) : resourceStatus?.status === 'DOWNLOADED' ? (
<>Download Completed: building tutorial video...</>
) : resourceStatus?.status !== 'READY' ? (
<>
{getDownloadProgress(
resourceStatus?.localChunkCount || 0,
resourceStatus?.totalChunkCount || 100
)}
</>
) : (
<>Fetching tutorial from the Qortal Network...</>
)}
</Typography>
</Box>
)}
{((!src && !isLoading) || !startPlay) && (
@@ -516,59 +554,61 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
zIndex={500}
bgcolor="rgba(0, 0, 0, 0.6)"
onClick={() => {
togglePlay()
togglePlay();
}}
sx={{
cursor: 'pointer'
cursor: 'pointer',
}}
>
<PlayArrow
sx={{
width: '50px',
height: '50px',
color: 'white'
color: 'white',
}}
/>
</Box>
)}
<Box sx={{
display: 'flex',
flexGrow: 1,
width: '100%',
height: 'calc(100% - 60px)',
}}>
<VideoElement
id={identifier}
ref={videoRef}
src={!startPlay ? '' : resourceStatus?.status === 'READY' ? src : ''}
poster={!startPlay ? poster : ""}
onTimeUpdate={updateProgress}
autoPlay={autoplay}
onClick={togglePlay}
onEnded={handleEnded}
// onLoadedMetadata={handleLoadedMetadata}
onCanPlay={handleCanPlay}
preload="metadata"
style={{
<Box
sx={{
display: 'flex',
flexGrow: 1,
width: '100%',
height: '100%',
...customStyle
height: 'calc(100% - 60px)',
}}
/>
>
<VideoElement
id={identifier}
ref={videoRef}
src={!startPlay ? '' : resourceStatus?.status === 'READY' ? src : ''}
poster={!startPlay ? poster : ''}
onTimeUpdate={updateProgress}
autoPlay={autoplay}
onClick={togglePlay}
onEnded={handleEnded}
// onLoadedMetadata={handleLoadedMetadata}
onCanPlay={handleCanPlay}
preload="metadata"
style={{
width: '100%',
height: '100%',
...customStyle,
}}
/>
</Box>
<ControlsContainer
sx={{
position: 'relative',
background: 'var(--bg-primary)',
width: '100%',
flexShrink: 0
flexShrink: 0,
}}
>
{isMobileView && canPlay ? (
{canPlay ? (
<>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)'
color: 'rgba(255, 255, 255, 0.7)',
}}
onClick={togglePlay}
>
@@ -577,77 +617,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)',
marginLeft: '15px'
}}
onClick={reloadVideo}
>
<Refresh />
</IconButton>
<Slider
value={progress}
onChange={onProgressChange}
min={0}
max={videoRef.current?.duration || 100}
sx={{ flexGrow: 1, mx: 2 }}
/>
<IconButton
edge="end"
color="inherit"
aria-label="menu"
onClick={handleMenuOpen}
>
<MoreIcon />
</IconButton>
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleMenuClose}
PaperProps={{
style: {
width: '250px'
}
}}
>
<MenuItem>
<VolumeUp />
<Slider
value={volume}
onChange={onVolumeChange}
min={0}
max={1}
step={0.01} />
</MenuItem>
<MenuItem onClick={() => increaseSpeed()}>
<Typography
sx={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: '14px'
}}
>
Speed: {playbackRate}x
</Typography>
</MenuItem>
<MenuItem onClick={toggleFullscreen}>
<Fullscreen />
</MenuItem>
</Menu>
</>
) : canPlay ? (
<>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)'
}}
onClick={togglePlay}
>
{playing ? <Pause /> : <PlayArrow />}
</IconButton>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)',
marginLeft: '15px'
marginLeft: '15px',
}}
onClick={reloadVideo}
>
@@ -669,7 +639,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
!videoRef.current?.duration || !progress
? 'hidden'
: 'visible',
flexShrink: 0
flexShrink: 0,
}}
>
{progress && videoRef.current?.duration && formatTime(progress)}/
@@ -680,7 +650,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)',
marginRight: '10px'
marginRight: '10px',
}}
onClick={toggleMute}
>
@@ -694,14 +664,14 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
step={0.01}
sx={{
maxWidth: '100px',
color: 'var(--Mail-Background)'
color: 'var(--Mail-Background)',
}}
/>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: '14px',
marginLeft: '5px'
marginLeft: '5px',
}}
onClick={(e) => increaseSpeed()}
>
@@ -709,7 +679,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
</IconButton>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)'
color: 'rgba(255, 255, 255, 0.7)',
}}
onClick={toggleFullscreen}
>
@@ -719,5 +689,5 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
) : null}
</ControlsContainer>
</VideoContainer>
)
}
);
};

View File

@@ -1,129 +1,130 @@
import { Box, ButtonBase, Typography } from "@mui/material";
import React from "react";
import ChatIcon from "@mui/icons-material/Chat";
import qTradeLogo from "../../assets/Icons/q-trade-logo.webp";
import AppsIcon from "@mui/icons-material/Apps";
import { executeEvent } from "../../utils/events";
import { Box, ButtonBase, Typography, useTheme } from '@mui/material';
import ChatIcon from '@mui/icons-material/Chat';
import qTradeLogo from '../../assets/Icons/q-trade-logo.webp';
import AppsIcon from '@mui/icons-material/Apps';
import { executeEvent } from '../../utils/events';
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
import { useTranslation } from 'react-i18next';
export const Explore = ({ setDesktopViewMode }) => {
const theme = useTheme();
const { t } = useTranslation(['core', 'tutorial']);
export const Explore = ({setDesktopViewMode}) => {
return (
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
display: 'flex',
gap: '20px',
flexWrap: 'wrap',
}}
>
<ButtonBase
sx={{
"&:hover": { backgroundColor: "secondary.main" },
transition: "all 0.1s ease-in-out",
padding: "5px",
borderRadius: "5px",
gap: "5px",
'&:hover': { backgroundColor: theme.palette.background.paper },
borderRadius: '5px',
gap: '5px',
padding: '5px',
transition: 'all 0.1s ease-in-out',
}}
onClick={async () => {
executeEvent("addTab", {
data: { service: "APP", name: "q-trade" },
});
executeEvent("open-apps-mode", {});
}}
executeEvent('addTab', {
data: { service: 'APP', name: 'q-trade' },
});
executeEvent('open-apps-mode', {});
}}
>
<img
style={{
borderRadius: "50%",
height: '30px'
borderRadius: '50%',
height: '30px',
}}
src={qTradeLogo}
/>
<Typography
sx={{
fontSize: "1rem",
fontSize: '1rem',
}}
>
Trade QORT
{t('tutorial:initial.trade_qort', { postProcess: 'capitalize' })}
</Typography>
</ButtonBase>
<ButtonBase
sx={{
"&:hover": { backgroundColor: "secondary.main" },
transition: "all 0.1s ease-in-out",
padding: "5px",
borderRadius: "5px",
gap: "5px",
'&:hover': { backgroundColor: theme.palette.background.paper },
borderRadius: '5px',
gap: '5px',
padding: '5px',
transition: 'all 0.1s ease-in-out',
}}
onClick={() => {
setDesktopViewMode('apps');
}}
onClick={()=> {
setDesktopViewMode('apps')
}}
>
<AppsIcon
sx={{
color: "white",
color: theme.palette.text.primary,
}}
/>
<Typography
sx={{
fontSize: "1rem",
fontSize: '1rem',
}}
>
See Apps
{t('tutorial:initial.see_apps', { postProcess: 'capitalize' })}
</Typography>
</ButtonBase>
<ButtonBase
sx={{
"&:hover": { backgroundColor: "secondary.main" },
transition: "all 0.1s ease-in-out",
padding: "5px",
borderRadius: "5px",
gap: "5px",
'&:hover': { backgroundColor: theme.palette.background.paper },
borderRadius: '5px',
gap: '5px',
padding: '5px',
transition: 'all 0.1s ease-in-out',
}}
onClick={async () => {
executeEvent("openGroupMessage", {
from: "0" ,
});
}}
executeEvent('openGroupMessage', {
from: '0',
});
}}
>
<ChatIcon
sx={{
color: "white",
color: theme.palette.text.primary,
}}
/>
<Typography
sx={{
fontSize: "1rem",
fontSize: '1rem',
}}
>
General Chat
{t('tutorial:initial.general_chat', { postProcess: 'capitalize' })}
</Typography>
</ButtonBase>
<ButtonBase
sx={{
"&:hover": { backgroundColor: "secondary.main" },
transition: "all 0.1s ease-in-out",
padding: "5px",
borderRadius: "5px",
gap: "5px",
'&:hover': { backgroundColor: theme.palette.background.paper },
transition: 'all 0.1s ease-in-out',
padding: '5px',
borderRadius: '5px',
gap: '5px',
}}
onClick={async () => {
executeEvent("openWalletsApp", {
});
}}
executeEvent('openWalletsApp', {});
}}
>
<AccountBalanceWalletIcon
sx={{
color: "white",
color: theme.palette.text.primary,
}}
/>
<Typography
sx={{
fontSize: "1rem",
fontSize: '1rem',
}}
>
Wallets
{t('core:wallet.wallet_other', { postProcess: 'capitalize' })}
</Typography>
</ButtonBase>
</Box>

View File

@@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useState } from 'react';
import {
Box,
ButtonBase,
@@ -8,58 +7,79 @@ import {
Popover,
Tooltip,
Typography,
} from "@mui/material";
import NotificationsIcon from "@mui/icons-material/Notifications";
import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet";
import { formatDate } from "../utils/time";
import { useHandlePaymentNotification } from "../hooks/useHandlePaymentNotification";
import { executeEvent } from "../utils/events";
useTheme,
} from '@mui/material';
import NotificationsIcon from '@mui/icons-material/Notifications';
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
import { formatDate } from '../utils/time';
import { useHandlePaymentNotification } from '../hooks/useHandlePaymentNotification';
import { executeEvent } from '../utils/events';
import { useTranslation } from 'react-i18next';
export const GeneralNotifications = ({ address }) => {
const [anchorEl, setAnchorEl] = useState(null);
const {latestTx,
const {
latestTx,
getNameOrAddressOfSenderMiddle,
hasNewPayment, setLastEnteredTimestampPayment, nameAddressOfSender} = useHandlePaymentNotification(address)
hasNewPayment,
setLastEnteredTimestampPayment,
nameAddressOfSender,
} = useHandlePaymentNotification(address);
const handlePopupClick = (event) => {
event.stopPropagation(); // Prevent parent onClick from firing
setAnchorEl(event.currentTarget);
};
const { t } = useTranslation(['core']);
const theme = useTheme();
return (
<>
<ButtonBase
onClick={(e) => {
handlePopupClick(e);
}}
style={{}}
>
<Tooltip
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>PAYMENT NOTIFICATION</span>}
placement="left"
arrow
sx={{ fontSize: "24" }}
slotProps={{
tooltip: {
sx: {
color: "#ffffff",
backgroundColor: "#444444",
},
},
arrow: {
sx: {
color: "#444444",
},
},
}}
>
<NotificationsIcon
sx={{
color: hasNewPayment ? "var(--unread)" : "rgba(255, 255, 255, 0.5)",
<Tooltip
title={
<span
style={{
color: theme.palette.text.primary,
fontSize: '14px',
fontWeight: 700,
textTransform: 'uppercase',
}}
>
{t('core:payment_notification')}
</span>
}
placement="left"
arrow
sx={{ fontSize: '24' }}
slotProps={{
tooltip: {
sx: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.paper,
},
},
arrow: {
sx: {
color: theme.palette.text.primary,
},
},
}}
/>
>
<NotificationsIcon
sx={{
color: hasNewPayment
? theme.palette.other.unread
: theme.palette.text.secondary,
}}
/>
</Tooltip>
</ButtonBase>
@@ -67,81 +87,93 @@ export const GeneralNotifications = ({ address }) => {
open={!!anchorEl}
anchorEl={anchorEl}
onClose={() => {
if(hasNewPayment){
setLastEnteredTimestampPayment(Date.now())
if (hasNewPayment) {
setLastEnteredTimestampPayment(Date.now());
}
setAnchorEl(null)
setAnchorEl(null);
}} // Close popover on click outside
>
<Box
sx={{
width: "300px",
maxWidth: "100%",
maxHeight: "60vh",
overflow: "auto",
padding: "5px",
display: "flex",
flexDirection: "column",
alignItems: hasNewPayment ? "flex-start" : "center",
alignItems: hasNewPayment ? 'flex-start' : 'center',
display: 'flex',
flexDirection: 'column',
maxHeight: '60vh',
maxWidth: '100%',
overflow: 'auto',
padding: '5px',
width: '300px',
}}
>
{!hasNewPayment && <Typography sx={{
userSelect: 'none'
}}>No new notifications</Typography>}
{!hasNewPayment && (
<Typography
sx={{
userSelect: 'none',
}}
>
No new notifications
</Typography>
)}
{hasNewPayment && (
<MenuItem
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
width: "100%",
alignItems: "flex-start",
textWrap: "auto",
alignItems: 'flex-start',
display: 'flex',
flexDirection: 'column',
gap: '5px',
textWrap: 'auto',
width: '100%',
}}
onClick={() => {
setAnchorEl(null)
executeEvent('openWalletsApp', {})
onClick={() => {
setAnchorEl(null);
executeEvent('openWalletsApp', {});
}}
>
<Card sx={{
padding: '10px',
width: '100%',
backgroundColor: "#1F2023",
gap: '5px',
display: 'flex',
flexDirection: 'column'
}}>
<Box
<Card
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
justifyContent: "space-between",
backgroundColor: '#1F2023',
display: 'flex',
flexDirection: 'column',
gap: '5px',
padding: '10px',
width: '100%',
}}
>
<AccountBalanceWalletIcon
<Box
sx={{
color: "white",
alignItems: 'center',
display: 'flex',
gap: '5px',
justifyContent: 'space-between',
}}
/>{" "}
{formatDate(latestTx?.timestamp)}
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
justifyContent: "space-between",
}}
>
<Typography>{latestTx?.amount}</Typography>
</Box>
<Typography sx={{
fontSize: '0.8rem'
}}>{nameAddressOfSender.current[latestTx?.creatorAddress] || getNameOrAddressOfSenderMiddle(latestTx?.creatorAddress)}</Typography>
>
<AccountBalanceWalletIcon
sx={{
color: theme.palette.text.primary,
}}
/>{' '}
{formatDate(latestTx?.timestamp)}
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '5px',
justifyContent: 'space-between',
}}
>
<Typography>{latestTx?.amount}</Typography>
</Box>
<Typography
sx={{
fontSize: '0.8rem',
}}
>
{nameAddressOfSender.current[latestTx?.creatorAddress] ||
getNameOrAddressOfSenderMiddle(latestTx?.creatorAddress)}
</Typography>
</Card>
</MenuItem>
)}

View File

@@ -1,10 +1,10 @@
import React from 'react'
import { JoinGroup } from './JoinGroup'
import React from 'react';
import { JoinGroup } from './JoinGroup';
export const GlobalActions = ({memberGroups}) => {
export const GlobalActions = () => {
return (
<>
<JoinGroup memberGroups={memberGroups} />
<JoinGroup />
</>
)
}
);
};

View File

@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useMemo, useState } from "react";
import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
import {
Box,
Button,
@@ -9,20 +9,26 @@ import {
DialogActions,
DialogContent,
Typography,
} from "@mui/material";
import { CustomButton, CustomButtonAccept } from "../../App-styles";
import { getBaseApiReact, MyContext } from "../../App";
import { getFee } from "../../background";
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
import { FidgetSpinner } from "react-loader-spinner";
useTheme,
} from '@mui/material';
import { CustomButton, CustomButtonAccept } from '../../styles/App-styles';
import { getBaseApiReact, MyContext } from '../../App';
import { getFee } from '../../background';
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { FidgetSpinner } from 'react-loader-spinner';
import { useAtom, useSetAtom } from 'jotai';
import { memberGroupsAtom, txListAtom } from '../../atoms/global';
export const JoinGroup = ({ memberGroups }) => {
const { show, setTxList } = useContext(MyContext);
export const JoinGroup = () => {
const { show } = useContext(MyContext);
const setTxList = useSetAtom(txListAtom);
const [memberGroups] = useAtom(memberGroupsAtom);
const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null);
const [groupInfo, setGroupInfo] = useState(null);
const [isLoadingInfo, setIsLoadingInfo] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const theme = useTheme();
const [isLoadingJoinGroup, setIsLoadingJoinGroup] = useState(false);
const handleJoinGroup = async (e) => {
setGroupInfo(null);
@@ -42,43 +48,45 @@ export const JoinGroup = ({ memberGroups }) => {
};
useEffect(() => {
subscribeToEvent("globalActionJoinGroup", handleJoinGroup);
subscribeToEvent('globalActionJoinGroup', handleJoinGroup);
return () => {
unsubscribeFromEvent("globalActionJoinGroup", handleJoinGroup);
unsubscribeFromEvent('globalActionJoinGroup', handleJoinGroup);
};
}, []);
const isInGroup = useMemo(()=> {
return !!memberGroups.find((item)=> +item?.groupId === +groupInfo?.groupId)
}, [memberGroups, groupInfo])
const isInGroup = useMemo(() => {
return !!memberGroups.find(
(item) => +item?.groupId === +groupInfo?.groupId
);
}, [memberGroups, groupInfo]);
const joinGroup = async (group, isOpen) => {
try {
const groupId = group.groupId;
const fee = await getFee("JOIN_GROUP");
const fee = await getFee('JOIN_GROUP');
await show({
message: "Would you like to perform an JOIN_GROUP transaction?",
publishFee: fee.fee + " QORT",
message: 'Would you like to perform an JOIN_GROUP transaction?',
publishFee: fee.fee + ' QORT',
});
setIsLoadingJoinGroup(true);
await new Promise((res, rej) => {
window
.sendMessage("joinGroup", {
.sendMessage('joinGroup', {
groupId,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
type: 'success',
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) {
setTxList((prev) => [
{
...response,
type: "joined-group",
type: 'joined-group',
label: `Joined Group ${group?.groupName}: awaiting confirmation`,
labelDone: `Joined Group ${group?.groupName}: success!`,
done: false,
@@ -90,7 +98,7 @@ export const JoinGroup = ({ memberGroups }) => {
setTxList((prev) => [
{
...response,
type: "joined-group-request",
type: 'joined-group-request',
label: `Requested to join Group ${group?.groupName}: awaiting confirmation`,
labelDone: `Requested to join Group ${group?.groupName}: success!`,
done: false,
@@ -105,7 +113,7 @@ export const JoinGroup = ({ memberGroups }) => {
return;
} else {
setInfoSnack({
type: "error",
type: 'error',
message: response?.error,
});
setOpenSnack(true);
@@ -114,8 +122,8 @@ export const JoinGroup = ({ memberGroups }) => {
})
.catch((error) => {
setInfoSnack({
type: "error",
message: error.message || "An error occurred",
type: 'error',
message: error.message || 'An error occurred',
});
setOpenSnack(true);
rej(error);
@@ -138,37 +146,37 @@ export const JoinGroup = ({ memberGroups }) => {
{!groupInfo && (
<Box
sx={{
width: "325px",
height: "150px",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: '325px',
height: '150px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{" "}
{' '}
<CircularProgress
size={25}
sx={{
color: "white",
color: theme.palette.text.primary,
}}
/>{" "}
/>{' '}
</Box>
)}
<Box
sx={{
width: "325px",
height: "auto",
maxHeight: "400px",
display: !groupInfo ? "none" : "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
width: '325px',
height: 'auto',
maxHeight: '400px',
display: !groupInfo ? 'none' : 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
padding: '10px',
}}
>
<Typography
sx={{
fontSize: "15px",
fontSize: '15px',
fontWeight: 600,
}}
>
@@ -176,7 +184,7 @@ export const JoinGroup = ({ memberGroups }) => {
</Typography>
<Typography
sx={{
fontSize: "15px",
fontSize: '15px',
fontWeight: 600,
}}
>
@@ -185,7 +193,7 @@ export const JoinGroup = ({ memberGroups }) => {
{groupInfo?.description && (
<Typography
sx={{
fontSize: "15px",
fontSize: '15px',
fontWeight: 600,
}}
>
@@ -193,19 +201,19 @@ export const JoinGroup = ({ memberGroups }) => {
</Typography>
)}
{isInGroup && (
<Typography
sx={{
fontSize: "14px",
fontWeight: 600,
}}
>
*You are already in this group!
</Typography>
<Typography
sx={{
fontSize: '14px',
fontWeight: 600,
}}
>
*You are already in this group!
</Typography>
)}
{!isInGroup && groupInfo?.isOpen === false && (
<Typography
sx={{
fontSize: "14px",
fontSize: '14px',
fontWeight: 600,
}}
>
@@ -216,32 +224,34 @@ export const JoinGroup = ({ memberGroups }) => {
</Box>
</DialogContent>
<DialogActions>
<ButtonBase onClick={() => {
<ButtonBase
onClick={() => {
joinGroup(groupInfo, groupInfo?.isOpen);
setIsOpen(false);
}} disabled={isInGroup}>
<CustomButtonAccept
color="black"
bgColor="var(--green)"
sx={{
minWidth: "102px",
height: "45px",
fontSize: '16px',
opacity: isInGroup ? 0.1 : 1
}}
disabled={isInGroup}
>
Join
</CustomButtonAccept>
<CustomButtonAccept
color="black"
bgColor={theme.palette.other.positive}
sx={{
minWidth: '102px',
height: '45px',
fontSize: '16px',
opacity: isInGroup ? 0.1 : 1,
}}
>
Join
</CustomButtonAccept>
</ButtonBase>
<CustomButtonAccept
color="black"
bgColor="var(--danger)"
bgColor={theme.palette.other.danger}
sx={{
minWidth: "102px",
height: "45px",
minWidth: '102px',
height: '45px',
}}
onClick={() => setIsOpen(false)}
>
@@ -259,14 +269,14 @@ export const JoinGroup = ({ memberGroups }) => {
{isLoadingJoinGroup && (
<Box
sx={{
position: "absolute",
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<FidgetSpinner

View File

@@ -1,19 +1,24 @@
import * as React from "react";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import ListItemText from "@mui/material/ListItemText";
import ListItemButton from "@mui/material/ListItemButton";
import List from "@mui/material/List";
import Divider from "@mui/material/Divider";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import CloseIcon from "@mui/icons-material/Close";
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 {
forwardRef,
Fragment,
ReactElement,
Ref,
SyntheticEvent,
useContext,
useEffect,
useState,
} from 'react';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import CloseIcon from '@mui/icons-material/Close';
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 {
Box,
Collapse,
@@ -24,51 +29,54 @@ import {
Tab,
Tabs,
styled,
} from "@mui/material";
import { AddGroupList } from "./AddGroupList";
import { UserListOfInvites } from "./UserListOfInvites";
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
import { getFee } from "../../background";
import { MyContext, isMobile } from "../../App";
import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
useTheme,
} from '@mui/material';
import { AddGroupList } from './AddGroupList';
import { UserListOfInvites } from './UserListOfInvites';
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { getFee } from '../../background';
import { MyContext } from '../../App';
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
import { useTranslation } from 'react-i18next';
import { useSetAtom } from 'jotai';
import { txListAtom } from '../../atoms/global';
export const Label = styled("label")(
({ theme }) => `
export const Label = styled('label')`
display: block;
font-family: 'IBM Plex Sans', sans-serif;
font-size: 14px;
display: block;
margin-bottom: 4px;
font-weight: 400;
`
);
const Transition = React.forwardRef(function Transition(
margin-bottom: 4px;
`;
const Transition = forwardRef(function Transition(
props: TransitionProps & {
children: React.ReactElement;
children: ReactElement;
},
ref: React.Ref<unknown>
ref: Ref<unknown>
) {
return <Slide direction="up" ref={ref} {...props} />;
});
export const AddGroup = ({ address, open, setOpen }) => {
const {show, setTxList} = React.useContext(MyContext)
const { show } = useContext(MyContext);
const setTxList = useSetAtom(txListAtom);
const [tab, setTab] = React.useState("create");
const [openAdvance, setOpenAdvance] = React.useState(false);
const [openAdvance, setOpenAdvance] = useState(false);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [groupType, setGroupType] = useState('1');
const [approvalThreshold, setApprovalThreshold] = useState('40');
const [minBlock, setMinBlock] = useState('5');
const [maxBlock, setMaxBlock] = useState('21600');
const [value, setValue] = useState(0);
const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null);
const [name, setName] = React.useState("");
const [description, setDescription] = React.useState("");
const [groupType, setGroupType] = React.useState("1");
const [approvalThreshold, setApprovalThreshold] = React.useState("40");
const [minBlock, setMinBlock] = React.useState("5");
const [maxBlock, setMaxBlock] = React.useState("21600");
const [value, setValue] = React.useState(0);
const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null);
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
const handleChange = (event: SyntheticEvent, newValue: number) => {
setValue(newValue);
};
const handleClose = () => {
setOpen(false);
};
@@ -89,400 +97,530 @@ export const AddGroup = ({ address, open, setOpen }) => {
setMaxBlock(event.target.value as string);
};
const { t } = useTranslation(['core', 'group']);
const theme = useTheme();
const handleCreateGroup = async () => {
try {
if(!name) throw new Error('Please provide a name')
if(!description) throw new Error('Please provide a description')
if (!name)
throw new Error(
t('group:message.error.name_required', {
postProcess: 'capitalize',
})
);
if (!description)
throw new Error(
t('group:message.error.description_required', {
postProcess: 'capitalize',
})
);
const fee = await getFee('CREATE_GROUP');
const fee = await getFee('CREATE_GROUP')
await show({
message: "Would you like to perform an CREATE_GROUP transaction?" ,
publishFee: fee.fee + ' QORT'
})
message: t('group:question.create_group', {
postProcess: 'capitalize',
}),
publishFee: fee.fee + ' QORT',
});
await new Promise((res, rej) => {
window.sendMessage("createGroup", {
groupName: name,
groupDescription: description,
groupType: +groupType,
groupApprovalThreshold: +approvalThreshold,
minBlock: +minBlock,
maxBlock: +maxBlock,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message: "Successfully created group. It may take a couple of minutes for the changes to propagate",
await new Promise((res, rej) => {
window
.sendMessage('createGroup', {
groupName: name,
groupDescription: description,
groupType: +groupType,
groupApprovalThreshold: +approvalThreshold,
minBlock: +minBlock,
maxBlock: +maxBlock,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
type: 'success',
message: t('group:message.success.group_creation', {
postProcess: 'capitalize',
}),
});
setOpenSnack(true);
setTxList((prev) => [
{
...response,
type: 'created-group',
label: t('group:message.success.group_creation_name', {
group_name: name,
postProcess: 'capitalize',
}),
labelDone: t('group:message.success.group_creation_label', {
group_name: name,
postProcess: 'capitalize',
}),
done: false,
},
...prev,
]);
res(response);
return;
}
rej({ message: response.error });
})
.catch((error) => {
rej({
message:
error.message ||
t('core:message.error.generic', { postProcess: 'capitalize' }),
});
setOpenSnack(true);
setTxList((prev) => [
{
...response,
type: 'created-group',
label: `Created group ${name}: awaiting confirmation`,
labelDone: `Created group ${name}: success!`,
done: false,
},
...prev,
]);
res(response);
return;
}
rej({ message: response.error });
})
.catch((error) => {
rej({ message: error.message || "An error occurred" });
});
});
});
} catch (error) {
setInfoSnack({
type: "error",
type: 'error',
message: error?.message,
});
setOpenSnack(true);
}
};
function CustomTabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
const openGroupInvitesRequestFunc = () => {
setValue(2);
};
const openGroupInvitesRequestFunc = ()=> {
setValue(2)
}
React.useEffect(() => {
subscribeToEvent("openGroupInvitesRequest", openGroupInvitesRequestFunc);
useEffect(() => {
subscribeToEvent('openGroupInvitesRequest', openGroupInvitesRequestFunc);
return () => {
unsubscribeFromEvent("openGroupInvitesRequest", openGroupInvitesRequestFunc);
unsubscribeFromEvent(
'openGroupInvitesRequest',
openGroupInvitesRequestFunc
);
};
}, []);
if (!open) return null;
return (
<React.Fragment>
<Fragment>
<Dialog
fullScreen
open={open}
onClose={handleClose}
TransitionComponent={Transition}
>
<AppBar sx={{ position: "relative", bgcolor: "#232428" }}>
<AppBar
sx={{
position: 'relative',
bgcolor: theme.palette.background.default,
}}
>
<Toolbar>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
Group Mgmt
<Typography sx={{ ml: 2, flex: 1 }} variant="h4" component="div">
{t('group:group.management', { postProcess: 'capitalize' })}
</Typography>
<IconButton
edge="start"
color="inherit"
onClick={handleClose}
aria-label="close"
color="inherit"
edge="start"
onClick={handleClose}
>
<CloseIcon />
</IconButton>
{/* <Button autoFocus color="inherit" onClick={handleClose}>
save
</Button> */}
</Toolbar>
</AppBar>
<Box
sx={{
bgcolor: "#27282c",
flexGrow: 1,
overflowY: "auto",
color: "white",
bgcolor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'flex',
flexDirection: 'column',
display: 'flex'
flexGrow: 1,
overflowY: 'auto',
}}
>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={value}
onChange={handleChange}
aria-label="basic tabs example"
variant={isMobile ? 'scrollable' : 'fullWidth'} // Scrollable on mobile, full width on desktop
scrollButtons="auto"
allowScrollButtonsMobile
sx={{
"& .MuiTabs-indicator": {
backgroundColor: "white",
},
}}
>
<Tab
label="Create Group"
{...a11yProps(0)}
sx={{
"&.Mui-selected": {
color: "white",
},
fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile
}}
/>
<Tab
label="Find Group"
{...a11yProps(1)}
sx={{
"&.Mui-selected": {
color: "white",
},
fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile
}}
/>
<Tab
label="Group Invites"
{...a11yProps(2)}
sx={{
"&.Mui-selected": {
color: "white",
},
fontSize: isMobile ? '0.75rem' : '1rem', // Adjust font size for mobile
}}
/>
</Tabs>
<Box
sx={{ borderBottom: 1, borderColor: theme.palette.text.secondary }}
>
<Tabs
value={value}
onChange={handleChange}
aria-label="basic tabs example"
variant={'fullWidth'}
scrollButtons="auto"
allowScrollButtonsMobile
sx={{
'& .MuiTabs-indicator': {
backgroundColor: theme.palette.background.default,
},
}}
>
<Tab
label={t('group:action.create_group', {
postProcess: 'capitalize',
})}
{...a11yProps(0)}
sx={{
'&.Mui-selected': {
color: theme.palette.text.primary,
},
fontSize: '1rem',
}}
/>
<Tab
label={t('group:action.find_group', {
postProcess: 'capitalize',
})}
{...a11yProps(1)}
sx={{
'&.Mui-selected': {
color: theme.palette.text.primary,
},
fontSize: '1rem',
}}
/>
<Tab
label={t('group:group.invites', {
postProcess: 'capitalize',
})}
{...a11yProps(2)}
sx={{
'&.Mui-selected': {
color: theme.palette.text.primary,
},
fontSize: '1rem',
}}
/>
</Tabs>
</Box>
{value === 0 && (
<Box sx={{
width: '100%',
padding: '25px'
}}>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "20px",
maxWidth: "500px",
width: '100%',
padding: '25px',
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
display: 'flex',
flexDirection: 'column',
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
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>
Group Approval Threshold (number / percentage of Admins that
must approve a transaction)
{t('group:group.name', {
postProcess: 'capitalize',
})}
</Label>
<Input
placeholder={t('group:group.name', {
postProcess: 'capitalize',
})}
value={name}
onChange={(e) => setName(e.target.value)}
/>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>
{t('group:group.description', {
postProcess: 'capitalize',
})}
</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>
<Input
placeholder={t('group:group.description', {
postProcess: 'capitalize',
})}
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>
Minimum Block delay for Group Transaction Approvals
{' '}
{t('group:group.type', {
postProcess: 'capitalize',
})}
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={minBlock}
label="Minimum Block delay"
onChange={handleChangeMinBlock}
value={groupType}
label="Group Type"
onChange={handleChangeGroupType}
>
<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>
<MenuItem value={1}>
{t('group:group.open', {
postProcess: 'capitalize',
})}
</MenuItem>
<MenuItem value={0}>
{t('group:group.closed', {
postProcess: 'capitalize',
})}
</MenuItem>
</Select>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
display: 'flex',
gap: '15px',
alignItems: 'center',
cursor: 'pointer',
}}
onClick={() => setOpenAdvance((prev) => !prev)}
>
<Typography>
{t('group:advanced_options', {
postProcess: 'capitalize',
})}
</Typography>
{openAdvance ? <ExpandLess /> : <ExpandMore />}
</Box>
<Collapse in={openAdvance} timeout="auto" unmountOnExit>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>
{t('group:approval_threshold', {
postProcess: 'capitalize',
})}
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={approvalThreshold}
label="Group Approval Threshold"
onChange={handleChangeApprovalThreshold}
>
<MenuItem value={0}>
{t('core.count.none', {
postProcess: 'capitalize',
})}
</MenuItem>
<MenuItem value={1}>
{t('core.count.one', {
postProcess: 'capitalize',
})}
</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>
{t('group.block_delay.minimum', {
postProcess: 'capitalize',
})}
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={minBlock}
label="Minimum Block delay"
onChange={handleChangeMinBlock}
>
<MenuItem value={5}>
{t('core.time.minute', { count: 5 })}
</MenuItem>
<MenuItem value={10}>
{t('core.time.minute', { count: 10 })}
</MenuItem>
<MenuItem value={30}>
{t('core.time.minute', { count: 30 })}
</MenuItem>
<MenuItem value={60}>
{t('core.time.hour', { count: 1 })}
</MenuItem>
<MenuItem value={180}>
{t('core.time.hour', { count: 3 })}
</MenuItem>
<MenuItem value={300}>
{t('core.time.hour', { count: 5 })}
</MenuItem>
<MenuItem value={420}>
{t('core.time.hour', { count: 7 })}
</MenuItem>
<MenuItem value={720}>
{t('core.time.hour', { count: 12 })}
</MenuItem>
<MenuItem value={1440}>
{t('core.time.day', { count: 1 })}
</MenuItem>
<MenuItem value={4320}>
{t('core.time.day', { count: 3 })}
</MenuItem>
<MenuItem value={7200}>
{t('core.time.day', { count: 5 })}
</MenuItem>
<MenuItem value={10080}>
{t('core.time.day', { count: 7 })}
</MenuItem>
</Select>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>
{t('group.block_delay.maximum', {
postProcess: 'capitalize',
})}
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={maxBlock}
label="Maximum Block delay"
onChange={handleChangeMaxBlock}
>
<MenuItem value={60}>
{t('core.time.hour', { count: 1 })}
</MenuItem>
<MenuItem value={180}>
3{t('core.time.hour', { count: 3 })}
</MenuItem>
<MenuItem value={300}>
{t('core.time.hour', { count: 5 })}
</MenuItem>
<MenuItem value={420}>
{t('core.time.hour', { count: 7 })}
</MenuItem>
<MenuItem value={720}>
{t('core.time.hour', { count: 12 })}
</MenuItem>
<MenuItem value={1440}>
{t('core.time.day', { count: 1 })}
</MenuItem>
<MenuItem value={4320}>
{t('core.time.day', { count: 3 })}
</MenuItem>
<MenuItem value={7200}>
{t('core.time.day', { count: 5 })}
</MenuItem>
<MenuItem value={10080}>
{t('core.time.day', { count: 7 })}
</MenuItem>
<MenuItem value={14400}>
{t('core.time.day', { count: 10 })}
</MenuItem>
<MenuItem value={21600}>
{t('core.time.day', { count: 15 })}
</MenuItem>
</Select>
</Box>
</Collapse>
<Box
sx={{
display: 'flex',
width: '100%',
justifyContent: 'center',
}}
>
<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}
<Button
variant="contained"
color="primary"
onClick={handleCreateGroup}
>
<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>
{t('group.action.create', {
postProcess: 'capitalize',
})}
</Button>
</Box>
</Collapse>
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "center",
}}
>
<Button
variant="contained"
color="primary"
onClick={handleCreateGroup}
>
Create Group
</Button>
</Box>
</Box>
</Box>
)}
{value === 1 && (
<Box sx={{
width: '100%',
padding: '25px',
flexDirection: 'column',
flexGrow: 1,
display: 'flex'
}}>
<AddGroupList setOpenSnack={setOpenSnack} setInfoSnack={setInfoSnack} />
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
padding: '25px',
width: '100%',
}}
>
<AddGroupList
setOpenSnack={setOpenSnack}
setInfoSnack={setInfoSnack}
/>
</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>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} />
<CustomizedSnackbars
open={openSnack}
setOpen={setOpenSnack}
info={infoSnack}
setInfo={setInfoSnack}
/>
</Dialog>
</React.Fragment>
</Fragment>
);
};

View File

@@ -1,50 +1,58 @@
import {
Box,
Button,
ListItem,
ListItemButton,
ListItemText,
Popover,
TextField,
Typography,
} from "@mui/material";
import React, {
useTheme,
} from '@mui/material';
import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
} from 'react';
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List,
} from "react-virtualized";
import _ from "lodash";
import { MyContext, getBaseApiReact } from "../../App";
import { LoadingButton } from "@mui/lab";
import { getBaseApi, getFee } from "../../background";
} from 'react-virtualized';
import _ from 'lodash';
import { MyContext, getBaseApiReact } from '../../App';
import { LoadingButton } from '@mui/lab';
import { getFee } from '../../background';
import LockIcon from '@mui/icons-material/Lock';
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
import { Spacer } from "../../common/Spacer";
import { Spacer } from '../../common/Spacer';
import { useTranslation } from 'react-i18next';
import { useAtom, useSetAtom } from 'jotai';
import { memberGroupsAtom, txListAtom } from '../../atoms/global';
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 50,
});
export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
const { memberGroups, show, setTxList } = useContext(MyContext);
const { show } = useContext(MyContext);
const [memberGroups] = useAtom(memberGroupsAtom);
const setTxList = useSetAtom(txListAtom);
const { t } = useTranslation(['core', 'group']);
const [groups, setGroups] = useState([]);
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 listRef = useRef();
const [inputValue, setInputValue] = useState("");
const [inputValue, setInputValue] = useState('');
const [filteredItems, setFilteredItems] = useState(groups);
const [isLoading, setIsLoading] = useState(false);
const theme = useTheme();
const handleFilter = useCallback(
(query) => {
if (query) {
@@ -72,9 +80,7 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
const getGroups = async () => {
try {
const response = await fetch(
`${getBaseApiReact()}/groups/?limit=0`
);
const response = await fetch(`${getBaseApiReact()}/groups/?limit=0`);
const groupData = await response.json();
const filteredGroup = groupData.filter(
(item) => !memberGroups.find((group) => group.groupId === item.groupId)
@@ -103,30 +109,44 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
const handleJoinGroup = async (group, isOpen) => {
try {
const groupId = group.groupId;
const fee = await getFee('JOIN_GROUP')
await show({
message: "Would you like to perform an JOIN_GROUP transaction?" ,
publishFee: fee.fee + ' QORT'
})
const fee = await getFee('JOIN_GROUP');
await show({
message: t('group:question.join_group', {
postProcess: 'capitalize',
}),
publishFee: fee.fee + ' QORT',
});
setIsLoading(true);
await new Promise((res, rej) => {
window.sendMessage("joinGroup", {
groupId,
})
window
.sendMessage('joinGroup', {
groupId,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message: "Successfully requested to join group. It may take a couple of minutes for the changes to propagate",
type: 'success',
message: t('group:message.success.join_group', {
postProcess: 'capitalize',
}),
});
if (isOpen) {
setTxList((prev) => [
{
...response,
type: 'joined-group',
label: `Joined Group ${group?.groupName}: awaiting confirmation`,
labelDone: `Joined Group ${group?.groupName}: success!`,
label: t('group:message.success.group_join_label', {
group_name: group?.groupName,
postProcess: 'capitalize',
}),
labelDone: t('group:message.success.group_join_label', {
group_name: group?.groupName,
postProcess: 'capitalize',
}),
done: false,
groupId,
},
@@ -145,14 +165,14 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
...prev,
]);
}
setOpenSnack(true);
handlePopoverClose();
res(response);
return;
} else {
setInfoSnack({
type: "error",
type: 'error',
message: response?.error,
});
setOpenSnack(true);
@@ -161,18 +181,18 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
})
.catch((error) => {
setInfoSnack({
type: "error",
message: error.message || "An error occurred",
type: 'error',
message: error.message || 'An error occurred',
});
setOpenSnack(true);
rej(error);
});
});
setIsLoading(false);
} catch (error) {} finally {
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
@@ -195,30 +215,33 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
vertical: 'top',
horizontal: 'center',
}}
style={{ marginTop: "8px" }}
style={{ marginTop: '8px' }}
>
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
width: '325px',
height: '250px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
padding: '10px',
}}
>
<Typography>Join {group?.groupName}</Typography>
<Typography>
{t('core:action.join', { postProcess: 'capitalize' })}{' '}
{group?.groupName}
</Typography>
<Typography>
{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>
<LoadingButton
loading={isLoading}
@@ -226,7 +249,9 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
variant="contained"
onClick={() => handleJoinGroup(group, group?.isOpen)}
>
Join group
{t('group:action.join_group', {
postProcess: 'capitalize',
})}
</LoadingButton>
</Box>
</Popover>
@@ -234,16 +259,20 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
onClick={(event) => handlePopoverOpen(event, index)}
>
{group?.isOpen === false && (
<LockIcon sx={{
color: 'var(--green)'
}} />
)}
{group?.isOpen === true && (
<NoEncryptionGmailerrorredIcon sx={{
color: 'var(--danger)'
}} />
)}
<Spacer width="15px" />
<LockIcon
sx={{
color: theme.palette.other.positive,
}}
/>
)}
{group?.isOpen === true && (
<NoEncryptionGmailerrorredIcon
sx={{
color: theme.palette.other.danger,
}}
/>
)}
<Spacer width="15px" />
<ListItemText
primary={group?.groupName}
secondary={group?.description}
@@ -257,11 +286,13 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
};
return (
<Box sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1
}}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
}}
>
<p>Groups list</p>
<TextField
label="Search for Groups"
@@ -272,10 +303,10 @@ export const AddGroupList = ({ setInfoSnack, setOpenSnack }) => {
/>
<div
style={{
position: "relative",
width: "100%",
display: "flex",
flexDirection: "column",
position: 'relative',
width: '100%',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
}}
>

View File

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

View File

@@ -1,45 +0,0 @@
import { useMemo } from "react";
import DOMPurify from "dompurify";
import "react-quill/dist/quill.snow.css";
import "react-quill/dist/quill.core.css";
import "react-quill/dist/quill.bubble.css";
import { Box, styled } from "@mui/material";
import { convertQortalLinks } from "../../../utils/qortalLink";
const CrowdfundInlineContent = styled(Box)(({ theme }) => ({
display: "flex",
fontFamily: "Mulish",
fontSize: "19px",
fontWeight: 400,
letterSpacing: 0,
color: theme.palette.text.primary,
width: '100%'
}));
export const DisplayHtml = ({ html, textColor }: any) => {
const cleanContent = useMemo(() => {
if (!html) return null;
const sanitize: string = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
});
const anchorQortal = convertQortalLinks(sanitize);
return anchorQortal;
}, [html]);
if (!cleanContent) return null;
return (
<CrowdfundInlineContent>
<div
className="ql-editor-display"
style={{
color: textColor || 'white',
fontWeight: 400,
fontSize: '16px'
}}
dangerouslySetInnerHTML={{ __html: cleanContent }}
/>
</CrowdfundInlineContent>
);
};

View File

@@ -1,14 +1,13 @@
import React, {
FC,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Avatar, Box, Popover, Typography } from "@mui/material";
} from 'react';
import { Avatar, Box, Popover, Typography, useTheme } from '@mui/material';
// import { MAIL_SERVICE_TYPE, THREAD_SERVICE_TYPE } from "../../constants/mail";
import { Thread } from "./Thread";
import { Thread } from './Thread';
import {
AllThreadP,
ArrowDownIcon,
@@ -17,7 +16,6 @@ import {
ComposeIcon,
ComposeP,
GroupContainer,
GroupNameP,
InstanceFooter,
InstanceListContainer,
InstanceListContainerRow,
@@ -38,61 +36,68 @@ import {
ThreadSingleLastMessageP,
ThreadSingleLastMessageSpanP,
ThreadSingleTitle,
} from "./Mail-styles";
} from './Mail-styles';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
import { Spacer } from "../../../common/Spacer";
import { formatDate, formatTimestamp } from "../../../utils/time";
import LazyLoad from "../../../common/LazyLoad";
import { delay } from "../../../utils/helpers";
import { NewThread } from "./NewThread";
import { getBaseApi } from "../../../background";
import { decryptPublishes, getTempPublish, handleUnencryptedPublishes } from "../../Chat/GroupAnnouncements";
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 { Spacer } from '../../../common/Spacer';
import { formatDate, formatTimestamp } from '../../../utils/time';
import LazyLoad from '../../../common/LazyLoad';
import { delay } from '../../../utils/helpers';
import { NewThread } from './NewThread';
import {
decryptPublishes,
getTempPublish,
handleUnencryptedPublishes,
} from '../../Chat/GroupAnnouncements';
import CheckSVG from '../../../assets/svgs/Check.svg';
import ArrowDownSVG from '../../../assets/svgs/ArrowDown.svg';
import { LoadingSnackbar } from '../../Snackbar/LoadingSnackbar';
import { executeEvent } from '../../../utils/events';
import RefreshIcon from '@mui/icons-material/Refresh';
import { getArbitraryEndpointReact, getBaseApiReact, isMobile } from "../../../App";
import { WrapperUserAction } from "../../WrapperUserAction";
import { addDataPublishesFunc, getDataPublishesFunc } from "../Group";
const filterOptions = ["Recently active", "Newest", "Oldest"];
import { getArbitraryEndpointReact, getBaseApiReact } from '../../../App';
import { addDataPublishesFunc, getDataPublishesFunc } from '../Group';
import { useTranslation } from 'react-i18next';
import { SortIcon } from '../../../assets/Icons/SortIcon';
import { CustomButton } from '../../../styles/App-styles';
const filterOptions = ['Recently active', 'Newest', 'Oldest'];
import CheckIcon from '@mui/icons-material/Check';
export const threadIdentifier = 'DOCUMENT';
export const threadIdentifier = "DOCUMENT";
export const GroupMail = ({
selectedGroup,
userInfo,
getSecretKey,
secretKey,
defaultThread,
defaultThread,
setDefaultThread,
hide,
isPrivate
isPrivate,
}) => {
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 [recentThreads, setRecentThreads] = useState<any[]>([]);
const [allThreads, setAllThreads] = useState<any[]>([]);
const [members, setMembers] = useState<any>(null);
const [isOpenFilterList, setIsOpenFilterList] = useState<boolean>(false);
const anchorElInstanceFilter = useRef<any>(null);
const [tempPublishedList, setTempPublishedList] = useState([])
const dataPublishes = useRef({})
const [isLoading, setIsLoading] = useState(false)
const [tempPublishedList, setTempPublishedList] = useState([]);
const dataPublishes = useRef({});
const { t } = useTranslation(['core']);
const theme = useTheme();
const [isLoading, setIsLoading] = useState(false);
const groupIdRef = useRef<any>(null);
const groupId = useMemo(() => {
return selectedGroup?.groupId;
}, [selectedGroup]);
useEffect(()=> {
if(!groupId) return
(async ()=> {
const res = await getDataPublishesFunc(groupId, 'thread')
dataPublishes.current = res || {}
})()
}, [groupId])
useEffect(() => {
if (!groupId) return;
(async () => {
const res = await getDataPublishesFunc(groupId, 'thread');
dataPublishes.current = res || {};
})();
}, [groupId]);
useEffect(() => {
if (groupId !== groupIdRef?.current) {
@@ -103,55 +108,68 @@ export const GroupMail = ({
}
}, [groupId]);
const setTempData = async ()=> {
const setTempData = async () => {
try {
const getTempAnnouncements = await getTempPublish()
if(getTempAnnouncements?.thread){
let tempData = []
Object.keys(getTempAnnouncements?.thread || {}).map((key)=> {
const value = getTempAnnouncements?.thread[key]
if(value?.data?.groupId === groupIdRef?.current){
tempData.push(value.data)
const getTempAnnouncements = await getTempPublish();
if (getTempAnnouncements?.thread) {
let tempData = [];
Object.keys(getTempAnnouncements?.thread || {}).map((key) => {
const value = getTempAnnouncements?.thread[key];
if (value?.data?.groupId === groupIdRef?.current) {
tempData.push(value.data);
}
});
setTempPublishedList(tempData);
}
})
setTempPublishedList(tempData)
}
} catch (error) {
console.log(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 {
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];
return messageData.decryptedData;
};
const updateThreadActivity = async ({threadId, qortalName, groupId, thread}) => {
const updateThreadActivity = async ({
threadId,
qortalName,
groupId,
thread,
}) => {
try {
await new Promise((res, rej) => {
window.sendMessage("updateThreadActivity", {
threadId,
qortalName,
groupId,
thread,
})
window
.sendMessage('updateThreadActivity', {
threadId,
qortalName,
groupId,
thread,
})
.then((response) => {
if (!response?.error) {
res(response);
@@ -160,23 +178,20 @@ export const GroupMail = ({
rej(response.error);
})
.catch((error) => {
rej(error.message || "An error occurred");
rej(error.message || 'An error occurred');
});
});
} catch (error) {
} finally {
console.log(error);
}
};
const getAllThreads = React.useCallback(
async (groupId: string, mode: string, isInitial?: boolean) => {
try {
setIsLoading(true)
setIsLoading(true);
const offset = isInitial ? 0 : allThreads.length;
const isReverse = mode === "Newest" ? true : false;
const isReverse = mode === 'Newest' ? true : false;
if (isInitial) {
// dispatch(setIsLoadingCustom("Loading threads"));
}
@@ -184,9 +199,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 response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
@@ -209,21 +224,26 @@ export const GroupMail = ({
let threadRes = null;
try {
threadRes = await Promise.race([
getEncryptedResource({
name: message.name,
identifier: message.identifier,
resource: message
}, isPrivate),
getEncryptedResource(
{
name: message.name,
identifier: message.identifier,
resource: message,
},
isPrivate
),
delay(5000),
]);
} catch (error) {}
} catch (error) {
console.log(error);
}
if (threadRes?.title) {
fullObject = {
...message,
threadData: threadRes,
threadOwner: message?.name,
threadId: message.identifier
threadId: message.identifier,
};
}
}
@@ -245,13 +265,12 @@ export const GroupMail = ({
} else {
sorted = fullArrayMsg.sort((a: any, b: any) => a.created - b.created);
}
setAllThreads(sorted);
} catch (error) {
console.log({ error });
} finally {
if (isInitial) {
setIsLoading(false)
setIsLoading(false);
// dispatch(setIsLoadingCustom(null));
}
}
@@ -261,21 +280,21 @@ export const GroupMail = ({
const getMailMessages = React.useCallback(
async (groupId: string, members: any) => {
try {
setIsLoading(true)
setIsLoading(true);
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 response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
const messagesForThread: any = {};
for (const message of responseData) {
let str = message.identifier;
const parts = str.split("-");
const parts = str.split('-');
// Get the second last element
const secondLastId = parts[parts.length - 2];
@@ -295,16 +314,16 @@ export const GroupMail = ({
})
.sort((a, b) => b.created - a.created)
.slice(0, 10);
let fullThreadArray: any = [];
const getMessageForThreads = newArray.map(async (message: any) => {
try {
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 response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
@@ -324,11 +343,14 @@ export const GroupMail = ({
fullThreadArray.push(fullObject);
} else {
let threadRes = await Promise.race([
getEncryptedResource({
name: thread.name,
identifier: message.threadId,
resource: thread
}, isPrivate),
getEncryptedResource(
{
name: thread.name,
identifier: message.threadId,
resource: thread,
},
isPrivate
),
delay(10000),
]);
if (threadRes?.title) {
@@ -352,8 +374,9 @@ export const GroupMail = ({
);
setRecentThreads(sorted);
} catch (error) {
console.log(error);
} finally {
setIsLoading(false)
setIsLoading(false);
// dispatch(setIsLoadingCustom(null));
}
},
@@ -361,7 +384,6 @@ export const GroupMail = ({
);
const getMessages = React.useCallback(async () => {
// if ( !groupId || members?.length === 0) return;
if (!groupId || isPrivate === null) return;
@@ -371,23 +393,23 @@ export const GroupMail = ({
const interval = useRef<any>(null);
const firstMount = useRef(false);
const filterModeRef = useRef("");
const filterModeRef = useRef('');
useEffect(() => {
if(hide) return
if (hide) return;
if (filterModeRef.current !== filterMode) {
firstMount.current = false;
}
// if (groupId && !firstMount.current && members.length > 0) {
if (groupId && !firstMount.current && isPrivate !== null) {
if (filterMode === "Recently active") {
if (filterMode === 'Recently active') {
getMessages();
} else if (filterMode === "Newest") {
getAllThreads(groupId, "Newest", true);
} else if (filterMode === "Oldest") {
getAllThreads(groupId, "Oldest", true);
} else if (filterMode === 'Newest') {
getAllThreads(groupId, 'Newest', true);
} else if (filterMode === 'Oldest') {
getAllThreads(groupId, 'Oldest', true);
}
setTempData()
setTempData();
firstMount.current = true;
}
}, [groupId, members, filterMode, hide, isPrivate]);
@@ -421,26 +443,22 @@ export const GroupMail = ({
}
}
}
setMembers(members);
} catch (error) {
console.log({ error });
}
}, []);
let listOfThreadsToDisplay = recentThreads;
if (filterMode === "Newest" || filterMode === "Oldest") {
if (filterMode === 'Newest' || filterMode === 'Oldest') {
listOfThreadsToDisplay = allThreads;
}
const onSubmitNewThread = useCallback(
(val: any) => {
if (filterMode === "Recently active") {
if (filterMode === 'Recently active') {
setRecentThreads((prev) => [val, ...prev]);
} else if (filterMode === "Newest") {
} else if (filterMode === 'Newest') {
setAllThreads((prev) => [val, ...prev]);
}
},
@@ -461,72 +479,77 @@ export const GroupMail = ({
setIsOpenFilterList(false);
};
const refetchThreadsLists = useCallback(()=> {
if (filterMode === "Recently active") {
const refetchThreadsLists = useCallback(() => {
if (filterMode === 'Recently active') {
getMessages();
} else if (filterMode === "Newest") {
getAllThreads(groupId, "Newest", true);
} else if (filterMode === "Oldest") {
getAllThreads(groupId, "Oldest", true);
} else if (filterMode === 'Newest') {
getAllThreads(groupId, 'Newest', true);
} else if (filterMode === 'Oldest') {
getAllThreads(groupId, 'Oldest', true);
}
}, [filterMode, isPrivate])
}, [filterMode, isPrivate]);
const updateThreadActivityCurrentThread = ()=> {
if(!currentThread) return
const thread = currentThread
const updateThreadActivityCurrentThread = () => {
if (!currentThread) return;
const thread = currentThread;
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 thread = data
const setThreadFunc = (data) => {
const thread = data;
setCurrentThread(thread);
if(thread?.threadId && thread?.threadData?.name){
updateThreadActivity({
threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread
})
}
if (thread?.threadId && thread?.threadData?.name) {
updateThreadActivity({
threadId: thread?.threadId,
qortalName: thread?.threadData?.name,
groupId: groupId,
thread: thread,
});
}
setTimeout(() => {
executeEvent("threadFetchMode", {
mode: "last-page"
executeEvent('threadFetchMode', {
mode: 'last-page',
});
}, 300);
}
};
useEffect(()=> {
if(defaultThread){
setThreadFunc(defaultThread)
setDefaultThread(null)
useEffect(() => {
if (defaultThread) {
setThreadFunc(defaultThread);
setDefaultThread(null);
}
}, [defaultThread])
}, [defaultThread]);
const combinedListTempAndReal = useMemo(() => {
// Combine the two lists
const transformTempPublishedList = tempPublishedList.map((item)=> {
const transformTempPublishedList = tempPublishedList.map((item) => {
return {
...item,
threadData: item.tempData,
threadOwner: item?.name,
threadId: item.identifier
}
})
threadId: item.identifier,
};
});
const combined = [...transformTempPublishedList, ...listOfThreadsToDisplay];
// Remove duplicates based on the "identifier"
const uniqueItems = new Map();
combined.forEach(item => {
uniqueItems.set(item.threadId, item); // This will overwrite duplicates, keeping the last occurrence
combined.forEach((item) => {
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
const sortedList = Array.from(uniqueItems.values()).sort((a, b) =>
filterMode === 'Oldest'
? a.threadData?.createdAt - b.threadData?.createdAt
: b.threadData?.createdAt - a.threadData?.createdAt
);
filterMode === 'Oldest'
? a.threadData?.createdAt - b.threadData?.createdAt
: b.threadData?.createdAt - a.threadData?.createdAt
);
return sortedList;
}, [tempPublishedList, listOfThreadsToDisplay, filterMode]);
@@ -548,9 +571,9 @@ export const GroupMail = ({
return (
<GroupContainer
sx={{
position: "relative",
overflow: "auto",
width: "100%",
position: 'relative',
overflow: 'auto',
width: '100%',
}}
>
<Popover
@@ -558,19 +581,19 @@ export const GroupMail = ({
anchorEl={anchorElInstanceFilter.current}
onClose={handleCloseThreadFilterList}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
vertical: 'top',
horizontal: 'right',
}}
>
<InstanceListParent
sx={{
minHeight: "unset",
width: "auto",
padding: "0px",
minHeight: 'unset',
width: 'auto',
padding: '0px',
}}
>
<InstanceListHeader></InstanceListHeader>
@@ -583,13 +606,19 @@ export const GroupMail = ({
}}
sx={{
backgroundColor:
filterMode === filter ? "rgba(74, 158, 244, 1)" : "unset",
filterMode === filter
? theme.palette.action.selected
: 'unset',
}}
key={filter}
>
<InstanceListContainerRowCheck>
{filter === filterMode && (
<InstanceListContainerRowCheckIcon src={CheckSVG} />
<CheckIcon
sx={{
color: theme.palette.text.primary,
}}
/>
)}
</InstanceListContainerRowCheck>
<InstanceListContainerRowMain>
@@ -606,12 +635,11 @@ export const GroupMail = ({
</Popover>
<ThreadContainerFullWidth>
<ThreadContainer>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
alignItems: 'center',
display: 'flex',
justifyContent: 'space-between',
}}
>
<NewThread
@@ -626,7 +654,7 @@ export const GroupMail = ({
/>
<ComposeContainerBlank
sx={{
height: "auto",
height: 'auto',
}}
>
{selectedGroup && !currentThread && (
@@ -636,7 +664,7 @@ export const GroupMail = ({
}}
ref={anchorElInstanceFilter}
>
<ComposeIcon src={SortSVG} />
<SortIcon />
<SelectInstanceContainerFilterInner>
<ComposeP>Sort by</ComposeP>
@@ -647,18 +675,24 @@ export const GroupMail = ({
</ComposeContainerBlank>
</Box>
<Spacer height="30px" />
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<AllThreadP>{filterMode}</AllThreadP>
<Box
sx={{
alignItems: 'center',
display: 'flex',
justifyContent: 'space-between',
}}
>
<AllThreadP>{filterMode}</AllThreadP>
<RefreshIcon onClick={refetchThreadsLists} sx={{
color: 'white',
cursor: 'pointer'
}} />
<RefreshIcon
onClick={refetchThreadsLists}
sx={{
cursor: 'pointer',
color: theme.palette.text.primary,
}}
/>
</Box>
<Spacer height="30px" />
{combinedListTempAndReal.map((thread) => {
@@ -668,125 +702,131 @@ export const GroupMail = ({
];
const shouldAppearLighter =
hasViewedRecent &&
filterMode === "Recently active" &&
filterMode === 'Recently active' &&
thread?.threadData?.createdAt < hasViewedRecent?.timestamp;
return (
<SingleThreadParent
sx={{
flexWrap: 'wrap',
gap: '15px',
height: 'auto'
}}
sx={{
flexWrap: 'wrap',
gap: '15px',
height: 'auto',
}}
onClick={() => {
setCurrentThread(thread);
if(thread?.threadId && thread?.threadData?.name){
if (thread?.threadId && thread?.threadData?.name) {
updateThreadActivity({
threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread
})
threadId: thread?.threadId,
qortalName: thread?.threadData?.name,
groupId: groupId,
thread: thread,
});
}
}}
>
<Avatar
sx={{
height: "50px",
width: "50px",
height: '50px',
width: '50px',
}}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${thread?.threadData?.name}/qortal_avatar?async=true`}
alt={thread?.threadData?.name}
>
{thread?.threadData?.name?.charAt(0)}
</Avatar>
<ThreadInfoColumn>
<ThreadInfoColumnNameP>
<ThreadInfoColumnbyP>by </ThreadInfoColumnbyP>
{thread?.threadData?.name}
</ThreadInfoColumnNameP>
<ThreadInfoColumnTime>
{formatTimestamp(thread?.threadData?.createdAt)}
</ThreadInfoColumnTime>
</ThreadInfoColumn>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
width: '100%'
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: '100%',
}}
>
<ThreadSingleTitle
sx={{
fontWeight: shouldAppearLighter && 300,
fontSize: isMobile && '18px'
}}
>
{thread?.threadData?.title}
</ThreadSingleTitle>
<Spacer height="10px" />
{filterMode === "Recently active" && (
{filterMode === 'Recently active' && (
<div
style={{
display: "flex",
alignItems: "center",
alignItems: 'center',
display: 'flex',
}}
>
<ThreadSingleLastMessageP>
<ThreadSingleLastMessageSpanP>
last message:{" "}
last message:{' '}
</ThreadSingleLastMessageSpanP>
{formatDate(thread?.created)}
</ThreadSingleLastMessageP>
</div>
)}
</div>
<Box onClick={()=> {
setTimeout(() => {
executeEvent("threadFetchMode", {
mode: "last-page"
});
}, 300);
}} sx={{
position: 'absolute',
bottom: '2px',
right: '2px',
borderRadius: '5px',
backgroundColor: '#27282c',
display: 'flex',
gap: '10px',
alignItems: 'center',
padding: '5px',
cursor: 'pointer',
'&:hover': {
background: 'rgba(255, 255, 255, 0.60)'
}
}}>
<Typography sx={{
color: 'white',
fontSize: '12px'
}}>Last page</Typography>
<ArrowForwardIosIcon sx={{
color: 'white',
fontSize: '12px'
}} />
</Box>
<CustomButton
onClick={() => {
setTimeout(() => {
executeEvent('threadFetchMode', {
mode: 'last-page',
});
}, 300);
}}
sx={{
alignItems: 'center',
borderRadius: '5px',
bottom: '2px',
cursor: 'pointer',
display: 'flex',
gap: '10px',
padding: '5px',
position: 'absolute',
right: '2px',
minWidth: 'unset',
}}
>
<Typography
sx={{
fontSize: '12px',
}}
>
{t('core:page.last', {
postProcess: 'capitalize',
})}
</Typography>
<ArrowForwardIosIcon
sx={{
color: theme.palette.text.primary,
fontSize: '12px',
}}
/>
</CustomButton>
</SingleThreadParent>
);
})}
<Box
sx={{
width: "100%",
justifyContent: "center",
width: '100%',
justifyContent: 'center',
}}
>
{listOfThreadsToDisplay.length >= 20 &&
filterMode !== "Recently active" && (
filterMode !== 'Recently active' && (
<LazyLoad
onLoadMore={() => getAllThreads(groupId, filterMode, false)}
></LazyLoad>
@@ -797,7 +837,9 @@ export const GroupMail = ({
<LoadingSnackbar
open={isLoading}
info={{
message: "Loading threads... please wait.",
message: t('group:message.success.loading_threads', {
postProcess: 'capitalize',
}),
}}
/>
</GroupContainer>

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,33 @@
import React, { useEffect, useRef, useState } from "react";
import { Box, 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 ComposeIconSVG from "../../../assets/svgs/ComposeIcon.svg";
import React, { useEffect, useRef, useState } from 'react';
import { Box, CircularProgress, Input, useTheme } from '@mui/material';
import ShortUniqueId from 'short-unique-id';
import {
AttachmentContainer,
CloseContainer,
ComposeContainer,
ComposeIcon,
ComposeP,
InstanceFooter,
InstanceListContainer,
InstanceListHeader,
NewMessageAttachmentImg,
NewMessageCloseImg,
NewMessageHeaderP,
NewMessageInputRow,
NewMessageSendButton,
NewMessageSendP,
} from "./Mail-styles";
} from './Mail-styles';
import { ReusableModal } from "./ReusableModal";
import { Spacer } from "../../../common/Spacer";
import { formatBytes } from "../../../utils/Size";
import { CreateThreadIcon } from "../../../assets/svgs/CreateThreadIcon";
import { SendNewMessage } from "../../../assets/svgs/SendNewMessage";
import { TextEditor } from "./TextEditor";
import { MyContext, isMobile, pauseAllQueues, resumeAllQueues } 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";
import { ReusableModal } from './ReusableModal';
import { Spacer } from '../../../common/Spacer';
import { CreateThreadIcon } from '../../../assets/Icons/CreateThreadIcon';
import { SendNewMessage } from '../../../assets/Icons/SendNewMessage';
import { MyContext, pauseAllQueues, resumeAllQueues } 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';
import { useTranslation } from 'react-i18next';
import { ComposeIcon } from '../../../assets/Icons/ComposeIcon';
import CloseIcon from '@mui/icons-material/Close';
const uid = new ShortUniqueId({ length: 8 });
@@ -54,21 +46,21 @@ export function objectToBase64(obj: any) {
const jsonString = JSON.stringify(obj);
// 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
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
if (typeof reader.result === "string") {
if (typeof reader.result === 'string') {
// Remove 'data:application/json;base64,' prefix
const base64 = reader.result.replace(
"data:application/json;base64,",
""
'data:application/json;base64,',
''
);
resolve(base64);
} 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 = () => {
@@ -94,10 +86,11 @@ export const publishGroupEncryptedResource = async ({
identifier,
}) => {
return new Promise((res, rej) => {
window.sendMessage("publishGroupEncryptedResource", {
encryptedData,
identifier,
})
window
.sendMessage('publishGroupEncryptedResource', {
encryptedData,
identifier,
})
.then((response) => {
if (!response?.error) {
res(response);
@@ -106,19 +99,19 @@ export const publishGroupEncryptedResource = async ({
rej(response.error);
})
.catch((error) => {
rej(error.message || "An error occurred");
rej(error.message || 'An error occurred');
});
});
};
export const encryptSingleFunc = async (data: string, secretKeyObject: any) => {
try {
return new Promise((res, rej) => {
window.sendMessage("encryptSingle", {
data,
secretKeyObject,
})
window
.sendMessage('encryptSingle', {
data,
secretKeyObject,
})
.then((response) => {
if (!response?.error) {
res(response);
@@ -127,12 +120,14 @@ export const encryptSingleFunc = async (data: string, secretKeyObject: any) => {
rej(response.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 = ({
groupInfo,
members,
@@ -145,17 +140,18 @@ export const NewThread = ({
postReply,
myName,
setPostReply,
isPrivate
isPrivate,
}: NewMessageProps) => {
const { t } = useTranslation(['core', 'group']);
const { show } = React.useContext(MyContext);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [value, setValue] = useState("");
const [value, setValue] = useState('');
const [isSending, setIsSending] = useState(false);
const [threadTitle, setThreadTitle] = useState<string>("");
const [threadTitle, setThreadTitle] = useState<string>('');
const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null);
const editorRef = useRef(null);
const theme = useTheme();
const setEditorRef = (editorInstance) => {
editorRef.current = editorInstance;
};
@@ -168,44 +164,49 @@ export const NewThread = ({
const closeModal = () => {
setIsOpen(false);
setValue("");
if(setPostReply){
setPostReply(null)
setValue('');
if (setPostReply) {
setPostReply(null);
}
};
async function publishQDNResource() {
try {
pauseAllQueues()
if(isSending) return
setIsSending(true)
let name: string = "";
let errorMsg = "";
pauseAllQueues();
if (isSending) return;
setIsSending(true);
let name: string = '';
let errorMsg = '';
name = userInfo?.name || "";
name = userInfo?.name || '';
const missingFields: string[] = [];
if (!isMessage && !threadTitle) {
errorMsg = "Please provide a thread title";
errorMsg = t('group:question.provide_thread', {
postProcess: 'capitalize',
});
}
if (!name) {
errorMsg = "Cannot send a message without a access to your name";
errorMsg = t('group:message.error.access_name', {
postProcess: 'capitalize',
});
}
if (!groupInfo) {
errorMsg = "Cannot access group information";
errorMsg = t('group:message.error.group_info', {
postProcess: 'capitalize',
});
}
// if (!description) missingFields.push('subject')
if (missingFields.length > 0) {
const missingFieldsString = missingFields.join(", ");
const missingFieldsString = missingFields.join(', ');
const errMsg = `Missing: ${missingFieldsString}`;
errorMsg = errMsg;
errorMsg = errMsg; // TODO translate
}
if (errorMsg) {
// dispatch(
// setNotification({
@@ -217,17 +218,17 @@ export const NewThread = ({
}
const htmlContent = editorRef.current.getHTML();
if (!htmlContent?.trim() || htmlContent?.trim() === "<p></p>")
throw new Error("Please provide a first message to the thread");
const fee = await getFee("ARBITRARY");
if (!htmlContent?.trim() || htmlContent?.trim() === '<p></p>')
throw new Error('Please provide a first message to the thread');
const fee = await getFee('ARBITRARY');
let feeToShow = fee.fee;
if (!isMessage) {
feeToShow = +feeToShow * 2;
}
await show({
message: "Would you like to perform a ARBITRARY transaction?",
publishFee: feeToShow + " QORT",
message: 'Would you like to perform a ARBITRARY transaction?',
publishFee: feeToShow + ' QORT',
});
let reply = null;
@@ -245,20 +246,21 @@ export const NewThread = ({
threadOwner: currentThread?.threadData?.name || name,
reply,
};
const secretKey = isPrivate === false ? null : await getSecretKey(false, true);
const secretKey =
isPrivate === false ? null : await getSecretKey(false, true);
if (!secretKey && isPrivate) {
throw new Error("Cannot get group secret key");
throw new Error('Cannot get group secret key');
}
if (!isMessage) {
const idThread = uid.rnd();
const idMsg = uid.rnd();
const messageToBase64 = await objectToBase64(mailObject);
const encryptSingleFirstPost = isPrivate === false ? messageToBase64 : await encryptSingleFunc(
messageToBase64,
secretKey
);
const encryptSingleFirstPost =
isPrivate === false
? messageToBase64
: await encryptSingleFunc(messageToBase64, secretKey);
const threadObject = {
title: threadTitle,
groupId: groupInfo.id,
@@ -267,10 +269,10 @@ export const NewThread = ({
};
const threadToBase64 = await objectToBase64(threadObject);
const encryptSingleThread = isPrivate === false ? threadToBase64 : await encryptSingleFunc(
threadToBase64,
secretKey
);
const encryptSingleThread =
isPrivate === false
? threadToBase64
: await encryptSingleFunc(threadToBase64, secretKey);
let identifierThread = `grp-${groupInfo.groupId}-thread-${idThread}`;
await publishGroupEncryptedResource({
identifier: identifierThread,
@@ -288,23 +290,27 @@ export const NewThread = ({
service: 'DOCUMENT',
tempData: threadObject,
created: Date.now(),
groupId: groupInfo.groupId
}
groupId: groupInfo.groupId,
};
const dataToSaveToStoragePost = {
name: myName,
identifier: identifierPost,
service: 'DOCUMENT',
tempData: mailObject,
created: Date.now(),
threadId: identifierThread
}
await saveTempPublish({data: dataToSaveToStorage, key: 'thread'})
await saveTempPublish({data: dataToSaveToStoragePost, key: 'thread-post'})
setInfoSnack({
type: "success",
message: "Successfully created thread. It may take some time for the publish to propagate",
threadId: identifierThread,
};
await saveTempPublish({ data: dataToSaveToStorage, key: 'thread' });
await saveTempPublish({
data: dataToSaveToStoragePost,
key: 'thread-post',
});
setOpenSnack(true)
setInfoSnack({
type: 'success',
message:
'Successfully created thread. It may take some time for the publish to propagate',
});
setOpenSnack(true);
// dispatch(
// setNotification({
@@ -313,35 +319,36 @@ export const NewThread = ({
// })
// );
if (publishCallback) {
publishCallback()
publishCallback();
}
closeModal();
} 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 messageToBase64 = await objectToBase64(mailObject);
const encryptSinglePost = isPrivate === false ? messageToBase64 : await encryptSingleFunc(
messageToBase64,
secretKey
);
const encryptSinglePost =
isPrivate === false
? messageToBase64
: await encryptSingleFunc(messageToBase64, secretKey);
const idMsg = uid.rnd();
let identifier = `thmsg-${idThread}-${idMsg}`;
const res = await publishGroupEncryptedResource({
identifier: identifier,
encryptedData: encryptSinglePost,
});
const dataToSaveToStoragePost = {
threadId: idThread,
name: myName,
identifier: identifier,
service: 'DOCUMENT',
tempData: mailObject,
created: Date.now()
}
await saveTempPublish({data: dataToSaveToStoragePost, key: 'thread-post'})
created: Date.now(),
};
await saveTempPublish({
data: dataToSaveToStoragePost,
key: 'thread-post',
});
// await qortalRequest(multiplePublishMsg);
// dispatch(
// setNotification({
@@ -350,12 +357,13 @@ export const NewThread = ({
// })
// );
setInfoSnack({
type: "success",
message: "Successfully created post. It may take some time for the publish to propagate",
type: 'success',
message:
'Successfully created post. It may take some time for the publish to propagate',
});
setOpenSnack(true)
if(publishCallback){
publishCallback()
setOpenSnack(true);
if (publishCallback) {
publishCallback();
}
// messageCallback({
// identifier,
@@ -369,17 +377,16 @@ export const NewThread = ({
closeModal();
} catch (error: any) {
if(error?.message){
if (error?.message) {
setInfoSnack({
type: "error",
type: 'error',
message: error?.message,
});
setOpenSnack(true)
setOpenSnack(true);
}
} finally {
setIsSending(false);
resumeAllQueues()
resumeAllQueues();
}
}
@@ -389,56 +396,63 @@ export const NewThread = ({
return (
<Box
sx={{
display: "flex",
display: 'flex',
}}
>
<ComposeContainer
sx={{
padding: isMobile ? '5px' : "15px",
justifyContent: isMobile ? 'flex-start' : 'revert'
padding: '15px',
justifyContent: 'revert',
}}
onClick={() => setIsOpen(true)}
>
<ComposeIcon src={ComposeIconSVG} />
<ComposeP>{currentThread ? "New Post" : "New Thread"}</ComposeP>
<ComposeIcon />
<ComposeP>{currentThread ? 'New Post' : 'New Thread'}</ComposeP>
</ComposeContainer>
<ReusableModal
open={isOpen}
customStyles={{
maxHeight: isMobile ? '95svh' : "95vh",
maxWidth: "950px",
height: "700px",
borderRadius: "12px 12px 0px 0px",
background: "#434448",
padding: "0px",
gap: "0px",
maxHeight: '95vh',
maxWidth: '950px',
height: '700px',
borderRadius: '12px 12px 0px 0px',
background: theme.palette.background.paper,
padding: '0px',
gap: '0px',
}}
>
<InstanceListHeader
sx={{
height: isMobile ? 'auto' : "50px",
padding: isMobile ? '5px' : "20px 42px",
flexDirection: "row",
height: '50px',
padding: '20px 42px',
flexDirection: 'row',
alignItems: 'center',
justifyContent: "space-between",
backgroundColor: "#434448",
justifyContent: 'space-between',
backgroundColor: theme.palette.background.paper,
}}
>
<NewMessageHeaderP>
{isMessage ? "Post Message" : "New Thread"}
{isMessage ? 'Post Message' : 'New Thread'}
</NewMessageHeaderP>
<CloseContainer sx={{
height: '40px'
}} onClick={closeModal}>
<NewMessageCloseImg src={ModalCloseSVG} />
<CloseContainer
sx={{
height: '40px',
}}
onClick={closeModal}
>
<CloseIcon
sx={{
color: theme.palette.text.primary,
}}
/>
</CloseContainer>
</InstanceListHeader>
<InstanceListContainer
sx={{
backgroundColor: "#434448",
padding: isMobile ? '5px' : "20px 42px",
height: "calc(100% - 165px)",
backgroundColor: theme.palette.background.paper,
padding: '20px 42px',
height: 'calc(100% - 165px)',
flexShrink: 0,
}}
>
@@ -457,19 +471,17 @@ export const NewThread = ({
autoComplete="off"
autoCorrect="off"
sx={{
width: "100%",
color: "white",
"& .MuiInput-input::placeholder": {
color: "rgba(255,255,255, 0.70) !important",
fontSize: isMobile ? '14px' : "20px",
fontStyle: "normal",
width: '100%',
'& .MuiInput-input::placeholder': {
fontSize: '20px',
fontStyle: 'normal',
fontWeight: 400,
lineHeight: "120%", // 24px
letterSpacing: "0.15px",
lineHeight: '120%', // 24px
letterSpacing: '0.15px',
opacity: 1,
},
"&:focus": {
outline: "none",
'&:focus': {
outline: 'none',
},
// Add any additional styles for the input here
}}
@@ -481,21 +493,20 @@ export const NewThread = ({
{postReply && postReply.textContentV2 && (
<Box
sx={{
width: "100%",
maxHeight: "120px",
overflow: "auto",
width: '100%',
maxHeight: '120px',
overflow: 'auto',
}}
>
<MessageDisplay htmlContent={postReply?.textContentV2} />
</Box>
)}
{!isMobile && (
<Spacer height="30px" />
)}
<Spacer height="30px" />
<Box
sx={{
maxHeight: "40vh",
maxHeight: '40vh',
}}
>
<TipTap
@@ -505,41 +516,48 @@ export const NewThread = ({
overrideMobile
customEditorHeight="240px"
/>
{/* <TextEditor
inlineContent={value}
setInlineContent={(val: any) => {
setValue(val);
}}
/> */}
</Box>
</InstanceListContainer>
<InstanceFooter
sx={{
backgroundColor: "#434448",
padding: isMobile ? '5px' : "20px 42px",
alignItems: "center",
height: isMobile ? 'auto' : "90px",
backgroundColor: theme.palette.background.paper,
padding: '20px 42px',
alignItems: 'center',
height: '90px',
}}
>
<NewMessageSendButton onClick={sendMail}>
{isSending && (
<Box sx={{height: '100%', position: 'absolute', width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
<CircularProgress sx={{
}} size={'12px'} />
<Box
sx={{
height: '100%',
position: 'absolute',
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<CircularProgress
sx={{
color: theme.palette.text.primary,
}}
size={'12px'}
/>
</Box>
)}
<NewMessageSendP>
{isMessage ? "Post" : "Create Thread"}
{isMessage ? 'Post' : 'Create Thread'}
</NewMessageSendP>
{isMessage ? (
<SendNewMessage
opacity={1}
height="25px"
width="25px"
/>
<SendNewMessage />
) : (
<CreateThreadIcon
color={theme.palette.text.primary}
opacity={1}
height="25px"
width="25px"
@@ -547,9 +565,14 @@ export const NewThread = ({
)}
</NewMessageSendButton>
</InstanceFooter>
</ReusableModal>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} />
<CustomizedSnackbars
open={openSnack}
setOpen={setOpenSnack}
info={infoSnack}
setInfo={setInfoSnack}
/>
</Box>
);
};

View File

@@ -1,18 +1,24 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { createEditor} from 'slate';
import { withReact, Slate, Editable, RenderElementProps, RenderLeafProps } from 'slate-react';
import { createEditor } from 'slate';
import {
withReact,
Slate,
Editable,
RenderElementProps,
RenderLeafProps,
} from 'slate-react';
type ExtendedRenderElementProps = RenderElementProps & { mode?: string }
type ExtendedRenderElementProps = RenderElementProps & { mode?: string };
export const renderElement = ({
attributes,
children,
element,
mode
mode,
}: ExtendedRenderElementProps) => {
switch (element.type) {
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>
return <blockquote {...attributes}>{children}</blockquote>;
case 'heading-2':
return (
<h2
@@ -22,7 +28,7 @@ export const renderElement = ({
>
{children}
</h2>
)
);
case 'heading-3':
return (
<h3
@@ -32,21 +38,21 @@ export const renderElement = ({
>
{children}
</h3>
)
);
case 'code-block':
return (
<pre {...attributes} className="code-block">
<code>{children}</code>
</pre>
)
);
case 'code-line':
return <div {...attributes}>{children}</div>
return <div {...attributes}>{children}</div>;
case 'link':
return (
<a href={element.url} {...attributes}>
{children}
</a>
)
);
default:
return (
<p
@@ -56,24 +62,23 @@ export const renderElement = ({
>
{children}
</p>
)
);
}
}
};
export const renderLeaf = ({ attributes, children, leaf }: RenderLeafProps) => {
let el = children
let el = children;
if (leaf.bold) {
el = <strong>{el}</strong>
el = <strong>{el}</strong>;
}
if (leaf.italic) {
el = <em>{el}</em>
el = <em>{el}</em>;
}
if (leaf.underline) {
el = <u>{el}</u>
el = <u>{el}</u>;
}
if (leaf.link) {
@@ -81,39 +86,35 @@ export const renderLeaf = ({ attributes, children, leaf }: RenderLeafProps) => {
<a href={leaf.link} {...attributes}>
{el}
</a>
)
);
}
return <span {...attributes}>{el}</span>
}
return <span {...attributes}>{el}</span>;
};
interface ReadOnlySlateProps {
content: any
mode?: string
content: any;
mode?: string;
}
const ReadOnlySlate: React.FC<ReadOnlySlateProps> = ({ content, mode }) => {
const [load, setLoad] = useState(false)
const editor = useMemo(() => withReact(createEditor()), [])
const value = useMemo(() => content, [content])
const [load, setLoad] = useState(false);
const editor = useMemo(() => withReact(createEditor()), []);
const value = useMemo(() => content, [content]);
const performUpdate = useCallback(async()=> {
setLoad(true)
await new Promise<void>((res)=> {
const performUpdate = useCallback(async () => {
setLoad(true);
await new Promise<void>((res) => {
setTimeout(() => {
res()
res();
}, 250);
})
setLoad(false)
}, [])
useEffect(()=> {
});
setLoad(false);
}, []);
useEffect(() => {
performUpdate();
}, [value]);
performUpdate()
}, [value])
if(load) return null
if (load) return null;
return (
<Slate editor={editor} value={value} onChange={() => {}}>
@@ -123,7 +124,7 @@ const ReadOnlySlate: React.FC<ReadOnlySlateProps> = ({ content, mode }) => {
renderLeaf={renderLeaf}
/>
</Slate>
)
}
);
};
export default ReadOnlySlate;
export default ReadOnlySlate;

View File

@@ -1,13 +1,12 @@
import React from 'react'
import { Box, Modal, useTheme } from '@mui/material'
import { isMobile } from '../../../App'
import React from 'react';
import { Box, Modal, useTheme } from '@mui/material';
interface MyModalProps {
open: boolean
onClose?: () => void
onSubmit?: (obj: any) => Promise<void>
children: any
customStyles?: any
open: boolean;
onClose?: () => void;
onSubmit?: (obj: any) => Promise<void>;
children: any;
customStyles?: any;
}
export const ReusableModal: React.FC<MyModalProps> = ({
@@ -15,9 +14,10 @@ export const ReusableModal: React.FC<MyModalProps> = ({
onClose,
onSubmit,
children,
customStyles = {}
customStyles = {},
}) => {
const theme = useTheme()
const theme = useTheme();
return (
<Modal
open={open}
@@ -32,27 +32,27 @@ export const ReusableModal: React.FC<MyModalProps> = ({
},
}}
disableAutoFocus
disableEnforceFocus
disableRestoreFocus
disableEnforceFocus
disableRestoreFocus
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: isMobile ? '95%' : '75%',
bgcolor: theme.palette.primary.main,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
gap: 2,
...customStyles
left: '50%',
p: 4,
position: 'absolute',
top: '50%',
transform: 'translate(-50%, -50%)',
width: '75%',
...customStyles,
}}
>
{children}
</Box>
</Modal>
)
}
);
};

View File

@@ -1,9 +1,8 @@
import React, { useState } from "react";
import { Avatar, Box, IconButton } from "@mui/material";
import DOMPurify from "dompurify";
import { useState } from 'react';
import { Avatar, Box, IconButton } from '@mui/material';
import DOMPurify from 'dompurify';
import FormatQuoteIcon from '@mui/icons-material/FormatQuote';
import MoreSVG from '../../../assets/svgs/More.svg'
import MoreSVG from '../../../assets/svgs/More.svg';
import {
MoreImg,
MoreP,
@@ -11,20 +10,18 @@ import {
ThreadInfoColumn,
ThreadInfoColumnNameP,
ThreadInfoColumnTime,
} from "./Mail-styles";
import { Spacer } from "../../../common/Spacer";
import { DisplayHtml } from "./DisplayHtml";
import { formatTimestampForum } from "../../../utils/time";
import ReadOnlySlate from "./ReadOnlySlate";
import { MessageDisplay } from "../../Chat/MessageDisplay";
import { getBaseApi } from "../../../background";
import { getBaseApiReact } from "../../../App";
import { WrapperUserAction } from "../../WrapperUserAction";
} from './Mail-styles';
import { Spacer } from '../../../common/Spacer';
import { formatTimestampForum } from '../../../utils/time';
import ReadOnlySlate from './ReadOnlySlate';
import { MessageDisplay } from '../../Chat/MessageDisplay';
import { getBaseApiReact } from '../../../App';
import { WrapperUserAction } from '../../WrapperUserAction';
export const ShowMessage = ({ message, openNewPostWithQuote, myName }: any) => {
const [expandAttachments, setExpandAttachments] = useState<boolean>(false);
let cleanHTML = "";
let cleanHTML = '';
if (message?.htmlContent) {
cleanHTML = DOMPurify.sanitize(message.htmlContent);
}
@@ -32,79 +29,96 @@ export const ShowMessage = ({ message, openNewPostWithQuote, myName }: any) => {
return (
<SingleTheadMessageParent
sx={{
height: "auto",
alignItems: "flex-start",
cursor: "default",
borderRadius: '35px 4px 4px 4px'
height: 'auto',
alignItems: 'flex-start',
cursor: 'default',
borderRadius: '35px 4px 4px 4px',
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
width: '100%'
alignItems: 'flex-start',
display: 'flex',
flexDirection: 'column',
width: '100%',
}}
>
<Box
sx={{
display: "flex",
alignItems: "flex-start",
gap: "10px",
alignItems: 'flex-start',
display: 'flex',
gap: '10px',
}}
>
<WrapperUserAction disabled={myName === message?.name} address={undefined} name={message?.name}>
<Avatar sx={{
height: '50px',
width: '50px'
}} src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${message?.name}/qortal_avatar?async=true`} alt={message?.name}>{message?.name?.charAt(0)}</Avatar>
</WrapperUserAction>
<WrapperUserAction
disabled={myName === message?.name}
address={undefined}
name={message?.name}
>
<Avatar
sx={{
height: '50px',
width: '50px',
}}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${message?.name}/qortal_avatar?async=true`}
alt={message?.name}
>
{message?.name?.charAt(0)}
</Avatar>
</WrapperUserAction>
<ThreadInfoColumn>
<WrapperUserAction disabled={myName === message?.name} address={undefined} name={message?.name}>
<ThreadInfoColumnNameP>{message?.name}</ThreadInfoColumnNameP>
<WrapperUserAction
disabled={myName === message?.name}
address={undefined}
name={message?.name}
>
<ThreadInfoColumnNameP>{message?.name}</ThreadInfoColumnNameP>
</WrapperUserAction>
<ThreadInfoColumnTime>
{formatTimestampForum(message?.created)}
</ThreadInfoColumnTime>
</ThreadInfoColumn>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
}}
>
{message?.attachments?.length > 0 && (
<Box
sx={{
width: "100%",
marginTop: "10px",
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
>
{message?.attachments
.map((file: any, index: number) => {
const isFirst = index === 0
return (
<Box
sx={{
display: expandAttachments ? "flex" : !expandAttachments && isFirst ? 'flex' : 'none',
alignItems: "center",
justifyContent: "flex-start",
width: "100%",
}}
>
{message?.attachments?.length > 0 && (
<Box
sx={{
width: '100%',
marginTop: '10px',
}}
>
{message?.attachments.map((file: any, index: number) => {
const isFirst = index === 0;
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
cursor: "pointer",
width: "auto",
display: expandAttachments
? 'flex'
: !expandAttachments && isFirst
? 'flex'
: 'none',
alignItems: 'center',
justifyContent: 'flex-start',
width: '100%',
}}
>
{/* <FileElement
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: '5px',
cursor: 'pointer',
width: 'auto',
}}
>
{/* <FileElement
fileInfo={{ ...file, mimeTypeSaved: file?.type }}
title={file?.filename}
mode="mail"
@@ -125,80 +139,91 @@ export const ShowMessage = ({ message, openNewPostWithQuote, myName }: any) => {
{file?.originalFilename || file?.filename}
</Typography>
</FileElement> */}
{message?.attachments?.length > 1 && isFirst && (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
}}
onClick={() => {
setExpandAttachments(prev => !prev);
}}
>
<MoreImg
{message?.attachments?.length > 1 && isFirst && (
<Box
sx={{
marginLeft: "5px",
transform: expandAttachments
? "rotate(180deg)"
: "unset",
display: 'flex',
alignItems: 'center',
gap: '5px',
}}
src={MoreSVG}
/>
<MoreP>
{expandAttachments ? 'hide' : `(${message?.attachments?.length - 1} more)`}
</MoreP>
</Box>
)}
onClick={() => {
setExpandAttachments((prev) => !prev);
}}
>
<MoreImg
sx={{
marginLeft: '5px',
transform: expandAttachments
? 'rotate(180deg)'
: 'unset',
}}
src={MoreSVG}
/>
<MoreP>
{expandAttachments
? 'hide'
: `(${message?.attachments?.length - 1} more)`}
</MoreP>
</Box>
)}
</Box>
</Box>
</Box>
);
})
}
</Box>
)}
</div>
);
})}
</Box>
)}
</div>
</Box>
<Spacer height="20px" />
{message?.reply?.textContentV2 && (
<>
<Box sx={{
width: '100%',
opacity: 0.7,
borderRadius: '5px',
border: '1px solid gray',
boxSizing: 'border-box',
padding: '5px'
}}>
<Box
sx={{
width: '100%',
opacity: 0.7,
borderRadius: '5px',
border: '1px solid gray',
boxSizing: 'border-box',
padding: '5px',
}}
>
<Box
sx={{
display: "flex",
alignItems: "flex-start",
gap: "10px",
sx={{
display: 'flex',
alignItems: 'flex-start',
gap: '10px',
}}
>
<Avatar
sx={{
height: '30px',
width: '30px',
}}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${message?.reply?.name}/qortal_avatar?async=true`}
alt={message?.reply?.name}
>
{message?.reply?.name?.charAt(0)}
</Avatar>
}}
>
<Avatar sx={{
height: '30px',
width: '30px'
}} src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${message?.reply?.name}/qortal_avatar?async=true`} alt={message?.reply?.name}>{message?.reply?.name?.charAt(0)}</Avatar>
<ThreadInfoColumn>
<ThreadInfoColumnNameP sx={{
fontSize: '14px'
}}>{message?.reply?.name}</ThreadInfoColumnNameP>
</ThreadInfoColumn>
</Box>
<MessageDisplay htmlContent={message?.reply?.textContentV2} />
</Box>
<Spacer height="20px" />
<ThreadInfoColumn>
<ThreadInfoColumnNameP
sx={{
fontSize: '14px',
}}
>
{message?.reply?.name}
</ThreadInfoColumnNameP>
</ThreadInfoColumn>
</Box>
<MessageDisplay htmlContent={message?.reply?.textContentV2} />
</Box>
<Spacer height="20px" />
</>
)}
{message?.textContent && (
<ReadOnlySlate content={message.textContent} mode="mail" />
)}
@@ -208,22 +233,18 @@ export const ShowMessage = ({ message, openNewPostWithQuote, myName }: any) => {
{message?.htmlContent && (
<div dangerouslySetInnerHTML={{ __html: cleanHTML }} />
)}
<Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'flex-end'
}}>
<IconButton
onClick={() => openNewPostWithQuote(message)}
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'flex-end',
}}
>
<FormatQuoteIcon />
</IconButton>
<IconButton onClick={() => openNewPostWithQuote(message)}>
<FormatQuoteIcon />
</IconButton>
</Box>
</Box>
</SingleTheadMessageParent>
);
};

View File

@@ -1,39 +0,0 @@
import React from "react";
import ReactQuill, { Quill } from "react-quill";
import "react-quill/dist/quill.snow.css";
import ImageResize from "quill-image-resize-module-react";
import './texteditor.css'
Quill.register("modules/imageResize", ImageResize);
const modules = {
imageResize: {
parchment: Quill.import("parchment"),
modules: ["Resize", "DisplaySize"],
},
toolbar: [
["bold", "italic", "underline", "strike"], // styled text
["blockquote", "code-block"], // blocks
[{ header: 1 }, { header: 2 }], // custom button values
[{ list: "ordered" }, { list: "bullet" }], // lists
[{ script: "sub" }, { script: "super" }], // superscript/subscript
[{ indent: "-1" }, { indent: "+1" }], // outdent/indent
[{ direction: "rtl" }], // text direction
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
[{ header: [1, 2, 3, 4, 5, 6, false] }], // custom button values
[{ color: [] }, { background: [] }], // dropdown with defaults
[{ font: [] }], // font family
[{ align: [] }], // text align
["clean"], // remove formatting
// ["image"], // image
],
};
export const TextEditor = ({ inlineContent, setInlineContent }: any) => {
return (
<ReactQuill
theme="snow"
value={inlineContent}
onChange={setInlineContent}
modules={modules}
/>
);
};

View File

@@ -1,329 +0,0 @@
import React, {
FC,
useCallback,
useEffect,
useRef,
useState
} from 'react'
import {
Box,
Skeleton,
} from '@mui/material'
import { ShowMessage } from './ShowMessageWithoutModal'
// import {
// setIsLoadingCustom,
// } from '../../state/features/globalSlice'
import { ComposeP, GroupContainer, GroupNameP, MailIconImg, ShowMessageReturnButton, SingleThreadParent, ThreadContainer, ThreadContainerFullWidth } from './Mail-styles'
import { Spacer } from '../../../common/Spacer'
import { threadIdentifier } from './GroupMail'
import LazyLoad from '../../../common/LazyLoad'
import ReturnSVG from '../../../assets/svgs/Return.svg'
import { NewThread } from './NewThread'
import { decryptPublishes } from '../../Chat/GroupAnnouncements'
import { getBaseApi } from '../../../background'
import { getArbitraryEndpointReact, getBaseApiReact } from '../../../App'
interface ThreadProps {
currentThread: any
groupInfo: any
closeThread: () => void
members: any
}
const getEncryptedResource = async ({name, identifier, secretKey})=> {
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64`
);
const data = await res.text();
const response = await decryptPublishes([{ data }], secretKey);
const messageData = response[0];
return messageData.decryptedData
}
export const Thread = ({
currentThread,
groupInfo,
closeThread,
members,
userInfo,
secretKey,
getSecretKey
}: ThreadProps) => {
const [messages, setMessages] = useState<any[]>([])
const [hashMapMailMessages, setHashMapMailMessages] = useState({})
const secretKeyRef = useRef(null)
useEffect(() => {
secretKeyRef.current = secretKey;
}, [secretKey]);
const getIndividualMsg = async (message: any) => {
try {
const responseDataMessage = await getEncryptedResource({identifier: message.identifier, name: message.name, secretKey})
const fullObject = {
...message,
...(responseDataMessage || {}),
id: message.identifier
}
setHashMapMailMessages((prev)=> {
return {
...prev,
[message.identifier]: fullObject
}
})
} catch (error) {}
}
const getMailMessages = React.useCallback(
async (groupInfo: any, reset?: boolean, hideAlert?: boolean) => {
try {
if(!hideAlert){
// dispatch(setIsLoadingCustom('Loading messages'))
}
let threadId = groupInfo.threadId
const offset = messages.length
const identifier = `thmsg-${threadId}`
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
const responseData = await response.json()
let fullArrayMsg = reset ? [] : [...messages]
let newMessages: any[] = []
for (const message of responseData) {
const index = fullArrayMsg.findIndex(
(p) => p.identifier === message.identifier
)
if (index !== -1) {
fullArrayMsg[index] = message
} else {
fullArrayMsg.push(message)
getIndividualMsg(message)
}
}
setMessages(fullArrayMsg)
} catch (error) {
} finally {
if(!hideAlert){
// dispatch(setIsLoadingCustom(null))
}
}
},
[messages, secretKey]
)
const getMessages = React.useCallback(async () => {
if (!currentThread || !secretKey) return
await getMailMessages(currentThread, true)
}, [getMailMessages, currentThread, secretKey])
const firstMount = useRef(false)
const saveTimestamp = useCallback((currentThread: any, username?: string)=> {
if(!currentThread?.threadData?.groupId || !currentThread?.threadId || !username) return
const threadIdForLocalStorage = `qmail_threads_${currentThread?.threadData?.groupId}_${currentThread?.threadId}`
const threads = JSON.parse(
localStorage.getItem(`qmail_threads_viewedtimestamp_${username}`) || "{}"
);
// Convert to an array of objects with identifier and all fields
let dataArray = Object.entries(threads).map(([identifier, value]) => ({
identifier,
...(value as any),
}));
// Sort the array based on timestamp in descending order
dataArray.sort((a, b) => b.timestamp - a.timestamp);
// Slice the array to keep only the first 500 elements
let latest500 = dataArray.slice(0, 500);
// Convert back to the original object format
let latest500Data: any = {};
latest500.forEach(item => {
const { identifier, ...rest } = item;
latest500Data[identifier] = rest;
});
latest500Data[threadIdForLocalStorage] = {
timestamp: Date.now(),
}
localStorage.setItem(
`qmail_threads_viewedtimestamp_${username}`,
JSON.stringify(latest500Data)
);
}, [])
useEffect(() => {
if (currentThread && secretKey) {
getMessages()
firstMount.current = true
// saveTimestamp(currentThread, user.name)
}
}, [ currentThread, secretKey])
const messageCallback = useCallback((msg: any) => {
// dispatch(addToHashMapMail(msg))
setMessages((prev) => [msg, ...prev])
}, [])
const interval = useRef<any>(null)
const checkNewMessages = React.useCallback(
async (groupInfo: any) => {
try {
let threadId = groupInfo.threadId
const identifier = `thmsg-${threadId}`
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
const responseData = await response.json()
const latestMessage = messages[0]
if (!latestMessage) return
const findMessage = responseData?.findIndex(
(item: any) => item?.identifier === latestMessage?.identifier
)
let sliceLength = responseData.length
if (findMessage !== -1) {
sliceLength = findMessage
}
const newArray = responseData.slice(0, findMessage).reverse()
let fullArrayMsg = [...messages]
for (const message of newArray) {
try {
const responseDataMessage = await getEncryptedResource({identifier: message.identifier, name: message.name, secretKey: secretKeyRef.current})
const fullObject = {
...message,
...(responseDataMessage || {}),
id: message.identifier
}
setHashMapMailMessages((prev)=> {
return {
...prev,
[message.identifier]: fullObject
}
})
const index = messages.findIndex(
(p) => p.identifier === fullObject.identifier
)
if (index !== -1) {
fullArrayMsg[index] = fullObject
} else {
fullArrayMsg.unshift(fullObject)
}
} catch (error) {}
}
setMessages(fullArrayMsg)
} catch (error) {
} finally {
}
},
[messages]
)
const checkNewMessagesFunc = useCallback(() => {
let isCalling = false
interval.current = setInterval(async () => {
if (isCalling) return
isCalling = true
const res = await checkNewMessages(currentThread)
isCalling = false
}, 8000)
}, [checkNewMessages, currentThread])
useEffect(() => {
checkNewMessagesFunc()
return () => {
if (interval?.current) {
clearInterval(interval.current)
}
}
}, [checkNewMessagesFunc])
if (!currentThread) return null
return (
<GroupContainer
sx={{
position: "relative",
overflow: 'auto',
width: '100%'
}}
>
<NewThread
groupInfo={groupInfo}
isMessage={true}
currentThread={currentThread}
messageCallback={messageCallback}
members={members}
userInfo={userInfo}
getSecretKey={getSecretKey}
/>
<ThreadContainerFullWidth>
<ThreadContainer>
<Spacer height="30px" />
<Box sx={{
width: '100%',
alignItems: 'center',
display: 'flex',
justifyContent: 'space-between'
}}>
<GroupNameP>{currentThread?.threadData?.title}</GroupNameP>
<ShowMessageReturnButton onClick={() => {
setMessages([])
closeThread()
}}>
<MailIconImg src={ReturnSVG} />
<ComposeP>Return to Threads</ComposeP>
</ShowMessageReturnButton>
</Box>
<Spacer height="60px" />
{messages.map((message) => {
let fullMessage = message
if (hashMapMailMessages[message?.identifier]) {
fullMessage = hashMapMailMessages[message.identifier]
return <ShowMessage key={message?.identifier} message={fullMessage} />
}
return (
<SingleThreadParent>
<Skeleton
variant="rectangular"
style={{
width: '100%',
height: 60,
borderRadius: '8px',
overflow: 'hidden'
}}
/>
</SingleThreadParent>
)
})}
</ThreadContainer>
</ThreadContainerFullWidth>
{messages.length >= 20 && (
<LazyLoad onLoadMore={()=> getMailMessages(currentThread, false, true)}></LazyLoad>
)}
</GroupContainer>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,71 +0,0 @@
.ql-editor {
min-height: 200px;
width: 100%;
color: black;
font-size: 16px;
font-family: Roboto;
max-height: 225px;
overflow-y: scroll;
padding: 0px !important;
}
.ql-editor::-webkit-scrollbar-track {
background-color: transparent;
cursor: default;
}
.ql-editor::-webkit-scrollbar-track:hover {
background-color: transparent;
}
.ql-editor::-webkit-scrollbar {
width: 16px;
height: 10px;
background-color: rgba(229, 229, 229, 0.70);
}
.ql-editor::-webkit-scrollbar-thumb {
background-color: #B0B0B0;
border-radius: 8px;
background-clip: content-box;
border: 4px solid transparent;
}
.ql-editor img {
cursor: default;
}
.ql-editor-display {
min-height: 20px;
width: 100%;
color: black;
font-size: 16px;
font-family: Roboto;
padding: 0px !important;
}
.ql-editor-display img {
cursor: default;
}
.ql-container {
font-size: 16px
}
.ql-toolbar .ql-stroke {
fill: none !important;
stroke: black !important;
}
.ql-toolbar .ql-fill {
fill: black !important;
stroke: none !important;
}
.ql-toolbar .ql-picker {
color: black !important;
}
.ql-toolbar .ql-picker-options {
background-color: white !important;
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,244 +1,282 @@
import * as React from "react";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Checkbox from "@mui/material/Checkbox";
import IconButton from "@mui/material/IconButton";
import CommentIcon from "@mui/icons-material/Comment";
import InfoIcon from "@mui/icons-material/Info";
import { RequestQueueWithPromise } from "../../utils/queue/queue";
import * as React from 'react';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import IconButton from '@mui/material/IconButton';
import { RequestQueueWithPromise } from '../../utils/queue/queue';
import GroupAddIcon from '@mui/icons-material/GroupAdd';
import { executeEvent } from "../../utils/events";
import { Box, ButtonBase, Collapse, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer";
import { CustomLoader } from "../../common/CustomLoader";
import { getBaseApi } from "../../background";
import { MyContext, getBaseApiReact, isMobile } from "../../App";
import { myGroupsWhereIAmAdminAtom } from "../../atoms/global";
import { useSetRecoilState } from "recoil";
import { executeEvent } from '../../utils/events';
import { Box, ButtonBase, Collapse, Typography, useTheme } from '@mui/material';
import { CustomLoader } from '../../common/CustomLoader';
import { getBaseApiReact } from '../../App';
import { myGroupsWhereIAmAdminAtom, txListAtom } from '../../atoms/global';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2)
import { useTranslation } from 'react-i18next';
import { useAtom, useSetAtom } from 'jotai';
export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2);
export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection, setMobileViewMode, setDesktopViewMode }) => {
const [isExpanded, setIsExpanded] = React.useState(false)
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([])
const [loading, setLoading] = React.useState(true)
const {txList, setTxList} = React.useContext(MyContext)
const setMyGroupsWhereIAmAdmin = useSetRecoilState(
myGroupsWhereIAmAdminAtom
export const GroupJoinRequests = ({
myAddress,
groups,
setOpenManageMembers,
getTimestampEnterChat,
setSelectedGroup,
setGroupSection,
setMobileViewMode,
setDesktopViewMode,
}) => {
const [isExpanded, setIsExpanded] = React.useState(false);
const { t } = useTranslation(['core', 'group']);
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState(
[]
);
const [loading, setLoading] = React.useState(true);
const [txList] = useAtom(txListAtom);
const setMyGroupsWhereIAmAdmin = useSetAtom(myGroupsWhereIAmAdminAtom);
const getJoinRequests = async ()=> {
const theme = useTheme();
const getJoinRequests = async () => {
try {
setLoading(true)
let groupsAsAdmin = []
const getAllGroupsAsAdmin = groups.filter((item)=> item.groupId !== '0').map(async (group)=> {
const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
return fetch(
`${getBaseApiReact()}/groups/members/${group.groupId}?limit=0&onlyAdmins=true`
setLoading(true);
let groupsAsAdmin = [];
const getAllGroupsAsAdmin = groups
.filter((item) => item.groupId !== '0')
.map(async (group) => {
const isAdminResponse = await requestQueueGroupJoinRequests.enqueue(
() => {
return fetch(
`${getBaseApiReact()}/groups/members/${group.groupId}?limit=0&onlyAdmins=true`
);
}
);
})
const isAdminData = await isAdminResponse.json()
const isAdminData = await isAdminResponse.json();
const findMyself = isAdminData?.members?.find((member)=> member.member === myAddress)
if(findMyself){
groupsAsAdmin.push(group)
}
return true
})
const findMyself = isAdminData?.members?.find(
(member) => member.member === myAddress
);
await Promise.all(getAllGroupsAsAdmin)
setMyGroupsWhereIAmAdmin(groupsAsAdmin)
const res = await Promise.all(groupsAsAdmin.map(async (group)=> {
if (findMyself) {
groupsAsAdmin.push(group);
}
return true;
});
const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
return fetch(
`${getBaseApiReact()}/groups/joinrequests/${group.groupId}`
);
})
await Promise.all(getAllGroupsAsAdmin);
setMyGroupsWhereIAmAdmin(groupsAsAdmin);
const res = await Promise.all(
groupsAsAdmin.map(async (group) => {
const joinRequestResponse =
await requestQueueGroupJoinRequests.enqueue(() => {
return fetch(
`${getBaseApiReact()}/groups/joinrequests/${group.groupId}`
);
});
const joinRequestData = await joinRequestResponse.json()
return {
group,
data: joinRequestData
}
}))
setGroupsWithJoinRequests(res)
const joinRequestData = await joinRequestResponse.json();
return {
group,
data: joinRequestData,
};
})
);
setGroupsWithJoinRequests(res);
} catch (error) {
console.log(error);
} finally {
setLoading(false)
setLoading(false);
}
}
};
React.useEffect(() => {
if (myAddress && groups.length > 0) {
getJoinRequests()
getJoinRequests();
} else {
setLoading(false)
setLoading(false);
}
}, [myAddress, groups]);
const filteredJoinRequests = React.useMemo(()=> {
return groupsWithJoinRequests.map((group)=> {
const filteredGroupRequests = group?.data?.filter((gd)=> {
const findJoinRequsetInTxList = txList?.find((tx)=> tx?.groupId === group?.group?.groupId && tx?.qortalAddress === gd?.joiner && tx?.type === 'join-request-accept')
const filteredJoinRequests = React.useMemo(() => {
return groupsWithJoinRequests.map((group) => {
const filteredGroupRequests = group?.data?.filter((gd) => {
const findJoinRequsetInTxList = txList?.find(
(tx) =>
tx?.groupId === group?.group?.groupId &&
tx?.qortalAddress === gd?.joiner &&
tx?.type === 'join-request-accept'
);
if(findJoinRequsetInTxList) return false
return true
})
if (findJoinRequsetInTxList) return false;
return true;
});
return {
...group,
data: filteredGroupRequests
}
})
}, [groupsWithJoinRequests, txList])
data: filteredGroupRequests,
};
});
}, [groupsWithJoinRequests, txList]);
return (
<Box sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: 'center'
}}>
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<ButtonBase
sx={{
width: "322px",
display: "flex",
flexDirection: "row",
width: '322px',
display: 'flex',
flexDirection: 'row',
padding: '0px 20px',
gap: '10px',
justifyContent: 'flex-start'
justifyContent: 'flex-start',
}}
onClick={()=> setIsExpanded((prev)=> !prev)}
onClick={() => setIsExpanded((prev) => !prev)}
>
<Typography
sx={{
fontSize: "1rem",
fontSize: '1rem',
}}
>
Join Requests {filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length > 0 && ` (${filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length})`}
{t('group:join_requests', { postProcess: 'capitalize' })}{' '}
{filteredJoinRequests?.filter((group) => group?.data?.length > 0)
?.length > 0 &&
` (${filteredJoinRequests?.filter((group) => group?.data?.length > 0)?.length})`}
</Typography>
{isExpanded ? <ExpandLessIcon sx={{
marginLeft: 'auto'
}} /> : (
<ExpandMoreIcon sx={{
marginLeft: 'auto'
}}/>
)}
{isExpanded ? (
<ExpandLessIcon
sx={{
marginLeft: 'auto',
}}
/>
) : (
<ExpandMoreIcon
sx={{
marginLeft: 'auto',
}}
/>
)}
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box
sx={{
width: "322px",
height: isMobile ? "165px" : "250px",
display: "flex",
flexDirection: "column",
bgcolor: "background.paper",
padding: "20px",
borderRadius: '19px'
}}
>
{loading && filteredJoinRequests.length === 0 && (
<Box sx={{
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
<CustomLoader />
</Box>
)}
{!loading && (filteredJoinRequests.length === 0 || filteredJoinRequests?.filter((group)=> group?.data?.length > 0).length === 0) && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: 'center',
height: '100%',
}}
>
<Typography
sx={{
fontSize: "11px",
fontWeight: 400,
color: 'rgba(255, 255, 255, 0.2)'
}}
>
Nothing to display
</Typography>
</Box>
)}
<List className="scrollable-container" sx={{ width: "100%", maxWidth: 360, bgcolor: "background.paper", maxHeight: '300px', overflow: 'auto' }}>
{filteredJoinRequests?.map((group)=> {
if(group?.data?.length === 0) return null
return (
<ListItem
key={group?.groupId}
onClick={()=> {
setSelectedGroup(group?.group)
setMobileViewMode('group')
getTimestampEnterChat()
setGroupSection("announcement")
setOpenManageMembers(true)
if(!isMobile){
setDesktopViewMode('chat')
}
setTimeout(() => {
executeEvent("openGroupJoinRequest", {});
}, 300);
}}
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box
sx={{
marginBottom: '20px'
bgcolor: 'background.paper',
borderRadius: '19px',
display: 'flex',
flexDirection: 'column',
height: '250px',
padding: '20px',
width: '322px',
}}
disablePadding
secondaryAction={
<IconButton edge="end" aria-label="comments">
<GroupAddIcon
sx={{
color: "white",
fontSize: '18px'
}}
/>
</IconButton>
}
>
<ListItemButton sx={{
padding: "0px",
}} disableRipple role={undefined} dense>
<ListItemText sx={{
"& .MuiTypography-root": {
fontSize: "13px",
{loading && filteredJoinRequests.length === 0 && (
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
}}
>
<CustomLoader />
</Box>
)}
{!loading &&
(filteredJoinRequests.length === 0 ||
filteredJoinRequests?.filter((group) => group?.data?.length > 0)
.length === 0) && (
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
>
<Typography
sx={{
fontSize: '11px',
fontWeight: 400,
},
}} primary={`${group?.group?.groupName} has ${group?.data?.length} pending join requests.`} />
</ListItemButton>
</ListItem>
)
})}
</List>
</Box>
</Collapse>
color: 'rgba(255, 255, 255, 0.2)',
}}
>
{t('group:message.generic.no_display', {
postProcess: 'capitalize',
})}
</Typography>
</Box>
)}
<List
className="scrollable-container"
sx={{
bgcolor: 'background.paper',
maxHeight: '300px',
maxWidth: 360,
overflow: 'auto',
width: '100%',
}}
>
{filteredJoinRequests?.map((group) => {
if (group?.data?.length === 0) return null;
return (
<ListItem
key={group?.groupId}
onClick={() => {
setSelectedGroup(group?.group);
setMobileViewMode('group');
getTimestampEnterChat();
setGroupSection('announcement');
setOpenManageMembers(true);
setDesktopViewMode('chat');
setTimeout(() => {
executeEvent('openGroupJoinRequest', {});
}, 300);
}}
sx={{
marginBottom: '20px',
}}
disablePadding
secondaryAction={
<IconButton edge="end" aria-label="comments">
<GroupAddIcon
sx={{
color: theme.palette.text.primary,
fontSize: '18px',
}}
/>
</IconButton>
}
>
<ListItemButton
sx={{
padding: '0px',
}}
disableRipple
role={undefined}
dense
>
<ListItemText
sx={{
'& .MuiTypography-root': {
fontSize: '13px',
fontWeight: 400,
},
}}
primary={`${group?.group?.groupName} has ${group?.data?.length} pending join requests.`}
/>
</ListItemButton>
</ListItem>
);
})}
</List>
</Box>
</Collapse>
</Box>
);
};

View File

@@ -0,0 +1,352 @@
import {
Avatar,
Box,
ButtonBase,
List,
ListItem,
ListItemAvatar,
ListItemText,
useTheme,
} from '@mui/material';
import React, { useCallback } from 'react';
import { IconWrapper } from '../Desktop/DesktopFooter';
import { HubsIcon } from '../../assets/Icons/HubsIcon';
import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
import { ContextMenu } from '../ContextMenu';
import { getBaseApiReact } from '../../App';
import { formatEmailDate } from './QMailMessages';
import CampaignIcon from '@mui/icons-material/Campaign';
import MarkChatUnreadIcon from '@mui/icons-material/MarkChatUnread';
import LockIcon from '@mui/icons-material/Lock';
import { CustomButton } from '../../styles/App-styles';
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import PersonOffIcon from '@mui/icons-material/PersonOff';
import {
groupAnnouncementSelector,
groupChatTimestampSelector,
groupPropertySelector,
groupsOwnerNamesSelector,
isRunningPublicNodeAtom,
timestampEnterDataSelector,
} from '../../atoms/global';
import { timeDifferenceForNotificationChats } from './Group';
import { useAtom, useAtomValue } from 'jotai';
export const GroupList = ({
selectGroupFunc,
setDesktopSideView,
groupChatHasUnread,
groupsAnnHasUnread,
desktopSideView,
directChatHasUnread,
chatMode,
groups,
selectedGroup,
getUserSettings,
setOpenAddGroup,
setIsOpenBlockedUserModal,
myAddress,
}) => {
const theme = useTheme();
const [isRunningPublicNode] = useAtom(isRunningPublicNodeAtom);
return (
<div
style={{
display: 'flex',
width: '380px',
flexDirection: 'column',
alignItems: 'flex-start',
height: '100%',
background: theme.palette.background.surface,
borderRadius: '0px 15px 15px 0px',
padding: '0px 2px',
}}
>
<Box
sx={{
width: '100%',
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
gap: '10px',
}}
>
<ButtonBase
onClick={() => {
setDesktopSideView('groups');
}}
>
<IconWrapper
color={
groupChatHasUnread || groupsAnnHasUnread
? theme.palette.other.unread
: desktopSideView === 'groups'
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="Groups"
selected={desktopSideView === 'groups'}
customWidth="75px"
>
<HubsIcon
height={24}
color={
groupChatHasUnread || groupsAnnHasUnread
? theme.palette.other.unread
: desktopSideView === 'groups'
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
</ButtonBase>
<ButtonBase
onClick={() => {
setDesktopSideView('directs');
}}
>
<IconWrapper
customWidth="75px"
color={
directChatHasUnread
? theme.palette.other.unread
: desktopSideView === 'directs'
? theme.palette.text.primary
: theme.palette.text.secondary
}
label="Messaging"
selected={desktopSideView === 'directs'}
>
<MessagingIcon
height={24}
color={
directChatHasUnread
? theme.palette.other.unread
: desktopSideView === 'directs'
? theme.palette.text.primary
: theme.palette.text.secondary
}
/>
</IconWrapper>
</ButtonBase>
</Box>
<div
style={{
alignItems: 'flex-start',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
left: chatMode === 'directs' && '-1000px',
overflowY: 'auto',
position: chatMode === 'directs' && 'fixed',
visibility: chatMode === 'directs' && 'hidden',
width: '100%',
}}
>
<List
sx={{
width: '100%',
}}
className="group-list"
dense={false}
>
{groups.map((group: any) => (
<GroupItem
selectGroupFunc={selectGroupFunc}
key={group.groupId}
group={group}
selectedGroup={selectedGroup}
getUserSettings={getUserSettings}
myAddress={myAddress}
/>
))}
</List>
</div>
<div
style={{
display: 'flex',
gap: '10px',
justifyContent: 'center',
padding: '10px',
width: '100%',
}}
>
<>
<CustomButton
onClick={() => {
setOpenAddGroup(true);
}}
>
<AddCircleOutlineIcon
sx={{
color: theme.palette.text.primary,
}}
/>
Group
</CustomButton>
{!isRunningPublicNode && (
<CustomButton
onClick={() => {
setIsOpenBlockedUserModal(true);
}}
sx={{
minWidth: 'unset',
padding: '10px',
}}
>
<PersonOffIcon
sx={{
color: theme.palette.text.primary,
}}
/>
</CustomButton>
)}
</>
</div>
</div>
);
};
const GroupItem = React.memo(
({ selectGroupFunc, group, selectedGroup, getUserSettings, myAddress }) => {
const theme = useTheme();
const ownerName = useAtomValue(groupsOwnerNamesSelector(group?.groupId));
const announcement = useAtomValue(
groupAnnouncementSelector(group?.groupId)
);
const groupProperty = useAtomValue(groupPropertySelector(group?.groupId));
const groupChatTimestamp = useAtomValue(
groupChatTimestampSelector(group?.groupId)
);
const timestampEnterData = useAtomValue(
timestampEnterDataSelector(group?.groupId)
);
const selectGroupHandler = useCallback(() => {
selectGroupFunc(group);
}, [group, selectGroupFunc]);
return (
<ListItem
onClick={selectGroupHandler}
sx={{
display: 'flex',
background:
group?.groupId === selectedGroup?.groupId &&
theme.palette.action.selected,
borderRadius: '2px',
cursor: 'pointer',
flexDirection: 'column',
padding: '10px',
width: '100%',
'&:hover': {
backgroundColor: 'action.hover', // background on hover
},
}}
>
<ContextMenu getUserSettings={getUserSettings} groupId={group.groupId}>
<Box
sx={{
alignItems: 'center',
display: 'flex',
width: '100%',
}}
>
<ListItemAvatar>
{ownerName ? (
<Avatar
alt={group?.groupName?.charAt(0)}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
ownerName
}/qortal_group_avatar_${group?.groupId}?async=true`}
>
{group?.groupName?.charAt(0).toUpperCase()}
</Avatar>
) : (
<Avatar alt={group?.groupName?.charAt(0)}>
{' '}
{group?.groupName?.charAt(0).toUpperCase() || 'G'}
</Avatar>
)}
</ListItemAvatar>
<ListItemText
primary={group.groupId === '0' ? 'General' : group.groupName}
secondary={
!group?.timestamp
? 'no messages'
: `last message: ${formatEmailDate(group?.timestamp)}`
}
primaryTypographyProps={{
style: {
color:
group?.groupId === selectedGroup?.groupId &&
theme.palette.text.primary,
fontSize: '16px',
},
}} // Change the color of the primary text
secondaryTypographyProps={{
style: {
color:
group?.groupId === selectedGroup?.groupId &&
theme.palette.text.primary,
fontSize: '12px',
},
}}
sx={{
width: '150px',
fontFamily: 'Inter',
fontSize: '16px',
}}
/>
{announcement && !announcement?.seentimestamp && (
<CampaignIcon
sx={{
color: theme.palette.other.unread,
marginRight: '5px',
marginBottom: 'auto',
}}
/>
)}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: '5px',
justifyContent: 'flex-start',
height: '100%',
marginBottom: 'auto',
}}
>
{group?.data &&
groupChatTimestamp &&
group?.sender !== myAddress &&
group?.timestamp &&
((!timestampEnterData &&
Date.now() - group?.timestamp <
timeDifferenceForNotificationChats) ||
timestampEnterData < group?.timestamp) && (
<MarkChatUnreadIcon
sx={{
color: theme.palette.other.unread,
}}
/>
)}
{groupProperty?.isOpen === false && (
<LockIcon
sx={{
color: theme.palette.other.positive,
marginBottom: 'auto',
}}
/>
)}
</Box>
</Box>
</ContextMenu>
</ListItem>
);
}
);

View File

@@ -1,202 +0,0 @@
import React, { useState } from "react";
import {
Button,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
Badge,
Box,
} from "@mui/material";
import ForumIcon from "@mui/icons-material/Forum";
import GroupIcon from "@mui/icons-material/Group";
import { ArrowDownIcon } from "../../assets/Icons/ArrowDownIcon";
import { NotificationIcon2 } from "../../assets/Icons/NotificationIcon2";
import { ChatIcon } from "../../assets/Icons/ChatIcon";
import { ThreadsIcon } from "../../assets/Icons/ThreadsIcon";
import { MembersIcon } from "../../assets/Icons/MembersIcon";
export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers, goToAnnouncements, goToChat, hasUnreadChat, hasUnreadAnnouncements }) => {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
return (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
marginTop: '14px',
marginBottom: '14px'
}}
>
<Button
aria-controls={open ? "home-menu" : undefined}
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
onClick={handleClick}
variant="contained"
sx={{
backgroundColor: "var(--bg-primary)",
width: "148px",
borderRadius: "5px",
fontSize: "12px",
fontWeight: 600,
color: "#fff",
textTransform: "none",
padding: '5px',
height: '25px'
}}
>
<Box
sx={{
display: "flex",
gap: "6px",
alignItems: "center",
justifyContent: "space-between",
width: '100%'
}}
>
<Box
sx={{
display: "flex",
gap: "6px",
alignItems: "center",
}}
>
{groupSection === "announcement" &&(
<> <NotificationIcon2 color={hasUnreadAnnouncements || hasUnreadChat ? 'var(--danger)' : 'white'} /> {" Announcements"}</>
)}
{groupSection === "chat" &&(
<> <ChatIcon color={hasUnreadAnnouncements || hasUnreadChat ? 'var(--danger)' : 'white'} /> {" Group Chats"}</>
)}
{groupSection === "forum" &&(
<> <ThreadsIcon color={hasUnreadAnnouncements || hasUnreadChat ? 'var(--danger)' : 'white'} /> {" Threads"}</>
)}
</Box>
<ArrowDownIcon color="white" />
</Box>
</Button>
<Menu
id="home-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
"aria-labelledby": "basic-button",
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
slotProps={{
paper: {
sx: {
backgroundColor: 'var(--bg-primary)',
color: '#fff',
width: '148px',
borderRadius: '5px'
},
},
}}
sx={{
marginTop: '10px'
}}
>
<MenuItem
onClick={() => {
goToChat()
handleClose();
}}
>
<ListItemIcon sx={{
minWidth: '24px !important'
}}>
<ChatIcon color={hasUnreadChat ? 'var(--danger)' : "#fff"} />
</ListItemIcon>
<ListItemText sx={{
"& .MuiTypography-root": {
fontSize: "12px",
fontWeight: 600,
color: hasUnreadChat ? "var(--danger)" :"#fff"
},
}} primary="Chat" />
</MenuItem>
<MenuItem
onClick={() => {
goToAnnouncements()
handleClose();
}}
>
<ListItemIcon sx={{
minWidth: '24px !important'
}}>
<NotificationIcon2 color={hasUnreadAnnouncements ? 'var(--danger)' : "#fff" } />
</ListItemIcon>
<ListItemText sx={{
"& .MuiTypography-root": {
fontSize: "12px",
fontWeight: 600,
color: hasUnreadAnnouncements ? "var(--danger)" :"#fff"
},
}} primary="Announcements" />
</MenuItem>
<MenuItem
onClick={() => {
setGroupSection("forum");
handleClose();
}}
>
<ListItemIcon sx={{
minWidth: '24px !important'
}}>
<ThreadsIcon color={"#fff"} />
</ListItemIcon>
<ListItemText sx={{
"& .MuiTypography-root": {
fontSize: "12px",
fontWeight: 600,
},
}} primary="Threads" />
</MenuItem>
<MenuItem
onClick={() => {
setOpenManageMembers(true)
handleClose();
}}
>
<ListItemIcon sx={{
minWidth: '24px !important'
}}>
<MembersIcon sx={{ color: "#fff" }} />
</ListItemIcon>
<ListItemText sx={{
"& .MuiTypography-root": {
fontSize: "12px",
fontWeight: 600,
},
}} primary="Members" />
</MenuItem>
</Menu>
</Box>
);
};

View File

@@ -1,112 +0,0 @@
import { Box, Button, Typography } from "@mui/material";
import React from "react";
import { Spacer } from "../../common/Spacer";
import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched";
import { ThingsToDoInitial } from "./ThingsToDoInitial";
import { GroupJoinRequests } from "./GroupJoinRequests";
import { GroupInvites } from "./GroupInvites";
import RefreshIcon from "@mui/icons-material/Refresh";
export const Home = ({
refreshHomeDataFunc,
myAddress,
isLoadingGroups,
balance,
userInfo,
groups,
setGroupSection,
setSelectedGroup,
getTimestampEnterChat,
setOpenManageMembers,
setOpenAddGroup,
setMobileViewMode,
setDesktopViewMode
}) => {
return (
<Box
sx={{
display: "flex",
width: "100%",
flexDirection: "column",
height: "100%",
overflow: "auto",
alignItems: "center",
}}
>
<Spacer height="20px" />
<Typography
sx={{
color: "rgba(255, 255, 255, 1)",
fontWeight: 400,
fontSize: userInfo?.name?.length > 15 ? "16px" : "20px",
padding: '10px'
}}
>
Welcome
{userInfo?.name ? (
<span
style={{
fontStyle: "italic",
}}
>{`, ${userInfo?.name}`}</span>
) : null}
</Typography>
<Spacer height="26px" />
{/* <Box
sx={{
display: "flex",
width: "100%",
justifyContent: "flex-start",
}}
>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={refreshHomeDataFunc}
sx={{
color: "white",
}}
>
Refresh home data
</Button>
</Box> */}
{!isLoadingGroups && (
<Box
sx={{
display: "flex",
gap: "15px",
flexWrap: "wrap",
justifyContent: "center",
}}
>
<ThingsToDoInitial
balance={balance}
myAddress={myAddress}
name={userInfo?.name}
hasGroups={groups?.length !== 0}
/>
<ListOfThreadPostsWatched />
<GroupJoinRequests
setGroupSection={setGroupSection}
setSelectedGroup={setSelectedGroup}
getTimestampEnterChat={getTimestampEnterChat}
setOpenManageMembers={setOpenManageMembers}
myAddress={myAddress}
groups={groups}
setMobileViewMode={setMobileViewMode}
setDesktopViewMode={setDesktopViewMode}
/>
<GroupInvites
setOpenAddGroup={setOpenAddGroup}
myAddress={myAddress}
groups={groups}
setMobileViewMode={setMobileViewMode}
/>
</Box>
)}
<Spacer height="180px" />
</Box>
);
};

View File

@@ -1,16 +1,16 @@
import { Box, Button, Divider, Typography } from "@mui/material";
import React from "react";
import { Spacer } from "../../common/Spacer";
import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched";
import { ThingsToDoInitial } from "./ThingsToDoInitial";
import { GroupJoinRequests } from "./GroupJoinRequests";
import { GroupInvites } from "./GroupInvites";
import RefreshIcon from "@mui/icons-material/Refresh";
import { ListOfGroupPromotions } from "./ListOfGroupPromotions";
import { QortPrice } from "../Home/QortPrice";
import ExploreIcon from "@mui/icons-material/Explore";
import { Explore } from "../Explore/Explore";
import { NewUsersCTA } from "../Home/NewUsersCTA";
import { Box, Divider, Typography, useTheme } from '@mui/material';
import React from 'react';
import { Spacer } from '../../common/Spacer';
import { ThingsToDoInitial } from './ThingsToDoInitial';
import { GroupJoinRequests } from './GroupJoinRequests';
import { GroupInvites } from './GroupInvites';
import { ListOfGroupPromotions } from './ListOfGroupPromotions';
import { QortPrice } from '../Home/QortPrice';
import ExploreIcon from '@mui/icons-material/Explore';
import { Explore } from '../Explore/Explore';
import { NewUsersCTA } from '../Home/NewUsersCTA';
import { useTranslation } from 'react-i18next';
export const HomeDesktop = ({
refreshHomeDataFunc,
myAddress,
@@ -30,93 +30,97 @@ export const HomeDesktop = ({
}) => {
const [checked1, setChecked1] = React.useState(false);
const [checked2, setChecked2] = React.useState(false);
React.useEffect(() => {
if (balance && +balance >= 6) {
setChecked1(true);
}
}, [balance]);
React.useEffect(() => {
if (name) setChecked2(true);
}, [name]);
const isLoaded = React.useMemo(()=> {
if(userInfo !== null) return true
return false
}, [ userInfo])
const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> {
if(isLoaded && checked1 && checked2) return true
return false
}, [checked1, isLoaded, checked2])
const { t } = useTranslation(['core']);
const theme = useTheme();
React.useEffect(() => {
if (balance && +balance >= 6) {
setChecked1(true);
}
}, [balance]);
React.useEffect(() => {
if (name) setChecked2(true);
}, [name]);
const isLoaded = React.useMemo(() => {
if (userInfo !== null) return true;
return false;
}, [userInfo]);
const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(() => {
if (isLoaded && checked1 && checked2) return true;
return false;
}, [checked1, isLoaded, checked2]);
return (
<Box
sx={{
display: desktopViewMode === "home" ? "flex" : "none",
width: "100%",
flexDirection: "column",
height: "100%",
overflow: "auto",
alignItems: "center",
alignItems: 'center',
display: desktopViewMode === 'home' ? 'flex' : 'none',
flexDirection: 'column',
height: '100%',
overflow: 'auto',
width: '100%',
}}
>
<Spacer height="20px" />
<Box
sx={{
display: "flex",
width: "100%",
flexDirection: "column",
height: "100%",
alignItems: "flex-start",
maxWidth: "1036px",
alignItems: 'flex-start',
display: 'flex',
flexDirection: 'column',
height: '100%',
maxWidth: '1036px',
width: '100%',
}}
>
<Typography
sx={{
color: "rgba(255, 255, 255, 1)",
color: theme.palette.text.primary,
fontWeight: 400,
fontSize: userInfo?.name?.length > 15 ? "16px" : "20px",
padding: "10px",
fontSize: userInfo?.name?.length > 15 ? '16px' : '20px',
padding: '10px',
}}
>
Welcome
{t('core:welcome', { postProcess: 'capitalize' })}
{userInfo?.name ? (
<span
style={{
fontStyle: "italic",
fontStyle: 'italic',
}}
>{`, ${userInfo?.name}`}</span>
) : null}
</Typography>
<Spacer height="30px" />
{!isLoadingGroups && (
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
width: "100%",
justifyContent: "center",
display: 'flex',
flexWrap: 'wrap',
gap: '20px',
justifyContent: 'center',
width: '100%',
}}
>
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
flexDirection: "column",
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
gap: '20px',
}}
>
<Box
sx={{
width: "330px",
display: "flex",
alignItems: "center",
justifyContent: "center",
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
width: '330px',
}}
>
<ThingsToDoInitial
@@ -125,12 +129,12 @@ export const HomeDesktop = ({
name={userInfo?.name}
userInfo={userInfo}
hasGroups={
groups?.filter((item) => item?.groupId !== "0").length !== 0
groups?.filter((item) => item?.groupId !== '0').length !== 0
}
/>
</Box>
{desktopViewMode === "home" && (
{desktopViewMode === 'home' && (
<>
{/* <Box sx={{
width: '330px',
@@ -140,127 +144,103 @@ export const HomeDesktop = ({
}}>
<ListOfThreadPostsWatched />
</Box> */}
{hasDoneNameAndBalanceAndIsLoaded && (
<>
<Box
sx={{
width: "330px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<GroupJoinRequests
setGroupSection={setGroupSection}
setSelectedGroup={setSelectedGroup}
getTimestampEnterChat={getTimestampEnterChat}
setOpenManageMembers={setOpenManageMembers}
myAddress={myAddress}
groups={groups}
setMobileViewMode={setMobileViewMode}
setDesktopViewMode={setDesktopViewMode}
/>
</Box>
<Box
sx={{
width: "330px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<GroupInvites
setOpenAddGroup={setOpenAddGroup}
myAddress={myAddress}
groups={groups}
setMobileViewMode={setMobileViewMode}
/>
</Box>
</>
)}
{hasDoneNameAndBalanceAndIsLoaded && (
<>
<Box
sx={{
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
width: '330px',
}}
>
<GroupJoinRequests
setGroupSection={setGroupSection}
setSelectedGroup={setSelectedGroup}
getTimestampEnterChat={getTimestampEnterChat}
setOpenManageMembers={setOpenManageMembers}
myAddress={myAddress}
groups={groups}
setMobileViewMode={setMobileViewMode}
setDesktopViewMode={setDesktopViewMode}
/>
</Box>
<Box
sx={{
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
width: '330px',
}}
>
<GroupInvites
setOpenAddGroup={setOpenAddGroup}
myAddress={myAddress}
groups={groups}
setMobileViewMode={setMobileViewMode}
/>
</Box>
</>
)}
</>
)}
</Box>
<QortPrice />
</Box>
)}
{!isLoadingGroups && (
<>
<Spacer height="60px" />
<Divider
color="secondary"
sx={{
width: "100%",
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
}}
>
<ExploreIcon
<Divider
color="secondary"
sx={{
color: "white",
}}
/>{" "}
<Typography
sx={{
fontSize: "1rem",
width: '100%',
}}
>
Explore
</Typography>{" "}
</Box>
</Divider>
{!hasDoneNameAndBalanceAndIsLoaded && (
<Spacer height="40px" />
)}
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
width: "100%",
justifyContent: "center",
}}
>
{hasDoneNameAndBalanceAndIsLoaded && (
<ListOfGroupPromotions />
)}
<Explore setDesktopViewMode={setDesktopViewMode} />
</Box>
<NewUsersCTA balance={balance} />
</>
)}
</Box>
<Spacer height="26px" />
{/* <Box
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "flex-start",
alignItems: 'center',
display: 'flex',
gap: '10px',
}}
>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={refreshHomeDataFunc}
<ExploreIcon
sx={{
color: "white",
ccolor: theme.palette.text.primary,
}}
/>{' '}
<Typography
sx={{
fontSize: '1rem',
}}
>
Refresh home data
</Button>
</Box> */}
{t('tutorial:initial.explore', { postProcess: 'capitalize' })}
</Typography>{' '}
</Box>
</Divider>
{!hasDoneNameAndBalanceAndIsLoaded && <Spacer height="40px" />}
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: '20px',
justifyContent: 'center',
width: '100%',
}}
>
{hasDoneNameAndBalanceAndIsLoaded && <ListOfGroupPromotions />}
<Explore setDesktopViewMode={setDesktopViewMode} />
</Box>
<NewUsersCTA balance={balance} />
</>
)}
</Box>
<Spacer height="180px" />
</Box>

View File

@@ -1,50 +1,52 @@
import { LoadingButton } from "@mui/lab";
import {
Box,
Button,
Input,
MenuItem,
Select,
SelectChangeEvent,
} from "@mui/material";
import React, { useState } from "react";
import { Spacer } from "../../common/Spacer";
import { Label } from "./AddGroup";
import { getFee } from "../../background";
import { LoadingButton } from '@mui/lab';
import { Box, Input, MenuItem, Select, SelectChangeEvent } from '@mui/material';
import { useState } from 'react';
import { Spacer } from '../../common/Spacer';
import { Label } from './AddGroup';
import { getFee } from '../../background';
import { useTranslation } from 'react-i18next';
export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
const [value, setValue] = useState("");
const [value, setValue] = useState('');
const [expiryTime, setExpiryTime] = useState<string>('259200');
const [isLoadingInvite, setIsLoadingInvite] = useState(false)
const [isLoadingInvite, setIsLoadingInvite] = useState(false);
const { t } = useTranslation(['core', 'group']);
const inviteMember = async () => {
try {
const fee = await getFee('GROUP_INVITE')
const fee = await getFee('GROUP_INVITE');
await show({
message: "Would you like to perform a GROUP_INVITE transaction?" ,
publishFee: fee.fee + ' QORT'
})
setIsLoadingInvite(true)
message: t('group:question.group_invite', {
postProcess: 'capitalize',
}),
publishFee: fee.fee + ' QORT',
});
setIsLoadingInvite(true);
if (!expiryTime || !value) return;
new Promise((res, rej) => {
window.sendMessage("inviteToGroup", {
groupId,
qortalAddress: value,
inviteTime: +expiryTime,
})
window
.sendMessage('inviteToGroup', {
groupId,
qortalAddress: value,
inviteTime: +expiryTime,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message: `Successfully invited ${value}. It may take a couple of minutes for the changes to propagate`,
type: 'success',
message: t('group:message.success.group_invite', {
value: value,
postProcess: 'capitalize',
}),
});
setOpenSnack(true);
res(response);
setValue("");
setValue('');
return;
}
setInfoSnack({
type: "error",
type: 'error',
message: response?.error,
});
setOpenSnack(true);
@@ -52,16 +54,17 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
})
.catch((error) => {
setInfoSnack({
type: "error",
message: error?.message || "An error occurred",
type: 'error',
message: error?.message || 'An error occurred',
});
setOpenSnack(true);
rej(error);
});
});
} catch (error) {} finally {
setIsLoadingInvite(false)
} catch (error) {
console.log(error);
} finally {
setIsLoadingInvite(false);
}
};
@@ -72,40 +75,48 @@ export const InviteMember = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
display: 'flex',
flexDirection: 'column',
}}
>
Invite member
{t('group:action.invite_member', { postProcess: 'capitalize' })}
<Spacer height="20px" />
<Input
value={value}
placeholder="Name or address"
onChange={(e) => setValue(e.target.value)}
/>
<Spacer height="20px" />
<Label>Invitation Expiry Time</Label>
<Spacer height="20px" />
<Label>
{t('group:invitation_expiry', { postProcess: 'capitalize' })}
</Label>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={expiryTime}
label="Invitation Expiry Time"
label={t('group:invitation_expiry', { postProcess: 'capitalize' })}
onChange={handleChange}
>
<MenuItem value={10800}>3 hours</MenuItem>
<MenuItem value={21600}>6 hours</MenuItem>
<MenuItem value={43200}>12 hours</MenuItem>
<MenuItem value={86400}>1 day</MenuItem>
<MenuItem value={259200}>3 days</MenuItem>
<MenuItem value={432000}>5 days</MenuItem>
<MenuItem value={604800}>7 days</MenuItem>
<MenuItem value={864000}>10 days</MenuItem>
<MenuItem value={1296000}>15 days</MenuItem>
<MenuItem value={2592000}>30 days</MenuItem>
<MenuItem value={10800}>{t('core.time.hour', { count: 3 })}</MenuItem>
<MenuItem value={21600}>{t('core.time.hour', { count: 6 })}</MenuItem>
<MenuItem value={43200}>{t('core.time.hour', { count: 12 })}</MenuItem>
<MenuItem value={86400}>{t('core.time.day', { count: 1 })}</MenuItem>
<MenuItem value={259200}>{t('core.time.day', { count: 3 })}</MenuItem>
<MenuItem value={432000}>{t('core.time.day', { count: 5 })}</MenuItem>
<MenuItem value={604800}>{t('core.time.day', { count: 7 })}</MenuItem>
<MenuItem value={864000}>{t('core.time.day', { count: 10 })}</MenuItem>
<MenuItem value={1296000}>{t('core.time.day', { count: 15 })}</MenuItem>
<MenuItem value={2592000}>{t('core.time.day', { count: 30 })}</MenuItem>
</Select>
<Spacer height="20px" />
<LoadingButton variant="contained" loadingPosition="start" loading={isLoadingInvite} onClick={inviteMember}>Invite</LoadingButton>
<LoadingButton
variant="contained"
loadingPosition="start"
loading={isLoadingInvite}
onClick={inviteMember}
>
{t('core:action.invite', { postProcess: 'capitalize' })}
</LoadingButton>
</Box>
);
};

View File

@@ -1,16 +1,32 @@
import React, { useEffect, useRef, useState } from 'react';
import { Avatar, Box, Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover } from '@mui/material';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
import { useEffect, useRef, useState } from 'react';
import {
Avatar,
Box,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemText,
Popover,
} from '@mui/material';
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List,
} from 'react-virtualized';
import { getNameInfo } from './Group';
import { getBaseApi, getFee } from '../../background';
import { getFee } from '../../background';
import { LoadingButton } from '@mui/lab';
import { getBaseApiReact } from '../../App';
import { useTranslation } from 'react-i18next';
export const getMemberInvites = async (groupNumber) => {
const response = await fetch(`${getBaseApiReact()}/groups/bans/${groupNumber}?limit=0`);
const response = await fetch(
`${getBaseApiReact()}/groups/bans/${groupNumber}?limit=0`
);
const groupData = await response.json();
return groupData;
}
};
const getNames = async (listOfMembers, includeNoNames) => {
let members = [];
@@ -20,14 +36,14 @@ const getNames = async (listOfMembers, includeNoNames) => {
const name = await getNameInfo(member.offender);
if (name) {
members.push({ ...member, name });
} else if(includeNoNames){
members.push({ ...member, name: name || "" });
} else if (includeNoNames) {
members.push({ ...member, name: name || '' });
}
}
}
}
return members;
}
};
const cache = new CellMeasurerCache({
fixedWidth: true,
@@ -40,6 +56,7 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
const [openPopoverIndex, setOpenPopoverIndex] = useState(null); // Track which list item has the popover open
const listRef = useRef();
const [isLoadingUnban, setIsLoadingUnban] = useState(false);
const { t } = useTranslation(['core', 'group']);
const getInvites = async (groupId) => {
try {
@@ -49,7 +66,7 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
} catch (error) {
console.error(error);
}
}
};
useEffect(() => {
if (groupId) {
@@ -67,33 +84,36 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
setOpenPopoverIndex(null);
};
const handleCancelBan = async (address)=> {
const handleCancelBan = async (address) => {
try {
const fee = await getFee('CANCEL_GROUP_BAN')
const fee = await getFee('CANCEL_GROUP_BAN');
await show({
message: "Would you like to perform a CANCEL_GROUP_BAN transaction?" ,
publishFee: fee.fee + ' QORT'
})
setIsLoadingUnban(true)
new Promise((res, rej)=> {
window.sendMessage("cancelBan", {
groupId,
qortalAddress: address,
})
message: t('group:question.cancel_ban', { postProcess: 'capitalize' }),
publishFee: fee.fee + ' QORT',
});
setIsLoadingUnban(true);
new Promise((res, rej) => {
window
.sendMessage('cancelBan', {
groupId,
qortalAddress: address,
})
.then((response) => {
if (!response?.error) {
res(response);
setIsLoadingUnban(false);
setInfoSnack({
type: "success",
message: "Successfully unbanned user. It may take a couple of minutes for the changes to propagate",
type: 'success',
message: t('group:message.success.unbanned_user', {
postProcess: 'capitalize',
}),
});
handlePopoverClose();
setOpenSnack(true);
return;
}
setInfoSnack({
type: "error",
type: 'error',
message: response?.error,
});
setOpenSnack(true);
@@ -101,24 +121,23 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
})
.catch((error) => {
setInfoSnack({
type: "error",
message: error.message || "An error occurred",
type: 'error',
message: error.message || 'An error occurred',
});
setOpenSnack(true);
rej(error);
});
})
});
} catch (error) {
console.log(error);
} finally {
setIsLoadingUnban(false)
setIsLoadingUnban(false);
}
}
};
const rowRenderer = ({ index, key, parent, style }) => {
const member = bans[index];
return (
<CellMeasurer
key={key}
@@ -135,36 +154,50 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
vertical: 'top',
horizontal: 'center',
}}
style={{ marginTop: "8px" }}
style={{ marginTop: '8px' }}
>
<Box
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
width: '325px',
height: '250px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
padding: '10px',
}}
>
<LoadingButton loading={isLoadingUnban}
<LoadingButton
loading={isLoadingUnban}
loadingPosition="start"
variant="contained" onClick={()=> handleCancelBan(member?.offender)}>Cancel Ban</LoadingButton>
variant="contained"
onClick={() => handleCancelBan(member?.offender)}
>
{t('group:action.cancel_ban', {
postProcess: 'capitalize',
})}
</LoadingButton>
</Box>
</Popover>
<ListItemButton onClick={(event) => handlePopoverOpen(event, index)}>
<ListItemButton
onClick={(event) => handlePopoverOpen(event, index)}
>
<ListItemAvatar>
<Avatar
alt={member?.name}
src={member?.name ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${member?.name}/qortal_avatar?async=true` : ''}
src={
member?.name
? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${member?.name}/qortal_avatar?async=true`
: ''
}
/>
</ListItemAvatar>
<ListItemText primary={member?.name || member?.offender} />
@@ -178,8 +211,17 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
return (
<div>
<p>Ban list</p>
<div style={{ position: 'relative', height: '500px', width: '100%', display: 'flex', flexDirection: 'column', flexShrink: 1 }}>
<p>{t('group:ban_list', { postProcess: 'capitalize' })}</p>
<div
style={{
position: 'relative',
height: '500px',
width: '100%',
display: 'flex',
flexDirection: 'column',
flexShrink: 1,
}}
>
<AutoSizer>
{({ height, width }) => (
<List
@@ -196,4 +238,4 @@ export const ListOfBans = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
</div>
</div>
);
}
};

View File

@@ -4,7 +4,7 @@ import React, {
useEffect,
useRef,
useState,
} from "react";
} from 'react';
import {
Avatar,
Box,
@@ -16,53 +16,48 @@ import {
DialogContent,
DialogContentText,
DialogTitle,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemText,
MenuItem,
Popover,
Select,
TextField,
Typography,
} from "@mui/material";
import { getNameInfo } from "./Group";
import { getBaseApi, getFee } from "../../background";
import { LoadingButton } from "@mui/lab";
import LockIcon from "@mui/icons-material/Lock";
import NoEncryptionGmailerrorredIcon from "@mui/icons-material/NoEncryptionGmailerrorred";
useTheme,
} from '@mui/material';
import { LoadingButton } from '@mui/lab';
import LockIcon from '@mui/icons-material/Lock';
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
import {
MyContext,
getArbitraryEndpointReact,
getBaseApiReact,
isMobile,
} from "../../App";
import { Spacer } from "../../common/Spacer";
import { CustomLoader } from "../../common/CustomLoader";
import { RequestQueueWithPromise } from "../../utils/queue/queue";
import { useRecoilState } from "recoil";
} from '../../App';
import { Spacer } from '../../common/Spacer';
import { CustomLoader } from '../../common/CustomLoader';
import { RequestQueueWithPromise } from '../../utils/queue/queue';
import {
myGroupsWhereIAmAdminAtom,
promotionTimeIntervalAtom,
promotionsAtom,
} from "../../atoms/global";
import { Label } from "./AddGroup";
import ShortUniqueId from "short-unique-id";
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
import { getGroupNames } from "./UserListOfInvites";
import { WrapperUserAction } from "../WrapperUserAction";
import { useVirtualizer } from "@tanstack/react-virtual";
import ErrorBoundary from "../../common/ErrorBoundary";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
export const requestQueuePromos = new RequestQueueWithPromise(20);
txListAtom,
} from '../../atoms/global';
import { Label } from './AddGroup';
import ShortUniqueId from 'short-unique-id';
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
import { getGroupNames } from './UserListOfInvites';
import { useVirtualizer } from '@tanstack/react-virtual';
import ErrorBoundary from '../../common/ErrorBoundary';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import { getFee } from '../../background';
import { useAtom, useSetAtom } from 'jotai';
export const requestQueuePromos = new RequestQueueWithPromise(3);
export function utf8ToBase64(inputString: string): string {
// Encode the string as UTF-8
const utf8String = encodeURIComponent(inputString).replace(
/%([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
@@ -83,14 +78,15 @@ export const ListOfGroupPromotions = () => {
const [selectedGroup, setSelectedGroup] = useState(null);
const [loading, setLoading] = useState(false);
const [isShowModal, setIsShowModal] = useState(false);
const [text, setText] = useState("");
const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState(
const [text, setText] = useState('');
const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useAtom(
myGroupsWhereIAmAdminAtom
);
const [promotions, setPromotions] = useRecoilState(promotionsAtom);
const [promotionTimeInterval, setPromotionTimeInterval] = useRecoilState(
const [promotions, setPromotions] = useAtom(promotionsAtom);
const [promotionTimeInterval, setPromotionTimeInterval] = useAtom(
promotionTimeIntervalAtom
);
const [isExpanded, setIsExpanded] = React.useState(false);
const [openSnack, setOpenSnack] = useState(false);
@@ -98,8 +94,10 @@ export const ListOfGroupPromotions = () => {
const [fee, setFee] = useState(null);
const [isLoadingJoinGroup, setIsLoadingJoinGroup] = useState(false);
const [isLoadingPublish, setIsLoadingPublish] = useState(false);
const { show, setTxList } = useContext(MyContext);
const { show } = useContext(MyContext);
const setTxList = useSetAtom(txListAtom);
const theme = useTheme();
const listRef = useRef();
const rowVirtualizer = useVirtualizer({
count: promotions.length,
@@ -115,10 +113,12 @@ export const ListOfGroupPromotions = () => {
useEffect(() => {
try {
(async () => {
const feeRes = await getFee("ARBITRARY");
const feeRes = await getFee('ARBITRARY');
setFee(feeRes?.fee);
})();
} catch (error) {}
} catch (error) {
console.log(error);
}
}, []);
const getPromotions = useCallback(async () => {
try {
@@ -126,9 +126,9 @@ export const ListOfGroupPromotions = () => {
const identifier = `group-promotions-ui24-`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
@@ -142,7 +142,7 @@ export const ListOfGroupPromotions = () => {
promo.name
}/${promo.identifier}`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
});
try {
@@ -164,7 +164,7 @@ export const ListOfGroupPromotions = () => {
}
}
} catch (error) {
console.error("Error fetching promo:", error);
console.error('Error fetching promo:', error);
}
});
}
@@ -222,10 +222,10 @@ export const ListOfGroupPromotions = () => {
await new Promise((res, rej) => {
window
.sendMessage("publishOnQDN", {
.sendMessage('publishOnQDN', {
data: data,
identifier: identifier,
service: "DOCUMENT",
service: 'DOCUMENT',
})
.then((response) => {
if (!response?.error) {
@@ -235,23 +235,23 @@ export const ListOfGroupPromotions = () => {
rej(response.error);
})
.catch((error) => {
rej(error.message || "An error occurred");
rej(error.message || 'An error occurred');
});
});
}); // TODO translate
setInfoSnack({
type: "success",
type: 'success',
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);
setText("");
setText('');
setSelectedGroup(null);
setIsShowModal(false);
} catch (error) {
setInfoSnack({
type: "error",
type: 'error',
message:
error?.message || "Error publishing the promotion. Please try again",
error?.message || 'Error publishing the promotion. Please try again',
});
setOpenSnack(true);
} finally {
@@ -262,30 +262,30 @@ export const ListOfGroupPromotions = () => {
const handleJoinGroup = async (group, isOpen) => {
try {
const groupId = group.groupId;
const fee = await getFee("JOIN_GROUP");
const fee = await getFee('JOIN_GROUP');
await show({
message: "Would you like to perform an JOIN_GROUP transaction?",
publishFee: fee.fee + " QORT",
message: 'Would you like to perform an JOIN_GROUP transaction?',
publishFee: fee.fee + ' QORT',
});
setIsLoadingJoinGroup(true);
await new Promise((res, rej) => {
window
.sendMessage("joinGroup", {
.sendMessage('joinGroup', {
groupId,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
type: 'success',
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) {
setTxList((prev) => [
{
...response,
type: "joined-group",
type: 'joined-group',
label: `Joined Group ${group?.groupName}: awaiting confirmation`,
labelDone: `Joined Group ${group?.groupName}: success!`,
done: false,
@@ -297,7 +297,7 @@ export const ListOfGroupPromotions = () => {
setTxList((prev) => [
{
...response,
type: "joined-group-request",
type: 'joined-group-request',
label: `Requested to join Group ${group?.groupName}: awaiting confirmation`,
labelDone: `Requested to join Group ${group?.groupName}: success!`,
done: false,
@@ -313,7 +313,7 @@ export const ListOfGroupPromotions = () => {
return;
} else {
setInfoSnack({
type: "error",
type: 'error',
message: response?.error,
});
setOpenSnack(true);
@@ -322,8 +322,8 @@ export const ListOfGroupPromotions = () => {
})
.catch((error) => {
setInfoSnack({
type: "error",
message: error.message || "An error occurred",
type: 'error',
message: error.message || 'An error occurred',
});
setOpenSnack(true);
rej(error);
@@ -339,55 +339,59 @@ export const ListOfGroupPromotions = () => {
return (
<Box
sx={{
width: "100%",
display: "flex",
marginTop: "20px",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: '100%',
display: 'flex',
marginTop: '20px',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Box sx={{
display: 'flex',
gap: '20px',
width: '100%',
justifyContent: 'space-between'
}}>
<Box
sx={{
display: 'flex',
gap: '20px',
width: '100%',
justifyContent: 'space-between',
}}
>
<ButtonBase
sx={{
display: "flex",
flexDirection: "row",
padding: `0px ${isExpanded ? "24px" : "20px"}`,
gap: "10px",
justifyContent: "flex-start",
alignSelf: isExpanded && "flex-start",
display: 'flex',
flexDirection: 'row',
padding: `0px ${isExpanded ? '24px' : '20px'}`,
gap: '10px',
justifyContent: 'flex-start',
alignSelf: isExpanded && 'flex-start',
}}
onClick={() => setIsExpanded((prev) => !prev)}
>
<Typography
sx={{
fontSize: "1rem",
fontSize: '1rem',
}}
>
Group promotions {promotions.length > 0 && ` (${promotions.length})`}
Group promotions{' '}
{promotions.length > 0 && ` (${promotions.length})`}
</Typography>
{isExpanded ? (
<ExpandLessIcon
sx={{
marginLeft: "auto",
marginLeft: 'auto',
}}
/>
) : (
<ExpandMoreIcon
sx={{
marginLeft: "auto",
marginLeft: 'auto',
}}
/>
)}
</ButtonBase>
<Box
style={{
width: "330px",
width: '330px',
}}
/>
</Box>
@@ -396,57 +400,59 @@ export const ListOfGroupPromotions = () => {
<>
<Box
sx={{
width: isMobile ? "320px" : "750px",
maxWidth: "90%",
display: "flex",
flexDirection: "column",
padding: "0px 20px",
width: '750px',
maxWidth: '90%',
display: 'flex',
flexDirection: 'column',
padding: '0px 20px',
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
width: '100%',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Typography
sx={{
fontSize: "13px",
fontSize: '13px',
fontWeight: 600,
}}
></Typography>
<Button
variant="contained"
onClick={() => setIsShowModal(true)}
sx={{
fontSize: "12px",
fontSize: '12px',
}}
>
Add Promotion
</Button>
</Box>
<Spacer height="10px" />
</Box>
<Box
sx={{
width: isMobile ? "320px" : "750px",
maxWidth: "90%",
maxHeight: "700px",
display: "flex",
flexDirection: "column",
bgcolor: "background.paper",
padding: "20px 0px",
borderRadius: "19px",
bgcolor: 'background.paper',
borderRadius: '19px',
display: 'flex',
flexDirection: 'column',
maxHeight: '700px',
maxWidth: '90%',
padding: '20px 0px',
width: '750px',
}}
>
{loading && promotions.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
width: '100%',
display: 'flex',
justifyContent: 'center',
}}
>
<CustomLoader />
@@ -455,18 +461,18 @@ export const ListOfGroupPromotions = () => {
{!loading && promotions.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
>
<Typography
sx={{
fontSize: "11px",
fontSize: '11px',
fontWeight: 400,
color: "rgba(255, 255, 255, 0.2)",
color: 'rgba(255, 255, 255, 0.2)',
}}
>
Nothing to display
@@ -475,11 +481,11 @@ export const ListOfGroupPromotions = () => {
)}
<div
style={{
height: "600px",
position: "relative",
display: "flex",
flexDirection: "column",
width: "100%",
height: '600px',
position: 'relative',
display: 'flex',
flexDirection: 'column',
width: '100%',
}}
>
<div
@@ -487,24 +493,24 @@ export const ListOfGroupPromotions = () => {
className="scrollable-container"
style={{
flexGrow: 1,
overflow: "auto",
position: "relative",
display: "flex",
height: "0px",
overflow: 'auto',
position: 'relative',
display: 'flex',
height: '0px',
}}
>
<div
style={{
height: rowVirtualizer.getTotalSize(),
width: "100%",
width: '100%',
}}
>
<div
style={{
position: "absolute",
position: 'absolute',
top: 0,
left: 0,
width: "100%",
width: '100%',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
@@ -516,17 +522,17 @@ export const ListOfGroupPromotions = () => {
ref={rowVirtualizer.measureElement} //measure dynamic row height
key={promotion?.identifier}
style={{
position: "absolute",
position: 'absolute',
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
width: "100%", // Control width (90% of the parent)
padding: "10px 0",
display: "flex",
alignItems: "center",
overscrollBehavior: "none",
flexDirection: "column",
gap: "5px",
width: '100%', // Control width (90% of the parent)
padding: '10px 0',
display: 'flex',
alignItems: 'center',
overscrollBehavior: 'none',
flexDirection: 'column',
gap: '5px',
}}
>
<ErrorBoundary
@@ -538,75 +544,78 @@ export const ListOfGroupPromotions = () => {
>
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
padding: "0px 20px",
display: 'flex',
flexDirection: 'column',
width: '100%',
padding: '0px 20px',
}}
>
<Popover
open={openPopoverIndex === promotion?.groupId}
anchorEl={popoverAnchor}
onClose={(event, reason) => {
if (reason === "backdropClick") {
if (reason === 'backdropClick') {
// Prevent closing on backdrop click
return;
}
handlePopoverClose(); // Close only on other events like Esc key press
}}
anchorOrigin={{
vertical: "top",
horizontal: "center",
vertical: 'top',
horizontal: 'center',
}}
transformOrigin={{
vertical: "bottom",
horizontal: "center",
vertical: 'bottom',
horizontal: 'center',
}}
style={{ marginTop: "8px" }}
style={{ marginTop: '8px' }}
>
<Box
sx={{
width: "325px",
height: "auto",
maxHeight: "400px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
width: '325px',
height: 'auto',
maxHeight: '400px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
padding: '10px',
}}
>
<Typography
sx={{
fontSize: "13px",
fontSize: '13px',
fontWeight: 600,
}}
>
Group name: {` ${promotion?.groupName}`}
</Typography>
<Typography
sx={{
fontSize: "13px",
fontSize: '13px',
fontWeight: 600,
}}
>
Number of members:{" "}
Number of members:{' '}
{` ${promotion?.memberCount}`}
</Typography>
{promotion?.description && (
<Typography
sx={{
fontSize: "13px",
fontSize: '13px',
fontWeight: 600,
}}
>
{promotion?.description}
</Typography>
)}
{promotion?.isOpen === false && (
<Typography
sx={{
fontSize: "13px",
fontSize: '13px',
fontWeight: 600,
}}
>
@@ -615,14 +624,16 @@ export const ListOfGroupPromotions = () => {
your request
</Typography>
)}
<Spacer height="5px" />
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
width: "100%",
justifyContent: "center",
display: 'flex',
gap: '20px',
alignItems: 'center',
width: '100%',
justifyContent: 'center',
}}
>
<LoadingButton
@@ -633,6 +644,7 @@ export const ListOfGroupPromotions = () => {
>
Close
</LoadingButton>
<LoadingButton
loading={isLoadingJoinGroup}
loadingPosition="start"
@@ -652,23 +664,23 @@ export const ListOfGroupPromotions = () => {
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "15px",
display: 'flex',
alignItems: 'center',
gap: '15px',
}}
>
<Avatar
sx={{
backgroundColor: "#27282c",
color: "white",
backgroundColor: '#27282c',
color: theme.palette.text.primary,
}}
alt={promotion?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
@@ -677,75 +689,80 @@ export const ListOfGroupPromotions = () => {
>
{promotion?.name?.charAt(0)}
</Avatar>
<Typography
sx={{
fontWight: 600,
fontFamily: "Inter",
color: "cadetBlue",
fontFamily: 'Inter',
}}
>
{promotion?.name}
</Typography>
</Box>
<Typography
sx={{
fontWight: 600,
fontFamily: "Inter",
color: "cadetBlue",
fontFamily: 'Inter',
}}
>
{promotion?.groupName}
</Typography>
</Box>
<Spacer height="20px" />
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
display: 'flex',
gap: '20px',
alignItems: 'center',
}}
>
{promotion?.isOpen === false && (
<LockIcon
sx={{
color: "var(--green)",
color: theme.palette.other.positive,
}}
/>
)}
{promotion?.isOpen === true && (
<NoEncryptionGmailerrorredIcon
sx={{
color: "var(--danger)",
color: theme.palette.other.danger,
}}
/>
)}
<Typography
sx={{
fontSize: "15px",
fontSize: '15px',
fontWeight: 600,
}}
>
{promotion?.isOpen
? "Public group"
: "Private group"}
? 'Public group'
: 'Private group'}
</Typography>
</Box>
<Spacer height="20px" />
<Typography
sx={{
fontWight: 600,
fontFamily: "Inter",
color: "cadetBlue",
fontFamily: 'Inter',
}}
>
{promotion?.data}
</Typography>
<Spacer height="20px" />
<Box
sx={{
display: "flex",
justifyContent: "center",
width: "100%",
display: 'flex',
justifyContent: 'center',
width: '100%',
}}
>
<Button
@@ -754,14 +771,15 @@ export const ListOfGroupPromotions = () => {
handlePopoverOpen(event, promotion?.groupId)
}
sx={{
fontSize: "12px",
color: "white",
fontSize: '12px',
color: theme.palette.text.primary,
}}
>
Join Group: {` ${promotion?.groupName}`}
</Button>
</Box>
</Box>
<Spacer height="50px" />
</ErrorBoundary>
</div>
@@ -774,6 +792,7 @@ export const ListOfGroupPromotions = () => {
</Box>
</>
</Collapse>
<Spacer height="20px" />
{isShowModal && (
@@ -783,7 +802,7 @@ export const ListOfGroupPromotions = () => {
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Promote your group to non-members"}
{'Promote your group to non-members'}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
@@ -791,14 +810,14 @@ export const ListOfGroupPromotions = () => {
group.
</DialogContentText>
<DialogContentText id="alert-dialog-description2">
Max 200 characters. Publish Fee: {fee && fee} {" QORT"}
Max 200 characters. Publish Fee: {fee && fee} {' QORT'}
</DialogContentText>
<Spacer height="20px" />
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: "5px",
display: 'flex',
flexDirection: 'column',
gap: '5px',
}}
>
<Label>Select a group</Label>
@@ -832,11 +851,11 @@ export const ListOfGroupPromotions = () => {
}}
multiline={true}
sx={{
"& .MuiFormLabel-root": {
color: "white",
'& .MuiFormLabel-root': {
color: theme.palette.text.primary,
},
"& .MuiFormLabel-root.Mui-focused": {
color: "white",
'& .MuiFormLabel-root.Mui-focused': {
color: theme.palette.text.primary,
},
}}
/>

View File

@@ -1,16 +1,31 @@
import React, { useEffect, useRef, useState } from 'react';
import { Avatar, Box, Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover } from '@mui/material';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
import { useEffect, useRef, useState } from 'react';
import {
Avatar,
Box,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemText,
Popover,
} from '@mui/material';
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List,
} from 'react-virtualized';
import { getNameInfo } from './Group';
import { getBaseApi, getFee } from '../../background';
import { getFee } from '../../background';
import { LoadingButton } from '@mui/lab';
import { getBaseApiReact } from '../../App';
export const getMemberInvites = async (groupNumber) => {
const response = await fetch(`${getBaseApiReact()}/groups/invites/group/${groupNumber}?limit=0`);
const response = await fetch(
`${getBaseApiReact()}/groups/invites/group/${groupNumber}?limit=0`
);
const groupData = await response.json();
return groupData;
}
};
const getNames = async (listOfMembers, includeNoNames) => {
let members = [];
@@ -20,21 +35,26 @@ const getNames = async (listOfMembers, includeNoNames) => {
const name = await getNameInfo(member.invitee);
if (name) {
members.push({ ...member, name });
} else if(includeNoNames){
members.push({ ...member, name: name || "" });
} else if (includeNoNames) {
members.push({ ...member, name: name || '' });
}
}
}
}
return members;
}
};
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 50,
});
export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
export const ListOfInvites = ({
groupId,
setInfoSnack,
setOpenSnack,
show,
}) => {
const [invites, setInvites] = useState([]);
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
@@ -50,7 +70,7 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) =>
} catch (error) {
console.error(error);
}
}
};
useEffect(() => {
if (groupId) {
@@ -68,24 +88,27 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) =>
setOpenPopoverIndex(null);
};
const handleCancelInvitation = async (address)=> {
const handleCancelInvitation = async (address) => {
try {
const fee = await getFee('CANCEL_GROUP_INVITE')
// TODO translate
const fee = await getFee('CANCEL_GROUP_INVITE');
await show({
message: "Would you like to perform a CANCEL_GROUP_INVITE transaction?" ,
publishFee: fee.fee + ' QORT'
})
setIsLoadingCancelInvite(true)
await new Promise((res, rej)=> {
window.sendMessage("cancelInvitationToGroup", {
groupId,
qortalAddress: address,
})
message: 'Would you like to perform a CANCEL_GROUP_INVITE transaction?',
publishFee: fee.fee + ' QORT',
});
setIsLoadingCancelInvite(true);
await new Promise((res, rej) => {
window
.sendMessage('cancelInvitationToGroup', {
groupId,
qortalAddress: address,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message: "Successfully canceled invitation. It may take a couple of minutes for the changes to propagate",
type: 'success',
message:
'Successfully canceled invitation. It may take a couple of minutes for the changes to propagate',
});
setOpenSnack(true);
handlePopoverClose();
@@ -94,7 +117,7 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) =>
return;
}
setInfoSnack({
type: "error",
type: 'error',
message: response?.error,
});
setOpenSnack(true);
@@ -102,24 +125,22 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) =>
})
.catch((error) => {
setInfoSnack({
type: "error",
message: error.message || "An error occurred",
type: 'error',
message: error.message || 'An error occurred',
});
setOpenSnack(true);
rej(error);
});
})
});
} catch (error) {
} finally {
setIsLoadingCancelInvite(false)
setIsLoadingCancelInvite(false);
}
}
};
const rowRenderer = ({ index, key, parent, style }) => {
const member = invites[index];
return (
<CellMeasurer
key={key}
@@ -136,36 +157,47 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) =>
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
vertical: 'top',
horizontal: 'center',
}}
style={{ marginTop: "8px" }}
style={{ marginTop: '8px' }}
>
<Box
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
width: '325px',
height: '250px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
padding: '10px',
}}
>
<LoadingButton loading={isLoadingCancelInvite}
<LoadingButton
loading={isLoadingCancelInvite}
loadingPosition="start"
variant="contained" onClick={()=> handleCancelInvitation(member?.invitee)}>Cancel Invitation</LoadingButton>
variant="contained"
onClick={() => handleCancelInvitation(member?.invitee)}
>
Cancel Invitation
</LoadingButton>
</Box>
</Popover>
<ListItemButton onClick={(event) => handlePopoverOpen(event, index)}>
<ListItemButton
onClick={(event) => handlePopoverOpen(event, index)}
>
<ListItemAvatar>
<Avatar
alt={member?.name}
src={member?.name ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${member?.name}/qortal_avatar?async=true` : ''}
src={
member?.name
? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${member?.name}/qortal_avatar?async=true`
: ''
}
/>
</ListItemAvatar>
<ListItemText primary={member?.name || member?.invitee} />
@@ -180,7 +212,16 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) =>
return (
<div>
<p>Invitees list</p>
<div style={{ position: 'relative', height: '500px', width: '100%', display: 'flex', flexDirection: 'column', flexShrink: 1 }}>
<div
style={{
position: 'relative',
height: '500px',
width: '100%',
display: 'flex',
flexDirection: 'column',
flexShrink: 1,
}}
>
<AutoSizer>
{({ height, width }) => (
<List
@@ -197,4 +238,4 @@ export const ListOfInvites = ({ groupId, setInfoSnack, setOpenSnack, show }) =>
</div>
</div>
);
}
};

View File

@@ -1,16 +1,33 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Avatar, Box, Button, ListItem, ListItemAvatar, ListItemButton, ListItemText, Popover } from '@mui/material';
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
import { useEffect, useRef, useState } from 'react';
import {
Avatar,
Box,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemText,
Popover,
} from '@mui/material';
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List,
} from 'react-virtualized';
import { getNameInfo } from './Group';
import { getBaseApi, getFee } from '../../background';
import { LoadingButton } from '@mui/lab';
import { MyContext, getBaseApiReact } from '../../App';
import { getBaseApiReact } from '../../App';
import { txListAtom } from '../../atoms/global';
import { useAtom } from 'jotai';
export const getMemberInvites = async (groupNumber) => {
const response = await fetch(`${getBaseApiReact()}/groups/joinrequests/${groupNumber}?limit=0`);
const response = await fetch(
`${getBaseApiReact()}/groups/joinrequests/${groupNumber}?limit=0`
);
const groupData = await response.json();
return groupData;
}
};
const getNames = async (listOfMembers, includeNoNames) => {
let members = [];
@@ -19,24 +36,29 @@ const getNames = async (listOfMembers, includeNoNames) => {
if (member.joiner) {
const name = await getNameInfo(member.joiner);
if (name) {
members.push({ ...member, name: name || "" });
} else if(includeNoNames){
members.push({ ...member, name: name || "" });
members.push({ ...member, name: name || '' });
} else if (includeNoNames) {
members.push({ ...member, name: name || '' });
}
}
}
}
return members;
}
};
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 50,
});
export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show }) => {
export const ListOfJoinRequests = ({
groupId,
setInfoSnack,
setOpenSnack,
show,
}) => {
const [invites, setInvites] = useState([]);
const {txList, setTxList} = useContext(MyContext)
const [txList, setTxList] = useAtom(txListAtom);
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
@@ -51,7 +73,7 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show }
} catch (error) {
console.error(error);
}
}
};
useEffect(() => {
if (groupId) {
@@ -69,31 +91,33 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show }
setOpenPopoverIndex(null);
};
const handleAcceptJoinRequest = async (address)=> {
const handleAcceptJoinRequest = async (address) => {
try {
const fee = await getFee('GROUP_INVITE')
const fee = await getFee('GROUP_INVITE'); // TODO translate
await show({
message: "Would you like to perform a GROUP_INVITE transaction?" ,
publishFee: fee.fee + ' QORT'
})
setIsLoadingAccept(true)
await new Promise((res, rej)=> {
window.sendMessage("inviteToGroup", {
groupId,
qortalAddress: address,
inviteTime: 10800,
})
message: 'Would you like to perform a GROUP_INVITE transaction?',
publishFee: fee.fee + ' QORT',
});
setIsLoadingAccept(true);
await new Promise((res, rej) => {
window
.sendMessage('inviteToGroup', {
groupId,
qortalAddress: address,
inviteTime: 10800,
})
.then((response) => {
if (!response?.error) {
setIsLoadingAccept(false);
setInfoSnack({
type: "success",
message: "Successfully accepted join request. It may take a couple of minutes for the changes to propagate",
type: 'success',
message:
'Successfully accepted join request. It may take a couple of minutes for the changes to propagate',
});
setOpenSnack(true);
handlePopoverClose();
res(response);
setTxList((prev) => [
{
...response,
@@ -106,12 +130,12 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show }
},
...prev,
]);
return;
}
setInfoSnack({
type: "error",
type: 'error',
message: response?.error,
});
setOpenSnack(true);
@@ -119,25 +143,28 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show }
})
.catch((error) => {
setInfoSnack({
type: "error",
message: error?.message || "An error occurred",
type: 'error',
message: error?.message || 'An error occurred',
});
setOpenSnack(true);
rej(error);
});
})
});
} catch (error) {
} finally {
setIsLoadingAccept(false)
setIsLoadingAccept(false);
}
}
};
const rowRenderer = ({ index, key, parent, style }) => {
const member = invites[index];
const findJoinRequsetInTxList = txList?.find((tx)=> tx?.groupId === groupId && tx?.qortalAddress === member?.joiner && tx?.type === 'join-request-accept')
if(findJoinRequsetInTxList) return null
const findJoinRequsetInTxList = txList?.find(
(tx) =>
tx?.groupId === groupId &&
tx?.qortalAddress === member?.joiner &&
tx?.type === 'join-request-accept'
);
if (findJoinRequsetInTxList) return null;
return (
<CellMeasurer
key={key}
@@ -154,36 +181,47 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show }
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
vertical: 'top',
horizontal: 'center',
}}
style={{ marginTop: "8px" }}
style={{ marginTop: '8px' }}
>
<Box
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
width: '325px',
height: '250px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
padding: '10px',
}}
>
<LoadingButton loading={isLoadingAccept}
<LoadingButton
loading={isLoadingAccept}
loadingPosition="start"
variant="contained" onClick={()=> handleAcceptJoinRequest(member?.joiner)}>Accept</LoadingButton>
variant="contained"
onClick={() => handleAcceptJoinRequest(member?.joiner)}
>
Accept
</LoadingButton>
</Box>
</Popover>
<ListItemButton onClick={(event) => handlePopoverOpen(event, index)}>
<ListItemButton
onClick={(event) => handlePopoverOpen(event, index)}
>
<ListItemAvatar>
<Avatar
alt={member?.name}
src={member?.name ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${member?.name}/qortal_avatar?async=true` : ''}
src={
member?.name
? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${member?.name}/qortal_avatar?async=true`
: ''
}
/>
</ListItemAvatar>
<ListItemText primary={member?.name || member?.joiner} />
@@ -198,7 +236,16 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show }
return (
<div>
<p>Join request list</p>
<div style={{ position: 'relative', height: '500px', width: '100%', display: 'flex', flexDirection: 'column', flexShrink: 1 }}>
<div
style={{
position: 'relative',
height: '500px',
width: '100%',
display: 'flex',
flexDirection: 'column',
flexShrink: 1,
}}
>
<AutoSizer>
{({ height, width }) => (
<List
@@ -215,4 +262,4 @@ export const ListOfJoinRequests = ({ groupId, setInfoSnack, setOpenSnack, show }
</div>
</div>
);
}
};

View File

@@ -1,29 +1,30 @@
import {
Avatar,
Box,
Button,
ListItem,
ListItemAvatar,
ListItemButton,
ListItemText,
Popover,
Typography,
} from "@mui/material";
import React, { useRef, useState } from "react";
useTheme,
} from '@mui/material';
import { useRef, useState } from 'react';
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List,
} from "react-virtualized";
import { LoadingButton } from "@mui/lab";
import { getBaseApi, getFee } from "../../background";
import { getBaseApiReact } from "../../App";
} from 'react-virtualized';
import { LoadingButton } from '@mui/lab';
import { getFee } from '../../background';
import { getBaseApiReact } from '../../App';
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 50,
});
const ListOfMembers = ({
members,
groupId,
@@ -39,8 +40,7 @@ const ListOfMembers = ({
const [isLoadingBan, setIsLoadingBan] = useState(false);
const [isLoadingMakeAdmin, setIsLoadingMakeAdmin] = useState(false);
const [isLoadingRemoveAdmin, setIsLoadingRemoveAdmin] = useState(false);
const theme = useTheme();
const listRef = useRef();
const handlePopoverOpen = (event, index) => {
@@ -55,23 +55,25 @@ const ListOfMembers = ({
const handleKick = async (address) => {
try {
const fee = await getFee("GROUP_KICK");
const fee = await getFee('GROUP_KICK');
await show({
message: "Would you like to perform a GROUP_KICK transaction?",
publishFee: fee.fee + " QORT",
message: 'Would you like to perform a GROUP_KICK transaction?',
publishFee: fee.fee + ' QORT',
});
setIsLoadingKick(true);
new Promise((res, rej) => {
window.sendMessage("kickFromGroup", {
groupId,
qortalAddress: address,
})
window
.sendMessage('kickFromGroup', {
groupId,
qortalAddress: address,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message: "Successfully kicked member from group. It may take a couple of minutes for the changes to propagate",
type: 'success',
message:
'Successfully kicked member from group. It may take a couple of minutes for the changes to propagate',
});
setOpenSnack(true);
handlePopoverClose();
@@ -79,7 +81,7 @@ const ListOfMembers = ({
return;
}
setInfoSnack({
type: "error",
type: 'error',
message: response?.error,
});
setOpenSnack(true);
@@ -87,38 +89,40 @@ const ListOfMembers = ({
})
.catch((error) => {
setInfoSnack({
type: "error",
message: error.message || "An error occurred",
type: 'error',
message: error.message || 'An error occurred',
});
setOpenSnack(true);
rej(error);
});
});
} catch (error) {
console.log(error);
} finally {
setIsLoadingKick(false);
}
};
const handleBan = async (address) => {
try {
const fee = await getFee("GROUP_BAN");
const fee = await getFee('GROUP_BAN'); // TODO translate
await show({
message: "Would you like to perform a GROUP_BAN transaction?",
publishFee: fee.fee + " QORT",
message: 'Would you like to perform a GROUP_BAN transaction?',
publishFee: fee.fee + ' QORT',
});
setIsLoadingBan(true);
await new Promise((res, rej) => {
window.sendMessage("banFromGroup", {
groupId,
qortalAddress: address,
rBanTime: 0,
})
window
.sendMessage('banFromGroup', {
groupId,
qortalAddress: address,
rBanTime: 0,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message: "Successfully banned member from group. It may take a couple of minutes for the changes to propagate",
type: 'success',
message:
'Successfully banned member from group. It may take a couple of minutes for the changes to propagate',
});
setOpenSnack(true);
handlePopoverClose();
@@ -126,7 +130,7 @@ const ListOfMembers = ({
return;
}
setInfoSnack({
type: "error",
type: 'error',
message: response?.error,
});
setOpenSnack(true);
@@ -134,13 +138,12 @@ const ListOfMembers = ({
})
.catch((error) => {
setInfoSnack({
type: "error",
message: error.message || "An error occurred",
type: 'error',
message: error.message || 'An error occurred',
});
setOpenSnack(true);
rej(error);
});
});
} catch (error) {
} finally {
@@ -150,22 +153,24 @@ const ListOfMembers = ({
const makeAdmin = async (address) => {
try {
const fee = await getFee("ADD_GROUP_ADMIN");
const fee = await getFee('ADD_GROUP_ADMIN');
await show({
message: "Would you like to perform a ADD_GROUP_ADMIN transaction?",
publishFee: fee.fee + " QORT",
message: 'Would you like to perform a ADD_GROUP_ADMIN transaction?',
publishFee: fee.fee + ' QORT',
});
setIsLoadingMakeAdmin(true);
await new Promise((res, rej) => {
window.sendMessage("makeAdmin", {
groupId,
qortalAddress: address,
})
window
.sendMessage('makeAdmin', {
groupId,
qortalAddress: address,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message: "Successfully made member an admin. It may take a couple of minutes for the changes to propagate",
type: 'success',
message:
'Successfully made member an admin. It may take a couple of minutes for the changes to propagate',
});
setOpenSnack(true);
handlePopoverClose();
@@ -173,7 +178,7 @@ const ListOfMembers = ({
return;
}
setInfoSnack({
type: "error",
type: 'error',
message: response?.error,
});
setOpenSnack(true);
@@ -181,13 +186,12 @@ const ListOfMembers = ({
})
.catch((error) => {
setInfoSnack({
type: "error",
message: error.message || "An error occurred",
type: 'error',
message: error.message || 'An error occurred',
});
setOpenSnack(true);
rej(error);
});
});
} catch (error) {
} finally {
@@ -197,22 +201,24 @@ const ListOfMembers = ({
const removeAdmin = async (address) => {
try {
const fee = await getFee("REMOVE_GROUP_ADMIN");
const fee = await getFee('REMOVE_GROUP_ADMIN');
await show({
message: "Would you like to perform a REMOVE_GROUP_ADMIN transaction?",
publishFee: fee.fee + " QORT",
message: 'Would you like to perform a REMOVE_GROUP_ADMIN transaction?',
publishFee: fee.fee + ' QORT',
});
setIsLoadingRemoveAdmin(true);
await new Promise((res, rej) => {
window.sendMessage("removeAdmin", {
groupId,
qortalAddress: address,
})
window
.sendMessage('removeAdmin', {
groupId,
qortalAddress: address,
})
.then((response) => {
if (!response?.error) {
setInfoSnack({
type: "success",
message: "Successfully removed member as an admin. It may take a couple of minutes for the changes to propagate",
type: 'success',
message:
'Successfully removed member as an admin. It may take a couple of minutes for the changes to propagate',
});
setOpenSnack(true);
handlePopoverClose();
@@ -220,7 +226,7 @@ const ListOfMembers = ({
return;
}
setInfoSnack({
type: "error",
type: 'error',
message: response?.error,
});
setOpenSnack(true);
@@ -228,13 +234,12 @@ const ListOfMembers = ({
})
.catch((error) => {
setInfoSnack({
type: "error",
message: error.message || "An error occurred",
type: 'error',
message: error.message || 'An error occurred',
});
setOpenSnack(true);
rej(error);
});
});
} catch (error) {
} finally {
@@ -260,24 +265,24 @@ const ListOfMembers = ({
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
vertical: 'top',
horizontal: 'center',
}}
style={{ marginTop: "8px" }}
style={{ marginTop: '8px' }}
>
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
width: '325px',
height: '250px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
padding: '10px',
}}
>
{isOwner && (
@@ -336,21 +341,28 @@ const ListOfMembers = ({
<ListItemAvatar>
<Avatar
alt={member?.name || member?.member}
src={member?.name ? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${member?.name}/qortal_avatar?async=true` : ''}
src={
member?.name
? `${getBaseApiReact()}/arbitrary/THUMBNAIL/${member?.name}/qortal_avatar?async=true`
: ''
}
/>
</ListItemAvatar>
<ListItemText
id={""}
id={''}
primary={member?.name || member?.member}
/>
{member?.isAdmin && (
<Typography sx={{
color: 'white',
marginLeft: 'auto'
}}>Admin</Typography>
)}
<Typography
sx={{
color: theme.palette.text.primary,
marginLeft: 'auto',
}}
>
Admin
</Typography>
)}
</ListItemButton>
</ListItem>
</div>
)}
@@ -363,11 +375,11 @@ const ListOfMembers = ({
<p>Member list</p>
<div
style={{
position: "relative",
height: "500px",
width: "100%",
display: "flex",
flexDirection: "column",
position: 'relative',
height: '500px',
width: '100%',
display: 'flex',
flexDirection: 'column',
flexShrink: 1,
}}
>

View File

@@ -1,21 +1,14 @@
import * as React from "react";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Checkbox from "@mui/material/Checkbox";
import IconButton from "@mui/material/IconButton";
import CommentIcon from "@mui/icons-material/Comment";
import InfoIcon from "@mui/icons-material/Info";
import GroupAddIcon from "@mui/icons-material/GroupAdd";
import { executeEvent } from "../../utils/events";
import { Box, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer";
import { getGroupNames } from "./UserListOfInvites";
import { CustomLoader } from "../../common/CustomLoader";
import VisibilityIcon from "@mui/icons-material/Visibility";
import { isMobile } from "../../App";
import * as React from 'react';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import IconButton from '@mui/material/IconButton';
import { executeEvent } from '../../utils/events';
import { Box, Typography } from '@mui/material';
import { Spacer } from '../../common/Spacer';
import { CustomLoader } from '../../common/CustomLoader';
import VisibilityIcon from '@mui/icons-material/Visibility';
export const ListOfThreadPostsWatched = () => {
const [posts, setPosts] = React.useState([]);
@@ -24,33 +17,33 @@ export const ListOfThreadPostsWatched = () => {
const getPosts = async () => {
try {
await new Promise((res, rej) => {
window.sendMessage("getThreadActivity", {})
.then((response) => {
if (!response?.error) {
if (!response) {
res(null);
window
.sendMessage('getThreadActivity', {})
.then((response) => {
if (!response?.error) {
if (!response) {
res(null);
return;
}
const uniquePosts = response.reduce((acc, current) => {
const x = acc.find(
(item) => item?.thread?.threadId === current?.thread?.threadId
);
if (!x) {
return acc.concat([current]);
} else {
return acc;
}
}, []);
setPosts(uniquePosts);
res(uniquePosts);
return;
}
const uniquePosts = response.reduce((acc, current) => {
const x = acc.find(
(item) => item?.thread?.threadId === current?.thread?.threadId
);
if (!x) {
return acc.concat([current]);
} else {
return acc;
}
}, []);
setPosts(uniquePosts);
res(uniquePosts);
return;
}
rej(response.error);
})
.catch((error) => {
rej(error.message || "An error occurred");
});
rej(response.error);
})
.catch((error) => {
rej(error.message || 'An error occurred'); // TODO translate
});
});
} catch (error) {
} finally {
@@ -63,49 +56,50 @@ export const ListOfThreadPostsWatched = () => {
}, []);
return (
<Box sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: 'center'
}}>
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Box
sx={{
width: "322px",
display: "flex",
flexDirection: "column",
width: '322px',
display: 'flex',
flexDirection: 'column',
padding: '0px 20px',
}}
>
<Typography
sx={{
fontSize: "13px",
fontSize: '13px',
fontWeight: 600,
}}
>
New Thread Posts:
</Typography>
<Spacer height="10px" />
<Spacer height="10px" />
</Box>
<Box
sx={{
width: "322px",
height: isMobile ? "165px" : "250px",
display: "flex",
flexDirection: "column",
bgcolor: "background.paper",
padding: "20px",
borderRadius: '19px'
bgcolor: 'background.paper',
borderRadius: '19px',
display: 'flex',
flexDirection: 'column',
height: '250px',
padding: '20px',
width: '322px',
}}
>
{loading && posts.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
width: '100%',
display: 'flex',
justifyContent: 'center',
}}
>
<CustomLoader />
@@ -114,19 +108,18 @@ export const ListOfThreadPostsWatched = () => {
{!loading && posts.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
>
<Typography
sx={{
fontSize: "11px",
fontSize: '11px',
fontWeight: 400,
color: 'rgba(255, 255, 255, 0.2)'
color: 'rgba(255, 255, 255, 0.2)',
}}
>
Nothing to display
@@ -134,47 +127,46 @@ export const ListOfThreadPostsWatched = () => {
</Box>
)}
{posts?.length > 0 && (
<List
className="scrollable-container"
sx={{
width: "100%",
maxWidth: 360,
bgcolor: "background.paper",
maxHeight: "300px",
overflow: "auto",
}}
>
{posts?.map((post) => {
return (
<ListItem
key={post?.thread?.threadId}
onClick={() => {
executeEvent("openThreadNewPost", {
data: post,
});
}}
disablePadding
secondaryAction={
<IconButton edge="end" aria-label="comments">
<VisibilityIcon
sx={{
color: "red",
}}
/>
</IconButton>
}
>
<ListItemButton disableRipple role={undefined} dense>
<ListItemText
primary={`New post in ${post?.thread?.threadData?.title}`}
/>
</ListItemButton>
</ListItem>
);
})}
</List>
<List
className="scrollable-container"
sx={{
width: '100%',
maxWidth: 360,
bgcolor: 'background.paper',
maxHeight: '300px',
overflow: 'auto',
}}
>
{posts?.map((post) => {
return (
<ListItem
key={post?.thread?.threadId}
onClick={() => {
executeEvent('openThreadNewPost', {
data: post,
});
}}
disablePadding
secondaryAction={
<IconButton edge="end" aria-label="comments">
<VisibilityIcon
sx={{
color: 'red',
}}
/>
</IconButton>
}
>
<ListItemButton disableRipple role={undefined} dense>
<ListItemText
primary={`New post in ${post?.thread?.threadData?.title}`}
/>
</ListItemButton>
</ListItem>
);
})}
</List>
)}
</Box>
</Box>
);

View File

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

View File

@@ -1,279 +1,304 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import moment from 'moment'
import { Box, ButtonBase, Collapse, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer";
import { getBaseApiReact, isMobile } from "../../App";
import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
import { useCallback, useEffect, useMemo, useState } from 'react';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import moment from 'moment';
import { Box, ButtonBase, Collapse, Typography, useTheme } from '@mui/material';
import { getBaseApiReact } from '../../App';
import MailIcon from '@mui/icons-material/Mail';
import MailOutlineIcon from '@mui/icons-material/MailOutline';
import { executeEvent } from '../../utils/events';
import { CustomLoader } from '../../common/CustomLoader';
import { useRecoilState } from 'recoil';
import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import MarkEmailUnreadIcon from '@mui/icons-material/MarkEmailUnread';
import { useAtom } from 'jotai';
export const isLessThanOneWeekOld = (timestamp) => {
// Current time in milliseconds
const now = Date.now();
// 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
return timestamp > oneWeekAgo;
};
export function formatEmailDate(timestamp: number) {
const date = moment(timestamp);
const now = moment();
const date = moment(timestamp);
const now = moment();
if (date.isSame(now, 'day')) {
// If the email was received today, show the time
return date.format('h:mm A');
} else if (date.isSame(now, 'year')) {
// If the email was received this year, show the month and day
return date.format('MMM D');
} else {
// For older emails, show the full date
return date.format('MMM D, YYYY');
}
if (date.isSame(now, 'day')) {
// If the email was received today, show the time
return date.format('h:mm A');
} else if (date.isSame(now, 'year')) {
// If the email was received this year, show the month and day
return date.format('MMM D');
} else {
// For older emails, show the full date
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 () => {
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();
setMails(mailData);
} catch (error) {
console.error(error);
} finally {
setLoading(false)
export const QMailMessages = ({ userName, userAddress }) => {
const [isExpanded, setIsExpanded] = useState(false);
const [mails, setMails] = useAtom(mailsAtom);
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useAtom(
qMailLastEnteredTimestampAtom
);
}
}, [])
const [loading, setLoading] = useState(true);
const theme = useTheme();
const getTimestamp = async () => {
try {
return new Promise((res, rej) => {
window.sendMessage("getEnteredQmailTimestamp")
.then((response) => {
if (!response?.error) {
if(response?.timestamp){
setLastEnteredTimestamp(response?.timestamp)
}
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();
setMails(mailData);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}, []);
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);
})
.catch((error) => {
rej(error.message || "An error occurred");
});
}
rej(response.error);
})
.catch((error) => {
rej(error.message || 'An error occurred'); // TODO translate
});
} catch (error) {}
};
useEffect(() => {
getTimestamp()
if(!userName || !userAddress) return
getMails();
});
} catch (error) {
console.log(error);
}
};
const interval = setInterval(() => {
getTimestamp()
getMails();
}, 300000);
return () => clearInterval(interval);
}, [getMails, userName, userAddress]);
useEffect(() => {
getTimestamp();
if (!userName || !userAddress) return;
getMails();
const anyUnread = useMemo(()=> {
let unread = false
mails.forEach((mail)=> {
if(!lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created) || (lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created) && lastEnteredTimestamp < mail?.created)){
unread = true
}
})
return unread
}, [mails, lastEnteredTimestamp])
const interval = setInterval(() => {
getTimestamp();
getMails();
}, 300000);
return () => clearInterval(interval);
}, [getMails, userName, userAddress]);
const anyUnread = useMemo(() => {
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 (
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<ButtonBase
sx={{
width: "322px",
display: "flex",
flexDirection: "row",
gap: '10px',
padding: "0px 20px",
justifyContent: 'flex-start'
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
onClick={()=> setIsExpanded((prev)=> !prev)}
>
<Typography
<ButtonBase
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>
<MarkEmailUnreadIcon sx={{
color: anyUnread ? 'var(--unread)' : 'white'
}}/>
{isExpanded ? <ExpandLessIcon sx={{
marginLeft: 'auto'
}} /> : (
<ExpandMoreIcon sx={{
color: anyUnread ? 'var(--unread)' : 'white',
marginLeft: 'auto'
}} />
)}
</ButtonBase>
<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
<Typography
sx={{
fontSize: '1rem',
}}
>
Latest Q-Mails
</Typography>
<MarkEmailUnreadIcon
sx={{
color: anyUnread
? theme.palette.other.unread
: theme.palette.text.primary,
}}
/>
{isExpanded ? (
<ExpandLessIcon
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
marginLeft: 'auto',
}}
>
<CustomLoader />
</Box>
/>
) : (
<ExpandMoreIcon
sx={{
color: anyUnread
? theme.palette.other.unread
: theme.palette.text.primary,
marginLeft: 'auto',
}}
/>
)}
{!loading && mails.length === 0 && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: 'center',
height: '100%',
}}
>
<Typography
</ButtonBase>
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
<Box
className="scrollable-container"
sx={{
bgcolor: theme.palette.background.paper,
borderRadius: '19px',
display: 'flex',
flexDirection: 'column',
height: '250px',
overflow: 'auto',
padding: '20px',
width: '322px',
}}
>
{loading && mails.length === 0 && (
<Box
sx={{
fontSize: "11px",
fontWeight: 400,
color: 'rgba(255, 255, 255, 0.2)'
width: '100%',
display: 'flex',
justifyContent: 'center',
}}
>
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())
<CustomLoader />
</Box>
)}
{!loading && mails.length === 0 && (
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
>
<Typography
sx={{
fontSize: '11px',
fontWeight: 400,
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={{
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
sx={{
"& .MuiTypography-root": {
fontSize: "13px",
fontWeight: 400,
},
}}
primary={`From: ${mail?.name}`}
secondary={`${formatEmailDate(mail?.created)}`}
/>
<ListItemIcon
sx={{
justifyContent: "flex-end",
}}
>
{!lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created) ? (
<MailIcon sx={{
color: 'var(--unread)'
}} />
) : !lastEnteredTimestamp ? (
<MailOutlineIcon sx={{
color: 'white'
}} />
): (lastEnteredTimestamp < mail?.created) && isLessThanOneWeekOld(mail?.created) ? (
<MailIcon sx={{
color: 'var(--unread)'
}} />
) : (
<MailOutlineIcon sx={{
color: 'white'
}} />
)
}
</ListItemIcon>
</ListItemButton>
</ListItem>
)
{!lastEnteredTimestamp &&
isLessThanOneWeekOld(mail?.created) ? (
<MailIcon
sx={{
color: theme.palette.other.unread,
}}
/>
) : !lastEnteredTimestamp ? (
<MailOutlineIcon
sx={{
color: theme.palette.text.primary,
}}
/>
) : lastEnteredTimestamp < mail?.created &&
isLessThanOneWeekOld(mail?.created) ? (
<MailIcon
sx={{
color: theme.palette.other.unread,
}}
/>
) : (
<MailOutlineIcon
sx={{
color: theme.palette.text.primary,
}}
/>
)}
</ListItemIcon>
</ListItemButton>
</ListItem>
);
})}
</List>
</List>
</Box>
</Collapse>
</Box>
</Collapse>
</Box>
)
}
);
};

View File

@@ -1,39 +1,25 @@
import * as React from "react";
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import ListItemText from "@mui/material/ListItemText";
import ListItemButton from "@mui/material/ListItemButton";
import List from "@mui/material/List";
import Divider from "@mui/material/Divider";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import CloseIcon from "@mui/icons-material/Close";
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";
import {
ChangeEvent,
forwardRef,
Fragment,
ReactElement,
Ref,
useEffect,
useState,
} from 'react';
import Dialog from '@mui/material/Dialog';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import CloseIcon from '@mui/icons-material/Close';
import Slide from '@mui/material/Slide';
import { TransitionProps } from '@mui/material/transitions';
import { Box, FormControlLabel, Switch, styled, useTheme } from '@mui/material';
import { enabledDevModeAtom } from '../../atoms/global';
function a11yProps(index: number) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`,
};
}
import ThemeManager from '../Theme/ThemeManager';
import { useAtom } from 'jotai';
const LocalNodeSwitch = styled(Switch)(({ theme }) => ({
padding: 8,
@@ -49,13 +35,13 @@ const LocalNodeSwitch = styled(Switch)(({ theme }) => ({
},
'&::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(
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>')`,
left: 12,
},
'&::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(
theme.palette.getContrastText(theme.palette.primary.main),
theme.palette.getContrastText(theme.palette.primary.main)
)}" d="M19,13H5V11H19V13Z" /></svg>')`,
right: 12,
},
@@ -68,44 +54,43 @@ const LocalNodeSwitch = styled(Switch)(({ theme }) => ({
},
}));
const Transition = React.forwardRef(function Transition(
const Transition = forwardRef(function Transition(
props: TransitionProps & {
children: React.ReactElement;
children: ReactElement;
},
ref: React.Ref<unknown>
ref: Ref<unknown>
) {
return <Slide direction="up" ref={ref} {...props} />;
});
export const Settings = ({
address,
open,
setOpen,
}) => {
const [checked, setChecked] = React.useState(false);
const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom)
export const Settings = ({ address, open, setOpen }) => {
const [checked, setChecked] = useState(false);
const [isEnabledDevMode, setIsEnabledDevMode] = useAtom(enabledDevModeAtom);
const theme = useTheme();
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked);
window.sendMessage("addUserSettings", {
keyValue: {
key: 'disable-push-notifications',
value: event.target.checked,
},
})
window
.sendMessage('addUserSettings', {
keyValue: {
key: 'disable-push-notifications',
value: event.target.checked,
},
})
.then((response) => {
if (response?.error) {
console.error("Error adding user settings:", response.error);
console.error('Error adding user settings:', response.error);
} else {
console.log("User settings added successfully");
console.log('User settings added successfully'); // TODO translate
}
})
.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 = () => {
@@ -115,9 +100,10 @@ export const Settings = ({
const getUserSettings = async () => {
try {
return new Promise((res, rej) => {
window.sendMessage("getUserSettings", {
key: "disable-push-notifications",
})
window
.sendMessage('getUserSettings', {
key: 'disable-push-notifications',
})
.then((response) => {
if (!response?.error) {
setChecked(response || false);
@@ -127,34 +113,32 @@ export const Settings = ({
rej(response.error);
})
.catch((error) => {
rej(error.message || "An error occurred");
rej(error.message || 'An error occurred');
});
});
} catch (error) {
console.log("error", error);
console.log('error', error);
}
};
React.useEffect(() => {
useEffect(() => {
getUserSettings();
}, []);
return (
<React.Fragment>
<Fragment>
<Dialog
fullScreen
open={open}
onClose={handleClose}
TransitionComponent={Transition}
>
<AppBar sx={{ position: "relative", bgcolor: "#232428" }}>
<AppBar sx={{ position: 'relative' }}>
<Toolbar>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
<Typography sx={{ ml: 2, flex: 1 }} variant="h4" component="div">
General Settings
</Typography>
<IconButton
edge="start"
color="inherit"
@@ -165,21 +149,21 @@ export const Settings = ({
</IconButton>
</Toolbar>
</AppBar>
<Box
sx={{
bgcolor: "#27282c",
flexGrow: 1,
overflowY: "auto",
color: "white",
padding: "20px",
overflowY: 'auto',
color: theme.palette.text.primary,
padding: '20px',
flexDirection: 'column',
display: 'flex',
gap: '20px'
gap: '20px',
}}
>
<FormControlLabel
sx={{
color: "white",
color: theme.palette.text.primary,
}}
control={
<LocalNodeSwitch checked={checked} onChange={handleChange} />
@@ -188,20 +172,24 @@ export const Settings = ({
/>
{window?.electronAPI && (
<FormControlLabel
sx={{
color: "white",
}}
control={
<LocalNodeSwitch checked={isEnabledDevMode} onChange={(e)=> {
setIsEnabledDevMode(e.target.checked)
localStorage.setItem('isEnabledDevMode', JSON.stringify(e.target.checked))
}} />
}
label="Enable dev mode"
/>
control={
<LocalNodeSwitch
checked={isEnabledDevMode}
onChange={(e) => {
setIsEnabledDevMode(e.target.checked);
localStorage.setItem(
'isEnabledDevMode',
JSON.stringify(e.target.checked)
);
}}
/>
}
label="Enable dev mode"
/>
)}
<ThemeManager />
</Box>
</Dialog>
</React.Fragment>
</Fragment>
);
};

View File

@@ -1,28 +1,26 @@
import * as React from "react";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import Checkbox from "@mui/material/Checkbox";
import IconButton from "@mui/material/IconButton";
import CommentIcon from "@mui/icons-material/Comment";
import InfoIcon from "@mui/icons-material/Info";
import { Box, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer";
import { isMobile } from "../../App";
import { QMailMessages } from "./QMailMessages";
import { executeEvent } from "../../utils/events";
import * as React from 'react';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { Box, Typography, useTheme } from '@mui/material';
import { Spacer } from '../../common/Spacer';
import { QMailMessages } from './QMailMessages';
import { executeEvent } from '../../utils/events';
import { useTranslation } from 'react-i18next';
export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInfo }) => {
export const ThingsToDoInitial = ({
myAddress,
name,
hasGroups,
balance,
userInfo,
}) => {
const [checked1, setChecked1] = React.useState(false);
const [checked2, setChecked2] = React.useState(false);
// const [checked3, setChecked3] = React.useState(false);
// React.useEffect(() => {
// if (hasGroups) setChecked3(true);
// }, [hasGroups]);
const { t } = useTranslation(['core', 'tutorial']);
const theme = useTheme();
React.useEffect(() => {
if (balance && +balance >= 6) {
@@ -30,201 +28,171 @@ export const ThingsToDoInitial = ({ myAddress, name, hasGroups, balance, userInf
}
}, [balance]);
React.useEffect(() => {
if (name) setChecked2(true);
}, [name]);
const isLoaded = React.useMemo(() => {
if (userInfo !== null) return true;
return false;
}, [userInfo]);
const isLoaded = React.useMemo(()=> {
if(userInfo !== null) return true
return false
}, [ userInfo])
const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(() => {
if (isLoaded && checked1 && checked2) return true;
return false;
}, [checked1, isLoaded, checked2]);
const hasDoneNameAndBalanceAndIsLoaded = React.useMemo(()=> {
if(isLoaded && checked1 && checked2) return true
return false
}, [checked1, isLoaded, checked2])
if(hasDoneNameAndBalanceAndIsLoaded){
return (
<QMailMessages userAddress={userInfo?.address} userName={userInfo?.name} />
);
}
if(!isLoaded) return null
if (hasDoneNameAndBalanceAndIsLoaded) {
return (
<QMailMessages
userAddress={userInfo?.address}
userName={userInfo?.name}
/>
);
}
if (!isLoaded) return null;
return (
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Box
sx={{
width: "322px",
display: "flex",
flexDirection: "column",
padding: "0px 20px",
width: '322px',
display: 'flex',
flexDirection: 'column',
padding: '0px 20px',
}}
>
<Typography
sx={{
fontSize: "1rem",
fontSize: '1rem',
fontWeight: 600,
}}
>
{!isLoaded ? 'Loading...' : 'Getting Started' }
{!isLoaded
? t('core:loading', { postProcess: 'capitalize' })
: t('tutorial:initial.getting_started', {
postProcess: 'capitalize',
})}
</Typography>
<Spacer height="10px" />
</Box>
<Box
sx={{
width: "322px",
display: "flex",
flexDirection: "column",
bgcolor: "background.paper",
padding: "20px",
borderRadius: "19px",
bgcolor: theme.palette.background.paper,
borderRadius: '19px',
display: 'flex',
flexDirection: 'column',
padding: '20px',
width: '322px',
}}
>
{isLoaded && (
<List sx={{ width: "100%", maxWidth: 360 }}>
<ListItem
disablePadding
sx={{
marginBottom: '20px'
}}
>
<ListItemButton
sx={{
padding: "0px",
}}
disableRipple
role={undefined}
dense
onClick={()=> {
executeEvent("openBuyQortInfo", {})
}}
>
<ListItemText
sx={{
"& .MuiTypography-root": {
fontSize: "1rem",
fontWeight: 400,
},
}}
primary={`Have at least 6 QORT in your wallet`}
/>
<ListItemIcon
sx={{
justifyContent: "flex-end",
}}
>
<Box
sx={{
height: "18px",
width: "18px",
borderRadius: "50%",
backgroundColor: checked1 ? "rgba(9, 182, 232, 1)" : "transparent",
outline: "1px solid rgba(9, 182, 232, 1)",
}}
/>
{/* <Checkbox
edge="start"
checked={checked1}
tabIndex={-1}
disableRipple
disabled={true}
sx={{
"&.Mui-checked": {
color: "white", // Customize the color when checked
},
"& .MuiSvgIcon-root": {
color: "white",
},
}}
/> */}
</ListItemIcon>
</ListItemButton>
</ListItem>
<ListItem
sx={{
marginBottom: '20px'
}}
// secondaryAction={
// <IconButton edge="end" aria-label="comments">
// <InfoIcon
// sx={{
// color: "white",
// }}
// />
// </IconButton>
// }
disablePadding
>
<ListItemButton sx={{
padding: "0px",
}} disableRipple role={undefined} dense>
<ListItemText onClick={() => {
executeEvent('openRegisterName', {})
}} sx={{
"& .MuiTypography-root": {
fontSize: "1rem",
fontWeight: 400,
},
}} primary={`Register a name`} />
<ListItemIcon sx={{
justifyContent: "flex-end",
}}>
<Box
sx={{
height: "18px",
width: "18px",
borderRadius: "50%",
backgroundColor: checked2 ? "rgba(9, 182, 232, 1)" : "transparent",
outline: "1px solid rgba(9, 182, 232, 1)",
}}
/>
</ListItemIcon>
</ListItemButton>
</ListItem>
{/* <ListItem
disablePadding
>
<ListItemButton sx={{
padding: "0px",
}} disableRipple role={undefined} dense>
<ListItemText sx={{
"& .MuiTypography-root": {
fontSize: "13px",
fontWeight: 400,
},
}} primary={`Join a group`} />
<ListItemIcon sx={{
justifyContent: "flex-end",
}}>
<Box
sx={{
height: "18px",
width: "18px",
borderRadius: "50%",
backgroundColor: checked3 ? "rgba(9, 182, 232, 1)" : "transparent",
outline: "1px solid rgba(9, 182, 232, 1)",
}}
/>
</ListItemIcon>
</ListItemButton>
</ListItem> */}
</List>
<List sx={{ width: '100%', maxWidth: 360 }}>
<ListItem
disablePadding
sx={{
marginBottom: '20px',
}}
>
<ListItemButton
sx={{
padding: '0px',
}}
disableRipple
role={undefined}
dense
onClick={() => {
executeEvent('openBuyQortInfo', {});
}}
>
<ListItemText
sx={{
'& .MuiTypography-root': {
fontSize: '1rem',
fontWeight: 400,
},
}}
primary={t('tutorial:initial.6_qort', {
postProcess: 'capitalize',
})}
/>
<ListItemIcon
sx={{
justifyContent: 'flex-end',
}}
>
<Box
sx={{
height: '18px',
width: '18px',
borderRadius: '50%',
backgroundColor: checked1
? 'rgba(9, 182, 232, 1)'
: 'transparent',
outline: '1px solid rgba(9, 182, 232, 1)',
}}
/>
</ListItemIcon>
</ListItemButton>
</ListItem>
<ListItem
sx={{
marginBottom: '20px',
}}
disablePadding
>
<ListItemButton
sx={{
padding: '0px',
}}
disableRipple
role={undefined}
dense
>
<ListItemText
onClick={() => {
executeEvent('openRegisterName', {});
}}
sx={{
'& .MuiTypography-root': {
fontSize: '1rem',
fontWeight: 400,
},
}}
primary={t('tutorial:initial.register_name', {
postProcess: 'capitalize',
})}
/>
<ListItemIcon
sx={{
justifyContent: 'flex-end',
}}
>
<Box
sx={{
height: '18px',
width: '18px',
borderRadius: '50%',
backgroundColor: checked2
? 'rgba(9, 182, 232, 1)'
: 'transparent',
outline: '1px solid rgba(9, 182, 232, 1)',
}}
/>
</ListItemIcon>
</ListItemButton>
</ListItem>
</List>
)}
</Box>
</Box>
);

View File

@@ -1,240 +1,271 @@
import { Box, Button, ListItem, ListItemButton, ListItemText, Popover, Typography } from '@mui/material';
import React, { useContext, useEffect, useRef, useState } from 'react'
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
import {
Box,
ListItem,
ListItemButton,
ListItemText,
Popover,
Typography,
useTheme,
} from '@mui/material';
import { useContext, useEffect, useRef, useState } from 'react';
import {
AutoSizer,
CellMeasurer,
CellMeasurerCache,
List,
} from 'react-virtualized';
import { MyContext, getBaseApiReact } from '../../App';
import { LoadingButton } from '@mui/lab';
import { getBaseApi, getFee } from '../../background';
import { getFee } from '../../background';
import LockIcon from '@mui/icons-material/Lock';
import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
import { Spacer } from "../../common/Spacer";
import { Spacer } from '../../common/Spacer';
import { useSetAtom } from 'jotai';
import { txListAtom } from '../../atoms/global';
const cache = new CellMeasurerCache({
fixedWidth: true,
defaultHeight: 50,
});
fixedWidth: true,
defaultHeight: 50,
});
const getGroupInfo = async (groupId)=> {
const getGroupInfo = async (groupId) => {
const response = await fetch(`${getBaseApiReact()}/groups/` + groupId);
const groupData = await response.json();
if (groupData) {
return groupData
}
}
export const getGroupNames = async (listOfGroups) => {
let groups = [];
if (listOfGroups && Array.isArray(listOfGroups)) {
for (const group of listOfGroups) {
const groupInfo = await getGroupInfo(group.groupId);
if (groupInfo) {
groups.push({ ...group, ...groupInfo });
}
}
}
return groups;
return groupData;
}
export const UserListOfInvites = ({myAddress, setInfoSnack, setOpenSnack}) => {
const {txList, setTxList, show} = useContext(MyContext)
const [invites, setInvites] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
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 listRef = useRef();
const getRequests = async () => {
try {
const response = await fetch(`${getBaseApiReact()}/groups/invites/${myAddress}/?limit=0`);
const inviteData = await response.json();
const resMoreData = await getGroupNames(inviteData)
setInvites(resMoreData);
} catch (error) {
console.error(error);
};
export const getGroupNames = async (listOfGroups) => {
let groups = [];
if (listOfGroups && Array.isArray(listOfGroups)) {
for (const group of listOfGroups) {
const groupInfo = await getGroupInfo(group.groupId);
if (groupInfo) {
groups.push({ ...group, ...groupInfo });
}
}
useEffect(() => {
getRequests();
}, []);
const handlePopoverOpen = (event, index) => {
setPopoverAnchor(event.currentTarget);
setOpenPopoverIndex(index);
};
const handlePopoverClose = () => {
setPopoverAnchor(null);
setOpenPopoverIndex(null);
};
const handleJoinGroup = async (groupId, groupName)=> {
try {
const fee = await getFee('JOIN_GROUP')
await show({
message: "Would you like to perform an JOIN_GROUP transaction?" ,
publishFee: fee.fee + ' QORT'
})
}
return groups;
};
setIsLoading(true);
export const UserListOfInvites = ({
myAddress,
setInfoSnack,
setOpenSnack,
}) => {
const { show } = useContext(MyContext);
const setTxList = useSetAtom(txListAtom);
await new Promise((res, rej)=> {
window.sendMessage("joinGroup", {
const [invites, setInvites] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const theme = useTheme();
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 listRef = useRef();
const getRequests = async () => {
try {
const response = await fetch(
`${getBaseApiReact()}/groups/invites/${myAddress}/?limit=0`
);
const inviteData = await response.json();
const resMoreData = await getGroupNames(inviteData);
setInvites(resMoreData);
} catch (error) {
console.error(error);
}
};
useEffect(() => {
getRequests();
}, []);
const handlePopoverOpen = (event, index) => {
setPopoverAnchor(event.currentTarget);
setOpenPopoverIndex(index);
};
const handlePopoverClose = () => {
setPopoverAnchor(null);
setOpenPopoverIndex(null);
};
const handleJoinGroup = async (groupId, groupName) => {
try {
const fee = await getFee('JOIN_GROUP'); // TODO translate
await show({
message: 'Would you like to perform an JOIN_GROUP transaction?',
publishFee: fee.fee + ' QORT',
});
setIsLoading(true);
await new Promise((res, rej) => {
window
.sendMessage('joinGroup', {
groupId,
})
.then((response) => {
if (!response?.error) {
setTxList((prev) => [
{
...response,
type: 'joined-group',
label: `Joined Group ${groupName}: awaiting confirmation`,
labelDone: `Joined Group ${groupName}: success!`,
done: false,
groupId,
},
...prev,
]);
res(response);
setInfoSnack({
type: "success",
message: "Successfully requested to join group. It may take a couple of minutes for the changes to propagate",
});
setOpenSnack(true);
handlePopoverClose();
return;
}
.then((response) => {
if (!response?.error) {
setTxList((prev) => [
{
...response,
type: 'joined-group',
label: `Joined Group ${groupName}: awaiting confirmation`,
labelDone: `Joined Group ${groupName}: success!`,
done: false,
groupId,
},
...prev,
]);
res(response);
setInfoSnack({
type: "error",
message: response?.error,
type: 'success',
message:
'Successfully requested to join group. It may take a couple of minutes for the changes to propagate',
});
setOpenSnack(true);
rej(response.error);
})
.catch((error) => {
setInfoSnack({
type: "error",
message: error.message || "An error occurred",
});
setOpenSnack(true);
rej(error);
handlePopoverClose();
return;
}
setInfoSnack({
type: 'error',
message: response?.error,
});
})
} catch (error) {
} finally {
setIsLoading(false);
}
setOpenSnack(true);
rej(response.error);
})
.catch((error) => {
setInfoSnack({
type: 'error',
message: error.message || 'An error occurred',
});
setOpenSnack(true);
rej(error);
});
});
} catch (error) {
} finally {
setIsLoading(false);
}
const rowRenderer = ({ index, key, parent, style }) => {
const invite = invites[index];
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
{({ measure }) => (
<div style={style} onLoad={measure}>
<ListItem disablePadding>
<Popover
open={openPopoverIndex === index}
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
style={{ marginTop: "8px" }}
>
<Box
};
const rowRenderer = ({ index, key, parent, style }) => {
const invite = invites[index];
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
{({ measure }) => (
<div style={style} onLoad={measure}>
<ListItem disablePadding>
<Popover
open={openPopoverIndex === index}
anchorEl={popoverAnchor}
onClose={handlePopoverClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
style={{ marginTop: '8px' }}
>
<Box
sx={{
width: "325px",
height: "250px",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "10px",
padding: "10px",
width: '325px',
height: '250px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
padding: '10px',
}}
>
<Typography>Join {invite?.groupName}</Typography>
<LoadingButton
<Typography>Join {invite?.groupName}</Typography>
<LoadingButton
loading={isLoading}
loadingPosition="start"
variant="contained" onClick={()=> handleJoinGroup(invite?.groupId, invite?.groupName)}>Join group</LoadingButton>
</Box>
</Popover>
<ListItemButton onClick={(event) => handlePopoverOpen(event, index)}>
variant="contained"
onClick={() =>
handleJoinGroup(invite?.groupId, invite?.groupName)
}
>
Join group
</LoadingButton>
</Box>
</Popover>
<ListItemButton
onClick={(event) => handlePopoverOpen(event, index)}
>
{invite?.isOpen === false && (
<LockIcon sx={{
color: 'var(--green)'
}} />
<LockIcon
sx={{
color: theme.palette.other.positive,
}}
/>
)}
{invite?.isOpen === true && (
<NoEncryptionGmailerrorredIcon
sx={{
color: theme.palette.other.danger,
}}
/>
)}
<Spacer width="15px" />
<ListItemText
primary={invite?.groupName}
secondary={invite?.description}
/>
</ListItemButton>
</ListItem>
</div>
)}
{invite?.isOpen === true && (
<NoEncryptionGmailerrorredIcon sx={{
color: 'var(--danger)'
}} />
)}
<Spacer width="15px" />
<ListItemText primary={invite?.groupName} secondary={invite?.description} />
</ListItemButton>
</ListItem>
</div>
)}
</CellMeasurer>
);
};
return (
<Box sx={{
</CellMeasurer>
);
};
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1
}}>
<p>Invite list</p>
<div
flexGrow: 1,
}}
>
<p>Invite list</p>
<div
style={{
position: "relative",
width: "100%",
display: "flex",
flexDirection: "column",
position: 'relative',
width: '100%',
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
}}
>
<AutoSizer>
{({ height, width }) => (
<List
ref={listRef}
width={width}
height={height}
rowCount={invites.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
deferredMeasurementCache={cache}
/>
)}
</AutoSizer>
</div>
</Box>
);
}
<AutoSizer>
{({ height, width }) => (
<List
ref={listRef}
width={width}
height={height}
rowCount={invites.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
deferredMeasurementCache={cache}
/>
)}
</AutoSizer>
</div>
</Box>
);
};

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