mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-07-24 10:41:24 +00:00
Merge branch 'feature/q-app-support'
This commit is contained in:
210
src/components/Apps/AppInfo.tsx
Normal file
210
src/components/Apps/AppInfo.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppCircleLabel,
|
||||
AppDownloadButton,
|
||||
AppDownloadButtonText,
|
||||
AppInfoAppName,
|
||||
AppInfoSnippetContainer,
|
||||
AppInfoSnippetLeft,
|
||||
AppInfoSnippetMiddle,
|
||||
AppInfoSnippetRight,
|
||||
AppInfoUserName,
|
||||
AppsCategoryInfo,
|
||||
AppsCategoryInfoLabel,
|
||||
AppsCategoryInfoSub,
|
||||
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";
|
||||
|
||||
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";
|
||||
|
||||
export const AppInfo = ({ app, myName }) => {
|
||||
const isInstalled = app?.status?.status === "READY";
|
||||
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
||||
|
||||
const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === app?.name && item?.service === app?.service)
|
||||
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
|
||||
|
||||
return (
|
||||
<AppsLibraryContainer
|
||||
sx={{
|
||||
height: !isMobile && "100%",
|
||||
justifyContent: !isMobile && "flex-start",
|
||||
alignItems: isMobile && 'center'
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: "500px",
|
||||
width: '90%'
|
||||
}}>
|
||||
|
||||
|
||||
{!isMobile && <Spacer height="30px" />}
|
||||
<AppsWidthLimiter>
|
||||
<AppInfoSnippetContainer>
|
||||
<AppInfoSnippetLeft
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
gap: "18px",
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer
|
||||
sx={{
|
||||
width: "auto",
|
||||
}}
|
||||
>
|
||||
<AppCircle
|
||||
sx={{
|
||||
border: "none",
|
||||
height: "100px",
|
||||
width: "100px",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "43px",
|
||||
width: "43px",
|
||||
"& img": {
|
||||
objectFit: "fill",
|
||||
},
|
||||
}}
|
||||
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>
|
||||
<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)
|
||||
);
|
||||
} 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>
|
||||
</Box>
|
||||
</AppsLibraryContainer>
|
||||
);
|
||||
};
|
157
src/components/Apps/AppInfoSnippet.tsx
Normal file
157
src/components/Apps/AppInfoSnippet.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppDownloadButton,
|
||||
AppDownloadButtonText,
|
||||
AppInfoAppName,
|
||||
AppInfoSnippetContainer,
|
||||
AppInfoSnippetLeft,
|
||||
AppInfoSnippetMiddle,
|
||||
AppInfoSnippetRight,
|
||||
AppInfoUserName,
|
||||
} from "./Apps-styles";
|
||||
import { Avatar, ButtonBase } from "@mui/material";
|
||||
import { getBaseApiReact, isMobile } from "../../App";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
|
||||
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 }) => {
|
||||
|
||||
const isInstalled = app?.status?.status === 'READY'
|
||||
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
||||
|
||||
const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === app?.name && item?.service === app?.service)
|
||||
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
|
||||
return (
|
||||
<AppInfoSnippetContainer>
|
||||
<AppInfoSnippetLeft>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
height: "80px",
|
||||
width: "60px",
|
||||
}}
|
||||
onClick={()=> {
|
||||
if(isFromCategory){
|
||||
executeEvent("selectedAppInfoCategory", {
|
||||
data: app,
|
||||
});
|
||||
return
|
||||
}
|
||||
executeEvent("selectedAppInfo", {
|
||||
data: app,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer>
|
||||
<AppCircle
|
||||
sx={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "31px",
|
||||
width: "31px",
|
||||
'& 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,
|
||||
});
|
||||
}}>
|
||||
<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>
|
||||
</AppDownloadButton>
|
||||
</AppInfoSnippetRight>
|
||||
</AppInfoSnippetContainer>
|
||||
);
|
||||
};
|
519
src/components/Apps/AppPublish.tsx
Normal file
519
src/components/Apps/AppPublish.tsx
Normal file
@@ -0,0 +1,519 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppCircleLabel,
|
||||
AppDownloadButton,
|
||||
AppDownloadButtonText,
|
||||
AppInfoAppName,
|
||||
AppInfoSnippetContainer,
|
||||
AppInfoSnippetLeft,
|
||||
AppInfoSnippetMiddle,
|
||||
AppInfoSnippetRight,
|
||||
AppInfoUserName,
|
||||
AppLibrarySubTitle,
|
||||
AppPublishTagsContainer,
|
||||
AppsLibraryContainer,
|
||||
AppsParent,
|
||||
AppsWidthLimiter,
|
||||
PublishQAppCTAButton,
|
||||
PublishQAppChoseFile,
|
||||
PublishQAppInfo,
|
||||
} 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";
|
||||
|
||||
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",
|
||||
},
|
||||
"&:hover": {
|
||||
borderColor: "none", // Border color on hover
|
||||
},
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "none", // Border color when focused
|
||||
},
|
||||
"&.Mui-disabled": {
|
||||
opacity: 0.5, // Lower opacity when disabled
|
||||
},
|
||||
"& .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
|
||||
},
|
||||
});
|
||||
|
||||
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 [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 [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 { getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
"application/zip": [".zip"], // Only accept zip files
|
||||
},
|
||||
maxSize: maxFileSize, // Set the max size based on appType
|
||||
multiple: false, // Disable multiple file uploads
|
||||
onDrop: (acceptedFiles) => {
|
||||
if (acceptedFiles.length > 0) {
|
||||
setFile(acceptedFiles[0]); // Set the file name
|
||||
}
|
||||
},
|
||||
onDropRejected: (fileRejections) => {
|
||||
fileRejections.forEach(({ file, errors }) => {
|
||||
errors.forEach((error) => {
|
||||
if (error.code === "file-too-large") {
|
||||
console.error(
|
||||
`File ${file.name} is too large. Max size allowed is ${
|
||||
maxFileSize / (1024 * 1024)
|
||||
} MB.`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const getQapp = React.useCallback(async (name, appType) => {
|
||||
try {
|
||||
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",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response?.ok) return;
|
||||
const responseData = await response.json();
|
||||
|
||||
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] || "");
|
||||
}
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setIsLoading("");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!name || !appType) return;
|
||||
getQapp(name, appType);
|
||||
}, [name, appType]);
|
||||
|
||||
const publishApp = async () => {
|
||||
try {
|
||||
const data = {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
category,
|
||||
appType,
|
||||
file,
|
||||
};
|
||||
const requiredFields = [
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"category",
|
||||
"appType",
|
||||
"file",
|
||||
];
|
||||
|
||||
const missingFields: string[] = [];
|
||||
requiredFields.forEach((field) => {
|
||||
if (!data[field]) {
|
||||
missingFields.push(field);
|
||||
}
|
||||
});
|
||||
if (missingFields.length > 0) {
|
||||
const missingFieldsString = missingFields.join(", ");
|
||||
const errorMsg = `Missing fields: ${missingFieldsString}`;
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
const fee = await getFee("ARBITRARY");
|
||||
|
||||
await show({
|
||||
message: "Would you like to publish this app?",
|
||||
publishFee: fee.fee + " QORT",
|
||||
});
|
||||
setIsLoading("Publishing... Please wait.");
|
||||
const fileBase64 = await fileToBase64(file);
|
||||
await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "publishOnQDN",
|
||||
payload: {
|
||||
data: fileBase64,
|
||||
service: appType,
|
||||
title,
|
||||
description,
|
||||
category,
|
||||
tag1,
|
||||
tag2,
|
||||
tag3,
|
||||
tag4,
|
||||
tag5,
|
||||
uploadType: 'zip'
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (!response?.error) {
|
||||
res(response);
|
||||
return;
|
||||
}
|
||||
rej(response.error);
|
||||
}
|
||||
);
|
||||
});
|
||||
setInfoSnack({
|
||||
type: "success",
|
||||
message:
|
||||
"Successfully published. Please wait a couple minutes for the network to propogate the changes.",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
const dataObj = {
|
||||
name: name,
|
||||
service: appType,
|
||||
metadata: {
|
||||
title: title,
|
||||
description: description,
|
||||
category: category,
|
||||
},
|
||||
created: Date.now(),
|
||||
};
|
||||
executeEvent("addTab", {
|
||||
data: dataObj,
|
||||
});
|
||||
} catch (error) {
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
message: error?.message || "Unable to publish app",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
} finally {
|
||||
setIsLoading("");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<AppsLibraryContainer sx={{
|
||||
height: !isMobile ? '100%' : 'auto',
|
||||
paddingTop: !isMobile && '30px',
|
||||
alignItems: !isMobile && 'center'
|
||||
}}>
|
||||
<AppsWidthLimiter sx={{
|
||||
width: !isMobile ? 'auto' : '90%'
|
||||
}}>
|
||||
<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>
|
||||
<CustomSelect
|
||||
placeholder="Select Name/App"
|
||||
displayEmpty
|
||||
value={name}
|
||||
onChange={(event) => setName(event?.target.value)}
|
||||
>
|
||||
<CustomMenuItem value="">
|
||||
<em
|
||||
style={{
|
||||
color: "var(--50-white, #FFFFFF80)",
|
||||
}}
|
||||
>
|
||||
Select Name/App
|
||||
</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>
|
||||
<CustomSelect
|
||||
placeholder="SERVICE TYPE"
|
||||
displayEmpty
|
||||
value={appType}
|
||||
onChange={(event) => setAppType(event?.target.value)}
|
||||
>
|
||||
<CustomMenuItem value="">
|
||||
<em
|
||||
style={{
|
||||
color: "var(--50-white, #FFFFFF80)",
|
||||
}}
|
||||
>
|
||||
Select App Type
|
||||
</em>{" "}
|
||||
{/* This is the placeholder item */}
|
||||
</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>
|
||||
<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",
|
||||
}}
|
||||
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",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Spacer height="15px" />
|
||||
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>Category</InputLabel>
|
||||
<CustomSelect
|
||||
displayEmpty
|
||||
placeholder="Select Category"
|
||||
value={category}
|
||||
onChange={(event) => setCategory(event?.target.value)}
|
||||
>
|
||||
<CustomMenuItem value="">
|
||||
<em
|
||||
style={{
|
||||
color: "var(--50-white, #FFFFFF80)",
|
||||
}}
|
||||
>
|
||||
Select Category
|
||||
</em>{" "}
|
||||
{/* This is the placeholder item */}
|
||||
</CustomMenuItem>
|
||||
{categories?.map((category) => {
|
||||
return (
|
||||
<CustomMenuItem value={category?.id}>
|
||||
{category?.name}
|
||||
</CustomMenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomSelect>
|
||||
<Spacer height="15px" />
|
||||
<InputLabel sx={{ color: '#888', 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",
|
||||
}}
|
||||
placeholder="Tag 1"
|
||||
inputProps={{
|
||||
"aria-label": "Tag 1",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
<InputBase
|
||||
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",
|
||||
}}
|
||||
placeholder="Tag 2"
|
||||
inputProps={{
|
||||
"aria-label": "Tag 2",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
<InputBase
|
||||
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",
|
||||
}}
|
||||
placeholder="Tag 3"
|
||||
inputProps={{
|
||||
"aria-label": "Tag 3",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
<InputBase
|
||||
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",
|
||||
}}
|
||||
placeholder="Tag 4"
|
||||
inputProps={{
|
||||
"aria-label": "Tag 4",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
<InputBase
|
||||
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",
|
||||
}}
|
||||
placeholder="Tag 5"
|
||||
inputProps={{
|
||||
"aria-label": "Tag 5",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
</AppPublishTagsContainer>
|
||||
<Spacer height="30px" />
|
||||
<PublishQAppInfo>
|
||||
Select .zip file containing static content:{" "}
|
||||
</PublishQAppInfo>
|
||||
<Spacer height="10px" />
|
||||
<PublishQAppInfo>{`(${
|
||||
appType === "APP" ? "50mb" : "400mb"
|
||||
} MB maximum)`}</PublishQAppInfo>
|
||||
{file && (
|
||||
<>
|
||||
<Spacer height="5px" />
|
||||
<PublishQAppInfo>{`Selected: (${file?.name})`}</PublishQAppInfo>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Spacer height="18px" />
|
||||
<PublishQAppChoseFile {...getRootProps()}>
|
||||
{" "}
|
||||
<input {...getInputProps()} />
|
||||
Choose File
|
||||
</PublishQAppChoseFile>
|
||||
<Spacer height="35px" />
|
||||
<PublishQAppCTAButton
|
||||
sx={{
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onClick={publishApp}
|
||||
>
|
||||
Publish
|
||||
</PublishQAppCTAButton>
|
||||
</AppsWidthLimiter>
|
||||
<LoadingSnackbar
|
||||
open={!!isLoading}
|
||||
info={{
|
||||
message: isLoading,
|
||||
}}
|
||||
/>
|
||||
<CustomizedSnackbars
|
||||
duration={3500}
|
||||
open={openSnack}
|
||||
setOpen={setOpenSnack}
|
||||
info={infoSnack}
|
||||
setInfo={setInfoSnack}
|
||||
/>
|
||||
|
||||
</AppsLibraryContainer>
|
||||
);
|
||||
};
|
243
src/components/Apps/AppRating.tsx
Normal file
243
src/components/Apps/AppRating.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
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";
|
||||
|
||||
export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => {
|
||||
const [value, setValue] = useState(0);
|
||||
const { show } = useContext(MyContext);
|
||||
const [hasPublishedRating, setHasPublishedRating] = useState<null | boolean>(
|
||||
null
|
||||
);
|
||||
const [pollInfo, setPollInfo] = useState(null);
|
||||
const [votesInfo, setVotesInfo] = useState(null);
|
||||
const [openSnack, setOpenSnack] = useState(false);
|
||||
const [infoSnack, setInfoSnack] = useState(null);
|
||||
const hasCalledRef = useRef(false);
|
||||
|
||||
const getRating = useCallback(async (name, service) => {
|
||||
try {
|
||||
hasCalledRef.current = true;
|
||||
const pollName = `app-library-${service}-rating-${name}`;
|
||||
const url = `${getBaseApiReact()}/polls/${pollName}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const responseData = await response.json();
|
||||
if (responseData?.message?.includes("POLL_NO_EXISTS")) {
|
||||
setHasPublishedRating(false);
|
||||
} else if (responseData?.pollName) {
|
||||
setPollInfo(responseData);
|
||||
setHasPublishedRating(true);
|
||||
const urlVotes = `${getBaseApiReact()}/polls/votes/${pollName}`;
|
||||
|
||||
const responseVotes = await fetch(urlVotes, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const responseDataVotes = await responseVotes.json();
|
||||
setVotesInfo(responseDataVotes);
|
||||
const voteCount = responseDataVotes.voteCounts;
|
||||
// Include initial value vote in the calculation
|
||||
const ratingVotes = voteCount.filter(
|
||||
(vote) => !vote.optionName.startsWith("initialValue-")
|
||||
);
|
||||
const initialValueVote = voteCount.find((vote) =>
|
||||
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],
|
||||
10
|
||||
);
|
||||
ratingVotes.push({
|
||||
optionName: initialRating.toString(),
|
||||
voteCount: 1,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate the weighted average
|
||||
let totalScore = 0;
|
||||
let totalVotes = 0;
|
||||
|
||||
ratingVotes.forEach((vote) => {
|
||||
const rating = parseInt(vote.optionName, 10); // Extract rating value (1-5)
|
||||
const count = vote.voteCount;
|
||||
totalScore += rating * count; // Weighted score
|
||||
totalVotes += count; // Total number of votes
|
||||
});
|
||||
|
||||
// Calculate average rating (ensure no division by zero)
|
||||
const averageRating = totalVotes > 0 ? totalScore / totalVotes : 0;
|
||||
setValue(averageRating);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.message?.includes("POLL_NO_EXISTS")) {
|
||||
setHasPublishedRating(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (hasCalledRef.current) return;
|
||||
if (!app) return;
|
||||
getRating(app?.name, app?.service);
|
||||
}, [getRating, app?.name]);
|
||||
|
||||
const rateFunc = async (event, newValue) => {
|
||||
try {
|
||||
if (!myName) throw new Error("You need a name to rate.");
|
||||
if (!app?.name) return;
|
||||
const fee = await getFee("ARBITRARY");
|
||||
|
||||
await show({
|
||||
message: `Would you like to rate this app a rating of ${newValue}?`,
|
||||
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) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "CREATE_POLL",
|
||||
type: "qortalRequest",
|
||||
payload: {
|
||||
pollName: pollName,
|
||||
pollDescription: `Rating for ${app.service} ${app.name}`,
|
||||
pollOptions: pollOptions,
|
||||
pollOwnerAddress: myName,
|
||||
},
|
||||
},
|
||||
(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);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const pollName = `app-library-${app.service}-rating-${app.name}`;
|
||||
const optionIndex = pollInfo?.pollOptions.findIndex(
|
||||
(option) => +option.optionName === +newValue
|
||||
);
|
||||
if (isNaN(optionIndex) || optionIndex === -1)
|
||||
throw new Error("Cannot find rating option");
|
||||
await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "VOTE_ON_POLL",
|
||||
type: "qortalRequest",
|
||||
payload: {
|
||||
pollName: pollName,
|
||||
optionIndex,
|
||||
},
|
||||
},
|
||||
(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) {
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
message: error.message || "An error occurred while trying to rate.",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: ratingCountPosition === "top" ? "column" : "row",
|
||||
}}
|
||||
>
|
||||
{ratingCountPosition === "top" && (
|
||||
<>
|
||||
<AppInfoUserName>
|
||||
{(votesInfo?.totalVotes ?? 0) +
|
||||
(votesInfo?.voteCounts?.length === 6 ? 1 : 0)}{" "}
|
||||
{" RATINGS"}
|
||||
</AppInfoUserName>
|
||||
<Spacer height="6px" />
|
||||
<AppInfoUserName>{value?.toFixed(1)}</AppInfoUserName>
|
||||
<Spacer height="6px" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Rating
|
||||
value={value}
|
||||
onChange={rateFunc}
|
||||
precision={1}
|
||||
readOnly={hasPublishedRating === null}
|
||||
size="small"
|
||||
icon={<StarFilledIcon />}
|
||||
emptyIcon={<StarEmptyIcon />}
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "2px",
|
||||
}}
|
||||
/>
|
||||
{ratingCountPosition === "right" && (
|
||||
<AppInfoUserName>
|
||||
{(votesInfo?.totalVotes ?? 0) +
|
||||
(votesInfo?.voteCounts?.length === 6 ? 1 : 0)}
|
||||
</AppInfoUserName>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<CustomizedSnackbars
|
||||
duration={2000}
|
||||
open={openSnack}
|
||||
setOpen={setOpenSnack}
|
||||
info={infoSnack}
|
||||
setInfo={setInfoSnack}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
149
src/components/Apps/AppViewer.tsx
Normal file
149
src/components/Apps/AppViewer.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
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}, iframeRef) => {
|
||||
const { rootHeight } = useContext(MyContext);
|
||||
// const iframeRef = useRef(null);
|
||||
const { document, window: frameWindow } = useFrame();
|
||||
const {path, history, changeCurrentIndex} = useQortalMessageListener(frameWindow, iframeRef, app?.tabId)
|
||||
const [url, setUrl] = useState('')
|
||||
console.log('historyreact', history)
|
||||
|
||||
useEffect(()=> {
|
||||
setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? `/${app?.path}` : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}`)
|
||||
}, [app?.service, app?.name, app?.identifier, app?.path])
|
||||
const defaultUrl = useMemo(()=> {
|
||||
return url
|
||||
}, [url])
|
||||
|
||||
|
||||
|
||||
const refreshAppFunc = (e) => {
|
||||
const {tabId} = e.detail
|
||||
if(tabId === app?.tabId){
|
||||
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]);
|
||||
|
||||
// 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];
|
||||
|
||||
// Signal non-manual navigation
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ action: 'PERFORMING_NON_MANUAL' }, '*'
|
||||
);
|
||||
console.log('previousPageIndex', previousPageIndex)
|
||||
// Update the current index locally
|
||||
changeCurrentIndex(previousPageIndex);
|
||||
|
||||
// Create a navigation promise with a 200ms timeout
|
||||
const navigationPromise = new Promise((resolve, reject) => {
|
||||
function handleNavigationSuccess(event) {
|
||||
console.log('listeninghandlenav', 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);
|
||||
|
||||
// Send the navigation command after setting up the listener and timeout
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ action: 'NAVIGATE_TO_PATH', path: previousPath, requestedHandler: 'UI' }, '*'
|
||||
);
|
||||
});
|
||||
|
||||
// Execute navigation promise and handle timeout fallback
|
||||
try {
|
||||
await navigationPromise;
|
||||
console.log('Navigation succeeded within 200ms.');
|
||||
} catch (error) {
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ action: 'PERFORMING_NON_MANUAL' }, '*'
|
||||
);
|
||||
setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.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.');
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
console.log('iframeRef.contentWindow', iframeRef.current.contentWindow);
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ action: 'NAVIGATE_FORWARD'},
|
||||
'*'
|
||||
);
|
||||
} 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: !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">
|
||||
|
||||
</iframe>
|
||||
</Box>
|
||||
|
||||
);
|
||||
});
|
50
src/components/Apps/AppViewerContainer.tsx
Normal file
50
src/components/Apps/AppViewerContainer.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { useContext, } from 'react';
|
||||
import { AppViewer } from './AppViewer';
|
||||
import Frame from 'react-frame-component';
|
||||
import { MyContext, isMobile } from '../../App';
|
||||
|
||||
const AppViewerContainer = React.forwardRef(({ app, isSelected, hide }, ref) => {
|
||||
const { rootHeight } = useContext(MyContext);
|
||||
|
||||
|
||||
return (
|
||||
<Frame
|
||||
id={`browser-iframe-${app?.tabId}`}
|
||||
|
||||
head={
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
* {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
*::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
}
|
||||
.frame-content {
|
||||
overflow: hidden;
|
||||
height: ${!isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`};
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
}
|
||||
style={{
|
||||
display: (!isSelected || hide) && 'none',
|
||||
height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`,
|
||||
border: 'none',
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<AppViewer app={app} ref={ref} hide={!isSelected || hide} />
|
||||
</Frame>
|
||||
);
|
||||
});
|
||||
|
||||
export default AppViewerContainer;
|
311
src/components/Apps/Apps-styles.tsx
Normal file
311
src/components/Apps/Apps-styles.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
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: '12px',
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
width: '100%'
|
||||
}));
|
||||
export const AppLibrarySubTitle = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '16px',
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
}));
|
||||
export const AppCircle = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
width: "60px",
|
||||
flexDirection: "column",
|
||||
height: "60px",
|
||||
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 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 AppDownloadButton = styled(ButtonBase)(({ theme }) => ({
|
||||
backgroundColor: "#247C0E",
|
||||
width: '101px',
|
||||
height: '29px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: '25px',
|
||||
alignSelf: 'center'
|
||||
}));
|
||||
|
||||
export const AppDownloadButtonText = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
}));
|
||||
|
||||
export const AppPublishTagsContainer = styled(Box)(({theme})=> ({
|
||||
gap: '10px',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
display: 'flex'
|
||||
}))
|
||||
|
||||
|
||||
export const AppInfoSnippetMiddle = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
}));
|
||||
|
||||
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 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 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 TabParent = styled(Box)(({ theme }) => ({
|
||||
height: '36px',
|
||||
width: '36px',
|
||||
backgroundColor: '#434343',
|
||||
position: 'relative',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}));
|
||||
|
||||
export const PublishQAppCTAParent = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
backgroundColor: '#181C23'
|
||||
}));
|
||||
|
||||
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 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 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 AppsCategoryInfo = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
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'
|
||||
}));
|
326
src/components/Apps/Apps.tsx
Normal file
326
src/components/Apps/Apps.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
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>
|
||||
);
|
||||
};
|
188
src/components/Apps/AppsCategory.tsx
Normal file
188
src/components/Apps/AppsCategory.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
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",
|
||||
"qombo",
|
||||
"q-fund",
|
||||
"q-shop",
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
223
src/components/Apps/AppsCategoryDesktop.tsx
Normal file
223
src/components/Apps/AppsCategoryDesktop.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
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";
|
||||
import { AppsDesktopLibraryBody, AppsDesktopLibraryHeader } from "./AppsDesktop-styles";
|
||||
const officialAppList = [
|
||||
"q-tube",
|
||||
"q-blog",
|
||||
"q-share",
|
||||
"q-support",
|
||||
"q-mail",
|
||||
"qombo",
|
||||
"q-fund",
|
||||
"q-shop",
|
||||
];
|
||||
|
||||
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 AppsCategoryDesktop = ({
|
||||
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",
|
||||
padding: "0px",
|
||||
height: "100vh",
|
||||
overflow: "hidden",
|
||||
paddingTop: "30px",
|
||||
}}
|
||||
>
|
||||
<AppsDesktopLibraryHeader
|
||||
sx={{
|
||||
maxWidth: "1500px",
|
||||
width: "90%",
|
||||
}}
|
||||
>
|
||||
<AppsWidthLimiter
|
||||
sx={{
|
||||
alignItems: "flex-end",
|
||||
}}
|
||||
>
|
||||
<AppsSearchContainer sx={{
|
||||
width: "412px",
|
||||
}}>
|
||||
<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>
|
||||
</AppsWidthLimiter>
|
||||
</AppsDesktopLibraryHeader>
|
||||
<AppsDesktopLibraryBody
|
||||
sx={{
|
||||
height: `calc(100vh - 36px)`,
|
||||
overflow: "auto",
|
||||
padding: "0px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</AppsDesktopLibraryBody>
|
||||
</AppsLibraryContainer>
|
||||
);
|
||||
};
|
24
src/components/Apps/AppsDesktop-styles.tsx
Normal file
24
src/components/Apps/AppsDesktop-styles.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
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%'
|
||||
}));
|
427
src/components/Apps/AppsDesktop.tsx
Normal file
427
src/components/Apps/AppsDesktop.tsx
Normal file
@@ -0,0 +1,427 @@
|
||||
import React, { useCallback, 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";
|
||||
|
||||
const uid = new ShortUniqueId({ length: 8 });
|
||||
|
||||
export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktopSideView, hasUnreadDirects, isDirects, isGroups, hasUnreadGroups, toggleSideViewGroups, toggleSideViewDirects}) => {
|
||||
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",
|
||||
flexDirection: 'row'
|
||||
}}
|
||||
>
|
||||
|
||||
<Box sx={{
|
||||
width: '60px',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
gap: '30px'
|
||||
}}>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
paddingTop: '23px'
|
||||
}}
|
||||
onClick={() => {
|
||||
goToHome();
|
||||
|
||||
}}
|
||||
>
|
||||
|
||||
<HomeIcon
|
||||
height={34}
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
/>
|
||||
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView("directs");
|
||||
toggleSideViewDirects()
|
||||
}}
|
||||
>
|
||||
|
||||
<MessagingIcon
|
||||
height={30}
|
||||
color={
|
||||
hasUnreadDirects
|
||||
? "var(--unread)"
|
||||
: isDirects
|
||||
? "white"
|
||||
: "rgba(250, 250, 250, 0.5)"
|
||||
}
|
||||
/>
|
||||
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView("groups");
|
||||
toggleSideViewGroups()
|
||||
}}
|
||||
>
|
||||
<HubsIcon
|
||||
height={30}
|
||||
color={
|
||||
hasUnreadGroups
|
||||
? "var(--unread)"
|
||||
: isGroups
|
||||
? "white"
|
||||
: "rgba(250, 250, 250, 0.5)"
|
||||
}
|
||||
/>
|
||||
|
||||
</ButtonBase>
|
||||
<Save isDesktop />
|
||||
{mode !== 'home' && (
|
||||
<AppsNavBarDesktop />
|
||||
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
{mode === "home" && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
|
||||
<Spacer height="30px" />
|
||||
<AppsHomeDesktop 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}
|
||||
/>
|
||||
|
||||
{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}
|
||||
hide={isNewTabWindow}
|
||||
isSelected={tab?.tabId === selectedTab?.tabId}
|
||||
app={tab}
|
||||
ref={iframeRefs.current[tab.tabId]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{isNewTabWindow && mode === "viewer" && (
|
||||
<>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
|
||||
<Spacer height="30px" />
|
||||
<AppsHomeDesktop availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</AppsParent>
|
||||
);
|
||||
};
|
57
src/components/Apps/AppsHome.tsx
Normal file
57
src/components/Apps/AppsHome.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
73
src/components/Apps/AppsHomeDesktop.tsx
Normal file
73
src/components/Apps/AppsHomeDesktop.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
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 AppsHomeDesktop = ({
|
||||
setMode,
|
||||
myApp,
|
||||
myWebsite,
|
||||
availableQapps,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<AppsContainer
|
||||
sx={{
|
||||
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
<AppLibrarySubTitle
|
||||
sx={{
|
||||
fontSize: "30px",
|
||||
}}
|
||||
>
|
||||
Apps Dashboard
|
||||
</AppLibrarySubTitle>
|
||||
</AppsContainer>
|
||||
<Spacer height="45px" />
|
||||
<AppsContainer
|
||||
sx={{
|
||||
gap: "75px",
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setMode("library");
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer
|
||||
sx={{
|
||||
gap: !isMobile ? "10px" : "5px",
|
||||
}}
|
||||
>
|
||||
<AppCircle>
|
||||
<Add>+</Add>
|
||||
</AppCircle>
|
||||
<AppCircleLabel>Library</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
|
||||
<SortablePinnedApps
|
||||
isDesktop={true}
|
||||
availableQapps={availableQapps}
|
||||
myWebsite={myWebsite}
|
||||
myApp={myApp}
|
||||
/>
|
||||
</AppsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
322
src/components/Apps/AppsLibrary.tsx
Normal file
322
src/components/Apps/AppsLibrary.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
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",
|
||||
"qombo",
|
||||
"q-fund",
|
||||
"q-shop",
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
423
src/components/Apps/AppsLibraryDesktop.tsx
Normal file
423
src/components/Apps/AppsLibraryDesktop.tsx
Normal file
@@ -0,0 +1,423 @@
|
||||
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 qappLibraryText from "../../assets/svgs/qappLibraryText.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";
|
||||
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";
|
||||
const officialAppList = [
|
||||
"q-tube",
|
||||
"q-blog",
|
||||
"q-share",
|
||||
"q-support",
|
||||
"q-mail",
|
||||
"qombo",
|
||||
"q-fund",
|
||||
"q-shop",
|
||||
];
|
||||
|
||||
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 AppsLibraryDesktop = ({
|
||||
availableQapps,
|
||||
setMode,
|
||||
myName,
|
||||
hasPublishApp,
|
||||
isShow,
|
||||
categories = { categories },
|
||||
}) => {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const virtuosoRef = useRef();
|
||||
|
||||
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",
|
||||
padding: "0px",
|
||||
height: "100vh",
|
||||
overflow: "hidden",
|
||||
paddingTop: '30px'
|
||||
}}
|
||||
>
|
||||
|
||||
<AppsDesktopLibraryHeader
|
||||
sx={{
|
||||
maxWidth: "1500px",
|
||||
width: "90%",
|
||||
}}
|
||||
>
|
||||
<AppsWidthLimiter>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<img src={qappLibraryText} />
|
||||
<AppsSearchContainer
|
||||
sx={{
|
||||
width: "412px",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</AppsDesktopLibraryHeader>
|
||||
<AppsDesktopLibraryBody
|
||||
sx={{
|
||||
height: `calc(100vh - 36px)`,
|
||||
overflow: "auto",
|
||||
padding: "0px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<AppsDesktopLibraryBody
|
||||
sx={{
|
||||
height: `calc(100vh - 36px)`,
|
||||
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" />
|
||||
{searchedList?.length > 0 ? (
|
||||
<AppsWidthLimiter>
|
||||
<StyledVirtuosoContainer
|
||||
sx={{
|
||||
height: `calc(100vh - 36px - 90px)`,
|
||||
}}
|
||||
>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={searchedList}
|
||||
itemContent={rowRenderer}
|
||||
atBottomThreshold={50}
|
||||
followOutput="smooth"
|
||||
components={{
|
||||
Scroller: ScrollerStyled, // Use the styled scroller component
|
||||
}}
|
||||
/>
|
||||
</StyledVirtuosoContainer>
|
||||
</AppsWidthLimiter>
|
||||
) : (
|
||||
<>
|
||||
<AppLibrarySubTitle
|
||||
sx={{
|
||||
fontSize: "30px",
|
||||
}}
|
||||
>
|
||||
Official Apps
|
||||
</AppLibrarySubTitle>
|
||||
<Spacer height="45px" />
|
||||
<AppsContainer>
|
||||
{officialApps?.map((qapp) => {
|
||||
return (
|
||||
<ButtonBase
|
||||
sx={{
|
||||
height: "80px",
|
||||
width: "60px",
|
||||
}}
|
||||
onClick={() => {
|
||||
// executeEvent("addTab", {
|
||||
// data: qapp
|
||||
// })
|
||||
executeEvent("selectedAppInfo", {
|
||||
data: qapp,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer
|
||||
sx={{
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<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="80px" />
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
gap: "250px",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<AppLibrarySubTitle
|
||||
sx={{
|
||||
fontSize: "30px",
|
||||
width: "100%",
|
||||
textAlign: "start",
|
||||
}}
|
||||
>
|
||||
{hasPublishApp ? "Update Apps!" : "Create Apps!"}
|
||||
</AppLibrarySubTitle>
|
||||
<Spacer height="18px" />
|
||||
<PublishQAppCTAParent
|
||||
sx={{
|
||||
gap: "25px",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<AppLibrarySubTitle
|
||||
sx={{
|
||||
fontSize: "30px",
|
||||
}}
|
||||
>
|
||||
Categories
|
||||
</AppLibrarySubTitle>
|
||||
<Spacer height="18px" />
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{categories?.map((category) => {
|
||||
return (
|
||||
<ButtonBase
|
||||
key={category?.id}
|
||||
onClick={() => {
|
||||
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",
|
||||
}}
|
||||
>
|
||||
{category?.name}
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</AppsDesktopLibraryBody>
|
||||
</AppsDesktopLibraryBody>
|
||||
</AppsLibraryContainer>
|
||||
);
|
||||
};
|
347
src/components/Apps/AppsNavBar.tsx
Normal file
347
src/components/Apps/AppsNavBar.tsx
Normal file
@@ -0,0 +1,347 @@
|
||||
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) {
|
||||
try {
|
||||
// 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,
|
||||
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 = {
|
||||
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>
|
||||
);
|
||||
};
|
372
src/components/Apps/AppsNavBarDesktop.tsx
Normal file
372
src/components/Apps/AppsNavBarDesktop.tsx
Normal file
@@ -0,0 +1,372 @@
|
||||
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) {
|
||||
try {
|
||||
// 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,
|
||||
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 = {
|
||||
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 AppsNavBarDesktop = () => {
|
||||
const [tabs, setTabs] = useState([]);
|
||||
const [selectedTab, setSelectedTab] = useState(null);
|
||||
const [navigationController, setNavigationController] = useRecoilState(navigationControllerAtom)
|
||||
|
||||
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 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 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;
|
||||
|
||||
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
|
||||
sx={{
|
||||
position: "relative",
|
||||
flexDirection: "column",
|
||||
width: "60px",
|
||||
height: "unset",
|
||||
maxHeight: "70vh",
|
||||
borderRadius: "0px 30px 30px 0px",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<AppsNavBarLeft
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
executeEvent("navigateBack", selectedTab?.tabId);
|
||||
}}
|
||||
disabled={isDisableBackButton}
|
||||
sx={{
|
||||
opacity: !isDisableBackButton ? 1 : 0.1,
|
||||
cursor: !isDisableBackButton ? 'pointer': 'default'
|
||||
}}
|
||||
>
|
||||
<img src={NavBack} />
|
||||
</ButtonBase>
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
ref={tabsRef}
|
||||
aria-label="basic tabs example"
|
||||
variant="scrollable" // Make tabs scrollable
|
||||
scrollButtons={true}
|
||||
sx={{
|
||||
"& .MuiTabs-indicator": {
|
||||
backgroundColor: "white",
|
||||
},
|
||||
maxHeight: `320px`, // 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",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
176
src/components/Apps/SortablePinnedApps.tsx
Normal file
176
src/components/Apps/SortablePinnedApps.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } 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 { CSS } from '@dnd-kit/utilities';
|
||||
import { Avatar, ButtonBase } from '@mui/material';
|
||||
import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles';
|
||||
import { getBaseApiReact } from '../../App';
|
||||
import { executeEvent } from '../../utils/events';
|
||||
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { saveToLocalStorage } from './AppsNavBar';
|
||||
import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps';
|
||||
|
||||
const SortableItem = ({ id, name, app, isDesktop }) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
padding: '10px',
|
||||
border: '1px solid #ccc',
|
||||
marginBottom: '5px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
cursor: 'grab',
|
||||
color: 'black'
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenuPinnedApps app={app} isMine={!!app?.isMine}>
|
||||
<ButtonBase
|
||||
ref={setNodeRef} {...attributes} {...listeners}
|
||||
sx={{
|
||||
height: "80px",
|
||||
width: "60px",
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
}}
|
||||
onClick={()=> {
|
||||
executeEvent("addTab", {
|
||||
data: app
|
||||
})
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer sx={{
|
||||
border: "none",
|
||||
gap: isDesktop ? '10px': '5px'
|
||||
}}>
|
||||
<AppCircle
|
||||
sx={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "31px",
|
||||
width: "31px",
|
||||
'& img': {
|
||||
objectFit: 'fill',
|
||||
}
|
||||
}}
|
||||
alt={app?.metadata?.title || app?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "31px",
|
||||
height: "auto",
|
||||
}}
|
||||
// src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
</AppCircle>
|
||||
<AppCircleLabel>
|
||||
{app?.metadata?.title || app?.name}
|
||||
</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
</ContextMenuPinnedApps>
|
||||
);
|
||||
};
|
||||
|
||||
export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps = [] }) => {
|
||||
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
||||
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
|
||||
|
||||
const transformPinnedApps = useMemo(() => {
|
||||
|
||||
// Clone the existing pinned apps list
|
||||
let pinned = [...pinnedApps];
|
||||
|
||||
// Function to add or update `isMine` property
|
||||
const addOrUpdateIsMine = (pinnedList, appToCheck) => {
|
||||
if (!appToCheck) return pinnedList;
|
||||
|
||||
const existingIndex = pinnedList.findIndex(
|
||||
(item) => item?.service === appToCheck?.service && item?.name === appToCheck?.name
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// If the app is already in the list, update it with `isMine: true`
|
||||
pinnedList[existingIndex] = { ...pinnedList[existingIndex], isMine: true };
|
||||
} else {
|
||||
// If not in the list, add it with `isMine: true` at the beginning
|
||||
pinnedList.unshift({ ...appToCheck, isMine: true });
|
||||
}
|
||||
|
||||
return pinnedList;
|
||||
};
|
||||
|
||||
// Update or add `myWebsite` and `myApp` while preserving their positions
|
||||
pinned = addOrUpdateIsMine(pinned, myWebsite);
|
||||
pinned = addOrUpdateIsMine(pinned, myApp);
|
||||
|
||||
// Update pinned list based on availableQapps
|
||||
pinned = pinned.map((pin) => {
|
||||
const findIndex = availableQapps?.findIndex(
|
||||
(item) => item?.service === pin?.service && item?.name === pin?.name
|
||||
);
|
||||
if (findIndex !== -1) return {
|
||||
...availableQapps[findIndex],
|
||||
...pin
|
||||
}
|
||||
|
||||
return pin;
|
||||
});
|
||||
|
||||
return pinned;
|
||||
}, [myApp, myWebsite, pinnedApps, availableQapps]);
|
||||
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 10, // Set a distance to avoid triggering drag on small movements
|
||||
},
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
distance: 10, // Also apply to touch
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over) return; // Make sure the drop target exists
|
||||
|
||||
if (active.id !== over.id) {
|
||||
const oldIndex = transformPinnedApps.findIndex((item) => `${item?.service}-${item?.name}` === active.id);
|
||||
const newIndex = transformPinnedApps.findIndex((item) => `${item?.service}-${item?.name}` === over.id);
|
||||
|
||||
const newOrder = arrayMove(transformPinnedApps, oldIndex, newIndex);
|
||||
setPinnedApps(newOrder);
|
||||
saveToLocalStorage('ext_saved_settings','sortablePinnedApps', newOrder)
|
||||
setSettingsLocalLastUpdated(Date.now())
|
||||
}
|
||||
};
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={transformPinnedApps.map((app) => `${app?.service}-${app?.name}`)}>
|
||||
{transformPinnedApps.map((app) => (
|
||||
<SortableItem isDesktop={isDesktop} key={`${app?.service}-${app?.name}`} id={`${app?.service}-${app?.name}`} name={app?.name} app={app} />
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
61
src/components/Apps/TabComponent.tsx
Normal file
61
src/components/Apps/TabComponent.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
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';
|
||||
|
||||
const TabComponent = ({isSelected, app}) => {
|
||||
return (
|
||||
<ButtonBase onClick={()=> {
|
||||
if(isSelected){
|
||||
executeEvent('removeTab', {
|
||||
data: app
|
||||
})
|
||||
return
|
||||
}
|
||||
executeEvent('setSelectedTab', {
|
||||
data: app
|
||||
})
|
||||
}}>
|
||||
<TabParent sx={{
|
||||
border: isSelected && '1px solid #FFFFFF'
|
||||
}}>
|
||||
{isSelected && (
|
||||
|
||||
<img style={
|
||||
{
|
||||
position: 'absolute',
|
||||
top: '-5px',
|
||||
right: '-5px',
|
||||
zIndex: 1
|
||||
}
|
||||
} src={NavCloseTab}/>
|
||||
|
||||
) }
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "28px",
|
||||
width: "28px",
|
||||
}}
|
||||
alt={app?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "28px",
|
||||
height: "auto",
|
||||
}}
|
||||
src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
</TabParent>
|
||||
</ButtonBase>
|
||||
)
|
||||
}
|
||||
|
||||
export default TabComponent
|
484
src/components/Apps/useQortalMessageListener.tsx
Normal file
484
src/components/Apps/useQortalMessageListener.tsx
Normal file
@@ -0,0 +1,484 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import FileSaver from 'file-saver';
|
||||
import { executeEvent } from '../../utils/events';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { navigationControllerAtom } from '../../atoms/global';
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
||||
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");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function handleGetFileFromIndexedDB(fileId, sendResponse) {
|
||||
try {
|
||||
const db = await openIndexedDB();
|
||||
const transaction = db.transaction(["files"], "readonly");
|
||||
const objectStore = transaction.objectStore("files");
|
||||
|
||||
const getRequest = objectStore.get(fileId);
|
||||
|
||||
getRequest.onsuccess = async function (event) {
|
||||
if (getRequest.result) {
|
||||
const file = getRequest.result.data;
|
||||
|
||||
try {
|
||||
const base64String = await fileToBase64(file);
|
||||
|
||||
// Create a new transaction to delete the file
|
||||
const deleteTransaction = db.transaction(["files"], "readwrite");
|
||||
const deleteObjectStore = deleteTransaction.objectStore("files");
|
||||
const deleteRequest = deleteObjectStore.delete(fileId);
|
||||
|
||||
deleteRequest.onsuccess = function () {
|
||||
try {
|
||||
sendResponse({ result: base64String });
|
||||
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
};
|
||||
|
||||
deleteRequest.onerror = function () {
|
||||
console.error(`Error deleting file with ID ${fileId} from IndexedDB`);
|
||||
sendResponse({ result: null, error: "Failed to delete file from IndexedDB" });
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error converting file to Base64:", error);
|
||||
sendResponse({ result: null, error: "Failed to convert file to Base64" });
|
||||
}
|
||||
} else {
|
||||
console.error(`File with ID ${fileId} not found in IndexedDB`);
|
||||
sendResponse({ result: null, error: "File not found in IndexedDB" });
|
||||
}
|
||||
};
|
||||
|
||||
getRequest.onerror = function () {
|
||||
console.error(`Error retrieving file with ID ${fileId} from IndexedDB`);
|
||||
sendResponse({ result: null, error: "Error retrieving file from IndexedDB" });
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error opening IndexedDB:", error);
|
||||
sendResponse({ result: null, error: "Error opening IndexedDB" });
|
||||
}
|
||||
}
|
||||
|
||||
const UIQortalRequests = [
|
||||
'GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS',
|
||||
'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL',
|
||||
'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT', 'GET_USER_WALLET',
|
||||
'GET_WALLET_BALANCE', 'GET_USER_WALLET_INFO', 'GET_CROSSCHAIN_SERVER_INFO',
|
||||
'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE',
|
||||
'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER',
|
||||
'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY'
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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);
|
||||
|
||||
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");
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
const showSaveFilePicker = async (data) => {
|
||||
let blob
|
||||
let fileName
|
||||
try {
|
||||
const {filename, mimeType, fileHandleOptions, fileId} = data
|
||||
blob = await retrieveFileFromIndexedDB(fileId)
|
||||
fileName = filename
|
||||
|
||||
const fileHandle = await window.showSaveFilePicker({
|
||||
suggestedName: filename,
|
||||
types: [
|
||||
{
|
||||
description: mimeType,
|
||||
...fileHandleOptions
|
||||
}
|
||||
]
|
||||
})
|
||||
const writeFile = async (fileHandle, contents) => {
|
||||
const writable = await fileHandle.createWritable()
|
||||
await writable.write(contents)
|
||||
await writable.close()
|
||||
}
|
||||
writeFile(fileHandle, blob).then(() => console.log("FILE SAVED"))
|
||||
} catch (error) {
|
||||
FileSaver.saveAs(blob, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
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 = "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 = "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 + "_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) => {
|
||||
const [path, setPath] = useState('')
|
||||
const [history, setHistory] = useState({
|
||||
customQDNHistoryPaths: [],
|
||||
currentIndex: -1,
|
||||
isDOMContentLoaded: false
|
||||
})
|
||||
const setHasSettingsChangedAtom = useSetRecoilState(navigationControllerAtom);
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
if(tabId && !isNaN(history?.currentIndex)){
|
||||
setHasSettingsChangedAtom((prev)=> {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
hasBack: history?.currentIndex > 0,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [history?.currentIndex, tabId])
|
||||
|
||||
|
||||
const changeCurrentIndex = useCallback((value)=> {
|
||||
setHistory((prev)=> {
|
||||
return {
|
||||
...prev,
|
||||
currentIndex: value
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const resetHistory = useCallback(()=> {
|
||||
setHistory({
|
||||
customQDNHistoryPaths: [],
|
||||
currentIndex: -1,
|
||||
isManualNavigation: true,
|
||||
isDOMContentLoaded: false
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const listener = async (event) => {
|
||||
console.log('eventreactt', event)
|
||||
// event.preventDefault(); // Prevent default behavior
|
||||
// event.stopImmediatePropagation(); // Stop other listeners from firing
|
||||
|
||||
if (event?.data?.requestedHandler !== 'UI') return;
|
||||
|
||||
const sendMessageToRuntime = (message, eventPort) => {
|
||||
chrome?.runtime?.sendMessage(message, (response) => {
|
||||
if (response.error) {
|
||||
eventPort.postMessage({
|
||||
result: null,
|
||||
error: response,
|
||||
});
|
||||
} else {
|
||||
eventPort.postMessage({
|
||||
result: response,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 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 },
|
||||
event.ports[0]
|
||||
);
|
||||
} else if (
|
||||
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
|
||||
event?.data?.action === 'PUBLISH_QDN_RESOURCE' ||
|
||||
event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'SAVE_FILE'
|
||||
|
||||
) {
|
||||
let data;
|
||||
try {
|
||||
data = await storeFilesInIndexedDB(event.data);
|
||||
} catch (error) {
|
||||
console.error('Error storing files in IndexedDB:', error);
|
||||
event.ports[0].postMessage({
|
||||
result: null,
|
||||
error: 'Failed to store files in IndexedDB',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (data) {
|
||||
sendMessageToRuntime(
|
||||
{ action: event.data.action, type: 'qortalRequest', payload: data, isExtension: true },
|
||||
event.ports[0]
|
||||
);
|
||||
} else {
|
||||
event.ports[0].postMessage({
|
||||
result: null,
|
||||
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)
|
||||
} 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)) {
|
||||
console.log('customQDNHistoryPaths.length', prev?.customQDNHistoryPaths.length)
|
||||
return {
|
||||
...prev,
|
||||
currentIndex: prev.customQDNHistoryPaths.length - 1 === -1 ? 0 : prev.customQDNHistoryPaths.length - 1
|
||||
}
|
||||
}
|
||||
const copyHistory = {...prev}
|
||||
const paths = [...(copyHistory?.customQDNHistoryPaths || []), ...(event?.data?.payload?.customQDNHistoryPaths || [])]
|
||||
console.log('paths', paths)
|
||||
return {
|
||||
...prev,
|
||||
customQDNHistoryPaths: paths,
|
||||
currentIndex: paths.length - 1
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setHistory(event?.data?.payload)
|
||||
|
||||
}
|
||||
} else if(event?.data?.action === 'SET_TAB'){
|
||||
executeEvent("addTab", {
|
||||
data: event?.data?.payload
|
||||
})
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ action: 'SET_TAB_SUCCESS', requestedHandler: 'UI',payload: {
|
||||
name: event?.data?.payload?.name
|
||||
} }, '*'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Add the listener for messages coming from the frameWindow
|
||||
frameWindow.addEventListener('message', listener);
|
||||
|
||||
// Cleanup function to remove the event listener when the component is unmounted
|
||||
return () => {
|
||||
frameWindow.removeEventListener('message', listener);
|
||||
};
|
||||
|
||||
|
||||
}, []); // Empty dependency array to run once when the component mounts
|
||||
|
||||
chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) {
|
||||
if(message.action === "SHOW_SAVE_FILE_PICKER"){
|
||||
showSaveFilePicker(message?.data)
|
||||
}
|
||||
|
||||
else if (message.action === "getFileFromIndexedDB") {
|
||||
handleGetFileFromIndexedDB(message.fileId, sendResponse);
|
||||
return true; // Keep the message channel open for async response
|
||||
}
|
||||
});
|
||||
|
||||
return {path, history, resetHistory, changeCurrentIndex}
|
||||
};
|
||||
|
@@ -255,7 +255,7 @@ export const AnnouncementDiscussion = ({
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: isMobile ? '100%' : "100vh",
|
||||
height: isMobile ? '100%' : "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
|
@@ -11,7 +11,7 @@ import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
|
||||
import { getNameInfo } from '../Group/Group';
|
||||
import { Spacer } from '../../common/Spacer';
|
||||
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
|
||||
import { getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App';
|
||||
import { getBaseApiReact, getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App';
|
||||
import { getPublicKey } from '../../background';
|
||||
import { useMessageQueue } from '../../MessageQueueContext';
|
||||
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
|
||||
@@ -77,9 +77,28 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
}, [selectedDirect?.address])
|
||||
|
||||
|
||||
|
||||
const middletierFunc = async (data: any, selectedDirectAddress: string, myAddress: string) => {
|
||||
try {
|
||||
if (hasInitialized.current) {
|
||||
decryptMessages(data, true);
|
||||
return;
|
||||
}
|
||||
hasInitialized.current = true;
|
||||
const url = `${getBaseApiReact()}/chat/messages?involving=${selectedDirectAddress}&involving=${myAddress}&encoding=BASE64&limit=0&reverse=false`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
decryptMessages(responseData, false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const decryptMessages = (encryptedMessages: any[])=> {
|
||||
const decryptMessages = (encryptedMessages: any[], isInitiated: boolean)=> {
|
||||
try {
|
||||
return new Promise((res, rej)=> {
|
||||
chrome?.runtime?.sendMessage({ action: "decryptDirect", payload: {
|
||||
@@ -92,7 +111,7 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
processWithNewMessages(response, selectedDirect?.address)
|
||||
|
||||
res(response)
|
||||
if(hasInitialized.current){
|
||||
if(isInitiated){
|
||||
|
||||
const formatted = response.map((item: any)=> {
|
||||
return {
|
||||
@@ -127,7 +146,6 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
|
||||
const forceCloseWebSocket = () => {
|
||||
if (socketRef.current) {
|
||||
console.log('Force closing the WebSocket');
|
||||
clearTimeout(timeoutIdRef.current);
|
||||
clearTimeout(groupSocketTimeoutRef.current);
|
||||
socketRef.current.close(1000, 'forced');
|
||||
@@ -161,7 +179,6 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
socketRef.current = new WebSocket(socketLink);
|
||||
|
||||
socketRef.current.onopen = () => {
|
||||
console.log('WebSocket connection opened');
|
||||
setTimeout(pingWebSocket, 50); // Initial ping
|
||||
};
|
||||
|
||||
@@ -171,7 +188,8 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
clearTimeout(timeoutIdRef.current);
|
||||
groupSocketTimeoutRef.current = setTimeout(pingWebSocket, 45000); // Ping every 45 seconds
|
||||
} else {
|
||||
decryptMessages(JSON.parse(e.data));
|
||||
middletierFunc(JSON.parse(e.data), selectedDirect?.address, myAddress)
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -97,27 +97,28 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
}
|
||||
|
||||
const middletierFunc = async (data: any, groupId: string) => {
|
||||
try {
|
||||
if (hasInitialized.current) {
|
||||
decryptMessages(data, true);
|
||||
return;
|
||||
}
|
||||
hasInitialized.current = true;
|
||||
const url = `${getBaseApiReact()}/chat/messages?txGroupId=${groupId}&encoding=BASE64&limit=0&reverse=false`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
decryptMessages(responseData, false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
try {
|
||||
if (hasInitialized.current) {
|
||||
decryptMessages(data, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
hasInitialized.current = true;
|
||||
const url = `${getBaseApiReact()}/chat/messages?txGroupId=${groupId}&encoding=BASE64&limit=0&reverse=false`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
decryptMessages(responseData, false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const decryptMessages = ( encryptedMessages: any[], isInitiated: boolean )=> {
|
||||
|
||||
const decryptMessages = (encryptedMessages: any[], isInitiated: boolean )=> {
|
||||
try {
|
||||
if(!secretKeyRef.current){
|
||||
checkForFirstSecretKeyNotification(encryptedMessages)
|
||||
@@ -231,6 +232,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
}
|
||||
} )
|
||||
setMessages(formatted)
|
||||
|
||||
setChatReferences((prev) => {
|
||||
let organizedChatReferences = { ...prev };
|
||||
|
||||
|
@@ -1,21 +1,33 @@
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
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'
|
||||
|
||||
export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, handleReaction, chatReferences, tempChatReferences }) => {
|
||||
|
||||
const virtuosoRef = useRef();
|
||||
const parentRef = useRef();
|
||||
const [messages, setMessages] = useState(initialMessages);
|
||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||
const hasLoadedInitialRef = useRef(false);
|
||||
const isAtBottomRef = useRef(true); //
|
||||
const isAtBottomRef = useRef(true);
|
||||
// const [ref, inView] = useInView({
|
||||
// threshold: 0.7
|
||||
// })
|
||||
|
||||
// useEffect(() => {
|
||||
// if (inView) {
|
||||
|
||||
// }
|
||||
// }, [inView])
|
||||
// Update message list with unique signatures and tempMessages
|
||||
useEffect(() => {
|
||||
let uniqueInitialMessagesMap = new Map();
|
||||
|
||||
// Only add a message if it doesn't already exist in the Map
|
||||
initialMessages.forEach((message) => {
|
||||
uniqueInitialMessagesMap.set(message.signature, message);
|
||||
if (!uniqueInitialMessagesMap.has(message.signature)) {
|
||||
uniqueInitialMessagesMap.set(message.signature, message);
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueInitialMessages = Array.from(uniqueInitialMessagesMap.values()).sort(
|
||||
@@ -29,22 +41,14 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
|
||||
setTimeout(() => {
|
||||
const hasUnreadMessages = totalMessages.some((msg) => msg.unread && !msg?.chatReference);
|
||||
|
||||
if (virtuosoRef.current) {
|
||||
|
||||
|
||||
if (virtuosoRef.current && !isAtBottomRef.current && hasUnreadMessages) {
|
||||
|
||||
|
||||
|
||||
|
||||
setShowScrollButton(hasUnreadMessages);
|
||||
if (parentRef.current) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
|
||||
const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed
|
||||
if (!atBottom && hasUnreadMessages) {
|
||||
setShowScrollButton(hasUnreadMessages);
|
||||
} else {
|
||||
handleMessageSeen();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if (!hasLoadedInitialRef.current) {
|
||||
scrollToBottom(totalMessages);
|
||||
@@ -53,7 +57,14 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
}, 500);
|
||||
}, [initialMessages, tempMessages]);
|
||||
|
||||
|
||||
const scrollToBottom = (initialMsgs) => {
|
||||
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1;
|
||||
if (rowVirtualizer) {
|
||||
rowVirtualizer.scrollToIndex(index, { align: 'end' });
|
||||
}
|
||||
handleMessageSeen()
|
||||
};
|
||||
|
||||
|
||||
const handleMessageSeen = useCallback(() => {
|
||||
setMessages((prevMessages) =>
|
||||
@@ -62,34 +73,16 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
unread: false,
|
||||
}))
|
||||
);
|
||||
setShowScrollButton(false)
|
||||
}, []);
|
||||
|
||||
const scrollToItem = useCallback((index) => {
|
||||
if (virtuosoRef.current) {
|
||||
virtuosoRef.current.scrollToIndex({ index, behavior: 'smooth' });
|
||||
}
|
||||
}, []);
|
||||
// const scrollToBottom = (initialMsgs) => {
|
||||
// const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1;
|
||||
// if (parentRef.current) {
|
||||
// parentRef.current.scrollToIndex(index);
|
||||
// }
|
||||
// };
|
||||
|
||||
const scrollToBottom = (initialMsgs) => {
|
||||
|
||||
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1
|
||||
if (virtuosoRef.current) {
|
||||
virtuosoRef.current.scrollToIndex({ index});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleScroll = (scrollState) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollState;
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50;
|
||||
const hasUnreadMessages = messages.some((msg) => msg.unread);
|
||||
|
||||
if (isAtBottom) {
|
||||
handleMessageSeen();
|
||||
}
|
||||
|
||||
setShowScrollButton(!isAtBottom && hasUnreadMessages);
|
||||
};
|
||||
|
||||
const sentNewMessageGroupFunc = useCallback(() => {
|
||||
scrollToBottom();
|
||||
@@ -102,96 +95,134 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
};
|
||||
}, [sentNewMessageGroupFunc]);
|
||||
|
||||
const rowRenderer = (index) => {
|
||||
let message = messages[index];
|
||||
let replyIndex = messages.findIndex((msg)=> msg?.signature === message?.repliedTo)
|
||||
let reply
|
||||
let reactions = null
|
||||
if(message?.repliedTo && replyIndex !== -1){
|
||||
reply = messages[replyIndex]
|
||||
}
|
||||
if(message?.message && message?.groupDirectId){
|
||||
replyIndex = messages.findIndex((msg)=> msg?.signature === message?.message?.repliedTo)
|
||||
reply
|
||||
if(message?.message?.repliedTo && replyIndex !== -1){
|
||||
reply = messages[replyIndex]
|
||||
}
|
||||
message = {
|
||||
...(message?.message || {}),
|
||||
isTemp: true,
|
||||
unread: false
|
||||
const lastSignature = useMemo(()=> {
|
||||
if(!messages || messages?.length === 0) return null
|
||||
const lastIndex = messages.length - 1
|
||||
return messages[lastIndex]?.signature
|
||||
}, [messages])
|
||||
|
||||
|
||||
// Initialize the virtualizer
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: messages.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed
|
||||
overscan: 10, // Number of items to render outside the visible area to improve smoothness
|
||||
measureElement:
|
||||
typeof window !== 'undefined' &&
|
||||
navigator.userAgent.indexOf('Firefox') === -1
|
||||
? element => {
|
||||
return element?.getBoundingClientRect().height
|
||||
}
|
||||
}
|
||||
|
||||
if(chatReferences && chatReferences[message?.signature]){
|
||||
if(chatReferences[message.signature]?.reactions){
|
||||
reactions = chatReferences[message.signature]?.reactions
|
||||
}
|
||||
}
|
||||
let isUpdating = false
|
||||
if(tempChatReferences && tempChatReferences?.find((item)=> item?.chatReference === message?.signature)){
|
||||
isUpdating = true
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '10px 0', display: 'flex', justifyContent: 'center', width: '100%', minHeight: '50px' , overscrollBehavior: "none"}}>
|
||||
<MessageItem
|
||||
isLast={index === messages.length - 1}
|
||||
message={message}
|
||||
onSeen={handleMessageSeen}
|
||||
isTemp={!!message?.isTemp}
|
||||
myAddress={myAddress}
|
||||
onReply={onReply}
|
||||
reply={reply}
|
||||
replyIndex={replyIndex}
|
||||
scrollToItem={scrollToItem}
|
||||
handleReaction={handleReaction}
|
||||
reactions={reactions}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleAtBottomStateChange = (atBottom) => {
|
||||
isAtBottomRef.current = atBottom;
|
||||
if(atBottom){
|
||||
handleMessageSeen();
|
||||
setShowScrollButton(false)
|
||||
}
|
||||
};
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={messages}
|
||||
itemContent={rowRenderer}
|
||||
atBottomThreshold={50}
|
||||
followOutput="smooth"
|
||||
atBottomStateChange={handleAtBottomStateChange} // Detect bottom status
|
||||
increaseViewportBy={3000}
|
||||
|
||||
/>
|
||||
<>
|
||||
<div ref={parentRef} style={{ height: '100%', overflow: 'auto', position: 'relative', display: 'flex' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center', // Center items horizontally
|
||||
gap: '10px', // Add gap between items
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const index = virtualRow.index;
|
||||
let message = messages[index];
|
||||
let replyIndex = messages.findIndex((msg) => msg?.signature === message?.repliedTo);
|
||||
let reply;
|
||||
let reactions = null;
|
||||
|
||||
{showScrollButton && (
|
||||
<button
|
||||
onClick={()=> scrollToBottom()}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
backgroundColor: '#ff5a5f',
|
||||
color: 'white',
|
||||
padding: '10px 20px',
|
||||
borderRadius: '20px',
|
||||
cursor: 'pointer',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
Scroll to Unread Messages
|
||||
</button>
|
||||
)}
|
||||
if (message?.repliedTo && replyIndex !== -1) {
|
||||
reply = messages[replyIndex];
|
||||
}
|
||||
|
||||
if (message?.message && message?.groupDirectId) {
|
||||
replyIndex = messages.findIndex((msg) => msg?.signature === message?.message?.repliedTo);
|
||||
if (message?.message?.repliedTo && replyIndex !== -1) {
|
||||
reply = messages[replyIndex];
|
||||
}
|
||||
message = {
|
||||
...(message?.message || {}),
|
||||
isTemp: true,
|
||||
unread: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (chatReferences && chatReferences[message?.signature]) {
|
||||
if (chatReferences[message.signature]?.reactions) {
|
||||
reactions = chatReferences[message.signature]?.reactions;
|
||||
}
|
||||
}
|
||||
|
||||
let isUpdating = false;
|
||||
if (tempChatReferences && tempChatReferences?.find((item) => item?.chatReference === message?.signature)) {
|
||||
isUpdating = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-index={virtualRow.index} //needed for dynamic row height measurement
|
||||
ref={node => rowVirtualizer.measureElement(node)} //measure dynamic row height
|
||||
key={message.signature}
|
||||
style={{
|
||||
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',
|
||||
justifyContent: 'center',
|
||||
overscrollBehavior: 'none',
|
||||
}}
|
||||
>
|
||||
<MessageItem
|
||||
isLast={index === messages.length - 1}
|
||||
lastSignature={lastSignature}
|
||||
message={message}
|
||||
onSeen={handleMessageSeen}
|
||||
isTemp={!!message?.isTemp}
|
||||
myAddress={myAddress}
|
||||
onReply={onReply}
|
||||
reply={reply}
|
||||
replyIndex={replyIndex}
|
||||
scrollToItem={(idx) => rowVirtualizer.scrollToIndex(idx)}
|
||||
handleReaction={handleReaction}
|
||||
reactions={reactions}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{showScrollButton && (
|
||||
<button
|
||||
onClick={() => scrollToBottom()}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
backgroundColor: '#ff5a5f',
|
||||
color: 'white',
|
||||
padding: '10px 20px',
|
||||
borderRadius: '20px',
|
||||
cursor: 'pointer',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
Scroll to Unread Messages
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
@@ -29,7 +29,8 @@ export const MessageItem = ({
|
||||
scrollToItem,
|
||||
handleReaction,
|
||||
reactions,
|
||||
isUpdating
|
||||
isUpdating,
|
||||
lastSignature
|
||||
}) => {
|
||||
const { ref, inView } = useInView({
|
||||
threshold: 0.7, // Fully visible
|
||||
@@ -42,9 +43,10 @@ export const MessageItem = ({
|
||||
}
|
||||
}, [inView, message.id, message.unread, onSeen]);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={isLast ? ref : null}
|
||||
ref={lastSignature === message?.signature ? ref : null}
|
||||
style={{
|
||||
padding: "10px",
|
||||
backgroundColor: "#232428",
|
||||
|
144
src/components/ContextMenuPinnedApps.tsx
Normal file
144
src/components/ContextMenuPinnedApps.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material';
|
||||
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||
import { saveToLocalStorage } from './Apps/AppsNavBar';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { sortablePinnedAppsAtom } from '../atoms/global';
|
||||
|
||||
const CustomStyledMenu = styled(Menu)(({ theme }) => ({
|
||||
'& .MuiPaper-root': {
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '12px',
|
||||
padding: theme.spacing(1),
|
||||
boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
|
||||
},
|
||||
'& .MuiMenuItem-root': {
|
||||
fontSize: '14px',
|
||||
color: '#444',
|
||||
transition: '0.3s background-color',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f0f0f0',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const 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 handleContextMenu = (event) => {
|
||||
if(isMine) return
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
preventClick.current = true;
|
||||
setMenuPosition({
|
||||
mouseX: event.clientX,
|
||||
mouseY: event.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) => {
|
||||
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>
|
||||
);
|
||||
};
|
@@ -13,21 +13,24 @@ 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 AppIcon from "../../assets/svgs/AppIcon.svg";
|
||||
|
||||
const IconWrapper = ({ children, label, color, selected }) => {
|
||||
import { HomeIcon } from "../../assets/Icons/HomeIcon";
|
||||
import { Save } from "../Save/Save";
|
||||
|
||||
export const IconWrapper = ({ children, label, color, selected }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
gap: "5px",
|
||||
flexDirection: "column",
|
||||
height: '89px',
|
||||
width: '89px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: selected ? 'rgba(28, 29, 32, 1)' : 'transparent'
|
||||
height: "89px",
|
||||
width: "89px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: selected ? "rgba(28, 29, 32, 1)" : "transparent",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -69,9 +72,17 @@ export const DesktopFooter = ({
|
||||
isHome,
|
||||
isGroups,
|
||||
isDirects,
|
||||
setDesktopSideView
|
||||
setDesktopSideView,
|
||||
isApps,
|
||||
setDesktopViewMode,
|
||||
desktopViewMode,
|
||||
hide,
|
||||
setIsOpenSideViewDirects,
|
||||
setIsOpenSideViewGroups
|
||||
|
||||
}) => {
|
||||
const [value, setValue] = React.useState(0);
|
||||
|
||||
if(hide) return
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -82,37 +93,93 @@ export const DesktopFooter = ({
|
||||
alignItems: "center",
|
||||
height: "100px", // Footer height
|
||||
zIndex: 1,
|
||||
justifyContent: 'center'
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '20px'
|
||||
}}>
|
||||
<ButtonBase onClick={()=> {
|
||||
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>
|
||||
</ButtonBase>
|
||||
<ButtonBase onClick={()=> {
|
||||
setDesktopSideView('groups')
|
||||
}}>
|
||||
<IconWrapper color="rgba(250, 250, 250, 0.5)" label="Hubs" selected={isGroups}>
|
||||
<HubsIcon height={30} color={hasUnreadGroups ? "var(--unread)" : isGroups ? 'white' : "rgba(250, 250, 250, 0.5)"} />
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<ButtonBase onClick={()=> {
|
||||
setDesktopSideView('directs')
|
||||
}}>
|
||||
|
||||
<IconWrapper color="rgba(250, 250, 250, 0.5)" label="Messaging" selected={isDirects}>
|
||||
<MessagingIcon height={30} color={hasUnreadDirects ? "var(--unread)" : isDirects ? 'white' : "rgba(250, 250, 250, 0.5)"} />
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
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>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopViewMode('apps')
|
||||
setIsOpenSideViewDirects(false)
|
||||
setIsOpenSideViewGroups(false)
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
label="Apps"
|
||||
selected={isApps}
|
||||
>
|
||||
<img src={AppIcon} />
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView("groups");
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
label="Hubs"
|
||||
selected={isGroups}
|
||||
>
|
||||
<HubsIcon
|
||||
height={30}
|
||||
color={
|
||||
hasUnreadGroups
|
||||
? "var(--unread)"
|
||||
: isGroups
|
||||
? "white"
|
||||
: "rgba(250, 250, 250, 0.5)"
|
||||
}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView("directs");
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
label="Messaging"
|
||||
selected={isDirects}
|
||||
>
|
||||
<MessagingIcon
|
||||
height={30}
|
||||
color={
|
||||
hasUnreadDirects
|
||||
? "var(--unread)"
|
||||
: isDirects
|
||||
? "white"
|
||||
: "rgba(250, 250, 250, 0.5)"
|
||||
}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
|
||||
<Save isDesktop />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@@ -11,6 +11,7 @@ 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}) => {
|
||||
|
||||
const toggleDrawer = (newOpen: boolean) => () => {
|
||||
@@ -21,7 +22,7 @@ export const DrawerComponent = ({open, setOpen, children}) => {
|
||||
return (
|
||||
<div>
|
||||
<Drawer open={open} onClose={toggleDrawer(false)}>
|
||||
<Box sx={{ width: 400, height: '100%' }} role="presentation">
|
||||
<Box sx={{ width: isMobile ? '100vw' : '400px', height: '100%' }} role="presentation">
|
||||
|
||||
{children}
|
||||
</Box>
|
||||
|
@@ -536,10 +536,14 @@ export const GroupMail = ({
|
||||
});
|
||||
|
||||
// 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.threadData?.createdAt - a.threadData?.createdAt);
|
||||
const sortedList = Array.from(uniqueItems.values()).sort((a, b) =>
|
||||
filterMode === 'Oldest'
|
||||
? a.threadData?.createdAt - b.threadData?.createdAt
|
||||
: b.threadData?.createdAt - a.threadData?.createdAt
|
||||
);
|
||||
|
||||
return sortedList;
|
||||
}, [tempPublishedList, listOfThreadsToDisplay]);
|
||||
}, [tempPublishedList, listOfThreadsToDisplay, filterMode]);
|
||||
|
||||
if (currentThread)
|
||||
return (
|
||||
|
@@ -754,29 +754,7 @@ export const GroupContainer = styled(Box)`
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-track:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
height: 10px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #838eee;
|
||||
border-radius: 8px;
|
||||
background-clip: content-box;
|
||||
border: 4px solid transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #6270f0;
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
|
@@ -88,6 +88,9 @@ import { ExitIcon } from "../../assets/Icons/ExitIcon";
|
||||
import { HomeDesktop } from "./HomeDesktop";
|
||||
import { DesktopFooter } from "../Desktop/DesktopFooter";
|
||||
import { DesktopHeader } from "../Desktop/DesktopHeader";
|
||||
import { Apps } from "../Apps/Apps";
|
||||
import { AppsNavBar } from "../Apps/AppsNavBar";
|
||||
import { AppsDesktop } from "../Apps/AppsDesktop";
|
||||
|
||||
// let touchStartY = 0;
|
||||
// let disablePullToRefresh = false;
|
||||
@@ -373,7 +376,11 @@ export const Group = ({
|
||||
isOpenDrawerProfile,
|
||||
setIsOpenDrawerProfile,
|
||||
logoutFunc,
|
||||
setDesktopViewMode,
|
||||
desktopViewMode
|
||||
}: GroupProps) => {
|
||||
const [desktopSideView, setDesktopSideView] = useState('groups')
|
||||
|
||||
const [secretKey, setSecretKey] = useState(null);
|
||||
const [secretKeyPublishDate, setSecretKeyPublishDate] = useState(null);
|
||||
const lastFetchedSecretKey = useRef(null);
|
||||
@@ -418,7 +425,6 @@ export const Group = ({
|
||||
const [mutedGroups, setMutedGroups] = useState([]);
|
||||
const [mobileViewMode, setMobileViewMode] = useState("home");
|
||||
const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState("");
|
||||
const [desktopSideView, setDesktopSideView] = useState('groups')
|
||||
const isFocusedRef = useRef(true);
|
||||
const timestampEnterDataRef = useRef({});
|
||||
const selectedGroupRef = useRef(null);
|
||||
@@ -431,7 +437,22 @@ export const Group = ({
|
||||
const { clearStatesMessageQueueProvider } = useMessageQueue();
|
||||
const initiatedGetMembers = useRef(false);
|
||||
const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({});
|
||||
const [appsMode, setAppsMode] = useState('home')
|
||||
const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false)
|
||||
const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false)
|
||||
|
||||
const toggleSideViewDirects = ()=> {
|
||||
if(isOpenSideViewGroups){
|
||||
setIsOpenSideViewGroups(false)
|
||||
}
|
||||
setIsOpenSideViewDirects((prev)=> !prev)
|
||||
}
|
||||
const toggleSideViewGroups = ()=> {
|
||||
if(isOpenSideViewDirects){
|
||||
setIsOpenSideViewDirects(false)
|
||||
}
|
||||
setIsOpenSideViewGroups((prev)=> !prev)
|
||||
}
|
||||
useEffect(()=> {
|
||||
timestampEnterDataRef.current = timestampEnterData
|
||||
}, [timestampEnterData])
|
||||
@@ -821,98 +842,7 @@ export const Group = ({
|
||||
}
|
||||
}, [selectedGroup]);
|
||||
|
||||
// const handleNotification = async (data)=> {
|
||||
// try {
|
||||
// if(isFocusedRef.current){
|
||||
// throw new Error('isFocused')
|
||||
// }
|
||||
// const newActiveChats= data
|
||||
// const oldActiveChats = await new Promise((res, rej) => {
|
||||
// chrome?.runtime?.sendMessage(
|
||||
// {
|
||||
// action: "getChatHeads",
|
||||
// },
|
||||
// (response) => {
|
||||
// console.log({ response });
|
||||
// if (!response?.error) {
|
||||
// res(response);
|
||||
// }
|
||||
// rej(response.error);
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
|
||||
// let results = []
|
||||
// newActiveChats?.groups?.forEach(newChat => {
|
||||
// let isNewer = true;
|
||||
// oldActiveChats?.data?.groups?.forEach(oldChat => {
|
||||
// if (newChat?.timestamp <= oldChat?.timestamp) {
|
||||
// isNewer = false;
|
||||
// }
|
||||
// });
|
||||
// if (isNewer) {
|
||||
// results.push(newChat)
|
||||
// console.log('This newChat is newer than all oldChats:', newChat);
|
||||
// }
|
||||
// });
|
||||
|
||||
// if(results?.length > 0){
|
||||
// if (!lastGroupNotification.current || (Date.now() - lastGroupNotification.current >= 60000)) {
|
||||
// console.log((Date.now() - lastGroupNotification.current >= 60000), lastGroupNotification.current)
|
||||
// chrome?.runtime?.sendMessage(
|
||||
// {
|
||||
// action: "notification",
|
||||
// payload: {
|
||||
// },
|
||||
// },
|
||||
// (response) => {
|
||||
// console.log({ response });
|
||||
// if (!response?.error) {
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
// );
|
||||
// audio.play();
|
||||
// lastGroupNotification.current = Date.now()
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
// } catch (error) {
|
||||
// console.log('error not', error)
|
||||
// if(!isFocusedRef.current){
|
||||
// chrome?.runtime?.sendMessage(
|
||||
// {
|
||||
// action: "notification",
|
||||
// payload: {
|
||||
// },
|
||||
// },
|
||||
// (response) => {
|
||||
// console.log({ response });
|
||||
// if (!response?.error) {
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
// );
|
||||
// audio.play();
|
||||
// lastGroupNotification.current = Date.now()
|
||||
// }
|
||||
|
||||
// } finally {
|
||||
|
||||
// chrome?.runtime?.sendMessage(
|
||||
// {
|
||||
// action: "setChatHeads",
|
||||
// payload: {
|
||||
// data,
|
||||
// },
|
||||
// }
|
||||
// );
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
const getAdmins = async (groupId) => {
|
||||
try {
|
||||
@@ -1176,6 +1106,7 @@ export const Group = ({
|
||||
if (findDirect) {
|
||||
if(!isMobile){
|
||||
setDesktopSideView("directs");
|
||||
setDesktopViewMode('home')
|
||||
} else {
|
||||
setMobileViewModeKeepOpen("messaging");
|
||||
}
|
||||
@@ -1213,6 +1144,7 @@ export const Group = ({
|
||||
if (findDirect) {
|
||||
if(!isMobile){
|
||||
setDesktopSideView("directs");
|
||||
setDesktopViewMode('home')
|
||||
} else {
|
||||
setMobileViewModeKeepOpen("messaging");
|
||||
}
|
||||
@@ -1236,6 +1168,7 @@ export const Group = ({
|
||||
} else {
|
||||
if(!isMobile){
|
||||
setDesktopSideView("directs");
|
||||
setDesktopViewMode('home')
|
||||
} else {
|
||||
setMobileViewModeKeepOpen("messaging");
|
||||
}
|
||||
@@ -1402,6 +1335,8 @@ export const Group = ({
|
||||
setTimeout(() => {
|
||||
setSelectedGroup(findGroup);
|
||||
setMobileViewMode("group");
|
||||
setDesktopSideView('groups')
|
||||
setDesktopViewMode('home')
|
||||
getTimestampEnterChat();
|
||||
isLoadingOpenSectionFromNotification.current = false;
|
||||
}, 200);
|
||||
@@ -1449,7 +1384,8 @@ export const Group = ({
|
||||
setTimeout(() => {
|
||||
setSelectedGroup(findGroup);
|
||||
setMobileViewMode("group");
|
||||
|
||||
setDesktopSideView('groups')
|
||||
setDesktopViewMode('home')
|
||||
getGroupAnnouncements();
|
||||
}, 200);
|
||||
}
|
||||
@@ -1504,6 +1440,8 @@ export const Group = ({
|
||||
setTimeout(() => {
|
||||
setSelectedGroup(findGroup);
|
||||
setMobileViewMode("group");
|
||||
setDesktopSideView('groups')
|
||||
setDesktopViewMode('home')
|
||||
getGroupAnnouncements();
|
||||
}, 200);
|
||||
}
|
||||
@@ -1527,6 +1465,8 @@ export const Group = ({
|
||||
}
|
||||
if (!isMobile) {
|
||||
}
|
||||
setDesktopViewMode('home')
|
||||
|
||||
setGroupSection("default");
|
||||
clearAllQueues();
|
||||
await new Promise((res) => {
|
||||
@@ -1550,6 +1490,8 @@ export const Group = ({
|
||||
setMemberCountFromSecretKeyData(null);
|
||||
setTriedToFetchSecretKey(false);
|
||||
setFirstSecretKeyInCreation(false);
|
||||
setIsOpenSideViewDirects(false)
|
||||
setIsOpenSideViewGroups(false)
|
||||
};
|
||||
|
||||
const goToAnnouncements = async () => {
|
||||
@@ -2025,6 +1967,8 @@ export const Group = ({
|
||||
// }
|
||||
onClick={() => {
|
||||
setMobileViewMode("group");
|
||||
setDesktopSideView('groups')
|
||||
setDesktopViewMode('home')
|
||||
initiatedGetMembers.current = false;
|
||||
clearAllQueues();
|
||||
setSelectedDirect(null);
|
||||
@@ -2228,7 +2172,7 @@ export const Group = ({
|
||||
isThin={
|
||||
mobileViewMode === "groups" ||
|
||||
mobileViewMode === "group" ||
|
||||
mobileViewModeKeepOpen === "messaging"
|
||||
mobileViewModeKeepOpen === "messaging" || (mobileViewMode === "apps" && appsMode !== 'home')
|
||||
}
|
||||
logoutFunc={logoutFunc}
|
||||
goToHome={goToHome}
|
||||
@@ -2253,8 +2197,8 @@ export const Group = ({
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
{!isMobile && desktopSideView === 'groups' && renderGroups()}
|
||||
{!isMobile && desktopSideView === 'directs' && renderDirects()}
|
||||
{!isMobile && ((desktopSideView === 'groups' && desktopViewMode !== 'apps') || isOpenSideViewGroups) && renderGroups()}
|
||||
{!isMobile && ((desktopSideView === 'directs' && desktopViewMode !== 'apps') || isOpenSideViewDirects) && renderDirects()}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
@@ -2711,10 +2655,16 @@ export const Group = ({
|
||||
groupsAnnHasUnread}
|
||||
hasUnreadDirects={directChatHasUnread}
|
||||
myName={userInfo?.name || null}
|
||||
isHome={groupSection === "home"}
|
||||
isGroups={desktopSideView === 'groups'}
|
||||
isDirects={desktopSideView === 'directs'}
|
||||
isHome={groupSection === "home" && desktopViewMode === 'home'}
|
||||
isGroups={desktopSideView === 'groups' && desktopViewMode !== 'apps'}
|
||||
isDirects={desktopSideView === 'directs' && desktopViewMode !== 'apps'}
|
||||
setDesktopViewMode={setDesktopViewMode}
|
||||
isApps={desktopViewMode === 'apps'}
|
||||
setDesktopSideView={setDesktopSideView}
|
||||
desktopViewMode={desktopViewMode}
|
||||
hide={desktopViewMode === 'apps'}
|
||||
setIsOpenSideViewDirects={setIsOpenSideViewDirects}
|
||||
setIsOpenSideViewGroups={setIsOpenSideViewGroups}
|
||||
/>
|
||||
)}
|
||||
{isMobile && mobileViewMode === "home" && (
|
||||
@@ -2733,11 +2683,19 @@ export const Group = ({
|
||||
setMobileViewMode={setMobileViewMode}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
!isMobile && !selectedGroup &&
|
||||
groupSection === "home" && (
|
||||
|
||||
<HomeDesktop
|
||||
{isMobile && (
|
||||
<Apps mode={appsMode} setMode={setAppsMode} show={mobileViewMode === "apps"} myName={userInfo?.name} />
|
||||
)}
|
||||
{!isMobile && (
|
||||
<AppsDesktop toggleSideViewGroups={toggleSideViewGroups} toggleSideViewDirects={toggleSideViewDirects} goToHome={goToHome} mode={appsMode} setMode={setAppsMode} setDesktopSideView={setDesktopSideView} hasUnreadDirects={directChatHasUnread} show={desktopViewMode === "apps"} myName={userInfo?.name} isGroups={isOpenSideViewGroups}
|
||||
isDirects={isOpenSideViewDirects} hasUnreadGroups={groupChatHasUnread ||
|
||||
groupsAnnHasUnread} />
|
||||
)}
|
||||
|
||||
|
||||
{!isMobile && !selectedGroup &&
|
||||
groupSection === "home" && desktopViewMode !== "apps" && (
|
||||
<HomeDesktop
|
||||
refreshHomeDataFunc={refreshHomeDataFunc}
|
||||
myAddress={myAddress}
|
||||
isLoadingGroups={isLoadingGroups}
|
||||
@@ -2751,7 +2709,9 @@ export const Group = ({
|
||||
setOpenAddGroup={setOpenAddGroup}
|
||||
setMobileViewMode={setMobileViewMode}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
|
||||
|
||||
</Box>
|
||||
<AuthenticatedContainerInnerRight
|
||||
sx={{
|
||||
@@ -2759,188 +2719,10 @@ export const Group = ({
|
||||
width: "31px",
|
||||
// minWidth: "135px",
|
||||
padding: "5px",
|
||||
display: isMobile ? "none" : "flex",
|
||||
display: (isMobile || desktopViewMode === 'apps') ? "none" : "flex",
|
||||
}}
|
||||
>
|
||||
{/* <Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "3px",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={goToHome}
|
||||
>
|
||||
<HomeIcon
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: groupSection === "home" ? "#1444c7" : "white",
|
||||
opacity: groupSection === "home" ? 1 : 0.4,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: groupSection === "home" ? "#1444c7" : "white",
|
||||
opacity: groupSection === "home" ? 1 : 0.4,
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</Typography>
|
||||
</Box>
|
||||
{selectedGroup && (
|
||||
<>
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "3px",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={goToAnnouncements}
|
||||
>
|
||||
<CampaignIcon
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: isUnread
|
||||
? "red"
|
||||
: groupSection === "announcement"
|
||||
? "#1444c7"
|
||||
: "white",
|
||||
opacity: groupSection === "announcement" ? 1 : 0.4,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: isUnread
|
||||
? "red"
|
||||
: groupSection === "announcement"
|
||||
? "#1444c7"
|
||||
: "white",
|
||||
opacity: groupSection === "announcement" ? 1 : 0.4,
|
||||
}}
|
||||
>
|
||||
Announcements
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "3px",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={goToChat}
|
||||
>
|
||||
<ChatIcon
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: isUnreadChat
|
||||
? "red"
|
||||
: groupSection === "chat"
|
||||
? "#1444c7"
|
||||
: "white",
|
||||
opacity: groupSection === "chat" ? 1 : 0.4,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: isUnreadChat
|
||||
? "red"
|
||||
: groupSection === "chat"
|
||||
? "#1444c7"
|
||||
: "white",
|
||||
opacity: groupSection === "chat" ? 1 : 0.4,
|
||||
}}
|
||||
>
|
||||
Chat
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "3px",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
setGroupSection("forum");
|
||||
setSelectedDirect(null);
|
||||
setNewChat(false);
|
||||
}}
|
||||
>
|
||||
<ForumIcon
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: groupSection === "forum" ? "#1444c7" : "white",
|
||||
opacity: groupSection === "forum" ? 1 : 0.4,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: groupSection === "forum" ? "#1444c7" : "white",
|
||||
opacity: groupSection === "forum" ? 1 : 0.4,
|
||||
}}
|
||||
>
|
||||
Forum
|
||||
</Typography>
|
||||
</Box>
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
onClick={() => setOpenManageMembers(true)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "3px",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<PeopleIcon
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: "white",
|
||||
opacity: 0.4,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: "white",
|
||||
opacity: 0.4,
|
||||
}}
|
||||
>
|
||||
Members
|
||||
</Typography>
|
||||
</Box>
|
||||
<Spacer height="20px" />
|
||||
</>
|
||||
)} */}
|
||||
|
||||
{/* <SettingsIcon
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: "white",
|
||||
}}
|
||||
/> */}
|
||||
|
||||
</AuthenticatedContainerInnerRight>
|
||||
<LoadingSnackbar
|
||||
open={isLoadingGroup}
|
||||
@@ -2958,7 +2740,7 @@ export const Group = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isMobile && mobileViewMode === "home" && !mobileViewModeKeepOpen && (
|
||||
{(isMobile && mobileViewMode === "home" || (isMobile && mobileViewMode === "apps" && appsMode === 'home')) && !mobileViewModeKeepOpen && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
@@ -3000,240 +2782,13 @@ export const Group = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{(isMobile && mobileViewMode === "apps" && appsMode !== 'home') && !mobileViewModeKeepOpen && (
|
||||
<>
|
||||
<AppsNavBar />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// {isMobile && (
|
||||
// <Box
|
||||
// sx={{
|
||||
// display: "flex",
|
||||
// alignItems: "center",
|
||||
// justifyContent: "center",
|
||||
// flexDirection: "column",
|
||||
// width: "100%",
|
||||
// height: "75px", // Keep the height at 75px
|
||||
// background: "rgba(0, 0, 0, 0.1)",
|
||||
// padding: "0px", // Remove unnecessary padding
|
||||
// }}
|
||||
// >
|
||||
// <Grid
|
||||
// container
|
||||
// spacing={0.5}
|
||||
// sx={{ width: "100%", justifyContent: "space-around" }}
|
||||
// >
|
||||
// {selectedGroup && (
|
||||
// <>
|
||||
// <Grid item xs={4} sx={{
|
||||
// display: 'flex'
|
||||
// }}>
|
||||
// <Button
|
||||
// fullWidth
|
||||
// size="small"
|
||||
// variant="contained"
|
||||
// startIcon={<AnnouncementsIcon />}
|
||||
// sx={{
|
||||
// padding: "4px 6px",
|
||||
// color:
|
||||
// groupSection === "announcement" ? "black" : "white",
|
||||
// backgroundColor: isUnread
|
||||
// ? "red"
|
||||
// : groupSection === "announcement"
|
||||
// ? "white"
|
||||
// : "black",
|
||||
// "&:hover": {
|
||||
// backgroundColor: isUnread
|
||||
// ? "red"
|
||||
// : groupSection === "announcement"
|
||||
// ? "white"
|
||||
// : "black",
|
||||
// },
|
||||
// "&:active": {
|
||||
// backgroundColor: isUnread
|
||||
// ? "red"
|
||||
// : groupSection === "announcement"
|
||||
// ? "white"
|
||||
// : "black",
|
||||
// },
|
||||
// "&:focus": {
|
||||
// backgroundColor: isUnread
|
||||
// ? "red"
|
||||
// : groupSection === "announcement"
|
||||
// ? "white"
|
||||
// : "black",
|
||||
// },
|
||||
// }}
|
||||
// onClick={goToAnnouncements}
|
||||
// >
|
||||
// ANN
|
||||
// </Button>
|
||||
// </Grid>
|
||||
// <Grid item xs={4} sx={{
|
||||
// display: 'flex'
|
||||
// }}>
|
||||
// <Button
|
||||
// fullWidth
|
||||
// size="small"
|
||||
// variant="contained"
|
||||
// startIcon={<ChatIcon />}
|
||||
// sx={{
|
||||
// padding: "4px 6px",
|
||||
// color: groupSection === "chat" ? "black" : "white",
|
||||
// backgroundColor: isUnreadChat
|
||||
// ? "red"
|
||||
// : groupSection === "chat"
|
||||
// ? "white"
|
||||
// : "black",
|
||||
// "&:hover": {
|
||||
// backgroundColor: isUnreadChat
|
||||
// ? "red"
|
||||
// : groupSection === "chat"
|
||||
// ? "white"
|
||||
// : "black", // Same logic for hover
|
||||
// },
|
||||
// "&:active": {
|
||||
// backgroundColor: isUnreadChat
|
||||
// ? "red"
|
||||
// : groupSection === "chat"
|
||||
// ? "white"
|
||||
// : "black", // Same logic for active
|
||||
// },
|
||||
// "&:focus": {
|
||||
// backgroundColor: isUnreadChat
|
||||
// ? "red"
|
||||
// : groupSection === "chat"
|
||||
// ? "white"
|
||||
// : "black", // Same logic for focus
|
||||
// },
|
||||
// }}
|
||||
// onClick={goToChat}
|
||||
// >
|
||||
// Chat
|
||||
// </Button>
|
||||
// </Grid>
|
||||
// <Grid item xs={4} sx={{
|
||||
// display: 'flex'
|
||||
// }}>
|
||||
// <Button
|
||||
// fullWidth
|
||||
// size="small"
|
||||
// variant="contained"
|
||||
// startIcon={<ForumIcon />}
|
||||
// sx={{
|
||||
// padding: "4px 6px",
|
||||
// color: groupSection === "forum" ? "black" : "white",
|
||||
// backgroundColor:
|
||||
// groupSection === "forum" ? "white" : "black",
|
||||
// "&:hover": {
|
||||
// backgroundColor: groupSection === "forum" ? "white" : "black", // Hover state
|
||||
// },
|
||||
// "&:active": {
|
||||
// backgroundColor: groupSection === "forum" ? "white" : "black", // Active state
|
||||
// },
|
||||
// "&:focus": {
|
||||
// backgroundColor: groupSection === "forum" ? "white" : "black", // Focus state
|
||||
// },
|
||||
// }}
|
||||
// onClick={() => {
|
||||
// setSelectedDirect(null);
|
||||
// setNewChat(false)
|
||||
// setGroupSection("forum")
|
||||
// } }
|
||||
// >
|
||||
// Forum
|
||||
// </Button>
|
||||
// </Grid>
|
||||
// <Grid item xs={4} sx={{
|
||||
// display: 'flex'
|
||||
// }}>
|
||||
// <Button
|
||||
// fullWidth
|
||||
// size="small"
|
||||
// variant="contained"
|
||||
// startIcon={<GroupIcon />}
|
||||
// sx={{ padding: "4px 6px", backgroundColor: "black", "&:hover": {
|
||||
// backgroundColor: "black", // Hover state
|
||||
// },
|
||||
// "&:active": {
|
||||
// backgroundColor: "black", // Active state
|
||||
// },
|
||||
// "&:focus": {
|
||||
// backgroundColor: "black", // Focus state
|
||||
// }, }}
|
||||
// onClick={() => setOpenManageMembers(true)}
|
||||
// >
|
||||
// Members
|
||||
// </Button>
|
||||
// </Grid>
|
||||
// </>
|
||||
// )}
|
||||
|
||||
// {/* Second row: Groups, Home, Profile */}
|
||||
// <Grid item xs={4} sx={{
|
||||
// display: 'flex',
|
||||
// }}>
|
||||
// <Button
|
||||
// fullWidth
|
||||
// size="small"
|
||||
// variant="contained"
|
||||
// startIcon={<GroupIcon />}
|
||||
// sx={{
|
||||
// padding: "2px 4px",
|
||||
// backgroundColor:
|
||||
// groupChatHasUnread ||
|
||||
// groupsAnnHasUnread ||
|
||||
// directChatHasUnread
|
||||
// ? "red"
|
||||
// : "black",
|
||||
// "&:hover": {
|
||||
// backgroundColor:
|
||||
// groupChatHasUnread || groupsAnnHasUnread || directChatHasUnread
|
||||
// ? "red"
|
||||
// : "black", // Hover state follows the same logic
|
||||
// },
|
||||
// "&:active": {
|
||||
// backgroundColor:
|
||||
// groupChatHasUnread || groupsAnnHasUnread || directChatHasUnread
|
||||
// ? "red"
|
||||
// : "black", // Active state follows the same logic
|
||||
// },
|
||||
// "&:focus": {
|
||||
// backgroundColor:
|
||||
// groupChatHasUnread || groupsAnnHasUnread || directChatHasUnread
|
||||
// ? "red"
|
||||
// : "black", // Focus state follows the same logic
|
||||
// },
|
||||
// }}
|
||||
// onClick={() => {
|
||||
// setIsOpenDrawer(true);
|
||||
// setDrawerMode("groups");
|
||||
// }}
|
||||
// >
|
||||
// {chatMode === "groups" ? "Groups" : "Direct"}
|
||||
// </Button>
|
||||
// </Grid>
|
||||
// <Grid item xs={2} sx={{
|
||||
// display: 'flex',
|
||||
// justifyContent: 'center'
|
||||
// }}>
|
||||
// <IconButton
|
||||
// sx={{ padding: "0", color: "white" }} // Reduce padding for icons
|
||||
// onClick={goToHome}
|
||||
// >
|
||||
// <HomeIcon />
|
||||
// </IconButton>
|
||||
// </Grid>
|
||||
// <Grid item xs={2} sx={{
|
||||
// display: 'flex',
|
||||
// justifyContent: 'center'
|
||||
// }}>
|
||||
// <IconButton
|
||||
// sx={{ padding: "0", color: "white" }} // Reduce padding for icons
|
||||
// onClick={() => setIsOpenDrawerProfile(true)}
|
||||
// >
|
||||
// <PersonIcon />
|
||||
// </IconButton>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// </Box>
|
||||
// )}
|
||||
|
@@ -48,20 +48,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
|
||||
return true
|
||||
})
|
||||
|
||||
// const getJoinGroupRequests = groupsAsAdmin.map(async (group)=> {
|
||||
// console.log('getJoinGroupRequests', group)
|
||||
// const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
|
||||
// return fetch(
|
||||
// `${getBaseApiReact()}/groups/joinrequests/${group.groupId}`
|
||||
// );
|
||||
// })
|
||||
|
||||
// const joinRequestData = await joinRequestResponse.json()
|
||||
// return {
|
||||
// group,
|
||||
// data: joinRequestData
|
||||
// }
|
||||
// })
|
||||
|
||||
await Promise.all(getAllGroupsAsAdmin)
|
||||
const res = await Promise.all(groupsAsAdmin.map(async (group)=> {
|
||||
|
||||
|
@@ -134,6 +134,7 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers,
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: hasUnreadChat ? "var(--unread)" :"#fff"
|
||||
},
|
||||
}} primary="Chat" />
|
||||
</MenuItem>
|
||||
@@ -153,6 +154,7 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers,
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: hasUnreadAnnouncements ? "var(--unread)" :"#fff"
|
||||
},
|
||||
}} primary="Announcements" />
|
||||
</MenuItem>
|
||||
|
@@ -2,11 +2,14 @@ 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 LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
|
||||
import { CustomSvg } from "../../common/CustomSvg";
|
||||
import { WalletIcon } from "../../assets/Icons/WalletIcon";
|
||||
import { HubsIcon } from "../../assets/Icons/HubsIcon";
|
||||
@@ -132,6 +135,15 @@ export const MobileFooter = ({
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<ButtonBase onClick={()=> {
|
||||
if(mobileViewMode === 'home'){
|
||||
setMobileViewMode('apps')
|
||||
|
||||
} else {
|
||||
setMobileViewMode('home')
|
||||
|
||||
}
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "49px", // Slightly smaller inner circle
|
||||
@@ -144,8 +156,9 @@ export const MobileFooter = ({
|
||||
}}
|
||||
>
|
||||
{/* Custom Center Icon */}
|
||||
<img src={BottomLogo} alt="center-icon" />
|
||||
<img src={mobileViewMode === 'apps' ? LogoSelected : BottomLogo} alt="center-icon" />
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
|
||||
<BottomNavigation
|
||||
|
@@ -19,6 +19,11 @@ import { ArrowDownIcon } from "../../assets/Icons/ArrowDownIcon";
|
||||
import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
|
||||
import { MessagingIcon2 } from "../../assets/Icons/MessagingIcon2";
|
||||
import { HubsIcon } from "../../assets/Icons/HubsIcon";
|
||||
import { Save } from "../Save/Save";
|
||||
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
|
||||
import { useRecoilState } from "recoil";
|
||||
import { fullScreenAtom, hasSettingsChangedAtom } from "../../atoms/global";
|
||||
import { useAppFullScreen } from "../../useAppFullscreen";
|
||||
|
||||
const Header = ({
|
||||
logoutFunc,
|
||||
@@ -32,16 +37,11 @@ const Header = ({
|
||||
myName,
|
||||
setSelectedDirect,
|
||||
setNewChat
|
||||
// selectedGroup,
|
||||
// onHomeClick,
|
||||
// onLogoutClick,
|
||||
// onGroupChange,
|
||||
// onWalletClick,
|
||||
// onNotificationClick,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom);
|
||||
const {exitFullScreen} = useAppFullScreen(setFullScreen)
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
@@ -76,10 +76,10 @@ const Header = ({
|
||||
width: "75px",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="home"
|
||||
<ButtonBase
|
||||
|
||||
|
||||
|
||||
onClick={() => {
|
||||
setMobileViewModeKeepOpen("");
|
||||
goToHome();
|
||||
@@ -87,15 +87,24 @@ const Header = ({
|
||||
// onClick={onHomeClick}
|
||||
>
|
||||
<HomeIcon height={20} width={27} color="rgba(145, 145, 147, 1)" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="home"
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
|
||||
onClick={handleClick}
|
||||
>
|
||||
<NotificationIcon height={20} width={21} color={hasUnreadDirects || hasUnreadGroups ? "var(--unread)" : "rgba(145, 145, 147, 1)"} />
|
||||
</IconButton>
|
||||
</ButtonBase>
|
||||
{fullScreen && (
|
||||
<ButtonBase onClick={()=> {
|
||||
exitFullScreen()
|
||||
setFullScreen(false)
|
||||
}}>
|
||||
<CloseFullscreenIcon sx={{
|
||||
color: 'rgba(145, 145, 147, 1)'
|
||||
}} />
|
||||
</ButtonBase>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
{/* Center Title */}
|
||||
@@ -121,34 +130,25 @@ const Header = ({
|
||||
>
|
||||
{/* Right Logout Icon */}
|
||||
|
||||
<IconButton
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setMobileViewModeKeepOpen("messaging");
|
||||
}}
|
||||
edge="end"
|
||||
color="inherit"
|
||||
aria-label="logout"
|
||||
|
||||
// onClick={onLogoutClick}
|
||||
>
|
||||
<MessagingIcon2 height={20} color={hasUnreadDirects ? "var(--unread)" : "rgba(145, 145, 147, 1)"}
|
||||
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
</ButtonBase>
|
||||
<Save />
|
||||
<ButtonBase
|
||||
onClick={logoutFunc}
|
||||
edge="end"
|
||||
color="inherit"
|
||||
aria-label="logout"
|
||||
|
||||
// onClick={onLogoutClick}
|
||||
>
|
||||
<LogoutIcon
|
||||
height={20}
|
||||
width={21}
|
||||
color="rgba(145, 145, 147, 1)"
|
||||
/>
|
||||
</IconButton>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
<Menu
|
||||
@@ -203,7 +203,7 @@ const Header = ({
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: hasUnreadDirects ? "var(--unread)" :"rgba(250, 250, 250, 0.5)"
|
||||
color: hasUnreadGroups ? "var(--unread)" :"rgba(250, 250, 250, 0.5)"
|
||||
},
|
||||
}} primary="Hubs" />
|
||||
</MenuItem>
|
||||
@@ -247,16 +247,32 @@ const Header = ({
|
||||
}}
|
||||
>
|
||||
{/* Left Home Icon */}
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="home"
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "18px",
|
||||
width: "75px",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
|
||||
onClick={goToHome}
|
||||
// onClick={onHomeClick}
|
||||
>
|
||||
<HomeIcon color="rgba(145, 145, 147, 1)" />
|
||||
</IconButton>
|
||||
|
||||
</ButtonBase>
|
||||
{fullScreen && (
|
||||
<ButtonBase onClick={()=> {
|
||||
exitFullScreen()
|
||||
setFullScreen(false)
|
||||
}}>
|
||||
<CloseFullscreenIcon sx={{
|
||||
color: 'rgba(145, 145, 147, 1)'
|
||||
}} />
|
||||
</ButtonBase>
|
||||
)}
|
||||
</Box>
|
||||
{/* Center Title */}
|
||||
<Typography
|
||||
variant="h6"
|
||||
@@ -269,18 +285,26 @@ const Header = ({
|
||||
>
|
||||
QORTAL
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "30px",
|
||||
width: "75px",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
{/* Right Logout Icon */}
|
||||
<IconButton
|
||||
<Save />
|
||||
<ButtonBase
|
||||
onClick={logoutFunc}
|
||||
edge="end"
|
||||
color="inherit"
|
||||
aria-label="logout"
|
||||
|
||||
|
||||
// onClick={onLogoutClick}
|
||||
>
|
||||
<LogoutIcon color="rgba(145, 145, 147, 1)" />
|
||||
</IconButton>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
|
161
src/components/Save/Save.tsx
Normal file
161
src/components/Save/Save.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import isEqual from 'lodash/isEqual'; // Import deep comparison utility
|
||||
import { canSaveSettingToQdnAtom, hasSettingsChangedAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global';
|
||||
import { ButtonBase } from '@mui/material';
|
||||
import { objectToBase64 } from '../../qdn/encryption/group-encryption';
|
||||
import { MyContext } from '../../App';
|
||||
import { getFee } from '../../background';
|
||||
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
|
||||
import { SaveIcon } from '../../assets/svgs/SaveIcon';
|
||||
import { IconWrapper } from '../Desktop/DesktopFooter';
|
||||
export const Save = ({isDesktop}) => {
|
||||
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
||||
const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState(settingsQDNLastUpdatedAtom);
|
||||
const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom);
|
||||
const setHasSettingsChangedAtom = useSetRecoilState(hasSettingsChangedAtom);
|
||||
|
||||
const [canSave] = useRecoilState(canSaveSettingToQdnAtom);
|
||||
const [openSnack, setOpenSnack] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [infoSnack, setInfoSnack] = useState(null);
|
||||
const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom)
|
||||
|
||||
const { show } = useContext(MyContext);
|
||||
|
||||
const hasChanged = useMemo(()=> {
|
||||
const newChanges = {
|
||||
sortablePinnedApps: pinnedApps.map((item)=> {
|
||||
return {
|
||||
name: item?.name,
|
||||
service: item?.service
|
||||
}
|
||||
})
|
||||
}
|
||||
const oldChanges = {
|
||||
sortablePinnedApps: oldPinnedApps.map((item)=> {
|
||||
return {
|
||||
name: item?.name,
|
||||
service: item?.service
|
||||
}
|
||||
})
|
||||
}
|
||||
if(settingsQdnLastUpdated === -100) return false
|
||||
return !isEqual(oldChanges, newChanges) && settingsQdnLastUpdated < settingsLocalLastUpdated
|
||||
}, [oldPinnedApps, pinnedApps, settingsQdnLastUpdated, settingsLocalLastUpdated])
|
||||
|
||||
useEffect(()=> {
|
||||
setHasSettingsChangedAtom(hasChanged)
|
||||
}, [hasChanged])
|
||||
|
||||
const saveToQdn = async ()=> {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const data64 = await objectToBase64({
|
||||
sortablePinnedApps: pinnedApps.map((item)=> {
|
||||
return {
|
||||
name: item?.name,
|
||||
service: item?.service
|
||||
}
|
||||
})
|
||||
})
|
||||
const encryptData = await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "ENCRYPT_DATA",
|
||||
type: "qortalRequest",
|
||||
payload: {
|
||||
data64
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (response.error) {
|
||||
rej(response?.message);
|
||||
return;
|
||||
} else {
|
||||
res(response);
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
if(encryptData && !encryptData?.error){
|
||||
const fee = await getFee('ARBITRARY')
|
||||
|
||||
await show({
|
||||
message: "Would you like to publish your settings to QDN (encrypted) ?" ,
|
||||
publishFee: fee.fee + ' QORT'
|
||||
})
|
||||
const response = await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "publishOnQDN",
|
||||
payload: {
|
||||
data: encryptData,
|
||||
identifier: "ext_saved_settings",
|
||||
service: 'DOCUMENT_PRIVATE'
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
|
||||
if (!response?.error) {
|
||||
res(response);
|
||||
return
|
||||
}
|
||||
rej(response.error);
|
||||
}
|
||||
);
|
||||
});
|
||||
if(response?.identifier){
|
||||
setOldPinnedApps(pinnedApps)
|
||||
setSettingsQdnLastUpdated(Date.now())
|
||||
setInfoSnack({
|
||||
type: "success",
|
||||
message:
|
||||
"Sucessfully published to QDN",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
message:
|
||||
error?.message || "Unable to save to QDN",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ButtonBase onClick={saveToQdn} disabled={!hasChanged || !canSave || isLoading || settingsQdnLastUpdated === -100}>
|
||||
{isDesktop ? (
|
||||
<IconWrapper
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
label="Save"
|
||||
selected={false}
|
||||
>
|
||||
<SaveIcon
|
||||
color={settingsQdnLastUpdated === -100 ? '#8F8F91' : (hasChanged && !isLoading) ? '#5EB049' : '#8F8F91'}
|
||||
/>
|
||||
</IconWrapper>
|
||||
) : (
|
||||
<SaveIcon
|
||||
color={settingsQdnLastUpdated === -100 ? '#8F8F91' : (hasChanged && !isLoading) ? '#5EB049' : '#8F8F91'}
|
||||
/>
|
||||
)}
|
||||
|
||||
</ButtonBase>
|
||||
<CustomizedSnackbars
|
||||
duration={3500}
|
||||
open={openSnack}
|
||||
setOpen={setOpenSnack}
|
||||
info={infoSnack}
|
||||
setInfo={setInfoSnack}
|
||||
/>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
@@ -3,7 +3,7 @@ import Button from '@mui/material/Button';
|
||||
import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar';
|
||||
import Alert from '@mui/material/Alert';
|
||||
|
||||
export const CustomizedSnackbars = ({open, setOpen, info, setInfo}) => {
|
||||
export const CustomizedSnackbars = ({open, setOpen, info, setInfo, duration}) => {
|
||||
|
||||
|
||||
|
||||
@@ -19,9 +19,10 @@ export const CustomizedSnackbars = ({open, setOpen, info, setInfo}) => {
|
||||
setInfo(null)
|
||||
};
|
||||
|
||||
if(!open) return null
|
||||
return (
|
||||
<div>
|
||||
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={6000} onClose={handleClose}>
|
||||
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={duration || 6000} onClose={handleClose}>
|
||||
<Alert
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user