Merge branch 'feature/q-app-support'

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

View File

@ -1,7 +1,8 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Qortal Extension</title>

113
package-lock.json generated
View File

@ -9,12 +9,15 @@
"version": "0.0.0",
"dependencies": {
"@chatscope/chat-ui-kit-react": "^2.0.3",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.16.4",
"@mui/lab": "^5.0.0-alpha.173",
"@mui/material": "^5.16.7",
"@reduxjs/toolkit": "^2.2.7",
"@tanstack/react-virtual": "^3.10.8",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/user-event": "^14.5.2",
"@tiptap/extension-color": "^2.5.9",
@ -45,6 +48,7 @@
"react-countdown-circle-timer": "^3.2.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-frame-component": "^5.2.7",
"react-infinite-scroller": "^1.2.6",
"react-intersection-observer": "^9.13.0",
"react-qr-code": "^2.0.15",
@ -52,6 +56,7 @@
"react-redux": "^9.1.2",
"react-virtualized": "^9.22.5",
"react-virtuoso": "^4.10.4",
"recoil": "^0.7.7",
"short-unique-id": "^5.2.0",
"slate": "^0.103.0",
"slate-react": "^0.109.0",
@ -483,6 +488,55 @@
"resolved": "https://registry.npmjs.org/@chatscope/chat-ui-kit-styles/-/chat-ui-kit-styles-1.4.0.tgz",
"integrity": "sha512-016mBJD3DESw7Nh+lkKcPd22xG92ghA0VpIXIbjQtmXhC7Ve6wRazTy8z1Ahut+Tbv179+JxrftuMngsj/yV8Q=="
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz",
"integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz",
"integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.0",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz",
"integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.1.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
@ -1847,6 +1901,31 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@tanstack/react-virtual": {
"version": "3.10.8",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz",
"integrity": "sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==",
"dependencies": {
"@tanstack/virtual-core": "3.10.8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.10.8",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz",
"integrity": "sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testing-library/dom": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.3.0.tgz",
@ -5016,6 +5095,11 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
"node_modules/hamt_plus": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
"integrity": "sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA=="
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@ -9266,6 +9350,16 @@
"react": ">= 16.8 || 18.0.0"
}
},
"node_modules/react-frame-component": {
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/react-frame-component/-/react-frame-component-5.2.7.tgz",
"integrity": "sha512-ROjHtSLoSVYUBfTieazj/nL8jIX9rZFmHC0yXEU+dx6Y82OcBEGgU9o7VyHMrBFUN9FuQ849MtIPNNLsb4krbg==",
"peerDependencies": {
"prop-types": "^15.5.9",
"react": ">= 16.3",
"react-dom": ">= 16.3"
}
},
"node_modules/react-infinite-scroller": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz",
@ -9415,6 +9509,25 @@
"react-dom": ">=16 || >=17 || >= 18"
}
},
"node_modules/recoil": {
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz",
"integrity": "sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ==",
"dependencies": {
"hamt_plus": "1.0.2"
},
"peerDependencies": {
"react": ">=16.13.1"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",

View File

@ -13,12 +13,15 @@
},
"dependencies": {
"@chatscope/chat-ui-kit-react": "^2.0.3",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.16.4",
"@mui/lab": "^5.0.0-alpha.173",
"@mui/material": "^5.16.7",
"@reduxjs/toolkit": "^2.2.7",
"@tanstack/react-virtual": "^3.10.8",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/user-event": "^14.5.2",
"@tiptap/extension-color": "^2.5.9",
@ -49,6 +52,7 @@
"react-countdown-circle-timer": "^3.2.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-frame-component": "^5.2.7",
"react-infinite-scroller": "^1.2.6",
"react-intersection-observer": "^9.13.0",
"react-qr-code": "^2.0.15",
@ -56,6 +60,7 @@
"react-redux": "^9.1.2",
"react-virtualized": "^9.22.5",
"react-virtuoso": "^4.10.4",
"recoil": "^0.7.7",
"short-unique-id": "^5.2.0",
"slate": "^0.103.0",
"slate-react": "^0.109.0",

9
public/appsBg.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
(function() {
// Immediately disable qdnGatewayShowModal if it exists
// Now, let's wrap the handleResponse function with the new condition
const originalHandleResponse = window.handleResponse; // Save the original handleResponse function
if (typeof originalHandleResponse === 'function') {
// Create the wrapper function to enhance the original handleResponse
window.handleResponse = function(event, response) {
// Check if the response contains the specific error message
if (response && typeof response === 'string' && response.includes("Interactive features were requested")) {
console.log('Response contains "Interactive features were requested", skipping processing.');
return; // Skip further processing
}
// Call the original handleResponse for normal processing
originalHandleResponse(event, response);
};
console.log('handleResponse has been enhanced to skip specific error handling.');
} else {
console.log('No handleResponse function found to enhance.');
}
})();

View File

@ -0,0 +1,27 @@
(function() {
console.log('External script loaded to disable qdnGatewayShowModal');
const timeoutDuration = 5000; // Set timeout duration to 5 seconds (5000ms)
let elapsedTime = 0; // Track the time that has passed
// Poll for qdnGatewayShowModal and disable it once it's defined
const checkQdnGatewayInterval = setInterval(() => {
elapsedTime += 100; // Increment elapsed time by the polling interval (100ms)
if (typeof window.qdnGatewayShowModal === 'function') {
console.log('Disabling qdnGatewayShowModal');
// Disable qdnGatewayShowModal function
window.qdnGatewayShowModal = function(message) {
console.log('qdnGatewayShowModal function has been disabled.');
};
// Stop checking once qdnGatewayShowModal has been disabled
clearInterval(checkQdnGatewayInterval);
} else if (elapsedTime >= timeoutDuration) {
console.log('Timeout reached, stopping polling for qdnGatewayShowModal.');
clearInterval(checkQdnGatewayInterval); // Stop checking after 5 seconds
}
}, 100); // Check every 100ms
})();

7
public/document_end.js Normal file
View File

@ -0,0 +1,7 @@
const script2 = document.createElement('script');
script2.src = chrome.runtime.getURL('disable-gateway-message.js'); // Reference the external script
document.documentElement.appendChild(script2); // Inject it into the page
script2.onload = function() {
script2.remove(); // Clean up after the script has been injected and run
};

7
public/document_start.js Normal file
View File

@ -0,0 +1,7 @@
const script = document.createElement('script');
script.src = chrome.runtime.getURL('disable-gateway-popup.js'); // Reference the external script
document.documentElement.appendChild(script); // Inject it into the page
script.onload = function() {
script.remove(); // Clean up after the script has been injected and run
};

View File

@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Qortal",
"version": "2.1.1",
"version": "2.2.0",
"icons": {
"16": "qort.png",
"32": "qort.png",
@ -18,12 +18,34 @@
],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["document_start.js"],
"run_at": "document_start"
},
{
"matches": ["<all_urls>"],
"js": ["content-script.js"]
},
{
"matches": ["<all_urls>"],
"js": ["document_end.js"],
"run_at": "document_end"
}
],
"web_accessible_resources": [
{
"resources": ["disable-gateway-popup.js"],
"matches": ["<all_urls>"]
},
{
"resources": ["disable-gateway-message.js"],
"matches": ["<all_urls>"]
}
],
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://api.qortal.org https://api2.qortal.org https://appnode.qortal.org https://apinode.qortalnodes.live https://apinode1.qortalnodes.live https://apinode2.qortalnodes.live https://apinode3.qortalnodes.live https://apinode4.qortalnodes.live https://ext-node.qortal.link wss://appnode.qortal.org wss://ext-node.qortal.link ws://127.0.0.1:12391 http://127.0.0.1:12391 https://ext-node.qortal.link; "
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://*:* http://*:* wss://*:* ws://*:*;"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,643 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Spacer } from "../common/Spacer";
import { CustomButton, TextItalic, TextP, TextSpan } from "../App-styles";
import {
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControlLabel,
Input,
Switch,
Tooltip,
Typography,
} from "@mui/material";
import Logo1 from "../assets/svgs/Logo1.svg";
import Logo1Dark from "../assets/svgs/Logo1Dark.svg";
import Info from "../assets/svgs/Info.svg";
import { CustomizedSnackbars } from "../components/Snackbar/Snackbar";
import { set } from "lodash";
import { cleanUrl, isUsingLocal } from "../background";
const manifestData = chrome?.runtime?.getManifest();
export const NotAuthenticated = ({
getRootProps,
getInputProps,
setExtstate,
apiKey,
setApiKey,
globalApiKey,
handleSetGlobalApikey,
}) => {
const [isValidApiKey, setIsValidApiKey] = useState<boolean | null>(null);
const [hasLocalNode, setHasLocalNode] = useState<boolean | null>(null);
const [useLocalNode, setUseLocalNode] = useState(false);
const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null);
const [show, setShow] = React.useState(false);
const [mode, setMode] = React.useState("list");
const [customNodes, setCustomNodes] = React.useState(null);
const [currentNode, setCurrentNode] = React.useState({
url: "http://127.0.0.1:12391",
});
const [importedApiKey, setImportedApiKey] = React.useState(null);
//add and edit states
const [url, setUrl] = React.useState("http://");
const [customApikey, setCustomApiKey] = React.useState("");
const [customNodeToSaveIndex, setCustomNodeToSaveIndex] =
React.useState(null);
const importedApiKeyRef = useRef(null)
const currentNodeRef = useRef(null)
const hasLocalNodeRef = useRef(null)
const isLocal = cleanUrl(currentNode?.url) === "127.0.0.1:12391";
const handleFileChangeApiKey = (event) => {
const file = event.target.files[0]; // Get the selected file
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result; // Get the file content
setImportedApiKey(text); // Store the file content in the state
};
reader.readAsText(file); // Read the file as text
}
};
const checkIfUserHasLocalNode = useCallback(async () => {
try {
const url = `http://127.0.0.1:12391/admin/status`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data?.height) {
setHasLocalNode(true);
}
} catch (error) {}
}, []);
useEffect(() => {
checkIfUserHasLocalNode();
}, []);
useEffect(() => {
chrome?.runtime?.sendMessage(
{ action: "getCustomNodesFromStorage" },
(response) => {
if (response) {
setCustomNodes(response || []);
}
}
);
}, []);
useEffect(()=> {
importedApiKeyRef.current = importedApiKey
}, [importedApiKey])
useEffect(()=> {
currentNodeRef.current = currentNode
}, [currentNode])
useEffect(()=> {
hasLocalNodeRef.current = hasLocalNode
}, [hasLocalNode])
const validateApiKey = useCallback(async (key, fromStartUp) => {
try {
if(!currentNodeRef.current) return
const isLocalKey = cleanUrl(key?.url) === "127.0.0.1:12391";
if(isLocalKey && !hasLocalNodeRef.current && !fromStartUp){
throw new Error('Please turn on your local node')
}
const isCurrentNodeLocal = cleanUrl(currentNodeRef.current?.url) === "127.0.0.1:12391";
if(isLocalKey && !isCurrentNodeLocal) {
setIsValidApiKey(false);
setUseLocalNode(false);
return
}
let payload = {};
if (currentNodeRef.current?.url === "http://127.0.0.1:12391") {
payload = {
apikey: importedApiKeyRef.current || key?.apikey,
url: currentNodeRef.current?.url,
};
} else if(currentNodeRef.current) {
payload = currentNodeRef.current;
}
const url = `${payload?.url}/admin/apikey/test`;
const response = await fetch(url, {
method: "GET",
headers: {
accept: "text/plain",
"X-API-KEY": payload?.apikey, // Include the API key here
},
});
// Assuming the response is in plain text and will be 'true' or 'false'
const data = await response.text();
if (data === "true") {
chrome?.runtime?.sendMessage(
{ action: "setApiKey", payload },
(response) => {
if (response) {
handleSetGlobalApikey(payload);
setIsValidApiKey(true);
setUseLocalNode(true);
if(!fromStartUp){
setApiKey(payload)
}
}
}
);
} else {
setIsValidApiKey(false);
setUseLocalNode(false);
setInfoSnack({
type: "error",
message: "Select a valid apikey",
});
setOpenSnack(true);
}
} catch (error) {
setIsValidApiKey(false);
setUseLocalNode(false);
setInfoSnack({
type: "error",
message: error?.message || "Select a valid apikey",
});
setOpenSnack(true);
console.error("Error validating API key:", error);
}
}, []);
useEffect(() => {
if (apiKey) {
validateApiKey(apiKey, true);
}
}, [apiKey]);
const addCustomNode = () => {
setMode("add-node");
};
const saveCustomNodes = (myNodes) => {
let nodes = [...(myNodes || [])];
if (customNodeToSaveIndex !== null) {
nodes.splice(customNodeToSaveIndex, 1, {
url,
apikey: customApikey,
});
} else if (url && customApikey) {
nodes.push({
url,
apikey: customApikey,
});
}
setCustomNodes(nodes);
setCustomNodeToSaveIndex(null);
if (!nodes) return;
chrome?.runtime?.sendMessage(
{ action: "setCustomNodes", nodes },
(response) => {
if (response) {
setMode("list");
setUrl("http://");
setCustomApiKey("");
// add alert
}
}
);
};
return (
<>
<Spacer height="35px" />
<div
className="image-container"
style={{
width: "136px",
height: "154px",
}}
>
<img src={Logo1} className="base-image" />
<img src={Logo1Dark} className="hover-image" />
</div>
<Spacer height="30px" />
<TextP
sx={{
textAlign: "center",
lineHeight: "15px",
}}
>
WELCOME TO <TextItalic>YOUR</TextItalic> <br></br>
<TextSpan> QORTAL WALLET</TextSpan>
</TextP>
<Spacer height="30px" />
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
marginLeft: "28px",
}}
>
<CustomButton {...getRootProps()}>
<input {...getInputProps()} />
Authenticate
</CustomButton>
<Tooltip title="Authenticate by importing your Qortal JSON file" arrow>
<img src={Info} />
</Tooltip>
</Box>
<Spacer height="6px" />
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
marginLeft: "28px",
}}
>
<CustomButton
onClick={() => {
setExtstate("create-wallet");
}}
>
Create account
</CustomButton>
<img
src={Info}
style={{
visibility: "hidden",
}}
/>
</Box>
<Spacer height="15px" />
<Typography
sx={{
fontSize: "12px",
visibility: !useLocalNode && 'hidden'
}}
>
{"Using node: "} {currentNode?.url}
</Typography>
<>
<Spacer height="15px" />
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
flexDirection: "column",
}}
>
<>
<Box
sx={{
display: "flex",
gap: "10px",
alignItems: "center",
justifyContent: "center",
width: "100%",
}}
>
<FormControlLabel
control={
<Switch
sx={{
"& .MuiSwitch-switchBase.Mui-checked": {
color: "#5EB049",
},
"& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track":
{
backgroundColor: "white", // Change track color when checked
},
}}
checked={useLocalNode}
onChange={(event) => {
if (event.target.checked) {
validateApiKey(currentNode);
} else {
setCurrentNode({
url: "http://127.0.0.1:12391",
})
setUseLocalNode(false)
chrome?.runtime?.sendMessage(
{ action: "setApiKey", payload:null },
(response) => {
if (response) {
setApiKey(null);
handleSetGlobalApikey(null);
}
}
);
}
}}
disabled={false}
defaultChecked
/>
}
label={`Use ${isLocal ? 'Local' : 'Custom'} Node`}
/>
</Box>
{currentNode?.url === "http://127.0.0.1:12391" && (
<>
<Button size="small" variant="contained" component="label">
{apiKey ? "Change " : "Import "} apiKey.txt
<input
type="file"
accept=".txt"
hidden
onChange={handleFileChangeApiKey} // File input handler
/>
</Button>
<Typography sx={{
fontSize: '12px',
visibility: importedApiKey ? 'visible' : 'hidden'
}}>{`api key : ${importedApiKey}`}</Typography>
</>
)}
<Button
size="small"
onClick={() => {
setShow(true);
}}
variant="contained"
component="label"
>
Choose custom node
</Button>
</>
<Typography sx={{
color: "white",
fontSize: '12px'
}}>Build version: {manifestData?.version}</Typography>
</Box>
</>
<CustomizedSnackbars
open={openSnack}
setOpen={setOpenSnack}
info={infoSnack}
setInfo={setInfoSnack}
/>
{show && (
<Dialog
open={show}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
fullWidth
>
<DialogTitle id="alert-dialog-title">{"Custom nodes"}</DialogTitle>
<DialogContent>
<Box
sx={{
width: "100% !important",
overflow: "auto",
height: "60vh",
display: "flex",
flexDirection: "column",
}}
>
{mode === "list" && (
<Box
sx={{
gap: "20px",
display: "flex",
flexDirection: "column",
}}
>
<Box
sx={{
display: "flex",
gap: "10px",
flexDirection: "column",
}}
>
<Typography
sx={{
color: "white",
fontSize: "14px",
}}
>
http://127.0.0.1:12391
</Typography>
<Box
sx={{
display: "flex",
gap: "10px",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Button
disabled={currentNode?.url === "http://127.0.0.1:12391"}
size="small"
onClick={() => {
setCurrentNode({
url: "http://127.0.0.1:12391",
});
setMode("list");
setShow(false);
setUseLocalNode(false);
chrome?.runtime?.sendMessage(
{ action: "setApiKey", payload:null },
(response) => {
if (response) {
setApiKey(null);
handleSetGlobalApikey(null);
}
}
);
}}
variant="contained"
>
Choose
</Button>
</Box>
</Box>
{customNodes?.map((node, index) => {
return (
<Box
sx={{
display: "flex",
gap: "10px",
flexDirection: "column",
}}
>
<Typography
sx={{
color: "white",
fontSize: "14px",
}}
>
{node?.url}
</Typography>
<Box
sx={{
display: "flex",
gap: "10px",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Button
disabled={currentNode?.url === node?.url}
size="small"
onClick={() => {
setCurrentNode({
url: node?.url,
apikey: node?.apikey,
});
setMode("list");
setShow(false);
setIsValidApiKey(false);
setUseLocalNode(false);
chrome?.runtime?.sendMessage(
{ action: "setApiKey", payload:null },
(response) => {
if (response) {
setApiKey(null);
handleSetGlobalApikey(null);
}
}
);
}}
variant="contained"
>
Choose
</Button>
<Button
size="small"
onClick={() => {
setCustomApiKey(node?.apikey);
setUrl(node?.url);
setMode("add-node");
setCustomNodeToSaveIndex(index);
}}
variant="contained"
>
Edit
</Button>
<Button
size="small"
onClick={() => {
const nodesToSave = [
...(customNodes || []),
].filter((item) => item?.url !== node?.url);
saveCustomNodes(nodesToSave);
}}
variant="contained"
>
Remove
</Button>
</Box>
</Box>
);
})}
</Box>
)}
{mode === "add-node" && (
<Box
sx={{
display: "flex",
gap: "10px",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Input
placeholder="Url"
value={url}
onChange={(e) => {
setUrl(e.target.value);
}}
/>
<Input
placeholder="Api key"
value={customApikey}
onChange={(e) => {
setCustomApiKey(e.target.value);
}}
/>
</Box>
)}
</Box>
</DialogContent>
<DialogActions>
{mode === "list" && (
<>
<Button
variant="contained"
onClick={() => {
setShow(false);
}}
autoFocus
>
Close
</Button>
</>
)}
{mode === "list" && (
<Button variant="contained" onClick={addCustomNode}>
Add
</Button>
)}
{mode === "add-node" && (
<>
<Button
variant="contained"
onClick={() => {
setMode("list");
setCustomNodeToSaveIndex(null);
}}
>
Return to list
</Button>
<Button
variant="contained"
disabled={!customApikey || !url}
onClick={() => saveCustomNodes(customNodes)}
autoFocus
>
Save
</Button>
</>
)}
</DialogActions>
</Dialog>
)}
</>
);
};

View File

@ -0,0 +1,11 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.76596 7.53192C5.84584 7.53192 7.53192 5.84584 7.53192 3.76596C7.53192 1.68608 5.84584 0 3.76596 0C1.68608 0 0 1.68608 0 3.76596C0 5.84584 1.68608 7.53192 3.76596 7.53192Z" fill="#919193"/>
<path d="M15 7.53192C17.0799 7.53192 18.766 5.84584 18.766 3.76596C18.766 1.68608 17.0799 0 15 0C12.9201 0 11.2341 1.68608 11.2341 3.76596C11.2341 5.84584 12.9201 7.53192 15 7.53192Z" fill="#919193"/>
<path d="M26.234 7.53192C28.3139 7.53192 30 5.84584 30 3.76596C30 1.68608 28.3139 0 26.234 0C24.1542 0 22.4681 1.68608 22.4681 3.76596C22.4681 5.84584 24.1542 7.53192 26.234 7.53192Z" fill="#919193"/>
<path d="M3.76596 30.0001C5.84584 30.0001 7.53192 28.314 7.53192 26.2341C7.53192 24.1542 5.84584 22.4681 3.76596 22.4681C1.68608 22.4681 0 24.1542 0 26.2341C0 28.314 1.68608 30.0001 3.76596 30.0001Z" fill="#919193"/>
<path d="M15 30.0002C17.0799 30.0002 18.766 28.3141 18.766 26.2342C18.766 24.1543 17.0799 22.4683 15 22.4683C12.9201 22.4683 11.2341 24.1543 11.2341 26.2342C11.2341 28.3141 12.9201 30.0002 15 30.0002Z" fill="#919193"/>
<path d="M26.234 30.0002C28.3139 30.0002 30 28.3141 30 26.2342C30 24.1543 28.3139 22.4683 26.234 22.4683C24.1542 22.4683 22.4681 24.1543 22.4681 26.2342C22.4681 28.3141 24.1542 30.0002 26.234 30.0002Z" fill="#919193"/>
<path d="M3.76596 18.766C5.84584 18.766 7.53192 17.08 7.53192 15.0001C7.53192 12.9202 5.84584 11.2341 3.76596 11.2341C1.68608 11.2341 0 12.9202 0 15.0001C0 17.08 1.68608 18.766 3.76596 18.766Z" fill="#919193"/>
<path d="M15 18.766C17.0799 18.766 18.766 17.08 18.766 15.0001C18.766 12.9202 17.0799 11.2341 15 11.2341C12.9201 11.2341 11.2341 12.9202 11.2341 15.0001C11.2341 17.08 12.9201 18.766 15 18.766Z" fill="#919193"/>
<path d="M26.234 18.766C28.3139 18.766 30 17.08 30 15.0001C30 12.9202 28.3139 11.2341 26.234 11.2341C24.1542 11.2341 22.4681 12.9202 22.4681 15.0001C22.4681 17.08 24.1542 18.766 26.234 18.766Z" fill="#919193"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

After

Width:  |  Height:  |  Size: 365 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 821 KiB

View File

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

After

Width:  |  Height:  |  Size: 602 B

View File

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

After

Width:  |  Height:  |  Size: 396 B

View File

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

After

Width:  |  Height:  |  Size: 365 B

View File

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

After

Width:  |  Height:  |  Size: 874 B

View File

@ -0,0 +1,10 @@
import React from 'react'
export const SaveIcon = ({color = '#8F8F91'}) => {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.18182 0C0.976833 0 0 0.976833 0 2.18182V21.8182C0 23.0232 0.976833 24 2.18182 24H21.8182C23.0232 24 24 23.0232 24 21.8182V7.4492C24 6.87053 23.7701 6.31559 23.3609 5.90641L18.0936 0.639044C17.6844 0.229866 17.1295 0 16.5508 0H16.3636C15.7611 0 15.2727 0.488422 15.2727 1.09091V5.45455C15.2727 6.65953 14.2959 7.63636 13.0909 7.63636H6.54545C5.34047 7.63636 4.36364 6.65953 4.36364 5.45455V1.09091C4.36364 0.488422 3.87521 0 3.27273 0H2.18182ZM12 18.5455C13.8075 18.5455 15.2727 17.0803 15.2727 15.2727C15.2727 13.4652 13.8075 12 12 12C10.1925 12 8.72727 13.4652 8.72727 15.2727C8.72727 17.0803 10.1925 18.5455 12 18.5455Z" fill={color}/>
</svg>
)
}

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.08728 0.00158245C2.72507 0.00158245 0 2.7262 0 6.08784C0 9.44948 2.72507 12.1741 6.08728 12.1741C7.62099 12.1741 9.02317 11.6043 10.0947 10.6668L13.3088 13.8803C13.3881 13.9596 13.4911 14 13.595 14C13.6988 14 13.8018 13.9596 13.8811 13.8803C14.0396 13.7218 14.0396 13.4643 13.8811 13.3066L10.667 10.093C11.6047 9.02162 12.1746 7.62202 12.1746 6.08626C12.1746 2.72461 9.44951 0 6.0873 0L6.08728 0.00158245ZM6.08728 11.3626C3.17756 11.3626 0.811637 8.99707 0.811637 6.08784C0.811637 3.17861 3.17756 0.813083 6.08728 0.813083C8.997 0.813083 11.3629 3.17861 11.3629 6.08784C11.3629 8.99707 8.997 11.3626 6.08728 11.3626Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 748 B

View File

@ -0,0 +1,16 @@
import React from 'react';
export const StarEmptyIcon = () => {
return (
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.2726 0.162533L7.89126 3.31595C7.9357 3.40243 8.02078 3.46234 8.11994 3.47588L11.7399 3.98173C11.8542 3.99736 11.9496 4.07446 11.9853 4.18022C12.0206 4.28598 11.9913 4.40215 11.9084 4.47977L9.28882 6.93449V6.93397C9.21729 7.00117 9.18478 7.09807 9.20157 7.19288L9.81988 10.6588C9.83939 10.7682 9.79278 10.8786 9.69903 10.9443C9.60529 11.0094 9.48119 11.0182 9.37931 10.9667L6.14144 9.32987C6.05311 9.28559 5.9469 9.28559 5.85856 9.32987L2.62069 10.9667C2.51881 11.0182 2.39472 11.0094 2.30096 10.9443C2.20722 10.8786 2.16062 10.7682 2.18012 10.6588L2.79842 7.19288C2.81522 7.09807 2.78271 7.00117 2.71118 6.93397L0.0916083 4.47978C0.0086971 4.40216 -0.0205644 4.28599 0.0146582 4.18023C0.0504232 4.07448 0.145798 3.99738 0.260135 3.98175L3.88006 3.47589C3.97923 3.46235 4.0643 3.40244 4.10874 3.31596L5.7274 0.162545C5.77888 0.0630431 5.88455 0 5.99997 0C6.11539 0 6.22113 0.0630238 6.2726 0.162533Z" fill="#727376"/>
</svg>
);
};

View File

@ -0,0 +1,18 @@
import React from "react";
export const StarFilledIcon = () => {
return (
<svg
width="12"
height="11"
viewBox="0 0 12 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.2726 0.162533L7.89126 3.31595C7.9357 3.40243 8.02078 3.46234 8.11994 3.47588L11.7399 3.98173C11.8542 3.99736 11.9496 4.07446 11.9853 4.18022C12.0206 4.28598 11.9913 4.40215 11.9084 4.47977L9.28882 6.93449V6.93397C9.21729 7.00117 9.18478 7.09807 9.20157 7.19288L9.81988 10.6588C9.83939 10.7682 9.79278 10.8786 9.69903 10.9443C9.60529 11.0094 9.48119 11.0182 9.37931 10.9667L6.14144 9.32987C6.05311 9.28559 5.9469 9.28559 5.85856 9.32987L2.62069 10.9667C2.51881 11.0182 2.39472 11.0094 2.30096 10.9443C2.20722 10.8786 2.16062 10.7682 2.18012 10.6588L2.79842 7.19288C2.81522 7.09807 2.78271 7.00117 2.71118 6.93397L0.0916083 4.47978C0.0086971 4.40216 -0.0205644 4.28599 0.0146582 4.18023C0.0504232 4.07448 0.145798 3.99738 0.260135 3.98175L3.88006 3.47589C3.97923 3.46235 4.0643 3.40244 4.10874 3.31596L5.7274 0.162545C5.77888 0.0630431 5.88455 0 5.99997 0C6.11539 0 6.22113 0.0630238 6.2726 0.162533Z"
fill="white"
/>
</svg>
);
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,4 @@
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 18.9889C0 16.643 1.8685 14.7397 4.17153 14.7397C6.47456 14.7397 8.34305 16.643 8.34305 18.9889C8.34305 21.3349 6.47456 23.2382 4.17153 23.2382C1.8685 23.2382 0 21.3349 0 18.9889ZM33.8285 23.2382C36.1315 23.2382 38 21.3349 38 18.9889C38 16.643 36.1315 14.7397 33.8285 14.7397C31.5254 14.7397 29.6569 16.643 29.6569 18.9889C29.6569 21.3349 31.5254 23.2382 33.8285 23.2382ZM19.0109 23.2382C21.3139 23.2382 23.1824 21.3349 23.1824 18.9889C23.1824 16.643 21.3139 14.7397 19.0109 14.7397C16.7078 14.7397 14.8393 16.643 14.8393 18.9889C14.8393 21.3349 16.7078 23.2382 19.0109 23.2382ZM4.17153 8.49854C6.47456 8.49854 8.34305 6.59522 8.34305 4.24927C8.34305 1.90332 6.47456 0 4.17153 0C1.8685 0 0 1.90332 0 4.24927C0 6.59522 1.8685 8.49854 4.17153 8.49854ZM33.8285 8.49854C36.1315 8.49854 38 6.59522 38 4.24927C38 1.90332 36.1315 0 33.8285 0C31.5254 0 29.6569 1.90332 29.6569 4.24927C29.6569 6.59522 31.5254 8.49854 33.8285 8.49854ZM19.0109 8.49854C21.3139 8.49854 23.1824 6.59522 23.1824 4.24927C23.1824 1.90332 21.3139 0 19.0109 0C16.7078 0 14.8393 1.90332 14.8393 4.24927C14.8393 6.59522 16.7078 8.49854 19.0109 8.49854ZM4.17153 38C6.47456 38 8.34305 36.0967 8.34305 33.7507C8.34305 31.4048 6.47456 29.5015 4.17153 29.5015C1.8685 29.5015 0 31.4048 0 33.7507C0 36.0967 1.8685 38 4.17153 38ZM19.0109 38C21.3139 38 23.1824 36.0967 23.1824 33.7507C23.1824 31.4048 21.3139 29.5015 19.0109 29.5015C16.7078 29.5015 14.8393 31.4048 14.8393 33.7507C14.8393 36.0967 16.7078 38 19.0109 38Z" fill="#181C23"/>
<path d="M33.8285 38C36.1315 38 38 36.0967 38 33.7507C38 31.4048 36.1315 29.5015 33.8285 29.5015C31.5254 29.5015 29.6569 31.4048 29.6569 33.7507C29.6569 36.0967 31.5254 38 33.8285 38Z" fill="#181C23"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,4 @@
<svg width="301" height="26" viewBox="0 0 301 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5 15.3636H14.4091L16.1818 17.5909L19.1818 21L23.0909 25.7273H17.5L14.7273 22.5L12.8636 19.8182L9.5 15.3636ZM23.0455 12.3636C23.0455 14.9545 22.5417 17.1402 21.5341 18.9205C20.5265 20.6932 19.1667 22.0379 17.4545 22.9545C15.7424 23.8636 13.8333 24.3182 11.7273 24.3182C9.60606 24.3182 7.68939 23.8598 5.97727 22.9432C4.27273 22.0189 2.91667 20.6705 1.90909 18.8977C0.909091 17.1174 0.409091 14.9394 0.409091 12.3636C0.409091 9.77273 0.909091 7.59091 1.90909 5.81818C2.91667 4.03788 4.27273 2.69318 5.97727 1.78409C7.68939 0.867423 9.60606 0.40909 11.7273 0.40909C13.8333 0.40909 15.7424 0.867423 17.4545 1.78409C19.1667 2.69318 20.5265 4.03788 21.5341 5.81818C22.5417 7.59091 23.0455 9.77273 23.0455 12.3636ZM16.5455 12.3636C16.5455 10.9697 16.3598 9.79545 15.9886 8.84091C15.625 7.87879 15.0833 7.15151 14.3636 6.65909C13.6515 6.15909 12.7727 5.90909 11.7273 5.90909C10.6818 5.90909 9.79924 6.15909 9.07955 6.65909C8.36742 7.15151 7.82576 7.87879 7.45455 8.84091C7.09091 9.79545 6.90909 10.9697 6.90909 12.3636C6.90909 13.7576 7.09091 14.9356 7.45455 15.8977C7.82576 16.8523 8.36742 17.5795 9.07955 18.0795C9.79924 18.572 10.6818 18.8182 11.7273 18.8182C12.7727 18.8182 13.6515 18.572 14.3636 18.0795C15.0833 17.5795 15.625 16.8523 15.9886 15.8977C16.3598 14.9356 16.5455 13.7576 16.5455 12.3636ZM39.5455 10V14.7273H28.6364V10H39.5455ZM51.233 24H44.4148L52.0966 0.727272H60.733L68.4148 24H61.5966L56.5057 7.13636H56.3239L51.233 24ZM49.9602 14.8182H62.7784V19.5455H49.9602V14.8182ZM72.6562 24V0.727272H82.7017C84.429 0.727272 85.9403 1.06818 87.2358 1.75C88.5313 2.43182 89.5388 3.39015 90.2585 4.625C90.9782 5.85985 91.3381 7.30303 91.3381 8.95455C91.3381 10.6212 90.9669 12.0644 90.2244 13.2841C89.4896 14.5038 88.4555 15.4432 87.1222 16.1023C85.7964 16.7614 84.2472 17.0909 82.4744 17.0909H76.4744V12.1818H81.2017C81.9441 12.1818 82.5767 12.053 83.0994 11.7955C83.6297 11.5303 84.035 11.1553 84.3153 10.6705C84.6032 10.1856 84.7472 9.61364 84.7472 8.95455C84.7472 8.28788 84.6032 7.7197 84.3153 7.25C84.035 6.77273 83.6297 6.40909 83.0994 6.15909C82.5767 5.90151 81.9441 5.77273 81.2017 5.77273H78.9744V24H72.6562ZM95.6562 24V0.727272H105.702C107.429 0.727272 108.94 1.06818 110.236 1.75C111.531 2.43182 112.539 3.39015 113.259 4.625C113.978 5.85985 114.338 7.30303 114.338 8.95455C114.338 10.6212 113.967 12.0644 113.224 13.2841C112.49 14.5038 111.455 15.4432 110.122 16.1023C108.796 16.7614 107.247 17.0909 105.474 17.0909H99.4744V12.1818H104.202C104.944 12.1818 105.577 12.053 106.099 11.7955C106.63 11.5303 107.035 11.1553 107.315 10.6705C107.603 10.1856 107.747 9.61364 107.747 8.95455C107.747 8.28788 107.603 7.7197 107.315 7.25C107.035 6.77273 106.63 6.40909 106.099 6.15909C105.577 5.90151 104.944 5.77273 104.202 5.77273H101.974V24H95.6562ZM131.202 8C131.141 7.24242 130.857 6.65151 130.349 6.22727C129.849 5.80303 129.088 5.59091 128.065 5.59091C127.414 5.59091 126.88 5.67045 126.463 5.82954C126.054 5.98106 125.751 6.18939 125.554 6.45454C125.357 6.7197 125.255 7.02273 125.247 7.36364C125.232 7.64394 125.281 7.89773 125.395 8.125C125.516 8.3447 125.705 8.54545 125.963 8.72727C126.221 8.90151 126.55 9.06061 126.952 9.20455C127.353 9.34848 127.83 9.47727 128.384 9.59091L130.293 10C131.58 10.2727 132.683 10.6326 133.599 11.0795C134.516 11.5265 135.266 12.053 135.849 12.6591C136.433 13.2576 136.861 13.9318 137.134 14.6818C137.414 15.4318 137.558 16.25 137.565 17.1364C137.558 18.6667 137.175 19.9621 136.418 21.0227C135.66 22.0833 134.577 22.8902 133.168 23.4432C131.766 23.9962 130.08 24.2727 128.111 24.2727C126.088 24.2727 124.323 23.9735 122.815 23.375C121.315 22.7765 120.149 21.8561 119.315 20.6136C118.49 19.3636 118.073 17.7652 118.065 15.8182H124.065C124.103 16.5303 124.281 17.1288 124.599 17.6136C124.918 18.0985 125.365 18.4659 125.94 18.7159C126.524 18.9659 127.217 19.0909 128.02 19.0909C128.694 19.0909 129.259 19.0076 129.713 18.8409C130.168 18.6742 130.512 18.4432 130.747 18.1477C130.982 17.8523 131.103 17.5152 131.111 17.1364C131.103 16.7803 130.986 16.4697 130.759 16.2045C130.539 15.9318 130.175 15.6894 129.668 15.4773C129.16 15.2576 128.474 15.053 127.611 14.8636L125.293 14.3636C123.232 13.9167 121.607 13.1705 120.418 12.125C119.236 11.072 118.649 9.63636 118.656 7.81818C118.649 6.34091 119.043 5.04924 119.838 3.94318C120.641 2.82954 121.751 1.96212 123.168 1.34091C124.592 0.719696 126.224 0.40909 128.065 0.40909C129.944 0.40909 131.569 0.723484 132.94 1.35227C134.312 1.98106 135.368 2.86742 136.111 4.01136C136.861 5.14773 137.24 6.47727 137.247 8H131.202Z" fill="white"/>
<path d="M150.344 24V0.727272H156.662V18.9091H166.071V24H150.344ZM176.943 0.727272V24H170.625V0.727272H176.943ZM181.938 24V0.727272H192.028C193.801 0.727272 195.29 0.965909 196.494 1.44318C197.706 1.92045 198.619 2.5947 199.233 3.46591C199.854 4.33712 200.165 5.36364 200.165 6.54545C200.165 7.40151 199.975 8.18182 199.597 8.88636C199.225 9.59091 198.703 10.1818 198.028 10.6591C197.354 11.1288 196.566 11.4545 195.665 11.6364V11.8636C196.665 11.9015 197.574 12.1553 198.392 12.625C199.21 13.0871 199.862 13.7273 200.347 14.5455C200.831 15.3561 201.074 16.3106 201.074 17.4091C201.074 18.6818 200.741 19.8144 200.074 20.8068C199.415 21.7992 198.475 22.5795 197.256 23.1477C196.036 23.7159 194.581 24 192.892 24H181.938ZM188.256 18.9545H191.21C192.271 18.9545 193.066 18.7576 193.597 18.3636C194.127 17.9621 194.392 17.3712 194.392 16.5909C194.392 16.0455 194.267 15.5833 194.017 15.2045C193.767 14.8258 193.411 14.5379 192.949 14.3409C192.494 14.1439 191.945 14.0455 191.301 14.0455H188.256V18.9545ZM188.256 10.1364H190.847C191.4 10.1364 191.888 10.0492 192.312 9.875C192.737 9.70076 193.066 9.45076 193.301 9.125C193.544 8.79167 193.665 8.38636 193.665 7.90909C193.665 7.18939 193.407 6.64015 192.892 6.26136C192.377 5.875 191.725 5.68182 190.938 5.68182H188.256V10.1364ZM205.312 24V0.727272H215.358C217.085 0.727272 218.597 1.04167 219.892 1.67045C221.188 2.29924 222.195 3.20455 222.915 4.38636C223.634 5.56818 223.994 6.98485 223.994 8.63636C223.994 10.303 223.623 11.7083 222.881 12.8523C222.146 13.9962 221.112 14.8598 219.778 15.4432C218.453 16.0265 216.903 16.3182 215.131 16.3182H209.131V11.4091H213.858C214.6 11.4091 215.233 11.3182 215.756 11.1364C216.286 10.947 216.691 10.6477 216.972 10.2386C217.259 9.82955 217.403 9.29545 217.403 8.63636C217.403 7.9697 217.259 7.42803 216.972 7.01136C216.691 6.58712 216.286 6.27651 215.756 6.07954C215.233 5.875 214.6 5.77273 213.858 5.77273H211.631V24H205.312ZM218.949 13.3182L224.767 24H217.903L212.222 13.3182H218.949ZM234.733 24H227.915L235.597 0.727272H244.233L251.915 24H245.097L240.006 7.13636H239.824L234.733 24ZM233.46 14.8182H246.278V19.5455H233.46V14.8182ZM256.156 24V0.727272H266.202C267.929 0.727272 269.44 1.04167 270.736 1.67045C272.031 2.29924 273.039 3.20455 273.759 4.38636C274.478 5.56818 274.838 6.98485 274.838 8.63636C274.838 10.303 274.467 11.7083 273.724 12.8523C272.99 13.9962 271.955 14.8598 270.622 15.4432C269.296 16.0265 267.747 16.3182 265.974 16.3182H259.974V11.4091H264.702C265.444 11.4091 266.077 11.3182 266.599 11.1364C267.13 10.947 267.535 10.6477 267.815 10.2386C268.103 9.82955 268.247 9.29545 268.247 8.63636C268.247 7.9697 268.103 7.42803 267.815 7.01136C267.535 6.58712 267.13 6.27651 266.599 6.07954C266.077 5.875 265.444 5.77273 264.702 5.77273H262.474V24H256.156ZM269.793 13.3182L275.611 24H268.747L263.065 13.3182H269.793ZM277.483 0.727272H284.528L289.074 10.1818H289.256L293.801 0.727272H300.847L292.301 16.6818V24H286.028V16.6818L277.483 0.727272Z" fill="#0091E1"/>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

64
src/atoms/global.ts Normal file
View File

@ -0,0 +1,64 @@
import { atom } from 'recoil';
export const sortablePinnedAppsAtom = atom({
key: 'sortablePinnedAppsFromAtom',
default: [{
name: 'Q-Tube',
service: 'APP'
}, {
name: 'Q-Mail',
service: 'APP'
}, {
name: 'Q-Share',
service: 'APP'
}, {
name: 'Q-Blog',
service: 'APP'
}, {
name: 'Q-Fund',
service: 'APP'
}, {
name: 'Q-Shop',
service: 'APP'
},{
name: 'Qombo',
service: 'APP'
}
],
});
export const canSaveSettingToQdnAtom = atom({
key: 'canSaveSettingToQdnAtom',
default: false,
});
export const settingsQDNLastUpdatedAtom = atom({
key: 'settingsQDNLastUpdatedAtom',
default: -100,
});
export const settingsLocalLastUpdatedAtom = atom({
key: 'settingsLocalLastUpdatedAtom',
default: 0,
});
export const oldPinnedAppsAtom = atom({
key: 'oldPinnedAppsAtom',
default: [],
});
export const fullScreenAtom = atom({
key: 'fullScreenAtom',
default: false,
});
export const hasSettingsChangedAtom = atom({
key: 'hasSettingsChangedAtom',
default: false,
});
export const navigationControllerAtom = atom({
key: 'navigationControllerAtom',
default: {},
});

View File

@ -1,5 +1,7 @@
// @ts-nocheck
// import { encryptAndPublishSymmetricKeyGroupChat } from "./backgroundFunctions/encryption";
import './qortalRequests'
import { constant, isArray } from "lodash";
import {
decryptGroupEncryption,
@ -29,6 +31,19 @@ import { Sha256 } from "asmcrypto.js";
import { TradeBotRespondMultipleRequest } from "./transactions/TradeBotRespondMultipleRequest";
import { RESOURCE_TYPE_NUMBER_GROUP_CHAT_REACTIONS } from "./constants/resourceTypes";
export function cleanUrl(url) {
return url?.replace(/^(https?:\/\/)?(www\.)?/, '');
}
export function getProtocol(url) {
if (url?.startsWith('https://')) {
return 'https';
} else if (url?.startsWith('http://')) {
return 'http';
} else {
return 'unknown'; // If neither protocol is present
}
}
let lastGroupNotification;
export const groupApi = "https://ext-node.qortal.link";
export const groupApiSocket = "wss://ext-node.qortal.link";
@ -38,6 +53,7 @@ const timeDifferenceForNotificationChatsBackground = 600000;
const requestQueueAnnouncements = new RequestQueueWithPromise(1);
let isMobile = false;
const isMobileDevice = () => {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
@ -104,6 +120,17 @@ const getApiKeyFromStorage = async () => {
});
};
const getCustomNodesFromStorage = async () => {
return new Promise((resolve, reject) => {
chrome.storage.local.get("customNodes", (result) => {
if (chrome.runtime.lastError) {
return reject(chrome.runtime.lastError);
}
resolve(result.customNodes || null); // Return null if apiKey isn't found
});
});
};
// const getArbitraryEndpoint = ()=> {
// const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously
// if (apiKey) {
@ -115,7 +142,7 @@ const getApiKeyFromStorage = async () => {
const getArbitraryEndpoint = async () => {
const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously
if (apiKey) {
return `/arbitrary/resources/search`;
return `/arbitrary/resources/searchsimple`;
} else {
return `/arbitrary/resources/searchsimple`;
}
@ -128,23 +155,24 @@ export const getBaseApi = async (customApi?: string) => {
const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously
if (apiKey) {
return groupApiLocal;
return apiKey?.url;
} else {
return groupApi;
}
};
export const isUsingLocal = async () => {
export const createEndpointSocket = async (endpoint) => {
const apiKey = await getApiKeyFromStorage(); // Retrieve apiKey asynchronously
if (apiKey) {
return `${groupApiSocketLocal}${endpoint}`;
return true
} else {
return `${groupApiSocket}${endpoint}`;
return false;
}
};
export const createEndpoint = async (endpoint, customApi) => {
export const createEndpoint = async (endpoint, customApi?: string) => {
if (customApi) {
return `${customApi}${endpoint}`;
}
@ -154,7 +182,7 @@ export const createEndpoint = async (endpoint, customApi) => {
if (apiKey) {
// Check if the endpoint already contains a query string
const separator = endpoint.includes("?") ? "&" : "?";
return `${groupApiLocal}${endpoint}${separator}apiKey=${apiKey}`;
return `${apiKey?.url}${endpoint}${separator}apiKey=${apiKey?.apikey}`;
} else {
return `${groupApi}${endpoint}`;
}
@ -949,7 +977,7 @@ async function getAddressInfo(address) {
return data;
}
async function getKeyPair() {
export async function getKeyPair() {
const res = await chrome.storage.local.get(["keyPair"]);
if (res?.keyPair) {
return res.keyPair;
@ -958,7 +986,7 @@ async function getKeyPair() {
}
}
async function getSaveWallet() {
export async function getSaveWallet() {
const res = await chrome.storage.local.get(["walletInfo"]);
if (res?.walletInfo) {
return res.walletInfo;
@ -1007,7 +1035,7 @@ async function getTradesInfo(qortalAtAddresses) {
return trades; // Return the array of trade info objects
}
async function getBalanceInfo() {
export async function getBalanceInfo() {
const wallet = await getSaveWallet();
const address = wallet.address0;
const validApi = await getBaseApi();
@ -1057,7 +1085,7 @@ const processTransactionVersion2Chat = async (body: any, customApi) => {
});
};
const processTransactionVersion2 = async (body: any) => {
export const processTransactionVersion2 = async (body: any) => {
const url = await createEndpoint(`/transactions/process?apiVersion=2`);
try {
@ -1139,7 +1167,7 @@ const makeTransactionRequest = async (
return myTxnrequest;
};
const getLastRef = async () => {
export const getLastRef = async () => {
const wallet = await getSaveWallet();
const address = wallet.address0;
const validApi = await getBaseApi();
@ -1150,7 +1178,7 @@ const getLastRef = async () => {
const data = await response.text();
return data;
};
const sendQortFee = async () => {
export const sendQortFee = async (): Promise<number> => {
const validApi = await getBaseApi();
const response = await fetch(
validApi + "/transactions/unitfee?txType=PAYMENT"
@ -1350,6 +1378,24 @@ async function decryptWallet({ password, wallet, walletVersion }) {
publicKey: Base58.encode(keyPair.publicKey),
ltcPrivateKey: ltcPrivateKey,
ltcPublicKey: ltcPublicKey,
arrrSeed58: wallet2._addresses[0].arrrWallet.seed58,
btcAddress: wallet2._addresses[0].btcWallet.address,
btcPublicKey: wallet2._addresses[0].btcWallet.derivedMasterPublicKey,
btcPrivateKey: wallet2._addresses[0].btcWallet.derivedMasterPrivateKey,
ltcAddress: wallet2._addresses[0].ltcWallet.address,
dogeAddress: wallet2._addresses[0].dogeWallet.address,
dogePublicKey: wallet2._addresses[0].dogeWallet.derivedMasterPublicKey,
dogePrivateKey: wallet2._addresses[0].dogeWallet.derivedMasterPrivateKey,
dgbAddress: wallet2._addresses[0].dgbWallet.address,
dgbPublicKey: wallet2._addresses[0].dgbWallet.derivedMasterPublicKey,
dgbPrivateKey: wallet2._addresses[0].dgbWallet.derivedMasterPrivateKey,
rvnAddress: wallet2._addresses[0].rvnWallet.address,
rvnPublicKey: wallet2._addresses[0].rvnWallet.derivedMasterPublicKey,
rvnPrivateKey: wallet2._addresses[0].rvnWallet.derivedMasterPrivateKey
};
const dataString = JSON.stringify(toSave);
await new Promise((resolve, reject) => {
@ -1382,7 +1428,7 @@ async function decryptWallet({ password, wallet, walletVersion }) {
}
}
async function signChatFunc(chatBytesArray, chatNonce, customApi, keyPair) {
export async function signChatFunc(chatBytesArray, chatNonce, customApi, keyPair) {
let response;
try {
const signedChatBytes = signChat(chatBytesArray, chatNonce, keyPair);
@ -1407,7 +1453,7 @@ function sbrk(size, heap) {
return old;
}
const computePow = async ({ chatBytes, path, difficulty }) => {
export const computePow = async ({ chatBytes, path, difficulty }) => {
let response = null;
await new Promise((resolve, reject) => {
const _chatBytesArray = Object.keys(chatBytes).map(function (key) {
@ -1787,7 +1833,7 @@ async function createBuyOrderTx({ crosschainAtInfo, useLocal }) {
let responseVar
const txn = new TradeBotRespondMultipleRequest().createTransaction(message)
const apiKey = await getApiKeyFromStorage();
const responseFetch = await fetch(`http://127.0.0.1:12391/crosschain/tradebot/respondmultiple?apiKey=${apiKey}`, {
const responseFetch = await fetch(`${apiKey?.url}/crosschain/tradebot/respondmultiple?apiKey=${apiKey?.apikey}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -1971,7 +2017,7 @@ async function leaveGroup({ groupId }) {
return res;
}
async function joinGroup({ groupId }) {
export async function joinGroup({ groupId }) {
const wallet = await getSaveWallet();
const address = wallet.address0;
const lastReference = await getLastRef();
@ -2266,7 +2312,7 @@ async function inviteToGroup({ groupId, qortalAddress, inviteTime }) {
return res;
}
async function sendCoin({ password, amount, receiver }, skipConfirmPassword) {
export async function sendCoin({ password, amount, receiver }, skipConfirmPassword) {
try {
const confirmReceiver = await getNameOrAddress(receiver);
if (confirmReceiver.error)
@ -2498,7 +2544,7 @@ async function listenForChatMessageForBuyOrder({
}
}
function removeDuplicateWindow(popupUrl) {
export function removeDuplicateWindow(popupUrl) {
chrome.windows.getAll(
{ populate: true, windowTypes: ["popup"] },
(windows) => {
@ -2800,6 +2846,9 @@ async function getChatHeadsDirect() {
}
chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
if (request) {
console.log('REQUEST MESSAGE', request)
switch (request.action) {
case "version":
// Example: respond with the version
@ -3259,6 +3308,16 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
return true;
break;
}
case "setCustomNodes": {
const { nodes } = request;
// Save the customNodes in chrome.storage.local for persistence
chrome.storage.local.set({ customNodes: nodes }, () => {
sendResponse(true);
});
return true;
break;
}
case "getApiKey": {
getApiKeyFromStorage()
.then((res) => {
@ -3271,6 +3330,19 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
return true;
break;
}
case "getCustomNodesFromStorage": {
getCustomNodesFromStorage()
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
console.error(error.message);
});
return true;
break;
}
case "notifyAdminRegenerateSecretKey": {
const { groupName, adminAddress } = request.payload;
notifyAdminRegenerateSecretKey({ groupName, adminAddress })
@ -3873,19 +3945,35 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
break;
}
case "publishOnQDN": {
const { data, identifier, service } = request.payload;
const { data, identifier, service, title,
description,
category,
tag1,
tag2,
tag3,
tag4,
tag5, uploadType } = request.payload;
publishOnQDN({
data,
identifier,
service
service,
title,
description,
category,
tag1,
tag2,
tag3,
tag4,
tag5,
uploadType
})
.then((data) => {
sendResponse(data);
})
.catch((error) => {
console.error(error.message);
sendResponse({ error: error.message });
console.error(error?.message);
sendResponse({ error: error?.message || 'Unable to publish' });
});
return true;
break;
@ -4136,7 +4224,6 @@ chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
[
"keyPair",
"walletInfo",
"apiKey",
"active-groups-directs",
key1,
key2,

View File

@ -43,7 +43,7 @@ async function getSaveWallet() {
throw new Error("No wallet saved");
}
}
async function getNameInfo() {
export async function getNameInfo() {
const wallet = await getSaveWallet();
const address = wallet.address0;
const validApi = await getBaseApi()
@ -152,23 +152,40 @@ export const publishGroupEncryptedResource = async ({encryptedData, identifier})
throw new Error(error.message);
}
}
export const publishOnQDN = async ({data, identifier, service}) => {
try {
if(data && identifier && service){
export const publishOnQDN = async ({data, identifier, service, title,
description,
category,
tag1,
tag2,
tag3,
tag4,
tag5,
uploadType = 'file'
}) => {
if(data && service){
const registeredName = await getNameInfo()
if(!registeredName) throw new Error('You need a name to publish')
const res = await publishData({
registeredName, file: data, service, identifier, uploadType: 'file', isBase64: true, withFee: true
const res = await publishData({
registeredName, file: data, service, identifier, uploadType, isBase64: true, withFee: true, title,
description,
category,
tag1,
tag2,
tag3,
tag4,
tag5
})
return res
} else {
throw new Error('Cannot encrypt content')
throw new Error('Cannot publish content')
}
} catch (error: any) {
throw new Error(error.message);
}
}
export function uint8ArrayToBase64(uint8Array: any) {

View File

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

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

View File

@ -34,7 +34,8 @@
--bg-primary : rgba(31, 32, 35, 1);
--bg-2: #27282c;
--bg-3: rgba(0, 0, 0, 0.1);
--unread: rgba(255, 0, 0, 1);
--unread: #B14646;
--apps-circle: #1F2023
}
body {
@ -102,4 +103,8 @@ body {
html, body {
overscroll-behavior:none !important;
}
.swiper {
width: 100%;
}

View File

@ -6,7 +6,7 @@ import './index.css'
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { CssBaseline } from '@mui/material';
import { MessageQueueProvider } from './MessageQueueContext.tsx';
import { RecoilRoot } from 'recoil';
const theme = createTheme({
palette: {
primary: {
@ -50,7 +50,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<ThemeProvider theme={theme}>
<CssBaseline />
<MessageQueueProvider>
<RecoilRoot>
<App />
</RecoilRoot>
</MessageQueueProvider>
</ThemeProvider>
</React.StrictMode>,

View File

@ -67,7 +67,7 @@ export const createSymmetricKeyAndNonce = () => {
export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey }: any) => {
let combinedPublicKeys = publicKeys
let combinedPublicKeys = [...publicKeys, userPublicKey]
const decodedPrivateKey = Base58.decode(privateKey)
const publicKeysDuplicateFree = [...new Set(combinedPublicKeys)]
@ -275,11 +275,61 @@ export const decodeBase64ForUIChatMessages = (messages)=> {
export function decryptGroupDataQortalRequest(data64EncryptedData, privateKey) {
const allCombined = base64ToUint8Array(data64EncryptedData)
const str = "qortalGroupEncryptedData"
const strEncoder = new TextEncoder()
const strUint8Array = strEncoder.encode(str)
// Extract the nonce
const nonceStartPosition = strUint8Array.length
const nonceEndPosition = nonceStartPosition + 24 // Nonce is 24 bytes
const nonce = allCombined.slice(nonceStartPosition, nonceEndPosition)
// Extract the shared keyNonce
const keyNonceStartPosition = nonceEndPosition
const keyNonceEndPosition = keyNonceStartPosition + 24 // Nonce is 24 bytes
const keyNonce = allCombined.slice(keyNonceStartPosition, keyNonceEndPosition)
// Extract the sender's public key
const senderPublicKeyStartPosition = keyNonceEndPosition
const senderPublicKeyEndPosition = senderPublicKeyStartPosition + 32 // Public keys are 32 bytes
const senderPublicKey = allCombined.slice(senderPublicKeyStartPosition, senderPublicKeyEndPosition)
// Calculate count first
const countStartPosition = allCombined.length - 4 // 4 bytes before the end, since count is stored in Uint32 (4 bytes)
const countArray = allCombined.slice(countStartPosition, countStartPosition + 4)
const count = new Uint32Array(countArray.buffer)[0]
// Then use count to calculate encryptedData
const encryptedDataStartPosition = senderPublicKeyEndPosition // start position of encryptedData
const encryptedDataEndPosition = allCombined.length - ((count * (32 + 16)) + 4)
const encryptedData = allCombined.slice(encryptedDataStartPosition, encryptedDataEndPosition)
// Extract the encrypted keys
// 32+16 = 48
const combinedKeys = allCombined.slice(encryptedDataEndPosition, encryptedDataEndPosition + (count * 48))
if (!privateKey) {
throw new Error("Unable to retrieve keys")
}
const decodedPrivateKey = Base58.decode(privateKey)
const convertedPrivateKey = ed2curve.convertSecretKey(decodedPrivateKey)
const convertedSenderPublicKey = ed2curve.convertPublicKey(senderPublicKey)
const sharedSecret = new Uint8Array(32)
nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedSenderPublicKey)
for (let i = 0; i < count; i++) {
const encryptedKey = combinedKeys.slice(i * 48, (i + 1) * 48)
// Decrypt the symmetric key.
const decryptedKey = nacl.secretbox.open(encryptedKey, keyNonce, sharedSecret)
// If decryption was successful, decryptedKey will not be null.
if (decryptedKey) {
// Decrypt the data using the symmetric key.
const decryptedData = nacl.secretbox.open(encryptedData, nonce, decryptedKey)
// If decryption was successful, decryptedData will not be null.
if (decryptedData) {
return decryptedData
}
}
}
throw new Error("Unable to decrypt data")
}
export function decryptGroupData(data64EncryptedData: string, privateKey: string) {
const allCombined = base64ToUint8Array(data64EncryptedData)
const str = "qortalGroupEncryptedData"
const strEncoder = new TextEncoder()
@ -331,4 +381,43 @@ export function decryptGroupData(data64EncryptedData: string, privateKey: string
}
}
throw new Error("Unable to decrypt data")
}
export function uint8ArrayStartsWith(uint8Array, string) {
const stringEncoder = new TextEncoder()
const stringUint8Array = stringEncoder.encode(string)
if (uint8Array.length < stringUint8Array.length) {
return false
}
for (let i = 0; i < stringUint8Array.length; i++) {
if (uint8Array[i] !== stringUint8Array[i]) {
return false
}
}
return true
}
export function decryptDeprecatedSingle(uint8Array, publicKey, privateKey) {
const combinedData = uint8Array
const str = "qortalEncryptedData"
const strEncoder = new TextEncoder()
const strUint8Array = strEncoder.encode(str)
const strData = combinedData.slice(0, strUint8Array.length)
const nonce = combinedData.slice(strUint8Array.length, strUint8Array.length + 24)
const _encryptedData = combinedData.slice(strUint8Array.length + 24)
const _publicKey = window.parent.Base58.decode(publicKey)
if (!privateKey || !_publicKey) {
throw new Error("Unable to retrieve keys")
}
const convertedPrivateKey = ed2curve.convertSecretKey(privateKey)
const convertedPublicKey = ed2curve.convertPublicKey(_publicKey)
const sharedSecret = new Uint8Array(32)
nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey)
const _chatEncryptionSeed = new window.parent.Sha256().process(sharedSecret).finish().result
const _decryptedData = nacl.secretbox.open(_encryptedData, nonce, _chatEncryptionSeed)
if (!_decryptedData) {
throw new Error("Unable to decrypt")
}
return uint8ArrayToBase64(_decryptedData)
}

View File

@ -153,7 +153,6 @@ export const publishData = async ({
fee = feeAmount
} else if (withFee) {
const res = await getArbitraryFee()
if (res.fee) {
fee = res.fee
} else {
@ -162,9 +161,8 @@ export const publishData = async ({
}
let transactionBytes = await uploadData(registeredName, file, fee)
if (transactionBytes.error) {
throw new Error(transactionBytes.message || 'Error when uploading')
if (!transactionBytes || transactionBytes.error) {
throw new Error(transactionBytes?.message || 'Error when uploading')
} else if (transactionBytes.includes('Error 500 Internal Server Error')) {
throw new Error('Error when uploading')
}
@ -183,7 +181,7 @@ export const publishData = async ({
}
const uploadData = async (registeredName: string, file:any, fee: number) => {
if (identifier != null && identifier.trim().length > 0) {
let postBody = ''
let urlSuffix = ''
@ -211,8 +209,7 @@ export const publishData = async ({
}
let uploadDataUrl = `/arbitrary/${service}/${registeredName}${urlSuffix}`
if (identifier.trim().length > 0) {
if (identifier?.trim().length > 0) {
uploadDataUrl = `/arbitrary/${service}/${registeredName}/${identifier}${urlSuffix}`
}
@ -256,7 +253,7 @@ export const publishData = async ({
}
return await reusablePost(uploadDataUrl, postBody)
}
}
try {

428
src/qortalRequests.ts Normal file
View File

@ -0,0 +1,428 @@
import { addForeignServer, addListItems, createPoll, decryptData, deleteListItems, deployAt, encryptData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, updateForeignFee, voteOnPoll } from "./qortalRequests/get";
// Promisify chrome.storage.local.get
function getLocalStorage(key) {
return new Promise((resolve, reject) => {
chrome.storage.local.get([key], function (result) {
if (chrome.runtime.lastError) {
return reject(chrome.runtime.lastError);
}
resolve(result[key]);
});
});
}
// Promisify chrome.storage.local.set
function setLocalStorage(data) {
return new Promise((resolve, reject) => {
chrome.storage.local.set(data, function () {
if (chrome.runtime.lastError) {
return reject(chrome.runtime.lastError);
}
resolve();
});
});
}
export async function setPermission(key, value) {
try {
// Get the existing qortalRequestPermissions object
const qortalRequestPermissions = (await getLocalStorage('qortalRequestPermissions')) || {};
// Update the permission
qortalRequestPermissions[key] = value;
// Save the updated object back to storage
await setLocalStorage({ qortalRequestPermissions });
console.log('Permission set for', key);
} catch (error) {
console.error('Error setting permission:', error);
}
}
export async function getPermission(key) {
try {
// Get the qortalRequestPermissions object from storage
const qortalRequestPermissions = (await getLocalStorage('qortalRequestPermissions')) || {};
// Return the value for the given key, or null if it doesn't exist
return qortalRequestPermissions[key] || null;
} catch (error) {
console.error('Error getting permission:', error);
return null;
}
}
// TODO: GET_FRIENDS_LIST
// NOT SURE IF TO IMPLEMENT: LINK_TO_QDN_RESOURCE, QDN_RESOURCE_DISPLAYED, SET_TAB_NOTIFICATIONS
chrome?.runtime?.onMessage.addListener((request, sender, sendResponse) => {
if (request) {
const isFromExtension = request?.isExtension
switch (request.action) {
case "GET_USER_ACCOUNT": {
getUserAccount()
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: "Unable to get user account" });
});
break;
}
case "ENCRYPT_DATA": {
const data = request.payload;
encryptData(data, sender)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "DECRYPT_DATA": {
const data = request.payload;
decryptData(data)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "GET_LIST_ITEMS": {
const data = request.payload;
getListItems(data, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "ADD_LIST_ITEMS": {
const data = request.payload;
addListItems(data, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "DELETE_LIST_ITEM": {
const data = request.payload;
deleteListItems(data, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "PUBLISH_QDN_RESOURCE": {
const data = request.payload;
publishQDNResource(data, sender, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "PUBLISH_MULTIPLE_QDN_RESOURCES": {
const data = request.payload;
publishMultipleQDNResources(data, sender, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "VOTE_ON_POLL": {
const data = request.payload;
voteOnPoll(data, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "CREATE_POLL": {
const data = request.payload;
createPoll(data, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "SEND_CHAT_MESSAGE": {
const data = request.payload;
sendChatMessage(data, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "JOIN_GROUP": {
const data = request.payload;
joinGroup(data, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "SAVE_FILE": {
const data = request.payload;
saveFile(data, sender, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "DEPLOY_AT": {
const data = request.payload;
deployAt(data, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "GET_USER_WALLET": {
const data = request.payload;
getUserWallet(data, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "GET_WALLET_BALANCE": {
const data = request.payload;
getWalletBalance(data, false, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "GET_USER_WALLET_INFO": {
const data = request.payload;
getUserWalletInfo(data, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "GET_CROSSCHAIN_SERVER_INFO": {
const data = request.payload;
getCrossChainServerInfo(data)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "GET_TX_ACTIVITY_SUMMARY": {
const data = request.payload;
getTxActivitySummary(data)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "GET_FOREIGN_FEE": {
const data = request.payload;
getForeignFee(data)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "UPDATE_FOREIGN_FEE": {
const data = request.payload;
updateForeignFee(data)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "GET_SERVER_CONNECTION_HISTORY": {
const data = request.payload;
getServerConnectionHistory(data)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "SET_CURRENT_FOREIGN_SERVER": {
const data = request.payload;
setCurrentForeignServer(data)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "ADD_FOREIGN_SERVER": {
const data = request.payload;
addForeignServer(data)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "REMOVE_FOREIGN_SERVER": {
const data = request.payload;
removeForeignServer(data)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "GET_DAY_SUMMARY": {
const data = request.payload;
getDaySummary(data)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
case "SEND_COIN": {
const data = request.payload;
sendCoin(data, isFromExtension)
.then((res) => {
sendResponse(res);
})
.catch((error) => {
sendResponse({ error: error.message });
});
break;
}
}
}
return true;
});

2463
src/qortalRequests/get.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,73 @@
// @ts-nocheck
import { QORT_DECIMALS } from '../constants/constants'
import TransactionBase from './TransactionBase'
export default class CreatePollTransaction extends TransactionBase {
constructor() {
super()
this.type = 8
this._options = []
}
addOption(option) {
const optionBytes = this.constructor.utils.stringtoUTF8Array(option)
const optionLength = this.constructor.utils.int32ToBytes(optionBytes.length)
this._options.push({ length: optionLength, bytes: optionBytes })
}
set fee(fee) {
this._fee = fee * QORT_DECIMALS
this._feeBytes = this.constructor.utils.int64ToBytes(this._fee)
}
set ownerAddress(ownerAddress) {
this._ownerAddress = ownerAddress instanceof Uint8Array ? ownerAddress : this.constructor.Base58.decode(ownerAddress)
}
set rPollName(rPollName) {
this._rPollName = rPollName
this._rPollNameBytes = this.constructor.utils.stringtoUTF8Array(this._rPollName)
this._rPollNameLength = this.constructor.utils.int32ToBytes(this._rPollNameBytes.length)
}
set rPollDesc(rPollDesc) {
this._rPollDesc = rPollDesc
this._rPollDescBytes = this.constructor.utils.stringtoUTF8Array(this._rPollDesc)
this._rPollDescLength = this.constructor.utils.int32ToBytes(this._rPollDescBytes.length)
}
set rOptions(rOptions) {
const optionsArray = rOptions[0].split(', ').map(opt => opt.trim())
this._pollOptions = optionsArray
for (let i = 0; i < optionsArray.length; i++) {
this.addOption(optionsArray[i])
}
this._rNumberOfOptionsBytes = this.constructor.utils.int32ToBytes(optionsArray.length)
}
get params() {
const params = super.params
params.push(
this._ownerAddress,
this._rPollNameLength,
this._rPollNameBytes,
this._rPollDescLength,
this._rPollDescBytes,
this._rNumberOfOptionsBytes
)
// Push the dynamic options
for (let i = 0; i < this._options.length; i++) {
params.push(this._options[i].length, this._options[i].bytes)
}
params.push(this._feeBytes)
return params
}
}

View File

@ -0,0 +1,78 @@
// @ts-nocheck
import TransactionBase from './TransactionBase'
import { QORT_DECIMALS } from '../constants/constants'
export default class DeployAtTransaction extends TransactionBase {
constructor() {
super()
this.type = 16
}
set fee(fee) {
this._fee = fee * QORT_DECIMALS
this._feeBytes = this.constructor.utils.int64ToBytes(this._fee)
}
set rAmount(rAmount) {
this._rAmount = Math.round(rAmount * QORT_DECIMALS)
this._rAmountBytes = this.constructor.utils.int64ToBytes(this._rAmount)
}
set rName(rName) {
this._rName = rName
this._rNameBytes = this.constructor.utils.stringtoUTF8Array(this._rName.toLocaleLowerCase())
this._rNameLength = this.constructor.utils.int32ToBytes(this._rNameBytes.length)
}
set rDescription(rDescription) {
this._rDescription = rDescription
this._rDescriptionBytes = this.constructor.utils.stringtoUTF8Array(this._rDescription.toLocaleLowerCase())
this._rDescriptionLength = this.constructor.utils.int32ToBytes(this._rDescriptionBytes.length)
}
set atType(atType) {
this._atType = atType
this._atTypeBytes = this.constructor.utils.stringtoUTF8Array(this._atType)
this._atTypeLength = this.constructor.utils.int32ToBytes(this._atTypeBytes.length)
}
set rTags(rTags) {
this._rTags = rTags
this._rTagsBytes = this.constructor.utils.stringtoUTF8Array(this._rTags.toLocaleLowerCase())
this._rTagsLength = this.constructor.utils.int32ToBytes(this._rTagsBytes.length)
}
set rCreationBytes(rCreationBytes) {
const decode = this.constructor.Base58.decode(rCreationBytes)
this._rCreationBytes = this.constructor.utils.stringtoUTF8Array(decode)
this._rCreationBytesLength = this.constructor.utils.int32ToBytes(this._rCreationBytes.length)
}
set rAssetId(rAssetId) {
this._rAssetId = this.constructor.utils.int64ToBytes(rAssetId)
}
get params() {
const params = super.params
params.push(
this._rNameLength,
this._rNameBytes,
this._rDescriptionLength,
this._rDescriptionBytes,
this._atTypeLength,
this._atTypeBytes,
this._rTagsLength,
this._rTagsBytes,
this._rCreationBytesLength,
this._rCreationBytes,
this._rAmountBytes,
this._rAssetId,
this._feeBytes
)
return params
}
}

View File

@ -0,0 +1,38 @@
// @ts-nocheck
import { QORT_DECIMALS } from '../constants/constants'
import TransactionBase from './TransactionBase'
export default class VoteOnPollTransaction extends TransactionBase {
constructor() {
super()
this.type = 9
}
set fee(fee) {
this._fee = fee * QORT_DECIMALS
this._feeBytes = this.constructor.utils.int64ToBytes(this._fee)
}
set rPollName(rPollName) {
this._rPollName = rPollName
this._rPollNameBytes = this.constructor.utils.stringtoUTF8Array(this._rPollName)
this._rPollNameLength = this.constructor.utils.int32ToBytes(this._rPollNameBytes.length)
}
set rOptionIndex(rOptionIndex) {
this._rOptionIndex = rOptionIndex
this._rOptionIndexBytes = this.constructor.utils.int32ToBytes(this._rOptionIndex)
}
get params() {
const params = super.params
params.push(
this._rPollNameLength,
this._rPollNameBytes,
this._rOptionIndexBytes,
this._feeBytes
)
return params
}
}

View File

@ -14,11 +14,17 @@ import JoinGroupTransaction from './JoinGroupTransaction.js'
import AddGroupAdminTransaction from './AddGroupAdminTransaction.js'
import RemoveGroupAdminTransaction from './RemoveGroupAdminTransaction.js'
import RegisterNameTransaction from './RegisterNameTransaction.js'
import VoteOnPollTransaction from './VoteOnPollTransaction.js'
import CreatePollTransaction from './CreatePollTransaction.js'
import DeployAtTransaction from './DeployAtTransaction.js'
export const transactionTypes = {
3: RegisterNameTransaction,
2: PaymentTransaction,
8: CreatePollTransaction,
9: VoteOnPollTransaction,
16: DeployAtTransaction,
18: ChatTransaction,
181: GroupChatTransaction,
22: CreateGroupTransaction,

67
src/useAppFullscreen.tsx Normal file
View File

@ -0,0 +1,67 @@
import { useCallback, useEffect } from 'react';
import { isMobile } from './App';
export const useAppFullScreen = (setFullScreen) => {
const enterFullScreen = useCallback(() => {
const element = document.documentElement; // Target the entire HTML document
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) { // Firefox
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) { // Chrome, Safari and Opera
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) { // IE/Edge
element.msRequestFullscreen();
}
}, []);
const exitFullScreen = useCallback(() => {
if (document.fullscreenElement) {
document.exitFullscreen();
} else if (document.mozFullScreenElement) {
document.mozCancelFullScreen();
} else if (document.webkitFullscreenElement) {
document.webkitExitFullscreen();
} else if (document.msFullscreenElement) {
document.msExitFullscreen();
}
}, []);
const toggleFullScreen = useCallback(() => {
if(!isMobile || isMobile) return
if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
exitFullScreen();
setFullScreen(false)
} else {
enterFullScreen();
setFullScreen(true)
}
}, [enterFullScreen, exitFullScreen]);
// Listen for changes to fullscreen state
useEffect(() => {
const handleFullScreenChange = () => {
if (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
} else {
setFullScreen(false);
}
};
document.addEventListener('fullscreenchange', handleFullScreenChange);
document.addEventListener('webkitfullscreenchange', handleFullScreenChange); // Safari
document.addEventListener('mozfullscreenchange', handleFullScreenChange); // Firefox
document.addEventListener('MSFullscreenChange', handleFullScreenChange); // IE/Edge
return () => {
document.removeEventListener('fullscreenchange', handleFullScreenChange);
document.removeEventListener('webkitfullscreenchange', handleFullScreenChange);
document.removeEventListener('mozfullscreenchange', handleFullScreenChange);
document.removeEventListener('MSFullscreenChange', handleFullScreenChange);
};
}, []);
return { enterFullScreen, exitFullScreen, toggleFullScreen };
};

View File

@ -0,0 +1,94 @@
import React, { useCallback, useEffect } from 'react'
import { useRecoilState, useSetRecoilState } from 'recoil';
import { canSaveSettingToQdnAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global';
import { getArbitraryEndpointReact, getBaseApiReact } from './App';
import { decryptResource } from './components/Group/Group';
import { base64ToUint8Array, uint8ArrayToObject } from './backgroundFunctions/encryption';
function fetchFromLocalStorage(key) {
try {
const serializedValue = localStorage.getItem(key);
if (serializedValue === null) {
console.log(`No data found for key: ${key}`);
return null;
}
return JSON.parse(serializedValue);
} catch (error) {
console.error('Error fetching from localStorage:', error);
return null;
}
}
const getPublishRecord = async (myName) => {
// const validApi = await findUsableApi();
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=ext_saved_settings&exactmatchnames=true&limit=1&prefix=true&name=${myName}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error("network error");
}
const publishData = await response.json();
if(publishData?.length > 0) return {hasPublishRecord: true, timestamp: publishData[0]?.updated || publishData[0].created}
return {hasPublishRecord: false}
};
const getPublish = async (myName) => {
try {
let data
const res = await fetch(
`${getBaseApiReact()}/arbitrary/DOCUMENT_PRIVATE/${myName}/ext_saved_settings?encoding=base64`
);
data = await res.text();
if(!data) throw new Error('Unable to fetch publish')
const decryptedKey: any = await decryptResource(data);
const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
return decryptedKeyToObject
} catch (error) {
return null
}
};
export const useQortalGetSaveSettings = (myName) => {
const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom);
const setCanSave = useSetRecoilState(canSaveSettingToQdnAtom);
const setSettingsQDNLastUpdated = useSetRecoilState(settingsQDNLastUpdatedAtom);
const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom);
const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom)
const getSavedSettings = useCallback(async (myName, settingsLocalLastUpdated)=> {
try {
const {hasPublishRecord, timestamp} = await getPublishRecord(myName)
if(hasPublishRecord){
const settings = await getPublish(myName)
if(settings?.sortablePinnedApps && timestamp > settingsLocalLastUpdated){
setSortablePinnedApps(settings.sortablePinnedApps)
setSettingsQDNLastUpdated(timestamp || 0)
} else if(settings?.sortablePinnedApps){
setSettingsQDNLastUpdated(timestamp || 0)
setOldPinnedApps(settings.sortablePinnedApps)
}
if(!settings){
// set -100 to indicate that it couldn't fetch the publish
setSettingsQDNLastUpdated(-100)
}
} else {
setSettingsQDNLastUpdated( 0)
}
setCanSave(true)
} catch (error) {
}
}, [])
useEffect(()=> {
if(!myName || !settingsLocalLastUpdated) return
getSavedSettings(myName, settingsLocalLastUpdated)
}, [getSavedSettings, myName, settingsLocalLastUpdated])
}

View File

@ -0,0 +1,35 @@
import React, { useCallback, useEffect } from 'react'
import { useSetRecoilState } from 'recoil';
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from './atoms/global';
function fetchFromLocalStorage(key) {
try {
const serializedValue = localStorage.getItem(key);
if (serializedValue === null) {
console.log(`No data found for key: ${key}`);
return null;
}
return JSON.parse(serializedValue);
} catch (error) {
console.error('Error fetching from localStorage:', error);
return null;
}
}
export const useRetrieveDataLocalStorage = () => {
const setSortablePinnedApps = useSetRecoilState(sortablePinnedAppsAtom);
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
const getSortablePinnedApps = useCallback(()=> {
const pinnedAppsLocal = fetchFromLocalStorage('ext_saved_settings')
if(pinnedAppsLocal?.sortablePinnedApps){
setSortablePinnedApps(pinnedAppsLocal?.sortablePinnedApps)
}
setSettingsLocalLastUpdated(pinnedAppsLocal?.timestamp || -1)
}, [])
useEffect(()=> {
getSortablePinnedApps()
}, [getSortablePinnedApps])
}

56
src/utils/memeTypes.ts Normal file
View File

@ -0,0 +1,56 @@
export const mimeToExtensionMap = {
// Documents
"application/pdf": ".pdf",
"application/msword": ".doc",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
"application/vnd.ms-excel": ".xls",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
"application/vnd.ms-powerpoint": ".ppt",
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
"application/vnd.oasis.opendocument.text": ".odt",
"application/vnd.oasis.opendocument.spreadsheet": ".ods",
"application/vnd.oasis.opendocument.presentation": ".odp",
"text/plain": ".txt",
"text/csv": ".csv",
"text/html": ".html",
"application/xhtml+xml": ".xhtml",
"application/xml": ".xml",
"application/json": ".json",
// Images
"image/jpeg": ".jpg",
"image/png": ".png",
"image/gif": ".gif",
"image/webp": ".webp",
"image/svg+xml": ".svg",
"image/tiff": ".tif",
"image/bmp": ".bmp",
// Audio
"audio/mpeg": ".mp3",
"audio/ogg": ".ogg",
"audio/wav": ".wav",
"audio/webm": ".weba",
"audio/aac": ".aac",
// Video
"video/mp4": ".mp4",
"video/webm": ".webm",
"video/ogg": ".ogv",
"video/x-msvideo": ".avi",
"video/quicktime": ".mov",
"video/x-ms-wmv": ".wmv",
"video/mpeg": ".mpeg",
"video/3gpp": ".3gp",
"video/3gpp2": ".3g2",
"video/x-matroska": ".mkv",
"video/x-flv": ".flv",
// Archives
"application/zip": ".zip",
"application/x-rar-compressed": ".rar",
"application/x-tar": ".tar",
"application/x-7z-compressed": ".7z",
"application/x-gzip": ".gz",
"application/x-bzip2": ".bz2",
}