Merge branch 'feature/q-app-support'

This commit is contained in:
2024-10-28 05:03:00 +02:00
82 changed files with 12361 additions and 1677 deletions

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
});

View 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;

View 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'
}));

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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%'
}));

View 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>
);
};

View 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>
</>
);
};

View 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>
</>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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

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

View File

@@ -255,7 +255,7 @@ export const AnnouncementDiscussion = ({
return (
<div
style={{
height: isMobile ? '100%' : "100vh",
height: isMobile ? '100%' : "100%",
display: "flex",
flexDirection: "column",
width: "100%",

View File

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

View File

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

View File

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

View File

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

View 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>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
// )}

View File

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

View File

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

View File

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

View File

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

View 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}
/>
</>
)
}

View File

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