Merge branch 'feature/q-app-support'
@ -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
@ -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",
|
||||
|
@ -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
After Width: | Height: | Size: 1.2 MiB |
26
public/disable-gateway-message.js
Normal 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.');
|
||||
}
|
||||
|
||||
})();
|
27
public/disable-gateway-popup.js
Normal 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
@ -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
@ -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
|
||||
};
|
@ -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://*:*;"
|
||||
}
|
||||
}
|
||||
|
1545
src/App.tsx
643
src/ExtStates/NotAuthenticated.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
11
src/assets/svgs/AppIcon.svg
Normal 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 |
6
src/assets/svgs/ClearInput.svg
Normal 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 |
9
src/assets/svgs/LogoSelected.svg
Normal file
After Width: | Height: | Size: 821 KiB |
4
src/assets/svgs/NavAdd.svg
Normal 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 |
10
src/assets/svgs/NavBack.svg
Normal 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 |
6
src/assets/svgs/NavCloseTab.svg
Normal 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 |
10
src/assets/svgs/NavMoreMenu.svg
Normal 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 |
10
src/assets/svgs/SaveIcon.tsx
Normal 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>
|
||||
|
||||
)
|
||||
}
|
3
src/assets/svgs/Search.svg
Normal 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 |
16
src/assets/svgs/StarEmpty.tsx
Normal 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>
|
||||
|
||||
|
||||
|
||||
);
|
||||
};
|
18
src/assets/svgs/StarFilled.tsx
Normal 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>
|
||||
);
|
||||
};
|
3
src/assets/svgs/qappDevelopText.svg
Normal file
After Width: | Height: | Size: 13 KiB |
4
src/assets/svgs/qappDots.svg
Normal 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 |
4
src/assets/svgs/qappLibraryText.svg
Normal 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
@ -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: {},
|
||||
});
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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 || {})
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
210
src/components/Apps/AppInfo.tsx
Normal file
@ -0,0 +1,210 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppCircleLabel,
|
||||
AppDownloadButton,
|
||||
AppDownloadButtonText,
|
||||
AppInfoAppName,
|
||||
AppInfoSnippetContainer,
|
||||
AppInfoSnippetLeft,
|
||||
AppInfoSnippetMiddle,
|
||||
AppInfoSnippetRight,
|
||||
AppInfoUserName,
|
||||
AppsCategoryInfo,
|
||||
AppsCategoryInfoLabel,
|
||||
AppsCategoryInfoSub,
|
||||
AppsCategoryInfoValue,
|
||||
AppsInfoDescription,
|
||||
AppsLibraryContainer,
|
||||
AppsParent,
|
||||
AppsWidthLimiter,
|
||||
} from "./Apps-styles";
|
||||
import { Avatar, Box, ButtonBase, InputBase } from "@mui/material";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import { getBaseApiReact, isMobile } from "../../App";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
import { AppRating } from "./AppRating";
|
||||
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global";
|
||||
import { saveToLocalStorage } from "./AppsNavBar";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
|
||||
export const AppInfo = ({ app, myName }) => {
|
||||
const isInstalled = app?.status?.status === "READY";
|
||||
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
||||
|
||||
const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === app?.name && item?.service === app?.service)
|
||||
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
|
||||
|
||||
return (
|
||||
<AppsLibraryContainer
|
||||
sx={{
|
||||
height: !isMobile && "100%",
|
||||
justifyContent: !isMobile && "flex-start",
|
||||
alignItems: isMobile && 'center'
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxWidth: "500px",
|
||||
width: '90%'
|
||||
}}>
|
||||
|
||||
|
||||
{!isMobile && <Spacer height="30px" />}
|
||||
<AppsWidthLimiter>
|
||||
<AppInfoSnippetContainer>
|
||||
<AppInfoSnippetLeft
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
gap: "18px",
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer
|
||||
sx={{
|
||||
width: "auto",
|
||||
}}
|
||||
>
|
||||
<AppCircle
|
||||
sx={{
|
||||
border: "none",
|
||||
height: "100px",
|
||||
width: "100px",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "43px",
|
||||
width: "43px",
|
||||
"& img": {
|
||||
objectFit: "fill",
|
||||
},
|
||||
}}
|
||||
alt={app?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "43px",
|
||||
height: "auto",
|
||||
}}
|
||||
src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
</AppCircle>
|
||||
</AppCircleContainer>
|
||||
<AppInfoSnippetMiddle>
|
||||
<AppInfoAppName>
|
||||
{app?.metadata?.title || app?.name}
|
||||
</AppInfoAppName>
|
||||
<Spacer height="6px" />
|
||||
<AppInfoUserName>{app?.name}</AppInfoUserName>
|
||||
<Spacer height="3px" />
|
||||
</AppInfoSnippetMiddle>
|
||||
</AppInfoSnippetLeft>
|
||||
<AppInfoSnippetRight></AppInfoSnippetRight>
|
||||
</AppInfoSnippetContainer>
|
||||
<Spacer height="11px" />
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '20px'
|
||||
}}>
|
||||
<AppDownloadButton
|
||||
onClick={() => {
|
||||
setSortablePinnedApps((prev) => {
|
||||
let updatedApps;
|
||||
|
||||
if (isSelectedAppPinned) {
|
||||
// Remove the selected app if it is pinned
|
||||
updatedApps = prev.filter(
|
||||
(item) => !(item?.name === app?.name && item?.service === app?.service)
|
||||
);
|
||||
} else {
|
||||
// Add the selected app if it is not pinned
|
||||
updatedApps = [...prev, {
|
||||
name: app?.name,
|
||||
service: app?.service,
|
||||
}];
|
||||
}
|
||||
|
||||
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps)
|
||||
return updatedApps;
|
||||
});
|
||||
setSettingsLocalLastUpdated(Date.now())
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: "#359ff7ff",
|
||||
width: "100%",
|
||||
maxWidth: "320px",
|
||||
height: "29px",
|
||||
opacity: isSelectedAppPinned ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
<AppDownloadButtonText>
|
||||
{!isMobile ? (
|
||||
<>
|
||||
{isSelectedAppPinned ? 'Unpin from dashboard' : 'Pin to dashboard'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{isSelectedAppPinned ? 'Unpin' : 'Pin'}
|
||||
</>
|
||||
)}
|
||||
|
||||
</AppDownloadButtonText>
|
||||
</AppDownloadButton>
|
||||
<AppDownloadButton
|
||||
onClick={() => {
|
||||
executeEvent("addTab", {
|
||||
data: app,
|
||||
});
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: isInstalled ? "#0091E1" : "#247C0E",
|
||||
width: "100%",
|
||||
maxWidth: "320px",
|
||||
height: "29px",
|
||||
}}
|
||||
>
|
||||
<AppDownloadButtonText>
|
||||
{isInstalled ? "Open" : "Download"}
|
||||
</AppDownloadButtonText>
|
||||
</AppDownloadButton>
|
||||
</Box>
|
||||
|
||||
</AppsWidthLimiter>
|
||||
<Spacer height="20px" />
|
||||
<AppsWidthLimiter>
|
||||
<AppsCategoryInfo>
|
||||
<AppRating ratingCountPosition="top" myName={myName} app={app} />
|
||||
<Spacer width="16px" />
|
||||
<Spacer height="40px" width="1px" backgroundColor="white" />
|
||||
<Spacer width="16px" />
|
||||
<AppsCategoryInfoSub>
|
||||
<AppsCategoryInfoLabel>Category:</AppsCategoryInfoLabel>
|
||||
<Spacer height="4px" />
|
||||
<AppsCategoryInfoValue>
|
||||
{app?.metadata?.categoryName || "none"}
|
||||
</AppsCategoryInfoValue>
|
||||
</AppsCategoryInfoSub>
|
||||
</AppsCategoryInfo>
|
||||
<Spacer height="30px" />
|
||||
<AppInfoAppName>About this Q-App</AppInfoAppName>
|
||||
</AppsWidthLimiter>
|
||||
<Spacer height="20px" />
|
||||
<AppsInfoDescription>
|
||||
{app?.metadata?.description || "No description"}
|
||||
</AppsInfoDescription>
|
||||
</Box>
|
||||
</AppsLibraryContainer>
|
||||
);
|
||||
};
|
157
src/components/Apps/AppInfoSnippet.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import React from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppDownloadButton,
|
||||
AppDownloadButtonText,
|
||||
AppInfoAppName,
|
||||
AppInfoSnippetContainer,
|
||||
AppInfoSnippetLeft,
|
||||
AppInfoSnippetMiddle,
|
||||
AppInfoSnippetRight,
|
||||
AppInfoUserName,
|
||||
} from "./Apps-styles";
|
||||
import { Avatar, ButtonBase } from "@mui/material";
|
||||
import { getBaseApiReact, isMobile } from "../../App";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
import { AppRating } from "./AppRating";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global";
|
||||
import { saveToLocalStorage } from "./AppsNavBar";
|
||||
|
||||
export const AppInfoSnippet = ({ app, myName, isFromCategory }) => {
|
||||
|
||||
const isInstalled = app?.status?.status === 'READY'
|
||||
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
||||
|
||||
const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === app?.name && item?.service === app?.service)
|
||||
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
|
||||
return (
|
||||
<AppInfoSnippetContainer>
|
||||
<AppInfoSnippetLeft>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
height: "80px",
|
||||
width: "60px",
|
||||
}}
|
||||
onClick={()=> {
|
||||
if(isFromCategory){
|
||||
executeEvent("selectedAppInfoCategory", {
|
||||
data: app,
|
||||
});
|
||||
return
|
||||
}
|
||||
executeEvent("selectedAppInfo", {
|
||||
data: app,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer>
|
||||
<AppCircle
|
||||
sx={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "31px",
|
||||
width: "31px",
|
||||
'& img': {
|
||||
objectFit: 'fill',
|
||||
}
|
||||
}}
|
||||
alt={app?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "31px",
|
||||
height: "auto",
|
||||
}}
|
||||
src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
</AppCircle>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
<AppInfoSnippetMiddle>
|
||||
|
||||
<ButtonBase onClick={()=> {
|
||||
if(isFromCategory){
|
||||
executeEvent("selectedAppInfoCategory", {
|
||||
data: app,
|
||||
});
|
||||
return
|
||||
}
|
||||
executeEvent("selectedAppInfo", {
|
||||
data: app,
|
||||
});
|
||||
}}>
|
||||
<AppInfoAppName >
|
||||
{app?.metadata?.title || app?.name}
|
||||
</AppInfoAppName>
|
||||
</ButtonBase>
|
||||
<Spacer height="6px" />
|
||||
<AppInfoUserName>
|
||||
{ app?.name}
|
||||
</AppInfoUserName>
|
||||
<Spacer height="3px" />
|
||||
<AppRating app={app} myName={myName} />
|
||||
</AppInfoSnippetMiddle>
|
||||
</AppInfoSnippetLeft>
|
||||
<AppInfoSnippetRight sx={{
|
||||
gap: '10px'
|
||||
}}>
|
||||
{!isMobile && (
|
||||
<AppDownloadButton onClick={()=> {
|
||||
|
||||
setSortablePinnedApps((prev) => {
|
||||
let updatedApps;
|
||||
|
||||
if (isSelectedAppPinned) {
|
||||
// Remove the selected app if it is pinned
|
||||
updatedApps = prev.filter(
|
||||
(item) => !(item?.name === app?.name && item?.service === app?.service)
|
||||
);
|
||||
} else {
|
||||
// Add the selected app if it is not pinned
|
||||
updatedApps = [...prev, {
|
||||
name: app?.name,
|
||||
service: app?.service,
|
||||
}];
|
||||
}
|
||||
|
||||
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps)
|
||||
return updatedApps;
|
||||
});
|
||||
setSettingsLocalLastUpdated(Date.now())
|
||||
}} sx={{
|
||||
backgroundColor: '#359ff7ff',
|
||||
opacity: isSelectedAppPinned ? 0.6 : 1
|
||||
|
||||
}}>
|
||||
<AppDownloadButtonText> {isSelectedAppPinned ? 'Unpin' : 'Pin'}</AppDownloadButtonText>
|
||||
</AppDownloadButton>
|
||||
)}
|
||||
|
||||
<AppDownloadButton onClick={()=> {
|
||||
|
||||
executeEvent("addTab", {
|
||||
data: app
|
||||
})
|
||||
}} sx={{
|
||||
backgroundColor: isInstalled ? '#0091E1' : '#247C0E',
|
||||
|
||||
}}>
|
||||
<AppDownloadButtonText>{isInstalled ? 'Open' : 'Download'}</AppDownloadButtonText>
|
||||
</AppDownloadButton>
|
||||
</AppInfoSnippetRight>
|
||||
</AppInfoSnippetContainer>
|
||||
);
|
||||
};
|
519
src/components/Apps/AppPublish.tsx
Normal file
@ -0,0 +1,519 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppCircleLabel,
|
||||
AppDownloadButton,
|
||||
AppDownloadButtonText,
|
||||
AppInfoAppName,
|
||||
AppInfoSnippetContainer,
|
||||
AppInfoSnippetLeft,
|
||||
AppInfoSnippetMiddle,
|
||||
AppInfoSnippetRight,
|
||||
AppInfoUserName,
|
||||
AppLibrarySubTitle,
|
||||
AppPublishTagsContainer,
|
||||
AppsLibraryContainer,
|
||||
AppsParent,
|
||||
AppsWidthLimiter,
|
||||
PublishQAppCTAButton,
|
||||
PublishQAppChoseFile,
|
||||
PublishQAppInfo,
|
||||
} from "./Apps-styles";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
ButtonBase,
|
||||
InputBase,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
Select as BaseSelect,
|
||||
SelectProps,
|
||||
selectClasses,
|
||||
SelectRootSlotProps,
|
||||
} from "@mui/base/Select";
|
||||
import { Option as BaseOption, optionClasses } from "@mui/base/Option";
|
||||
import { styled } from "@mui/system";
|
||||
import UnfoldMoreRoundedIcon from "@mui/icons-material/UnfoldMoreRounded";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import { MyContext, getBaseApiReact, isMobile } from "../../App";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar";
|
||||
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
|
||||
import { getFee } from "../../background";
|
||||
import { fileToBase64 } from "../../utils/fileReading";
|
||||
|
||||
const CustomSelect = styled(Select)({
|
||||
border: "0.5px solid var(--50-white, #FFFFFF80)",
|
||||
padding: "0px 15px",
|
||||
borderRadius: "5px",
|
||||
height: "36px",
|
||||
width: "100%",
|
||||
maxWidth: "450px",
|
||||
"& .MuiSelect-select": {
|
||||
padding: "0px",
|
||||
},
|
||||
"&:hover": {
|
||||
borderColor: "none", // Border color on hover
|
||||
},
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "none", // Border color when focused
|
||||
},
|
||||
"&.Mui-disabled": {
|
||||
opacity: 0.5, // Lower opacity when disabled
|
||||
},
|
||||
"& .MuiSvgIcon-root": {
|
||||
color: "var(--50-white, #FFFFFF80)",
|
||||
},
|
||||
});
|
||||
|
||||
const CustomMenuItem = styled(MenuItem)({
|
||||
backgroundColor: "#1f1f1f", // Background for dropdown items
|
||||
color: "#ccc",
|
||||
"&:hover": {
|
||||
backgroundColor: "#333", // Darker background on hover
|
||||
},
|
||||
});
|
||||
|
||||
export const AppPublish = ({ names, categories }) => {
|
||||
const [name, setName] = useState("");
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [category, setCategory] = useState("");
|
||||
const [appType, setAppType] = useState("APP");
|
||||
const [file, setFile] = useState(null);
|
||||
const { show } = useContext(MyContext);
|
||||
|
||||
const [tag1, setTag1] = useState("");
|
||||
const [tag2, setTag2] = useState("");
|
||||
const [tag3, setTag3] = useState("");
|
||||
const [tag4, setTag4] = useState("");
|
||||
const [tag5, setTag5] = useState("");
|
||||
const [openSnack, setOpenSnack] = useState(false);
|
||||
const [infoSnack, setInfoSnack] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState("");
|
||||
const maxFileSize = appType === "APP" ? 50 * 1024 * 1024 : 400 * 1024 * 1024; // 50MB or 400MB
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
"application/zip": [".zip"], // Only accept zip files
|
||||
},
|
||||
maxSize: maxFileSize, // Set the max size based on appType
|
||||
multiple: false, // Disable multiple file uploads
|
||||
onDrop: (acceptedFiles) => {
|
||||
if (acceptedFiles.length > 0) {
|
||||
setFile(acceptedFiles[0]); // Set the file name
|
||||
}
|
||||
},
|
||||
onDropRejected: (fileRejections) => {
|
||||
fileRejections.forEach(({ file, errors }) => {
|
||||
errors.forEach((error) => {
|
||||
if (error.code === "file-too-large") {
|
||||
console.error(
|
||||
`File ${file.name} is too large. Max size allowed is ${
|
||||
maxFileSize / (1024 * 1024)
|
||||
} MB.`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const getQapp = React.useCallback(async (name, appType) => {
|
||||
try {
|
||||
setIsLoading("Loading app information");
|
||||
const url = `${getBaseApiReact()}/arbitrary/resources/search?service=${appType}&mode=ALL&name=${name}&includemetadata=true`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response?.ok) return;
|
||||
const responseData = await response.json();
|
||||
|
||||
if (responseData?.length > 0) {
|
||||
const myApp = responseData[0];
|
||||
setTitle(myApp?.metadata?.title || "");
|
||||
setDescription(myApp?.metadata?.description || "");
|
||||
setCategory(myApp?.metadata?.category || "");
|
||||
setTag1(myApp?.metadata?.tags[0] || "");
|
||||
setTag2(myApp?.metadata?.tags[1] || "");
|
||||
setTag3(myApp?.metadata?.tags[2] || "");
|
||||
setTag4(myApp?.metadata?.tags[3] || "");
|
||||
setTag5(myApp?.metadata?.tags[4] || "");
|
||||
}
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setIsLoading("");
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!name || !appType) return;
|
||||
getQapp(name, appType);
|
||||
}, [name, appType]);
|
||||
|
||||
const publishApp = async () => {
|
||||
try {
|
||||
const data = {
|
||||
name,
|
||||
title,
|
||||
description,
|
||||
category,
|
||||
appType,
|
||||
file,
|
||||
};
|
||||
const requiredFields = [
|
||||
"name",
|
||||
"title",
|
||||
"description",
|
||||
"category",
|
||||
"appType",
|
||||
"file",
|
||||
];
|
||||
|
||||
const missingFields: string[] = [];
|
||||
requiredFields.forEach((field) => {
|
||||
if (!data[field]) {
|
||||
missingFields.push(field);
|
||||
}
|
||||
});
|
||||
if (missingFields.length > 0) {
|
||||
const missingFieldsString = missingFields.join(", ");
|
||||
const errorMsg = `Missing fields: ${missingFieldsString}`;
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
const fee = await getFee("ARBITRARY");
|
||||
|
||||
await show({
|
||||
message: "Would you like to publish this app?",
|
||||
publishFee: fee.fee + " QORT",
|
||||
});
|
||||
setIsLoading("Publishing... Please wait.");
|
||||
const fileBase64 = await fileToBase64(file);
|
||||
await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "publishOnQDN",
|
||||
payload: {
|
||||
data: fileBase64,
|
||||
service: appType,
|
||||
title,
|
||||
description,
|
||||
category,
|
||||
tag1,
|
||||
tag2,
|
||||
tag3,
|
||||
tag4,
|
||||
tag5,
|
||||
uploadType: 'zip'
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (!response?.error) {
|
||||
res(response);
|
||||
return;
|
||||
}
|
||||
rej(response.error);
|
||||
}
|
||||
);
|
||||
});
|
||||
setInfoSnack({
|
||||
type: "success",
|
||||
message:
|
||||
"Successfully published. Please wait a couple minutes for the network to propogate the changes.",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
const dataObj = {
|
||||
name: name,
|
||||
service: appType,
|
||||
metadata: {
|
||||
title: title,
|
||||
description: description,
|
||||
category: category,
|
||||
},
|
||||
created: Date.now(),
|
||||
};
|
||||
executeEvent("addTab", {
|
||||
data: dataObj,
|
||||
});
|
||||
} catch (error) {
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
message: error?.message || "Unable to publish app",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
} finally {
|
||||
setIsLoading("");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<AppsLibraryContainer sx={{
|
||||
height: !isMobile ? '100%' : 'auto',
|
||||
paddingTop: !isMobile && '30px',
|
||||
alignItems: !isMobile && 'center'
|
||||
}}>
|
||||
<AppsWidthLimiter sx={{
|
||||
width: !isMobile ? 'auto' : '90%'
|
||||
}}>
|
||||
<AppLibrarySubTitle>Create Apps!</AppLibrarySubTitle>
|
||||
<Spacer height="18px" />
|
||||
<PublishQAppInfo>
|
||||
Note: Currently, only one App and Website is allowed per Name.
|
||||
</PublishQAppInfo>
|
||||
<Spacer height="18px" />
|
||||
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>Name/App</InputLabel>
|
||||
<CustomSelect
|
||||
placeholder="Select Name/App"
|
||||
displayEmpty
|
||||
value={name}
|
||||
onChange={(event) => setName(event?.target.value)}
|
||||
>
|
||||
<CustomMenuItem value="">
|
||||
<em
|
||||
style={{
|
||||
color: "var(--50-white, #FFFFFF80)",
|
||||
}}
|
||||
>
|
||||
Select Name/App
|
||||
</em>{" "}
|
||||
{/* This is the placeholder item */}
|
||||
</CustomMenuItem>
|
||||
{names.map((name) => {
|
||||
return <CustomMenuItem value={name}>{name}</CustomMenuItem>;
|
||||
})}
|
||||
</CustomSelect>
|
||||
<Spacer height="15px" />
|
||||
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>App service type</InputLabel>
|
||||
<CustomSelect
|
||||
placeholder="SERVICE TYPE"
|
||||
displayEmpty
|
||||
value={appType}
|
||||
onChange={(event) => setAppType(event?.target.value)}
|
||||
>
|
||||
<CustomMenuItem value="">
|
||||
<em
|
||||
style={{
|
||||
color: "var(--50-white, #FFFFFF80)",
|
||||
}}
|
||||
>
|
||||
Select App Type
|
||||
</em>{" "}
|
||||
{/* This is the placeholder item */}
|
||||
</CustomMenuItem>
|
||||
<CustomMenuItem value={"APP"}>App</CustomMenuItem>
|
||||
<CustomMenuItem value={"WEBSITE"}>Website</CustomMenuItem>
|
||||
</CustomSelect>
|
||||
<Spacer height="15px" />
|
||||
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>Title</InputLabel>
|
||||
<InputBase
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
sx={{
|
||||
border: "0.5px solid var(--50-white, #FFFFFF80)",
|
||||
padding: "0px 15px",
|
||||
borderRadius: "5px",
|
||||
height: "36px",
|
||||
width: "100%",
|
||||
maxWidth: "450px",
|
||||
}}
|
||||
placeholder="Title"
|
||||
inputProps={{
|
||||
"aria-label": "Title",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
<Spacer height="15px" />
|
||||
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>Description</InputLabel>
|
||||
<InputBase
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
sx={{
|
||||
border: "0.5px solid var(--50-white, #FFFFFF80)",
|
||||
padding: "0px 15px",
|
||||
borderRadius: "5px",
|
||||
height: "36px",
|
||||
width: "100%",
|
||||
maxWidth: "450px",
|
||||
}}
|
||||
placeholder="Description"
|
||||
inputProps={{
|
||||
"aria-label": "Description",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Spacer height="15px" />
|
||||
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>Category</InputLabel>
|
||||
<CustomSelect
|
||||
displayEmpty
|
||||
placeholder="Select Category"
|
||||
value={category}
|
||||
onChange={(event) => setCategory(event?.target.value)}
|
||||
>
|
||||
<CustomMenuItem value="">
|
||||
<em
|
||||
style={{
|
||||
color: "var(--50-white, #FFFFFF80)",
|
||||
}}
|
||||
>
|
||||
Select Category
|
||||
</em>{" "}
|
||||
{/* This is the placeholder item */}
|
||||
</CustomMenuItem>
|
||||
{categories?.map((category) => {
|
||||
return (
|
||||
<CustomMenuItem value={category?.id}>
|
||||
{category?.name}
|
||||
</CustomMenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomSelect>
|
||||
<Spacer height="15px" />
|
||||
<InputLabel sx={{ color: '#888', fontSize: '14px', marginBottom: '2px' }}>Tags</InputLabel>
|
||||
<AppPublishTagsContainer>
|
||||
<InputBase
|
||||
value={tag1}
|
||||
onChange={(e) => setTag1(e.target.value)}
|
||||
sx={{
|
||||
border: "0.5px solid var(--50-white, #FFFFFF80)",
|
||||
padding: "0px 15px",
|
||||
borderRadius: "5px",
|
||||
height: "36px",
|
||||
width: "100px",
|
||||
}}
|
||||
placeholder="Tag 1"
|
||||
inputProps={{
|
||||
"aria-label": "Tag 1",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
<InputBase
|
||||
value={tag2}
|
||||
onChange={(e) => setTag2(e.target.value)}
|
||||
sx={{
|
||||
border: "0.5px solid var(--50-white, #FFFFFF80)",
|
||||
padding: "0px 15px",
|
||||
borderRadius: "5px",
|
||||
height: "36px",
|
||||
width: "100px",
|
||||
}}
|
||||
placeholder="Tag 2"
|
||||
inputProps={{
|
||||
"aria-label": "Tag 2",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
<InputBase
|
||||
value={tag3}
|
||||
onChange={(e) => setTag3(e.target.value)}
|
||||
sx={{
|
||||
border: "0.5px solid var(--50-white, #FFFFFF80)",
|
||||
padding: "0px 15px",
|
||||
borderRadius: "5px",
|
||||
height: "36px",
|
||||
width: "100px",
|
||||
}}
|
||||
placeholder="Tag 3"
|
||||
inputProps={{
|
||||
"aria-label": "Tag 3",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
<InputBase
|
||||
value={tag4}
|
||||
onChange={(e) => setTag4(e.target.value)}
|
||||
sx={{
|
||||
border: "0.5px solid var(--50-white, #FFFFFF80)",
|
||||
padding: "0px 15px",
|
||||
borderRadius: "5px",
|
||||
height: "36px",
|
||||
width: "100px",
|
||||
}}
|
||||
placeholder="Tag 4"
|
||||
inputProps={{
|
||||
"aria-label": "Tag 4",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
<InputBase
|
||||
value={tag5}
|
||||
onChange={(e) => setTag5(e.target.value)}
|
||||
sx={{
|
||||
border: "0.5px solid var(--50-white, #FFFFFF80)",
|
||||
padding: "0px 15px",
|
||||
borderRadius: "5px",
|
||||
height: "36px",
|
||||
width: "100px",
|
||||
}}
|
||||
placeholder="Tag 5"
|
||||
inputProps={{
|
||||
"aria-label": "Tag 5",
|
||||
fontSize: "14px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
</AppPublishTagsContainer>
|
||||
<Spacer height="30px" />
|
||||
<PublishQAppInfo>
|
||||
Select .zip file containing static content:{" "}
|
||||
</PublishQAppInfo>
|
||||
<Spacer height="10px" />
|
||||
<PublishQAppInfo>{`(${
|
||||
appType === "APP" ? "50mb" : "400mb"
|
||||
} MB maximum)`}</PublishQAppInfo>
|
||||
{file && (
|
||||
<>
|
||||
<Spacer height="5px" />
|
||||
<PublishQAppInfo>{`Selected: (${file?.name})`}</PublishQAppInfo>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Spacer height="18px" />
|
||||
<PublishQAppChoseFile {...getRootProps()}>
|
||||
{" "}
|
||||
<input {...getInputProps()} />
|
||||
Choose File
|
||||
</PublishQAppChoseFile>
|
||||
<Spacer height="35px" />
|
||||
<PublishQAppCTAButton
|
||||
sx={{
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onClick={publishApp}
|
||||
>
|
||||
Publish
|
||||
</PublishQAppCTAButton>
|
||||
</AppsWidthLimiter>
|
||||
<LoadingSnackbar
|
||||
open={!!isLoading}
|
||||
info={{
|
||||
message: isLoading,
|
||||
}}
|
||||
/>
|
||||
<CustomizedSnackbars
|
||||
duration={3500}
|
||||
open={openSnack}
|
||||
setOpen={setOpenSnack}
|
||||
info={infoSnack}
|
||||
setInfo={setInfoSnack}
|
||||
/>
|
||||
|
||||
</AppsLibraryContainer>
|
||||
);
|
||||
};
|
243
src/components/Apps/AppRating.tsx
Normal file
@ -0,0 +1,243 @@
|
||||
import { Box, Rating, Typography } from "@mui/material";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { getFee } from "../../background";
|
||||
import { MyContext, getBaseApiReact } from "../../App";
|
||||
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
|
||||
import { StarFilledIcon } from "../../assets/svgs/StarFilled";
|
||||
import { StarEmptyIcon } from "../../assets/svgs/StarEmpty";
|
||||
import { AppInfoUserName } from "./Apps-styles";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
|
||||
export const AppRating = ({ app, myName, ratingCountPosition = "right" }) => {
|
||||
const [value, setValue] = useState(0);
|
||||
const { show } = useContext(MyContext);
|
||||
const [hasPublishedRating, setHasPublishedRating] = useState<null | boolean>(
|
||||
null
|
||||
);
|
||||
const [pollInfo, setPollInfo] = useState(null);
|
||||
const [votesInfo, setVotesInfo] = useState(null);
|
||||
const [openSnack, setOpenSnack] = useState(false);
|
||||
const [infoSnack, setInfoSnack] = useState(null);
|
||||
const hasCalledRef = useRef(false);
|
||||
|
||||
const getRating = useCallback(async (name, service) => {
|
||||
try {
|
||||
hasCalledRef.current = true;
|
||||
const pollName = `app-library-${service}-rating-${name}`;
|
||||
const url = `${getBaseApiReact()}/polls/${pollName}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const responseData = await response.json();
|
||||
if (responseData?.message?.includes("POLL_NO_EXISTS")) {
|
||||
setHasPublishedRating(false);
|
||||
} else if (responseData?.pollName) {
|
||||
setPollInfo(responseData);
|
||||
setHasPublishedRating(true);
|
||||
const urlVotes = `${getBaseApiReact()}/polls/votes/${pollName}`;
|
||||
|
||||
const responseVotes = await fetch(urlVotes, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const responseDataVotes = await responseVotes.json();
|
||||
setVotesInfo(responseDataVotes);
|
||||
const voteCount = responseDataVotes.voteCounts;
|
||||
// Include initial value vote in the calculation
|
||||
const ratingVotes = voteCount.filter(
|
||||
(vote) => !vote.optionName.startsWith("initialValue-")
|
||||
);
|
||||
const initialValueVote = voteCount.find((vote) =>
|
||||
vote.optionName.startsWith("initialValue-")
|
||||
);
|
||||
if (initialValueVote) {
|
||||
// Convert "initialValue-X" to just "X" and add it to the ratingVotes array
|
||||
const initialRating = parseInt(
|
||||
initialValueVote.optionName.split("-")[1],
|
||||
10
|
||||
);
|
||||
ratingVotes.push({
|
||||
optionName: initialRating.toString(),
|
||||
voteCount: 1,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate the weighted average
|
||||
let totalScore = 0;
|
||||
let totalVotes = 0;
|
||||
|
||||
ratingVotes.forEach((vote) => {
|
||||
const rating = parseInt(vote.optionName, 10); // Extract rating value (1-5)
|
||||
const count = vote.voteCount;
|
||||
totalScore += rating * count; // Weighted score
|
||||
totalVotes += count; // Total number of votes
|
||||
});
|
||||
|
||||
// Calculate average rating (ensure no division by zero)
|
||||
const averageRating = totalVotes > 0 ? totalScore / totalVotes : 0;
|
||||
setValue(averageRating);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.message?.includes("POLL_NO_EXISTS")) {
|
||||
setHasPublishedRating(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (hasCalledRef.current) return;
|
||||
if (!app) return;
|
||||
getRating(app?.name, app?.service);
|
||||
}, [getRating, app?.name]);
|
||||
|
||||
const rateFunc = async (event, newValue) => {
|
||||
try {
|
||||
if (!myName) throw new Error("You need a name to rate.");
|
||||
if (!app?.name) return;
|
||||
const fee = await getFee("ARBITRARY");
|
||||
|
||||
await show({
|
||||
message: `Would you like to rate this app a rating of ${newValue}?`,
|
||||
publishFee: fee.fee + " QORT",
|
||||
});
|
||||
|
||||
if (hasPublishedRating === false) {
|
||||
const pollName = `app-library-${app.service}-rating-${app.name}`;
|
||||
const pollOptions = [`1, 2, 3, 4, 5, initialValue-${newValue}`];
|
||||
await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "CREATE_POLL",
|
||||
type: "qortalRequest",
|
||||
payload: {
|
||||
pollName: pollName,
|
||||
pollDescription: `Rating for ${app.service} ${app.name}`,
|
||||
pollOptions: pollOptions,
|
||||
pollOwnerAddress: myName,
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (response.error) {
|
||||
rej(response?.message);
|
||||
return;
|
||||
} else {
|
||||
res(response);
|
||||
setInfoSnack({
|
||||
type: "success",
|
||||
message:
|
||||
"Successfully rated. Please wait a couple minutes for the network to propogate the changes.",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const pollName = `app-library-${app.service}-rating-${app.name}`;
|
||||
const optionIndex = pollInfo?.pollOptions.findIndex(
|
||||
(option) => +option.optionName === +newValue
|
||||
);
|
||||
if (isNaN(optionIndex) || optionIndex === -1)
|
||||
throw new Error("Cannot find rating option");
|
||||
await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "VOTE_ON_POLL",
|
||||
type: "qortalRequest",
|
||||
payload: {
|
||||
pollName: pollName,
|
||||
optionIndex,
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (response.error) {
|
||||
rej(response?.message);
|
||||
return;
|
||||
} else {
|
||||
res(response);
|
||||
setInfoSnack({
|
||||
type: "success",
|
||||
message:
|
||||
"Successfully rated. Please wait a couple minutes for the network to propogate the changes.",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
message: error.message || "An error occurred while trying to rate.",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: ratingCountPosition === "top" ? "column" : "row",
|
||||
}}
|
||||
>
|
||||
{ratingCountPosition === "top" && (
|
||||
<>
|
||||
<AppInfoUserName>
|
||||
{(votesInfo?.totalVotes ?? 0) +
|
||||
(votesInfo?.voteCounts?.length === 6 ? 1 : 0)}{" "}
|
||||
{" RATINGS"}
|
||||
</AppInfoUserName>
|
||||
<Spacer height="6px" />
|
||||
<AppInfoUserName>{value?.toFixed(1)}</AppInfoUserName>
|
||||
<Spacer height="6px" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Rating
|
||||
value={value}
|
||||
onChange={rateFunc}
|
||||
precision={1}
|
||||
readOnly={hasPublishedRating === null}
|
||||
size="small"
|
||||
icon={<StarFilledIcon />}
|
||||
emptyIcon={<StarEmptyIcon />}
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "2px",
|
||||
}}
|
||||
/>
|
||||
{ratingCountPosition === "right" && (
|
||||
<AppInfoUserName>
|
||||
{(votesInfo?.totalVotes ?? 0) +
|
||||
(votesInfo?.voteCounts?.length === 6 ? 1 : 0)}
|
||||
</AppInfoUserName>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<CustomizedSnackbars
|
||||
duration={2000}
|
||||
open={openSnack}
|
||||
setOpen={setOpenSnack}
|
||||
info={infoSnack}
|
||||
setInfo={setInfoSnack}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
149
src/components/Apps/AppViewer.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Avatar, Box, } from "@mui/material";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import { MyContext, getBaseApiReact, isMobile } from "../../App";
|
||||
|
||||
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
|
||||
import { useFrame } from "react-frame-component";
|
||||
import { useQortalMessageListener } from "./useQortalMessageListener";
|
||||
|
||||
|
||||
|
||||
|
||||
export const AppViewer = React.forwardRef(({ app , hide}, iframeRef) => {
|
||||
const { rootHeight } = useContext(MyContext);
|
||||
// const iframeRef = useRef(null);
|
||||
const { document, window: frameWindow } = useFrame();
|
||||
const {path, history, changeCurrentIndex} = useQortalMessageListener(frameWindow, iframeRef, app?.tabId)
|
||||
const [url, setUrl] = useState('')
|
||||
console.log('historyreact', history)
|
||||
|
||||
useEffect(()=> {
|
||||
setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? `/${app?.path}` : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}`)
|
||||
}, [app?.service, app?.name, app?.identifier, app?.path])
|
||||
const defaultUrl = useMemo(()=> {
|
||||
return url
|
||||
}, [url])
|
||||
|
||||
|
||||
|
||||
const refreshAppFunc = (e) => {
|
||||
const {tabId} = e.detail
|
||||
if(tabId === app?.tabId){
|
||||
const constructUrl = `${getBaseApiReact()}/render/${app?.service}/${app?.name}${path != null ? path : ''}?theme=dark&identifier=${app?.identifier != null ? app?.identifier : ''}&time=${new Date().getMilliseconds()}`
|
||||
setUrl(constructUrl)
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("refreshApp", refreshAppFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("refreshApp", refreshAppFunc);
|
||||
};
|
||||
}, [app, path]);
|
||||
|
||||
// Function to navigate back in iframe
|
||||
const navigateBackInIframe = async () => {
|
||||
if (iframeRef.current && iframeRef.current.contentWindow && history?.currentIndex > 0) {
|
||||
// Calculate the previous index and path
|
||||
const previousPageIndex = history.currentIndex - 1;
|
||||
const previousPath = history.customQDNHistoryPaths[previousPageIndex];
|
||||
|
||||
// Signal non-manual navigation
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ action: 'PERFORMING_NON_MANUAL' }, '*'
|
||||
);
|
||||
console.log('previousPageIndex', previousPageIndex)
|
||||
// Update the current index locally
|
||||
changeCurrentIndex(previousPageIndex);
|
||||
|
||||
// Create a navigation promise with a 200ms timeout
|
||||
const navigationPromise = new Promise((resolve, reject) => {
|
||||
function handleNavigationSuccess(event) {
|
||||
console.log('listeninghandlenav', event)
|
||||
if (event.data?.action === 'NAVIGATION_SUCCESS' && event.data.path === previousPath) {
|
||||
frameWindow.removeEventListener('message', handleNavigationSuccess);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
frameWindow.addEventListener('message', handleNavigationSuccess);
|
||||
|
||||
// Timeout after 200ms if no response
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('message', handleNavigationSuccess);
|
||||
reject(new Error("Navigation timeout"));
|
||||
}, 200);
|
||||
|
||||
// Send the navigation command after setting up the listener and timeout
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ action: 'NAVIGATE_TO_PATH', path: previousPath, requestedHandler: 'UI' }, '*'
|
||||
);
|
||||
});
|
||||
|
||||
// Execute navigation promise and handle timeout fallback
|
||||
try {
|
||||
await navigationPromise;
|
||||
console.log('Navigation succeeded within 200ms.');
|
||||
} catch (error) {
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ action: 'PERFORMING_NON_MANUAL' }, '*'
|
||||
);
|
||||
setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.previousPath != null ? previousPath : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}&time=${new Date().getMilliseconds()}&isManualNavigation=false`)
|
||||
// iframeRef.current.contentWindow.location.href = previousPath; // Fallback URL update
|
||||
}
|
||||
} else {
|
||||
console.log('Iframe not accessible or does not have a content window.');
|
||||
}
|
||||
};
|
||||
|
||||
const navigateBackAppFunc = (e) => {
|
||||
|
||||
navigateBackInIframe()
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if(!app?.tabId) return
|
||||
subscribeToEvent(`navigateBackApp-${app?.tabId}`, navigateBackAppFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent(`navigateBackApp-${app?.tabId}`, navigateBackAppFunc);
|
||||
};
|
||||
}, [app, history]);
|
||||
|
||||
|
||||
// Function to navigate back in iframe
|
||||
const navigateForwardInIframe = async () => {
|
||||
|
||||
|
||||
if (iframeRef.current && iframeRef.current.contentWindow) {
|
||||
console.log('iframeRef.contentWindow', iframeRef.current.contentWindow);
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ action: 'NAVIGATE_FORWARD'},
|
||||
'*'
|
||||
);
|
||||
} else {
|
||||
console.log('Iframe not accessible or does not have a content window.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
|
||||
<iframe ref={iframeRef} style={{
|
||||
height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px )`,
|
||||
border: 'none',
|
||||
width: '100%'
|
||||
}} id="browser-iframe" src={defaultUrl} sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals" allow="fullscreen">
|
||||
|
||||
</iframe>
|
||||
</Box>
|
||||
|
||||
);
|
||||
});
|
50
src/components/Apps/AppViewerContainer.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React, { useContext, } from 'react';
|
||||
import { AppViewer } from './AppViewer';
|
||||
import Frame from 'react-frame-component';
|
||||
import { MyContext, isMobile } from '../../App';
|
||||
|
||||
const AppViewerContainer = React.forwardRef(({ app, isSelected, hide }, ref) => {
|
||||
const { rootHeight } = useContext(MyContext);
|
||||
|
||||
|
||||
return (
|
||||
<Frame
|
||||
id={`browser-iframe-${app?.tabId}`}
|
||||
|
||||
head={
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
* {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
*::-webkit-scrollbar {
|
||||
display: none; /* Chrome, Safari, Opera */
|
||||
}
|
||||
.frame-content {
|
||||
overflow: hidden;
|
||||
height: ${!isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`};
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
}
|
||||
style={{
|
||||
display: (!isSelected || hide) && 'none',
|
||||
height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`,
|
||||
border: 'none',
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<AppViewer app={app} ref={ref} hide={!isSelected || hide} />
|
||||
</Frame>
|
||||
);
|
||||
});
|
||||
|
||||
export default AppViewerContainer;
|
311
src/components/Apps/Apps-styles.tsx
Normal file
@ -0,0 +1,311 @@
|
||||
import {
|
||||
AppBar,
|
||||
Button,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Box,
|
||||
TextField,
|
||||
InputLabel,
|
||||
ButtonBase,
|
||||
} from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
|
||||
export const AppsParent = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
alignItems: "center",
|
||||
overflow: 'auto',
|
||||
// For WebKit-based browsers (Chrome, Safari, etc.)
|
||||
"::-webkit-scrollbar": {
|
||||
width: "0px", // Set the width to 0 to hide the scrollbar
|
||||
height: "0px", // Set the height to 0 for horizontal scrollbar
|
||||
},
|
||||
|
||||
// For Firefox
|
||||
scrollbarWidth: "none", // Hides the scrollbar in Firefox
|
||||
|
||||
// Optional for better cross-browser consistency
|
||||
"-ms-overflow-style": "none" // Hides scrollbar in IE and Edge
|
||||
}));
|
||||
export const AppsContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
width: "90%",
|
||||
justifyContent: 'space-evenly',
|
||||
gap: '24px',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'flex-start',
|
||||
alignSelf: 'center'
|
||||
|
||||
}));
|
||||
export const AppsLibraryContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
export const AppsWidthLimiter = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
width: "90%",
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'flex-start',
|
||||
}));
|
||||
export const AppsSearchContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
width: "90%",
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#434343',
|
||||
borderRadius: '8px',
|
||||
padding: '0px 10px',
|
||||
height: '36px'
|
||||
}));
|
||||
export const AppsSearchLeft = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
width: "90%",
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
flexGrow: 1,
|
||||
flexShrink: 0
|
||||
}));
|
||||
export const AppsSearchRight = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
width: "90%",
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
flexShrink: 1
|
||||
}));
|
||||
export const AppCircleContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: '5px',
|
||||
alignItems: 'center',
|
||||
width: '100%'
|
||||
}));
|
||||
export const Add = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '36px',
|
||||
fontWeight: 500,
|
||||
lineHeight: '43.57px',
|
||||
textAlign: 'left'
|
||||
|
||||
}));
|
||||
export const AppCircleLabel = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
width: '100%'
|
||||
}));
|
||||
export const AppLibrarySubTitle = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '16px',
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
}));
|
||||
export const AppCircle = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
width: "60px",
|
||||
flexDirection: "column",
|
||||
height: "60px",
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: "var(--apps-circle)",
|
||||
border: '1px solid #FFFFFF'
|
||||
}));
|
||||
|
||||
export const AppInfoSnippetContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%'
|
||||
}));
|
||||
|
||||
export const AppInfoSnippetLeft = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
gap: '12px'
|
||||
}));
|
||||
export const AppInfoSnippetRight = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
export const AppDownloadButton = styled(ButtonBase)(({ theme }) => ({
|
||||
backgroundColor: "#247C0E",
|
||||
width: '101px',
|
||||
height: '29px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: '25px',
|
||||
alignSelf: 'center'
|
||||
}));
|
||||
|
||||
export const AppDownloadButtonText = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '14px',
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
}));
|
||||
|
||||
export const AppPublishTagsContainer = styled(Box)(({theme})=> ({
|
||||
gap: '10px',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
display: 'flex'
|
||||
}))
|
||||
|
||||
|
||||
export const AppInfoSnippetMiddle = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
}));
|
||||
|
||||
export const AppInfoAppName = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '16px',
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
textAlign: 'start'
|
||||
}));
|
||||
export const AppInfoUserName = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '13px',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.2,
|
||||
color: '#8D8F93',
|
||||
textAlign: 'start'
|
||||
}));
|
||||
|
||||
|
||||
export const AppsNavBarParent = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '60px',
|
||||
backgroundColor: '#1F2023',
|
||||
padding: '0px 10px',
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
zIndex: 1,
|
||||
}));
|
||||
|
||||
export const AppsNavBarLeft = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
flexGrow: 1
|
||||
}));
|
||||
export const AppsNavBarRight = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
export const TabParent = styled(Box)(({ theme }) => ({
|
||||
height: '36px',
|
||||
width: '36px',
|
||||
backgroundColor: '#434343',
|
||||
position: 'relative',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}));
|
||||
|
||||
export const PublishQAppCTAParent = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
backgroundColor: '#181C23'
|
||||
}));
|
||||
|
||||
export const PublishQAppCTALeft = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
export const PublishQAppCTARight = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
export const PublishQAppCTAButton = styled(ButtonBase)(({ theme }) => ({
|
||||
width: '101px',
|
||||
height: '29px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: '25px',
|
||||
border: '1px solid #FFFFFF'
|
||||
}));
|
||||
export const PublishQAppDotsBG = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
backgroundColor: '#4BBCFE'
|
||||
}));
|
||||
|
||||
export const PublishQAppInfo = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '10px',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.2,
|
||||
fontStyle: 'italic'
|
||||
}));
|
||||
|
||||
export const PublishQAppChoseFile = styled(ButtonBase)(({ theme }) => ({
|
||||
width: '101px',
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: '5px',
|
||||
backgroundColor: '#0091E1',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
fontSize: '10px'
|
||||
}));
|
||||
|
||||
|
||||
export const AppsCategoryInfo = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
export const AppsCategoryInfoSub = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
export const AppsCategoryInfoLabel = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '12px',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.2,
|
||||
color: '#8D8F93',
|
||||
}));
|
||||
export const AppsCategoryInfoValue = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
color: '#8D8F93',
|
||||
}));
|
||||
export const AppsInfoDescription = styled(Typography)(({ theme }) => ({
|
||||
fontSize: '13px',
|
||||
fontWeight: 300,
|
||||
lineHeight: 1.2,
|
||||
width: '90%',
|
||||
textAlign: 'start'
|
||||
}));
|
326
src/components/Apps/Apps.tsx
Normal file
@ -0,0 +1,326 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { AppsHome } from "./AppsHome";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { getBaseApiReact } from "../../App";
|
||||
import { AppInfo } from "./AppInfo";
|
||||
import {
|
||||
executeEvent,
|
||||
subscribeToEvent,
|
||||
unsubscribeFromEvent,
|
||||
} from "../../utils/events";
|
||||
import { AppsParent } from "./Apps-styles";
|
||||
import AppViewerContainer from "./AppViewerContainer";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import { AppPublish } from "./AppPublish";
|
||||
import { AppsCategory } from "./AppsCategory";
|
||||
import { AppsLibrary } from "./AppsLibrary";
|
||||
|
||||
const uid = new ShortUniqueId({ length: 8 });
|
||||
|
||||
export const Apps = ({ mode, setMode, show , myName}) => {
|
||||
const [availableQapps, setAvailableQapps] = useState([]);
|
||||
const [selectedAppInfo, setSelectedAppInfo] = useState(null);
|
||||
const [selectedCategory, setSelectedCategory] = useState(null)
|
||||
const [tabs, setTabs] = useState([]);
|
||||
const [selectedTab, setSelectedTab] = useState(null);
|
||||
const [isNewTabWindow, setIsNewTabWindow] = useState(false);
|
||||
const [categories, setCategories] = useState([])
|
||||
const iframeRefs = useRef({});
|
||||
|
||||
|
||||
const myApp = useMemo(()=> {
|
||||
|
||||
return availableQapps.find((app)=> app.name === myName && app.service === 'APP')
|
||||
}, [myName, availableQapps])
|
||||
const myWebsite = useMemo(()=> {
|
||||
|
||||
return availableQapps.find((app)=> app.name === myName && app.service === 'WEBSITE')
|
||||
}, [myName, availableQapps])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
executeEvent("setTabsToNav", {
|
||||
data: {
|
||||
tabs: tabs,
|
||||
selectedTab: selectedTab,
|
||||
isNewTabWindow: isNewTabWindow,
|
||||
},
|
||||
});
|
||||
}, 100);
|
||||
}, [show, tabs, selectedTab, isNewTabWindow]);
|
||||
|
||||
const getCategories = React.useCallback(async () => {
|
||||
try {
|
||||
const url = `${getBaseApiReact()}/arbitrary/categories`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response?.ok) return;
|
||||
const responseData = await response.json();
|
||||
|
||||
setCategories(responseData);
|
||||
|
||||
} catch (error) {
|
||||
} finally {
|
||||
// dispatch(setIsLoadingGlobal(false))
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getQapps = React.useCallback(async () => {
|
||||
try {
|
||||
let apps = [];
|
||||
let websites = [];
|
||||
// dispatch(setIsLoadingGlobal(true))
|
||||
const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&limit=0&includestatus=true&includemetadata=true`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response?.ok) return;
|
||||
const responseData = await response.json();
|
||||
const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&limit=0&includestatus=true&includemetadata=true`;
|
||||
|
||||
const responseWebsites = await fetch(urlWebsites, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!responseWebsites?.ok) return;
|
||||
const responseDataWebsites = await responseWebsites.json();
|
||||
|
||||
apps = responseData;
|
||||
websites = responseDataWebsites;
|
||||
const combine = [...apps, ...websites];
|
||||
setAvailableQapps(combine);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
// dispatch(setIsLoadingGlobal(false))
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
getQapps();
|
||||
getCategories()
|
||||
}, [getQapps, getCategories]);
|
||||
|
||||
const selectedAppInfoFunc = (e) => {
|
||||
const data = e.detail?.data;
|
||||
setSelectedAppInfo(data);
|
||||
setMode("appInfo");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("selectedAppInfo", selectedAppInfoFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("selectedAppInfo", selectedAppInfoFunc);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const selectedAppInfoCategoryFunc = (e) => {
|
||||
const data = e.detail?.data;
|
||||
setSelectedAppInfo(data);
|
||||
setMode("appInfo-from-category");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const selectedCategoryFunc = (e) => {
|
||||
const data = e.detail?.data;
|
||||
setSelectedCategory(data);
|
||||
setMode("category");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("selectedCategory", selectedCategoryFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("selectedCategory", selectedCategoryFunc);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
const navigateBackFunc = (e) => {
|
||||
if (['category', 'appInfo-from-category', 'appInfo', 'library', 'publish'].includes(mode)) {
|
||||
// Handle the various modes as needed
|
||||
if (mode === 'category') {
|
||||
setMode('library');
|
||||
setSelectedCategory(null);
|
||||
} else if (mode === 'appInfo-from-category') {
|
||||
setMode('category');
|
||||
} else if (mode === 'appInfo') {
|
||||
setMode('library');
|
||||
} else if (mode === 'library') {
|
||||
if (isNewTabWindow) {
|
||||
setMode('viewer');
|
||||
} else {
|
||||
setMode('home');
|
||||
}
|
||||
} else if (mode === 'publish') {
|
||||
setMode('library');
|
||||
}
|
||||
} else if(selectedTab?.tabId) {
|
||||
executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {})
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("navigateBack", navigateBackFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("navigateBack", navigateBackFunc);
|
||||
};
|
||||
}, [mode, selectedTab]);
|
||||
|
||||
const addTabFunc = (e) => {
|
||||
const data = e.detail?.data;
|
||||
const newTab = {
|
||||
...data,
|
||||
tabId: uid.rnd(),
|
||||
};
|
||||
setTabs((prev) => [...prev, newTab]);
|
||||
setSelectedTab(newTab);
|
||||
setMode("viewer");
|
||||
|
||||
setIsNewTabWindow(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("addTab", addTabFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("addTab", addTabFunc);
|
||||
};
|
||||
}, [tabs]);
|
||||
|
||||
const setSelectedTabFunc = (e) => {
|
||||
const data = e.detail?.data;
|
||||
|
||||
setSelectedTab(data);
|
||||
setTimeout(() => {
|
||||
executeEvent("setTabsToNav", {
|
||||
data: {
|
||||
tabs: tabs,
|
||||
selectedTab: data,
|
||||
isNewTabWindow: isNewTabWindow,
|
||||
},
|
||||
});
|
||||
}, 100);
|
||||
setIsNewTabWindow(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("setSelectedTab", setSelectedTabFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("setSelectedTab", setSelectedTabFunc);
|
||||
};
|
||||
}, [tabs, isNewTabWindow]);
|
||||
|
||||
const removeTabFunc = (e) => {
|
||||
const data = e.detail?.data;
|
||||
const copyTabs = [...tabs].filter((tab) => tab?.tabId !== data?.tabId);
|
||||
if (copyTabs?.length === 0) {
|
||||
setMode("home");
|
||||
} else {
|
||||
setSelectedTab(copyTabs[0]);
|
||||
}
|
||||
setTabs(copyTabs);
|
||||
setSelectedTab(copyTabs[0]);
|
||||
setTimeout(() => {
|
||||
executeEvent("setTabsToNav", {
|
||||
data: {
|
||||
tabs: copyTabs,
|
||||
selectedTab: copyTabs[0],
|
||||
},
|
||||
});
|
||||
}, 400);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("removeTab", removeTabFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("removeTab", removeTabFunc);
|
||||
};
|
||||
}, [tabs]);
|
||||
|
||||
const setNewTabWindowFunc = (e) => {
|
||||
setIsNewTabWindow(true);
|
||||
setSelectedTab(null)
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("newTabWindow", setNewTabWindowFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("newTabWindow", setNewTabWindowFunc);
|
||||
};
|
||||
}, [tabs]);
|
||||
|
||||
|
||||
return (
|
||||
<AppsParent
|
||||
sx={{
|
||||
display: !show && "none",
|
||||
}}
|
||||
>
|
||||
{mode !== "viewer" && !selectedTab && <Spacer height="30px" />}
|
||||
{mode === "home" && (
|
||||
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
)}
|
||||
|
||||
<AppsLibrary
|
||||
isShow={mode === "library" && !selectedTab}
|
||||
availableQapps={availableQapps}
|
||||
setMode={setMode}
|
||||
myName={myName}
|
||||
hasPublishApp={!!(myApp || myWebsite)}
|
||||
categories={categories}
|
||||
/>
|
||||
|
||||
{mode === "appInfo" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />}
|
||||
{mode === "appInfo-from-category" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />}
|
||||
<AppsCategory availableQapps={availableQapps} isShow={mode === 'category' && !selectedTab} category={selectedCategory} myName={myName} />
|
||||
{mode === "publish" && !selectedTab && <AppPublish names={myName ? [myName] : []} categories={categories} />}
|
||||
|
||||
{tabs.map((tab) => {
|
||||
if (!iframeRefs.current[tab.tabId]) {
|
||||
iframeRefs.current[tab.tabId] = React.createRef();
|
||||
}
|
||||
return (
|
||||
<AppViewerContainer
|
||||
key={tab?.tabId}
|
||||
hide={isNewTabWindow}
|
||||
isSelected={tab?.tabId === selectedTab?.tabId}
|
||||
app={tab}
|
||||
ref={iframeRefs.current[tab.tabId]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{isNewTabWindow && mode === "viewer" && (
|
||||
<>
|
||||
<Spacer height="30px" />
|
||||
<AppsHome availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
</>
|
||||
)}
|
||||
{mode !== "viewer" && !selectedTab && <Spacer height="180px" />}
|
||||
</AppsParent>
|
||||
);
|
||||
};
|
188
src/components/Apps/AppsCategory.tsx
Normal file
@ -0,0 +1,188 @@
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppCircleLabel,
|
||||
AppLibrarySubTitle,
|
||||
AppsContainer,
|
||||
AppsLibraryContainer,
|
||||
AppsParent,
|
||||
AppsSearchContainer,
|
||||
AppsSearchLeft,
|
||||
AppsSearchRight,
|
||||
AppsWidthLimiter,
|
||||
PublishQAppCTAButton,
|
||||
PublishQAppCTALeft,
|
||||
PublishQAppCTAParent,
|
||||
PublishQAppCTARight,
|
||||
PublishQAppDotsBG,
|
||||
} from "./Apps-styles";
|
||||
import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import { MyContext, getBaseApiReact } from "../../App";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
import IconSearch from "../../assets/svgs/Search.svg";
|
||||
import IconClearInput from "../../assets/svgs/ClearInput.svg";
|
||||
import qappDevelopText from "../../assets/svgs/qappDevelopText.svg";
|
||||
import qappDots from "../../assets/svgs/qappDots.svg";
|
||||
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { AppInfoSnippet } from "./AppInfoSnippet";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
const officialAppList = [
|
||||
"q-tube",
|
||||
"q-blog",
|
||||
"q-share",
|
||||
"q-support",
|
||||
"q-mail",
|
||||
"qombo",
|
||||
"q-fund",
|
||||
"q-shop",
|
||||
];
|
||||
|
||||
const ScrollerStyled = styled('div')({
|
||||
// Hide scrollbar for WebKit browsers (Chrome, Safari)
|
||||
"::-webkit-scrollbar": {
|
||||
width: "0px",
|
||||
height: "0px",
|
||||
},
|
||||
|
||||
// Hide scrollbar for Firefox
|
||||
scrollbarWidth: "none",
|
||||
|
||||
// Hide scrollbar for IE and older Edge
|
||||
"-ms-overflow-style": "none",
|
||||
});
|
||||
|
||||
const StyledVirtuosoContainer = styled('div')({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
// Hide scrollbar for WebKit browsers (Chrome, Safari)
|
||||
"::-webkit-scrollbar": {
|
||||
width: "0px",
|
||||
height: "0px",
|
||||
},
|
||||
|
||||
// Hide scrollbar for Firefox
|
||||
scrollbarWidth: "none",
|
||||
|
||||
// Hide scrollbar for IE and older Edge
|
||||
"-ms-overflow-style": "none",
|
||||
});
|
||||
|
||||
export const AppsCategory = ({ availableQapps, myName, category, isShow }) => {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const virtuosoRef = useRef();
|
||||
const { rootHeight } = useContext(MyContext);
|
||||
|
||||
|
||||
|
||||
const categoryList = useMemo(() => {
|
||||
return availableQapps.filter(
|
||||
(app) =>
|
||||
app?.metadata?.category === category?.id
|
||||
);
|
||||
}, [availableQapps, category]);
|
||||
|
||||
const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value
|
||||
|
||||
// Debounce logic
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(searchValue);
|
||||
}, 350);
|
||||
|
||||
// Cleanup timeout if searchValue changes before the timeout completes
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [searchValue]); // Runs effect when searchValue changes
|
||||
|
||||
// Example: Perform search or other actions based on debouncedValue
|
||||
|
||||
const searchedList = useMemo(() => {
|
||||
if (!debouncedValue) return categoryList
|
||||
return categoryList.filter((app) =>
|
||||
app.name.toLowerCase().includes(debouncedValue.toLowerCase())
|
||||
);
|
||||
}, [debouncedValue, categoryList]);
|
||||
|
||||
const rowRenderer = (index) => {
|
||||
|
||||
let app = searchedList[index];
|
||||
return <AppInfoSnippet key={`${app?.service}-${app?.name}`} app={app} myName={myName} isFromCategory={true} />;
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<AppsLibraryContainer sx={{
|
||||
display: !isShow && 'none'
|
||||
}}>
|
||||
<AppsWidthLimiter>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<AppsSearchContainer>
|
||||
<AppsSearchLeft>
|
||||
<img src={IconSearch} />
|
||||
<InputBase
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
sx={{ ml: 1, flex: 1 }}
|
||||
placeholder="Search for apps"
|
||||
inputProps={{
|
||||
"aria-label": "Search for apps",
|
||||
fontSize: "16px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
</AppsSearchLeft>
|
||||
<AppsSearchRight>
|
||||
{searchValue && (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setSearchValue("");
|
||||
}}
|
||||
>
|
||||
<img src={IconClearInput} />
|
||||
</ButtonBase>
|
||||
)}
|
||||
</AppsSearchRight>
|
||||
</AppsSearchContainer>
|
||||
</Box>
|
||||
</AppsWidthLimiter>
|
||||
<Spacer height="25px" />
|
||||
<AppsWidthLimiter>
|
||||
<AppLibrarySubTitle>{`Category: ${category?.name}`}</AppLibrarySubTitle>
|
||||
|
||||
<Spacer height="25px" />
|
||||
</AppsWidthLimiter>
|
||||
<AppsWidthLimiter>
|
||||
<StyledVirtuosoContainer sx={{
|
||||
height: rootHeight
|
||||
}}>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={searchedList}
|
||||
itemContent={rowRenderer}
|
||||
atBottomThreshold={50}
|
||||
followOutput="smooth"
|
||||
components={{
|
||||
Scroller: ScrollerStyled // Use the styled scroller component
|
||||
}}
|
||||
/>
|
||||
</StyledVirtuosoContainer>
|
||||
</AppsWidthLimiter>
|
||||
|
||||
</AppsLibraryContainer>
|
||||
);
|
||||
};
|
223
src/components/Apps/AppsCategoryDesktop.tsx
Normal file
@ -0,0 +1,223 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppCircleLabel,
|
||||
AppLibrarySubTitle,
|
||||
AppsContainer,
|
||||
AppsLibraryContainer,
|
||||
AppsParent,
|
||||
AppsSearchContainer,
|
||||
AppsSearchLeft,
|
||||
AppsSearchRight,
|
||||
AppsWidthLimiter,
|
||||
PublishQAppCTAButton,
|
||||
PublishQAppCTALeft,
|
||||
PublishQAppCTAParent,
|
||||
PublishQAppCTARight,
|
||||
PublishQAppDotsBG,
|
||||
} from "./Apps-styles";
|
||||
import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import { MyContext, getBaseApiReact } from "../../App";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
import IconSearch from "../../assets/svgs/Search.svg";
|
||||
import IconClearInput from "../../assets/svgs/ClearInput.svg";
|
||||
import qappDevelopText from "../../assets/svgs/qappDevelopText.svg";
|
||||
import qappDots from "../../assets/svgs/qappDots.svg";
|
||||
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { AppInfoSnippet } from "./AppInfoSnippet";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
import { AppsDesktopLibraryBody, AppsDesktopLibraryHeader } from "./AppsDesktop-styles";
|
||||
const officialAppList = [
|
||||
"q-tube",
|
||||
"q-blog",
|
||||
"q-share",
|
||||
"q-support",
|
||||
"q-mail",
|
||||
"qombo",
|
||||
"q-fund",
|
||||
"q-shop",
|
||||
];
|
||||
|
||||
const ScrollerStyled = styled("div")({
|
||||
// Hide scrollbar for WebKit browsers (Chrome, Safari)
|
||||
"::-webkit-scrollbar": {
|
||||
width: "0px",
|
||||
height: "0px",
|
||||
},
|
||||
|
||||
// Hide scrollbar for Firefox
|
||||
scrollbarWidth: "none",
|
||||
|
||||
// Hide scrollbar for IE and older Edge
|
||||
"-ms-overflow-style": "none",
|
||||
});
|
||||
|
||||
const StyledVirtuosoContainer = styled("div")({
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
||||
// Hide scrollbar for WebKit browsers (Chrome, Safari)
|
||||
"::-webkit-scrollbar": {
|
||||
width: "0px",
|
||||
height: "0px",
|
||||
},
|
||||
|
||||
// Hide scrollbar for Firefox
|
||||
scrollbarWidth: "none",
|
||||
|
||||
// Hide scrollbar for IE and older Edge
|
||||
"-ms-overflow-style": "none",
|
||||
});
|
||||
|
||||
export const AppsCategoryDesktop = ({
|
||||
availableQapps,
|
||||
myName,
|
||||
category,
|
||||
isShow,
|
||||
}) => {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const virtuosoRef = useRef();
|
||||
const { rootHeight } = useContext(MyContext);
|
||||
|
||||
const categoryList = useMemo(() => {
|
||||
return availableQapps.filter(
|
||||
(app) => app?.metadata?.category === category?.id
|
||||
);
|
||||
}, [availableQapps, category]);
|
||||
|
||||
const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value
|
||||
|
||||
// Debounce logic
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(searchValue);
|
||||
}, 350);
|
||||
|
||||
// Cleanup timeout if searchValue changes before the timeout completes
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [searchValue]); // Runs effect when searchValue changes
|
||||
|
||||
// Example: Perform search or other actions based on debouncedValue
|
||||
|
||||
const searchedList = useMemo(() => {
|
||||
if (!debouncedValue) return categoryList;
|
||||
return categoryList.filter((app) =>
|
||||
app.name.toLowerCase().includes(debouncedValue.toLowerCase())
|
||||
);
|
||||
}, [debouncedValue, categoryList]);
|
||||
|
||||
const rowRenderer = (index) => {
|
||||
let app = searchedList[index];
|
||||
return (
|
||||
<AppInfoSnippet
|
||||
key={`${app?.service}-${app?.name}`}
|
||||
app={app}
|
||||
myName={myName}
|
||||
isFromCategory={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppsLibraryContainer
|
||||
sx={{
|
||||
display: !isShow && "none",
|
||||
padding: "0px",
|
||||
height: "100vh",
|
||||
overflow: "hidden",
|
||||
paddingTop: "30px",
|
||||
}}
|
||||
>
|
||||
<AppsDesktopLibraryHeader
|
||||
sx={{
|
||||
maxWidth: "1500px",
|
||||
width: "90%",
|
||||
}}
|
||||
>
|
||||
<AppsWidthLimiter
|
||||
sx={{
|
||||
alignItems: "flex-end",
|
||||
}}
|
||||
>
|
||||
<AppsSearchContainer sx={{
|
||||
width: "412px",
|
||||
}}>
|
||||
<AppsSearchLeft>
|
||||
<img src={IconSearch} />
|
||||
<InputBase
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
sx={{ ml: 1, flex: 1 }}
|
||||
placeholder="Search for apps"
|
||||
inputProps={{
|
||||
"aria-label": "Search for apps",
|
||||
fontSize: "16px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
</AppsSearchLeft>
|
||||
<AppsSearchRight>
|
||||
{searchValue && (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setSearchValue("");
|
||||
}}
|
||||
>
|
||||
<img src={IconClearInput} />
|
||||
</ButtonBase>
|
||||
)}
|
||||
</AppsSearchRight>
|
||||
</AppsSearchContainer>
|
||||
</AppsWidthLimiter>
|
||||
</AppsDesktopLibraryHeader>
|
||||
<AppsDesktopLibraryBody
|
||||
sx={{
|
||||
height: `calc(100vh - 36px)`,
|
||||
overflow: "auto",
|
||||
padding: "0px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Spacer height="25px" />
|
||||
<AppsWidthLimiter>
|
||||
<AppLibrarySubTitle>{`Category: ${category?.name}`}</AppLibrarySubTitle>
|
||||
|
||||
<Spacer height="25px" />
|
||||
</AppsWidthLimiter>
|
||||
<AppsWidthLimiter>
|
||||
<StyledVirtuosoContainer
|
||||
sx={{
|
||||
height: rootHeight,
|
||||
}}
|
||||
>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={searchedList}
|
||||
itemContent={rowRenderer}
|
||||
atBottomThreshold={50}
|
||||
followOutput="smooth"
|
||||
components={{
|
||||
Scroller: ScrollerStyled, // Use the styled scroller component
|
||||
}}
|
||||
/>
|
||||
</StyledVirtuosoContainer>
|
||||
</AppsWidthLimiter>
|
||||
</AppsDesktopLibraryBody>
|
||||
</AppsLibraryContainer>
|
||||
);
|
||||
};
|
24
src/components/Apps/AppsDesktop-styles.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import {
|
||||
AppBar,
|
||||
Button,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Box,
|
||||
TextField,
|
||||
InputLabel,
|
||||
ButtonBase,
|
||||
} from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
|
||||
export const AppsDesktopLibraryHeader = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: 'column',
|
||||
flexShrink: 0,
|
||||
width: '100%'
|
||||
}));
|
||||
export const AppsDesktopLibraryBody = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
width: '100%'
|
||||
}));
|
427
src/components/Apps/AppsDesktop.tsx
Normal file
@ -0,0 +1,427 @@
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { AppsHomeDesktop } from "./AppsHomeDesktop";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { MyContext, getBaseApiReact } from "../../App";
|
||||
import { AppInfo } from "./AppInfo";
|
||||
import {
|
||||
executeEvent,
|
||||
subscribeToEvent,
|
||||
unsubscribeFromEvent,
|
||||
} from "../../utils/events";
|
||||
import { AppsParent } from "./Apps-styles";
|
||||
import AppViewerContainer from "./AppViewerContainer";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import { AppPublish } from "./AppPublish";
|
||||
import { AppsLibraryDesktop } from "./AppsLibraryDesktop";
|
||||
import { AppsCategoryDesktop } from "./AppsCategoryDesktop";
|
||||
import { AppsNavBarDesktop } from "./AppsNavBarDesktop";
|
||||
import { Box, ButtonBase } from "@mui/material";
|
||||
import { HomeIcon } from "../../assets/Icons/HomeIcon";
|
||||
import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
|
||||
import { Save } from "../Save/Save";
|
||||
import { HubsIcon } from "../../assets/Icons/HubsIcon";
|
||||
|
||||
const uid = new ShortUniqueId({ length: 8 });
|
||||
|
||||
export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktopSideView, hasUnreadDirects, isDirects, isGroups, hasUnreadGroups, toggleSideViewGroups, toggleSideViewDirects}) => {
|
||||
const [availableQapps, setAvailableQapps] = useState([]);
|
||||
const [selectedAppInfo, setSelectedAppInfo] = useState(null);
|
||||
const [selectedCategory, setSelectedCategory] = useState(null)
|
||||
const [tabs, setTabs] = useState([]);
|
||||
const [selectedTab, setSelectedTab] = useState(null);
|
||||
const [isNewTabWindow, setIsNewTabWindow] = useState(false);
|
||||
const [categories, setCategories] = useState([])
|
||||
const iframeRefs = useRef({});
|
||||
const myApp = useMemo(()=> {
|
||||
|
||||
return availableQapps.find((app)=> app.name === myName && app.service === 'APP')
|
||||
}, [myName, availableQapps])
|
||||
const myWebsite = useMemo(()=> {
|
||||
|
||||
return availableQapps.find((app)=> app.name === myName && app.service === 'WEBSITE')
|
||||
}, [myName, availableQapps])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
executeEvent("setTabsToNav", {
|
||||
data: {
|
||||
tabs: tabs,
|
||||
selectedTab: selectedTab,
|
||||
isNewTabWindow: isNewTabWindow,
|
||||
},
|
||||
});
|
||||
}, 100);
|
||||
}, [show, tabs, selectedTab, isNewTabWindow]);
|
||||
|
||||
const getCategories = React.useCallback(async () => {
|
||||
try {
|
||||
const url = `${getBaseApiReact()}/arbitrary/categories`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response?.ok) return;
|
||||
const responseData = await response.json();
|
||||
|
||||
setCategories(responseData);
|
||||
|
||||
} catch (error) {
|
||||
} finally {
|
||||
// dispatch(setIsLoadingGlobal(false))
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getQapps = React.useCallback(async () => {
|
||||
try {
|
||||
let apps = [];
|
||||
let websites = [];
|
||||
// dispatch(setIsLoadingGlobal(true))
|
||||
const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&limit=0&includestatus=true&includemetadata=true`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response?.ok) return;
|
||||
const responseData = await response.json();
|
||||
const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&limit=0&includestatus=true&includemetadata=true`;
|
||||
|
||||
const responseWebsites = await fetch(urlWebsites, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!responseWebsites?.ok) return;
|
||||
const responseDataWebsites = await responseWebsites.json();
|
||||
|
||||
apps = responseData;
|
||||
websites = responseDataWebsites;
|
||||
const combine = [...apps, ...websites];
|
||||
setAvailableQapps(combine);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
// dispatch(setIsLoadingGlobal(false))
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
getQapps();
|
||||
getCategories()
|
||||
}, [getQapps, getCategories]);
|
||||
|
||||
const selectedAppInfoFunc = (e) => {
|
||||
const data = e.detail?.data;
|
||||
setSelectedAppInfo(data);
|
||||
setMode("appInfo");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("selectedAppInfo", selectedAppInfoFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("selectedAppInfo", selectedAppInfoFunc);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const selectedAppInfoCategoryFunc = (e) => {
|
||||
const data = e.detail?.data;
|
||||
setSelectedAppInfo(data);
|
||||
setMode("appInfo-from-category");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("selectedAppInfoCategory", selectedAppInfoCategoryFunc);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const selectedCategoryFunc = (e) => {
|
||||
const data = e.detail?.data;
|
||||
setSelectedCategory(data);
|
||||
setMode("category");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("selectedCategory", selectedCategoryFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("selectedCategory", selectedCategoryFunc);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const navigateBackFunc = (e) => {
|
||||
if (['category', 'appInfo-from-category', 'appInfo', 'library', 'publish'].includes(mode)) {
|
||||
// Handle the various modes as needed
|
||||
if (mode === 'category') {
|
||||
setMode('library');
|
||||
setSelectedCategory(null);
|
||||
} else if (mode === 'appInfo-from-category') {
|
||||
setMode('category');
|
||||
} else if (mode === 'appInfo') {
|
||||
setMode('library');
|
||||
} else if (mode === 'library') {
|
||||
if (isNewTabWindow) {
|
||||
setMode('viewer');
|
||||
} else {
|
||||
setMode('home');
|
||||
}
|
||||
} else if (mode === 'publish') {
|
||||
setMode('library');
|
||||
}
|
||||
} else if(selectedTab?.tabId) {
|
||||
executeEvent(`navigateBackApp-${selectedTab?.tabId}`, {})
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("navigateBack", navigateBackFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("navigateBack", navigateBackFunc);
|
||||
};
|
||||
}, [mode, selectedTab]);
|
||||
|
||||
const addTabFunc = (e) => {
|
||||
const data = e.detail?.data;
|
||||
const newTab = {
|
||||
...data,
|
||||
tabId: uid.rnd(),
|
||||
};
|
||||
setTabs((prev) => [...prev, newTab]);
|
||||
setSelectedTab(newTab);
|
||||
setMode("viewer");
|
||||
|
||||
setIsNewTabWindow(false);
|
||||
};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("addTab", addTabFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("addTab", addTabFunc);
|
||||
};
|
||||
}, [tabs]);
|
||||
const setSelectedTabFunc = (e) => {
|
||||
const data = e.detail?.data;
|
||||
|
||||
setSelectedTab(data);
|
||||
setTimeout(() => {
|
||||
executeEvent("setTabsToNav", {
|
||||
data: {
|
||||
tabs: tabs,
|
||||
selectedTab: data,
|
||||
isNewTabWindow: isNewTabWindow,
|
||||
},
|
||||
});
|
||||
}, 100);
|
||||
setIsNewTabWindow(false);
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("setSelectedTab", setSelectedTabFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("setSelectedTab", setSelectedTabFunc);
|
||||
};
|
||||
}, [tabs, isNewTabWindow]);
|
||||
|
||||
const removeTabFunc = (e) => {
|
||||
const data = e.detail?.data;
|
||||
const copyTabs = [...tabs].filter((tab) => tab?.tabId !== data?.tabId);
|
||||
if (copyTabs?.length === 0) {
|
||||
setMode("home");
|
||||
} else {
|
||||
setSelectedTab(copyTabs[0]);
|
||||
}
|
||||
setTabs(copyTabs);
|
||||
setSelectedTab(copyTabs[0]);
|
||||
setTimeout(() => {
|
||||
executeEvent("setTabsToNav", {
|
||||
data: {
|
||||
tabs: copyTabs,
|
||||
selectedTab: copyTabs[0],
|
||||
},
|
||||
});
|
||||
}, 400);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("removeTab", removeTabFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("removeTab", removeTabFunc);
|
||||
};
|
||||
}, [tabs]);
|
||||
|
||||
const setNewTabWindowFunc = (e) => {
|
||||
setIsNewTabWindow(true);
|
||||
setSelectedTab(null)
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("newTabWindow", setNewTabWindowFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("newTabWindow", setNewTabWindowFunc);
|
||||
};
|
||||
}, [tabs]);
|
||||
|
||||
|
||||
return (
|
||||
<AppsParent
|
||||
sx={{
|
||||
display: !show && "none",
|
||||
flexDirection: 'row'
|
||||
}}
|
||||
>
|
||||
|
||||
<Box sx={{
|
||||
width: '60px',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
gap: '30px'
|
||||
}}>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
paddingTop: '23px'
|
||||
}}
|
||||
onClick={() => {
|
||||
goToHome();
|
||||
|
||||
}}
|
||||
>
|
||||
|
||||
<HomeIcon
|
||||
height={34}
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
/>
|
||||
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView("directs");
|
||||
toggleSideViewDirects()
|
||||
}}
|
||||
>
|
||||
|
||||
<MessagingIcon
|
||||
height={30}
|
||||
color={
|
||||
hasUnreadDirects
|
||||
? "var(--unread)"
|
||||
: isDirects
|
||||
? "white"
|
||||
: "rgba(250, 250, 250, 0.5)"
|
||||
}
|
||||
/>
|
||||
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView("groups");
|
||||
toggleSideViewGroups()
|
||||
}}
|
||||
>
|
||||
<HubsIcon
|
||||
height={30}
|
||||
color={
|
||||
hasUnreadGroups
|
||||
? "var(--unread)"
|
||||
: isGroups
|
||||
? "white"
|
||||
: "rgba(250, 250, 250, 0.5)"
|
||||
}
|
||||
/>
|
||||
|
||||
</ButtonBase>
|
||||
<Save isDesktop />
|
||||
{mode !== 'home' && (
|
||||
<AppsNavBarDesktop />
|
||||
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
{mode === "home" && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
|
||||
<Spacer height="30px" />
|
||||
<AppsHomeDesktop availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<AppsLibraryDesktop
|
||||
isShow={mode === "library" && !selectedTab}
|
||||
availableQapps={availableQapps}
|
||||
setMode={setMode}
|
||||
myName={myName}
|
||||
hasPublishApp={!!(myApp || myWebsite)}
|
||||
categories={categories}
|
||||
/>
|
||||
|
||||
{mode === "appInfo" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />}
|
||||
{mode === "appInfo-from-category" && !selectedTab && <AppInfo app={selectedAppInfo} myName={myName} />}
|
||||
<AppsCategoryDesktop availableQapps={availableQapps} isShow={mode === 'category' && !selectedTab} category={selectedCategory} myName={myName} />
|
||||
{mode === "publish" && !selectedTab && <AppPublish names={myName ? [myName] : []} categories={categories} />}
|
||||
{tabs.map((tab) => {
|
||||
if (!iframeRefs.current[tab.tabId]) {
|
||||
iframeRefs.current[tab.tabId] = React.createRef();
|
||||
}
|
||||
return (
|
||||
<AppViewerContainer
|
||||
key={tab?.tabId}
|
||||
hide={isNewTabWindow}
|
||||
isSelected={tab?.tabId === selectedTab?.tabId}
|
||||
app={tab}
|
||||
ref={iframeRefs.current[tab.tabId]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{isNewTabWindow && mode === "viewer" && (
|
||||
<>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
|
||||
<Spacer height="30px" />
|
||||
<AppsHomeDesktop availableQapps={availableQapps} setMode={setMode} myApp={myApp} myWebsite={myWebsite} />
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</AppsParent>
|
||||
);
|
||||
};
|
57
src/components/Apps/AppsHome.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppCircleLabel,
|
||||
AppLibrarySubTitle,
|
||||
AppsContainer,
|
||||
AppsParent,
|
||||
} from "./Apps-styles";
|
||||
import { Avatar, ButtonBase } from "@mui/material";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import { getBaseApiReact, isMobile } from "../../App";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
import { SortablePinnedApps } from "./SortablePinnedApps";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
|
||||
export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps }) => {
|
||||
return (
|
||||
<>
|
||||
<AppsContainer
|
||||
sx={{
|
||||
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
<AppLibrarySubTitle
|
||||
|
||||
>
|
||||
Apps Dashboard
|
||||
|
||||
</AppLibrarySubTitle>
|
||||
</AppsContainer>
|
||||
<Spacer height="20px" />
|
||||
|
||||
<AppsContainer>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setMode("library");
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer sx={{
|
||||
gap: !isMobile ? "10px" : "5px",
|
||||
}}>
|
||||
<AppCircle>
|
||||
<Add>+</Add>
|
||||
</AppCircle>
|
||||
<AppCircleLabel>Library</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
|
||||
<SortablePinnedApps availableQapps={availableQapps} myWebsite={myWebsite} myApp={myApp} />
|
||||
|
||||
</AppsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
73
src/components/Apps/AppsHomeDesktop.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppCircleLabel,
|
||||
AppLibrarySubTitle,
|
||||
AppsContainer,
|
||||
AppsParent,
|
||||
} from "./Apps-styles";
|
||||
import { Avatar, ButtonBase } from "@mui/material";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import { getBaseApiReact, isMobile } from "../../App";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
import { SortablePinnedApps } from "./SortablePinnedApps";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
|
||||
export const AppsHomeDesktop = ({
|
||||
setMode,
|
||||
myApp,
|
||||
myWebsite,
|
||||
availableQapps,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<AppsContainer
|
||||
sx={{
|
||||
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
<AppLibrarySubTitle
|
||||
sx={{
|
||||
fontSize: "30px",
|
||||
}}
|
||||
>
|
||||
Apps Dashboard
|
||||
</AppLibrarySubTitle>
|
||||
</AppsContainer>
|
||||
<Spacer height="45px" />
|
||||
<AppsContainer
|
||||
sx={{
|
||||
gap: "75px",
|
||||
justifyContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setMode("library");
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer
|
||||
sx={{
|
||||
gap: !isMobile ? "10px" : "5px",
|
||||
}}
|
||||
>
|
||||
<AppCircle>
|
||||
<Add>+</Add>
|
||||
</AppCircle>
|
||||
<AppCircleLabel>Library</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
|
||||
<SortablePinnedApps
|
||||
isDesktop={true}
|
||||
availableQapps={availableQapps}
|
||||
myWebsite={myWebsite}
|
||||
myApp={myApp}
|
||||
/>
|
||||
</AppsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
322
src/components/Apps/AppsLibrary.tsx
Normal file
@ -0,0 +1,322 @@
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppCircleLabel,
|
||||
AppLibrarySubTitle,
|
||||
AppsContainer,
|
||||
AppsLibraryContainer,
|
||||
AppsParent,
|
||||
AppsSearchContainer,
|
||||
AppsSearchLeft,
|
||||
AppsSearchRight,
|
||||
AppsWidthLimiter,
|
||||
PublishQAppCTAButton,
|
||||
PublishQAppCTALeft,
|
||||
PublishQAppCTAParent,
|
||||
PublishQAppCTARight,
|
||||
PublishQAppDotsBG,
|
||||
} from "./Apps-styles";
|
||||
import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import { MyContext, getBaseApiReact } from "../../App";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
import IconSearch from "../../assets/svgs/Search.svg";
|
||||
import IconClearInput from "../../assets/svgs/ClearInput.svg";
|
||||
import qappDevelopText from "../../assets/svgs/qappDevelopText.svg";
|
||||
import qappDots from "../../assets/svgs/qappDots.svg";
|
||||
import ReturnSVG from '../../assets/svgs/Return.svg'
|
||||
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { AppInfoSnippet } from "./AppInfoSnippet";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
import { ComposeP, MailIconImg, ShowMessageReturnButton } from "../Group/Forum/Mail-styles";
|
||||
const officialAppList = [
|
||||
"q-tube",
|
||||
"q-blog",
|
||||
"q-share",
|
||||
"q-support",
|
||||
"q-mail",
|
||||
"qombo",
|
||||
"q-fund",
|
||||
"q-shop",
|
||||
];
|
||||
|
||||
const ScrollerStyled = styled('div')({
|
||||
// Hide scrollbar for WebKit browsers (Chrome, Safari)
|
||||
"::-webkit-scrollbar": {
|
||||
width: "0px",
|
||||
height: "0px",
|
||||
},
|
||||
|
||||
// Hide scrollbar for Firefox
|
||||
scrollbarWidth: "none",
|
||||
|
||||
// Hide scrollbar for IE and older Edge
|
||||
"-ms-overflow-style": "none",
|
||||
});
|
||||
|
||||
const StyledVirtuosoContainer = styled('div')({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
// Hide scrollbar for WebKit browsers (Chrome, Safari)
|
||||
"::-webkit-scrollbar": {
|
||||
width: "0px",
|
||||
height: "0px",
|
||||
},
|
||||
|
||||
// Hide scrollbar for Firefox
|
||||
scrollbarWidth: "none",
|
||||
|
||||
// Hide scrollbar for IE and older Edge
|
||||
"-ms-overflow-style": "none",
|
||||
});
|
||||
|
||||
export const AppsLibrary = ({ availableQapps, setMode, myName, hasPublishApp, isShow, categories={categories} }) => {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const virtuosoRef = useRef();
|
||||
const { rootHeight } = useContext(MyContext);
|
||||
|
||||
const officialApps = useMemo(() => {
|
||||
return availableQapps.filter(
|
||||
(app) =>
|
||||
app.service === "APP" &&
|
||||
officialAppList.includes(app?.name?.toLowerCase())
|
||||
);
|
||||
}, [availableQapps]);
|
||||
|
||||
const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value
|
||||
|
||||
// Debounce logic
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(searchValue);
|
||||
}, 350);
|
||||
|
||||
// Cleanup timeout if searchValue changes before the timeout completes
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [searchValue]); // Runs effect when searchValue changes
|
||||
|
||||
// Example: Perform search or other actions based on debouncedValue
|
||||
|
||||
const searchedList = useMemo(() => {
|
||||
if (!debouncedValue) return [];
|
||||
return availableQapps.filter((app) =>
|
||||
app.name.toLowerCase().includes(debouncedValue.toLowerCase())
|
||||
);
|
||||
}, [debouncedValue]);
|
||||
|
||||
const rowRenderer = (index) => {
|
||||
|
||||
let app = searchedList[index];
|
||||
return <AppInfoSnippet key={`${app?.service}-${app?.name}`} app={app} myName={myName} />;
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<AppsLibraryContainer sx={{
|
||||
display: !isShow && 'none'
|
||||
}}>
|
||||
<AppsWidthLimiter>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<AppsSearchContainer>
|
||||
<AppsSearchLeft>
|
||||
<img src={IconSearch} />
|
||||
<InputBase
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
sx={{ ml: 1, flex: 1 }}
|
||||
placeholder="Search for apps"
|
||||
inputProps={{
|
||||
"aria-label": "Search for apps",
|
||||
fontSize: "16px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
</AppsSearchLeft>
|
||||
<AppsSearchRight>
|
||||
{searchValue && (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setSearchValue("");
|
||||
}}
|
||||
>
|
||||
<img src={IconClearInput} />
|
||||
</ButtonBase>
|
||||
)}
|
||||
</AppsSearchRight>
|
||||
</AppsSearchContainer>
|
||||
</Box>
|
||||
</AppsWidthLimiter>
|
||||
<Spacer height="25px" />
|
||||
<ShowMessageReturnButton sx={{
|
||||
padding: '2px'
|
||||
}} onClick={() => {
|
||||
executeEvent("navigateBack", {});
|
||||
|
||||
}}>
|
||||
<MailIconImg src={ReturnSVG} />
|
||||
<ComposeP>Return to Apps Dashboard</ComposeP>
|
||||
</ShowMessageReturnButton>
|
||||
<Spacer height="25px" />
|
||||
{searchedList?.length > 0 ? (
|
||||
<AppsWidthLimiter>
|
||||
<StyledVirtuosoContainer sx={{
|
||||
height: rootHeight
|
||||
}}>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={searchedList}
|
||||
itemContent={rowRenderer}
|
||||
atBottomThreshold={50}
|
||||
followOutput="smooth"
|
||||
components={{
|
||||
Scroller: ScrollerStyled // Use the styled scroller component
|
||||
}}
|
||||
/>
|
||||
</StyledVirtuosoContainer>
|
||||
</AppsWidthLimiter>
|
||||
) : (
|
||||
<>
|
||||
<AppsWidthLimiter>
|
||||
<AppLibrarySubTitle>Official Apps</AppLibrarySubTitle>
|
||||
<Spacer height="18px" />
|
||||
<AppsContainer>
|
||||
{officialApps?.map((qapp) => {
|
||||
return (
|
||||
<ButtonBase
|
||||
sx={{
|
||||
height: "80px",
|
||||
width: "60px",
|
||||
}}
|
||||
onClick={()=> {
|
||||
// executeEvent("addTab", {
|
||||
// data: qapp
|
||||
// })
|
||||
executeEvent("selectedAppInfo", {
|
||||
data: qapp,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer>
|
||||
<AppCircle
|
||||
sx={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "31px",
|
||||
width: "31px",
|
||||
}}
|
||||
alt={qapp?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
qapp?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "31px",
|
||||
height: "auto",
|
||||
}}
|
||||
src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
</AppCircle>
|
||||
<AppCircleLabel>
|
||||
{qapp?.metadata?.title || qapp?.name}
|
||||
</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
);
|
||||
})}
|
||||
</AppsContainer>
|
||||
<Spacer height="30px" />
|
||||
<AppLibrarySubTitle>{hasPublishApp ? 'Update Apps!' : 'Create Apps!'}</AppLibrarySubTitle>
|
||||
<Spacer height="18px" />
|
||||
</AppsWidthLimiter>
|
||||
<PublishQAppCTAParent>
|
||||
<PublishQAppCTALeft>
|
||||
<PublishQAppDotsBG>
|
||||
|
||||
<img src={qappDots} />
|
||||
</PublishQAppDotsBG>
|
||||
<Spacer width="29px" />
|
||||
<img src={qappDevelopText} />
|
||||
</PublishQAppCTALeft>
|
||||
<PublishQAppCTARight onClick={()=> {
|
||||
setMode('publish')
|
||||
}}>
|
||||
<PublishQAppCTAButton>
|
||||
{hasPublishApp ? 'Update' : 'Publish'}
|
||||
</PublishQAppCTAButton>
|
||||
<Spacer width="20px" />
|
||||
</PublishQAppCTARight>
|
||||
</PublishQAppCTAParent>
|
||||
<AppsWidthLimiter>
|
||||
|
||||
<Spacer height="18px" />
|
||||
<AppLibrarySubTitle>Categories</AppLibrarySubTitle>
|
||||
<Spacer height="18px" />
|
||||
<AppsWidthLimiter sx={{
|
||||
flexDirection: 'row',
|
||||
overflowX: 'auto',
|
||||
width: '100%',
|
||||
gap: '5px',
|
||||
"::-webkit-scrollbar": {
|
||||
width: "0px",
|
||||
height: "0px",
|
||||
},
|
||||
|
||||
// Hide scrollbar for Firefox
|
||||
scrollbarWidth: "none",
|
||||
|
||||
// Hide scrollbar for IE and older Edge
|
||||
"-ms-overflow-style": "none",
|
||||
}}>
|
||||
{categories?.map((category)=> {
|
||||
return (
|
||||
<ButtonBase key={category?.id} onClick={()=> {
|
||||
executeEvent('selectedCategory', {
|
||||
data: category
|
||||
})
|
||||
}}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '110px',
|
||||
width: '110px',
|
||||
background: 'linear-gradient(163.47deg, #4BBCFE 27.55%, #1386C9 86.56%)',
|
||||
color: '#1D1D1E',
|
||||
fontWeight: 700,
|
||||
fontSize: '16px',
|
||||
flexShrink: 0,
|
||||
borderRadius: '11px'
|
||||
}}>
|
||||
{category?.name}
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
)
|
||||
})}
|
||||
</AppsWidthLimiter>
|
||||
</AppsWidthLimiter>
|
||||
</>
|
||||
)}
|
||||
</AppsLibraryContainer>
|
||||
);
|
||||
};
|
423
src/components/Apps/AppsLibraryDesktop.tsx
Normal file
@ -0,0 +1,423 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
AppCircle,
|
||||
AppCircleContainer,
|
||||
AppCircleLabel,
|
||||
AppLibrarySubTitle,
|
||||
AppsContainer,
|
||||
AppsLibraryContainer,
|
||||
AppsParent,
|
||||
AppsSearchContainer,
|
||||
AppsSearchLeft,
|
||||
AppsSearchRight,
|
||||
AppsWidthLimiter,
|
||||
PublishQAppCTAButton,
|
||||
PublishQAppCTALeft,
|
||||
PublishQAppCTAParent,
|
||||
PublishQAppCTARight,
|
||||
PublishQAppDotsBG,
|
||||
} from "./Apps-styles";
|
||||
import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material";
|
||||
import { Add } from "@mui/icons-material";
|
||||
import { MyContext, getBaseApiReact } from "../../App";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
import IconSearch from "../../assets/svgs/Search.svg";
|
||||
import IconClearInput from "../../assets/svgs/ClearInput.svg";
|
||||
import qappDevelopText from "../../assets/svgs/qappDevelopText.svg";
|
||||
import qappLibraryText from "../../assets/svgs/qappLibraryText.svg";
|
||||
|
||||
import qappDots from "../../assets/svgs/qappDots.svg";
|
||||
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { AppInfoSnippet } from "./AppInfoSnippet";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { executeEvent } from "../../utils/events";
|
||||
import {
|
||||
AppsDesktopLibraryBody,
|
||||
AppsDesktopLibraryHeader,
|
||||
} from "./AppsDesktop-styles";
|
||||
import { AppsNavBarDesktop } from "./AppsNavBarDesktop";
|
||||
import ReturnSVG from '../../assets/svgs/Return.svg'
|
||||
import { ComposeP, MailIconImg, ShowMessageReturnButton } from "../Group/Forum/Mail-styles";
|
||||
const officialAppList = [
|
||||
"q-tube",
|
||||
"q-blog",
|
||||
"q-share",
|
||||
"q-support",
|
||||
"q-mail",
|
||||
"qombo",
|
||||
"q-fund",
|
||||
"q-shop",
|
||||
];
|
||||
|
||||
const ScrollerStyled = styled("div")({
|
||||
// Hide scrollbar for WebKit browsers (Chrome, Safari)
|
||||
"::-webkit-scrollbar": {
|
||||
width: "0px",
|
||||
height: "0px",
|
||||
},
|
||||
|
||||
// Hide scrollbar for Firefox
|
||||
scrollbarWidth: "none",
|
||||
|
||||
// Hide scrollbar for IE and older Edge
|
||||
"-ms-overflow-style": "none",
|
||||
});
|
||||
|
||||
const StyledVirtuosoContainer = styled("div")({
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
||||
// Hide scrollbar for WebKit browsers (Chrome, Safari)
|
||||
"::-webkit-scrollbar": {
|
||||
width: "0px",
|
||||
height: "0px",
|
||||
},
|
||||
|
||||
// Hide scrollbar for Firefox
|
||||
scrollbarWidth: "none",
|
||||
|
||||
// Hide scrollbar for IE and older Edge
|
||||
"-ms-overflow-style": "none",
|
||||
});
|
||||
|
||||
export const AppsLibraryDesktop = ({
|
||||
availableQapps,
|
||||
setMode,
|
||||
myName,
|
||||
hasPublishApp,
|
||||
isShow,
|
||||
categories = { categories },
|
||||
}) => {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const virtuosoRef = useRef();
|
||||
|
||||
const officialApps = useMemo(() => {
|
||||
return availableQapps.filter(
|
||||
(app) =>
|
||||
app.service === "APP" &&
|
||||
officialAppList.includes(app?.name?.toLowerCase())
|
||||
);
|
||||
}, [availableQapps]);
|
||||
|
||||
const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value
|
||||
|
||||
// Debounce logic
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(searchValue);
|
||||
}, 350);
|
||||
|
||||
// Cleanup timeout if searchValue changes before the timeout completes
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [searchValue]); // Runs effect when searchValue changes
|
||||
|
||||
// Example: Perform search or other actions based on debouncedValue
|
||||
|
||||
const searchedList = useMemo(() => {
|
||||
if (!debouncedValue) return [];
|
||||
return availableQapps.filter((app) =>
|
||||
app.name.toLowerCase().includes(debouncedValue.toLowerCase())
|
||||
);
|
||||
}, [debouncedValue]);
|
||||
|
||||
const rowRenderer = (index) => {
|
||||
let app = searchedList[index];
|
||||
return (
|
||||
<AppInfoSnippet
|
||||
key={`${app?.service}-${app?.name}`}
|
||||
app={app}
|
||||
myName={myName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppsLibraryContainer
|
||||
sx={{
|
||||
display: !isShow && "none",
|
||||
padding: "0px",
|
||||
height: "100vh",
|
||||
overflow: "hidden",
|
||||
paddingTop: '30px'
|
||||
}}
|
||||
>
|
||||
|
||||
<AppsDesktopLibraryHeader
|
||||
sx={{
|
||||
maxWidth: "1500px",
|
||||
width: "90%",
|
||||
}}
|
||||
>
|
||||
<AppsWidthLimiter>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<img src={qappLibraryText} />
|
||||
<AppsSearchContainer
|
||||
sx={{
|
||||
width: "412px",
|
||||
}}
|
||||
>
|
||||
<AppsSearchLeft>
|
||||
<img src={IconSearch} />
|
||||
<InputBase
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
sx={{ ml: 1, flex: 1 }}
|
||||
placeholder="Search for apps"
|
||||
inputProps={{
|
||||
"aria-label": "Search for apps",
|
||||
fontSize: "16px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
</AppsSearchLeft>
|
||||
<AppsSearchRight>
|
||||
{searchValue && (
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setSearchValue("");
|
||||
}}
|
||||
>
|
||||
<img src={IconClearInput} />
|
||||
</ButtonBase>
|
||||
)}
|
||||
</AppsSearchRight>
|
||||
</AppsSearchContainer>
|
||||
</Box>
|
||||
</AppsWidthLimiter>
|
||||
</AppsDesktopLibraryHeader>
|
||||
<AppsDesktopLibraryBody
|
||||
sx={{
|
||||
height: `calc(100vh - 36px)`,
|
||||
overflow: "auto",
|
||||
padding: "0px",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<AppsDesktopLibraryBody
|
||||
sx={{
|
||||
height: `calc(100vh - 36px)`,
|
||||
flexGrow: "unset",
|
||||
maxWidth: "1500px",
|
||||
width: "90%",
|
||||
}}
|
||||
>
|
||||
|
||||
<Spacer height="70px" />
|
||||
<ShowMessageReturnButton sx={{
|
||||
padding: '2px'
|
||||
}} onClick={() => {
|
||||
executeEvent("navigateBack", {});
|
||||
}}>
|
||||
<MailIconImg src={ReturnSVG} />
|
||||
<ComposeP>Return to Apps Dashboard</ComposeP>
|
||||
</ShowMessageReturnButton>
|
||||
<Spacer height="20px" />
|
||||
{searchedList?.length > 0 ? (
|
||||
<AppsWidthLimiter>
|
||||
<StyledVirtuosoContainer
|
||||
sx={{
|
||||
height: `calc(100vh - 36px - 90px)`,
|
||||
}}
|
||||
>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={searchedList}
|
||||
itemContent={rowRenderer}
|
||||
atBottomThreshold={50}
|
||||
followOutput="smooth"
|
||||
components={{
|
||||
Scroller: ScrollerStyled, // Use the styled scroller component
|
||||
}}
|
||||
/>
|
||||
</StyledVirtuosoContainer>
|
||||
</AppsWidthLimiter>
|
||||
) : (
|
||||
<>
|
||||
<AppLibrarySubTitle
|
||||
sx={{
|
||||
fontSize: "30px",
|
||||
}}
|
||||
>
|
||||
Official Apps
|
||||
</AppLibrarySubTitle>
|
||||
<Spacer height="45px" />
|
||||
<AppsContainer>
|
||||
{officialApps?.map((qapp) => {
|
||||
return (
|
||||
<ButtonBase
|
||||
sx={{
|
||||
height: "80px",
|
||||
width: "60px",
|
||||
}}
|
||||
onClick={() => {
|
||||
// executeEvent("addTab", {
|
||||
// data: qapp
|
||||
// })
|
||||
executeEvent("selectedAppInfo", {
|
||||
data: qapp,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer
|
||||
sx={{
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<AppCircle
|
||||
sx={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "31px",
|
||||
width: "31px",
|
||||
}}
|
||||
alt={qapp?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
qapp?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "31px",
|
||||
height: "auto",
|
||||
}}
|
||||
src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
</AppCircle>
|
||||
<AppCircleLabel>
|
||||
{qapp?.metadata?.title || qapp?.name}
|
||||
</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
);
|
||||
})}
|
||||
</AppsContainer>
|
||||
<Spacer height="80px" />
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
gap: "250px",
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<AppLibrarySubTitle
|
||||
sx={{
|
||||
fontSize: "30px",
|
||||
width: "100%",
|
||||
textAlign: "start",
|
||||
}}
|
||||
>
|
||||
{hasPublishApp ? "Update Apps!" : "Create Apps!"}
|
||||
</AppLibrarySubTitle>
|
||||
<Spacer height="18px" />
|
||||
<PublishQAppCTAParent
|
||||
sx={{
|
||||
gap: "25px",
|
||||
}}
|
||||
>
|
||||
<PublishQAppCTALeft>
|
||||
<PublishQAppDotsBG>
|
||||
<img src={qappDots} />
|
||||
</PublishQAppDotsBG>
|
||||
<Spacer width="29px" />
|
||||
<img src={qappDevelopText} />
|
||||
</PublishQAppCTALeft>
|
||||
<PublishQAppCTARight
|
||||
onClick={() => {
|
||||
setMode("publish");
|
||||
}}
|
||||
>
|
||||
<PublishQAppCTAButton>
|
||||
{hasPublishApp ? "Update" : "Publish"}
|
||||
</PublishQAppCTAButton>
|
||||
<Spacer width="20px" />
|
||||
</PublishQAppCTARight>
|
||||
</PublishQAppCTAParent>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<AppLibrarySubTitle
|
||||
sx={{
|
||||
fontSize: "30px",
|
||||
}}
|
||||
>
|
||||
Categories
|
||||
</AppLibrarySubTitle>
|
||||
<Spacer height="18px" />
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{categories?.map((category) => {
|
||||
return (
|
||||
<ButtonBase
|
||||
key={category?.id}
|
||||
onClick={() => {
|
||||
executeEvent("selectedCategory", {
|
||||
data: category,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "60px",
|
||||
padding: "0px 24px",
|
||||
border: "4px solid #10242F",
|
||||
borderRadius: "6px",
|
||||
boxShadow: "2px 4px 0px 0px #000000",
|
||||
}}
|
||||
>
|
||||
{category?.name}
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</AppsDesktopLibraryBody>
|
||||
</AppsDesktopLibraryBody>
|
||||
</AppsLibraryContainer>
|
||||
);
|
||||
};
|
347
src/components/Apps/AppsNavBar.tsx
Normal file
@ -0,0 +1,347 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
AppsNavBarLeft,
|
||||
AppsNavBarParent,
|
||||
AppsNavBarRight,
|
||||
} from "./Apps-styles";
|
||||
import NavBack from "../../assets/svgs/NavBack.svg";
|
||||
import NavAdd from "../../assets/svgs/NavAdd.svg";
|
||||
import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg";
|
||||
import {
|
||||
ButtonBase,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tab,
|
||||
Tabs,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
executeEvent,
|
||||
subscribeToEvent,
|
||||
unsubscribeFromEvent,
|
||||
} from "../../utils/events";
|
||||
import TabComponent from "./TabComponent";
|
||||
import PushPinIcon from "@mui/icons-material/PushPin";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import {
|
||||
navigationControllerAtom,
|
||||
settingsLocalLastUpdatedAtom,
|
||||
sortablePinnedAppsAtom,
|
||||
} from "../../atoms/global";
|
||||
|
||||
export function saveToLocalStorage(key, subKey, newValue) {
|
||||
try {
|
||||
// Fetch existing data
|
||||
const existingData = localStorage.getItem(key);
|
||||
let combinedData = {};
|
||||
|
||||
if (existingData) {
|
||||
// Parse the existing data
|
||||
const parsedData = JSON.parse(existingData);
|
||||
// Merge with the new data under the subKey
|
||||
combinedData = {
|
||||
...parsedData,
|
||||
timestamp: Date.now(), // Update the root timestamp
|
||||
[subKey]: newValue, // Assuming the data is an array
|
||||
};
|
||||
} else {
|
||||
// If no existing data, just use the new data under the subKey
|
||||
combinedData = {
|
||||
timestamp: Date.now(), // Set the initial root timestamp
|
||||
[subKey]: newValue,
|
||||
};
|
||||
}
|
||||
|
||||
// Save combined data back to localStorage
|
||||
const serializedValue = JSON.stringify(combinedData);
|
||||
localStorage.setItem(key, serializedValue);
|
||||
} catch (error) {
|
||||
console.error("Error saving to localStorage:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export const AppsNavBar = () => {
|
||||
const [tabs, setTabs] = useState([]);
|
||||
const [selectedTab, setSelectedTab] = useState(null);
|
||||
const [isNewTabWindow, setIsNewTabWindow] = useState(false);
|
||||
const tabsRef = useRef(null);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(
|
||||
sortablePinnedAppsAtom
|
||||
);
|
||||
const [navigationController, setNavigationController] = useRecoilState(navigationControllerAtom)
|
||||
|
||||
const isDisableBackButton = useMemo(()=> {
|
||||
if(selectedTab && navigationController[selectedTab?.tabId]?.hasBack) return false
|
||||
if(selectedTab && !navigationController[selectedTab?.tabId]?.hasBack) return true
|
||||
return false
|
||||
}, [navigationController, selectedTab])
|
||||
|
||||
const setSettingsLocalLastUpdated = useSetRecoilState(
|
||||
settingsLocalLastUpdatedAtom
|
||||
);
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added)
|
||||
if (tabsRef.current) {
|
||||
const tabElements = tabsRef.current.querySelectorAll(".MuiTab-root");
|
||||
if (tabElements.length > 0) {
|
||||
const lastTab = tabElements[tabElements.length - 1];
|
||||
lastTab.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "end",
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [tabs.length]); // Dependency on the number of tabs
|
||||
|
||||
const setTabsToNav = (e) => {
|
||||
const { tabs, selectedTab, isNewTabWindow } = e.detail?.data;
|
||||
|
||||
setTabs([...tabs]);
|
||||
setSelectedTab(!selectedTab ? null : { ...selectedTab });
|
||||
setIsNewTabWindow(isNewTabWindow);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("setTabsToNav", setTabsToNav);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("setTabsToNav", setTabsToNav);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isSelectedAppPinned = !!sortablePinnedApps?.find(
|
||||
(item) =>
|
||||
item?.name === selectedTab?.name && item?.service === selectedTab?.service
|
||||
);
|
||||
return (
|
||||
<AppsNavBarParent>
|
||||
<AppsNavBarLeft>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
executeEvent("navigateBack", selectedTab?.tabId);
|
||||
}}
|
||||
disabled={isDisableBackButton}
|
||||
sx={{
|
||||
opacity: !isDisableBackButton ? 1 : 0.1,
|
||||
cursor: !isDisableBackButton ? 'pointer': 'default'
|
||||
}}
|
||||
>
|
||||
<img src={NavBack} />
|
||||
</ButtonBase>
|
||||
<Tabs
|
||||
ref={tabsRef}
|
||||
aria-label="basic tabs example"
|
||||
variant="scrollable" // Make tabs scrollable
|
||||
scrollButtons={false}
|
||||
sx={{
|
||||
"& .MuiTabs-indicator": {
|
||||
backgroundColor: "white",
|
||||
},
|
||||
maxWidth: `calc(100vw - 150px)`, // Ensure the tabs container fits within the available space
|
||||
overflow: "hidden", // Prevents overflow on small screens
|
||||
}}
|
||||
>
|
||||
{tabs?.map((tab) => (
|
||||
<Tab
|
||||
key={tab.tabId}
|
||||
label={
|
||||
<TabComponent
|
||||
isSelected={
|
||||
tab?.tabId === selectedTab?.tabId && !isNewTabWindow
|
||||
}
|
||||
app={tab}
|
||||
/>
|
||||
} // Pass custom component
|
||||
sx={{
|
||||
"&.Mui-selected": {
|
||||
color: "white",
|
||||
},
|
||||
padding: "0px",
|
||||
margin: "0px",
|
||||
minWidth: "0px",
|
||||
width: "50px",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</AppsNavBarLeft>
|
||||
{selectedTab && (
|
||||
<AppsNavBarRight
|
||||
sx={{
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setSelectedTab(null);
|
||||
executeEvent("newTabWindow", {});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
height: "40px",
|
||||
width: "40px",
|
||||
}}
|
||||
src={NavAdd}
|
||||
/>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={(e) => {
|
||||
if (!selectedTab) return;
|
||||
handleClick(e);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
height: "34px",
|
||||
width: "34px",
|
||||
}}
|
||||
src={NavMoreMenu}
|
||||
/>
|
||||
</ButtonBase>
|
||||
</AppsNavBarRight>
|
||||
)}
|
||||
|
||||
<Menu
|
||||
id="navbar-more-mobile"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
"aria-labelledby": "basic-button",
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
backgroundColor: "var(--bg-primary)",
|
||||
color: "#fff",
|
||||
width: "148px",
|
||||
borderRadius: "5px",
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
marginTop: "10px",
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (!selectedTab) return;
|
||||
|
||||
setSortablePinnedApps((prev) => {
|
||||
let updatedApps;
|
||||
|
||||
if (isSelectedAppPinned) {
|
||||
// Remove the selected app if it is pinned
|
||||
updatedApps = prev.filter(
|
||||
(item) =>
|
||||
!(
|
||||
item?.name === selectedTab?.name &&
|
||||
item?.service === selectedTab?.service
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Add the selected app if it is not pinned
|
||||
updatedApps = [
|
||||
...prev,
|
||||
{
|
||||
name: selectedTab?.name,
|
||||
service: selectedTab?.service,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
saveToLocalStorage(
|
||||
"ext_saved_settings",
|
||||
"sortablePinnedApps",
|
||||
updatedApps
|
||||
);
|
||||
return updatedApps;
|
||||
});
|
||||
setSettingsLocalLastUpdated(Date.now());
|
||||
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: "24px !important",
|
||||
marginRight: "5px",
|
||||
}}
|
||||
>
|
||||
<PushPinIcon
|
||||
height={20}
|
||||
sx={{
|
||||
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)",
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)",
|
||||
},
|
||||
}}
|
||||
primary={`${isSelectedAppPinned ? "Unpin app" : "Pin app"}`}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
executeEvent("refreshApp", {
|
||||
tabId: selectedTab?.tabId,
|
||||
});
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: "24px !important",
|
||||
marginRight: "5px",
|
||||
}}
|
||||
>
|
||||
<RefreshIcon
|
||||
height={20}
|
||||
sx={{
|
||||
color: "rgba(250, 250, 250, 0.5)",
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: "rgba(250, 250, 250, 0.5)",
|
||||
},
|
||||
}}
|
||||
primary="Refresh"
|
||||
/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</AppsNavBarParent>
|
||||
);
|
||||
};
|
372
src/components/Apps/AppsNavBarDesktop.tsx
Normal file
@ -0,0 +1,372 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
AppsNavBarLeft,
|
||||
AppsNavBarParent,
|
||||
AppsNavBarRight,
|
||||
} from "./Apps-styles";
|
||||
import NavBack from "../../assets/svgs/NavBack.svg";
|
||||
import NavAdd from "../../assets/svgs/NavAdd.svg";
|
||||
import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg";
|
||||
import {
|
||||
ButtonBase,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Tab,
|
||||
Tabs,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
executeEvent,
|
||||
subscribeToEvent,
|
||||
unsubscribeFromEvent,
|
||||
} from "../../utils/events";
|
||||
import TabComponent from "./TabComponent";
|
||||
import PushPinIcon from "@mui/icons-material/PushPin";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import {
|
||||
navigationControllerAtom,
|
||||
settingsLocalLastUpdatedAtom,
|
||||
sortablePinnedAppsAtom,
|
||||
} from "../../atoms/global";
|
||||
|
||||
export function saveToLocalStorage(key, subKey, newValue) {
|
||||
try {
|
||||
// Fetch existing data
|
||||
const existingData = localStorage.getItem(key);
|
||||
let combinedData = {};
|
||||
|
||||
if (existingData) {
|
||||
// Parse the existing data
|
||||
const parsedData = JSON.parse(existingData);
|
||||
// Merge with the new data under the subKey
|
||||
combinedData = {
|
||||
...parsedData,
|
||||
timestamp: Date.now(), // Update the root timestamp
|
||||
[subKey]: newValue, // Assuming the data is an array
|
||||
};
|
||||
} else {
|
||||
// If no existing data, just use the new data under the subKey
|
||||
combinedData = {
|
||||
timestamp: Date.now(), // Set the initial root timestamp
|
||||
[subKey]: newValue,
|
||||
};
|
||||
}
|
||||
|
||||
// Save combined data back to localStorage
|
||||
const serializedValue = JSON.stringify(combinedData);
|
||||
localStorage.setItem(key, serializedValue);
|
||||
} catch (error) {
|
||||
console.error("Error saving to localStorage:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export const AppsNavBarDesktop = () => {
|
||||
const [tabs, setTabs] = useState([]);
|
||||
const [selectedTab, setSelectedTab] = useState(null);
|
||||
const [navigationController, setNavigationController] = useRecoilState(navigationControllerAtom)
|
||||
|
||||
const [isNewTabWindow, setIsNewTabWindow] = useState(false);
|
||||
const tabsRef = useRef(null);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(
|
||||
sortablePinnedAppsAtom
|
||||
);
|
||||
|
||||
|
||||
const setSettingsLocalLastUpdated = useSetRecoilState(
|
||||
settingsLocalLastUpdatedAtom
|
||||
);
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Scroll to the last tab whenever the tabs array changes (e.g., when a new tab is added)
|
||||
if (tabsRef.current) {
|
||||
const tabElements = tabsRef.current.querySelectorAll(".MuiTab-root");
|
||||
if (tabElements.length > 0) {
|
||||
const lastTab = tabElements[tabElements.length - 1];
|
||||
lastTab.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "end",
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [tabs.length]); // Dependency on the number of tabs
|
||||
|
||||
|
||||
|
||||
const isDisableBackButton = useMemo(()=> {
|
||||
if(selectedTab && navigationController[selectedTab?.tabId]?.hasBack) return false
|
||||
if(selectedTab && !navigationController[selectedTab?.tabId]?.hasBack) return true
|
||||
return false
|
||||
}, [navigationController, selectedTab])
|
||||
|
||||
|
||||
|
||||
|
||||
const setTabsToNav = (e) => {
|
||||
const { tabs, selectedTab, isNewTabWindow } = e.detail?.data;
|
||||
|
||||
setTabs([...tabs]);
|
||||
setSelectedTab(!selectedTab ? null : { ...selectedTab });
|
||||
setIsNewTabWindow(isNewTabWindow);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("setTabsToNav", setTabsToNav);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("setTabsToNav", setTabsToNav);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const isSelectedAppPinned = !!sortablePinnedApps?.find(
|
||||
(item) =>
|
||||
item?.name === selectedTab?.name && item?.service === selectedTab?.service
|
||||
);
|
||||
return (
|
||||
<AppsNavBarParent
|
||||
sx={{
|
||||
position: "relative",
|
||||
flexDirection: "column",
|
||||
width: "60px",
|
||||
height: "unset",
|
||||
maxHeight: "70vh",
|
||||
borderRadius: "0px 30px 30px 0px",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<AppsNavBarLeft
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
executeEvent("navigateBack", selectedTab?.tabId);
|
||||
}}
|
||||
disabled={isDisableBackButton}
|
||||
sx={{
|
||||
opacity: !isDisableBackButton ? 1 : 0.1,
|
||||
cursor: !isDisableBackButton ? 'pointer': 'default'
|
||||
}}
|
||||
>
|
||||
<img src={NavBack} />
|
||||
</ButtonBase>
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
ref={tabsRef}
|
||||
aria-label="basic tabs example"
|
||||
variant="scrollable" // Make tabs scrollable
|
||||
scrollButtons={true}
|
||||
sx={{
|
||||
"& .MuiTabs-indicator": {
|
||||
backgroundColor: "white",
|
||||
},
|
||||
maxHeight: `320px`, // Ensure the tabs container fits within the available space
|
||||
overflow: "hidden", // Prevents overflow on small screens
|
||||
}}
|
||||
>
|
||||
{tabs?.map((tab) => (
|
||||
<Tab
|
||||
key={tab.tabId}
|
||||
label={
|
||||
<TabComponent
|
||||
isSelected={
|
||||
tab?.tabId === selectedTab?.tabId && !isNewTabWindow
|
||||
}
|
||||
app={tab}
|
||||
/>
|
||||
} // Pass custom component
|
||||
sx={{
|
||||
"&.Mui-selected": {
|
||||
color: "white",
|
||||
},
|
||||
padding: "0px",
|
||||
margin: "0px",
|
||||
minWidth: "0px",
|
||||
width: "50px",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</AppsNavBarLeft>
|
||||
{selectedTab && (
|
||||
<AppsNavBarRight
|
||||
sx={{
|
||||
gap: "10px",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setSelectedTab(null);
|
||||
executeEvent("newTabWindow", {});
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
height: "40px",
|
||||
width: "40px",
|
||||
}}
|
||||
src={NavAdd}
|
||||
/>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={(e) => {
|
||||
if (!selectedTab) return;
|
||||
handleClick(e);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
height: "34px",
|
||||
width: "34px",
|
||||
}}
|
||||
src={NavMoreMenu}
|
||||
/>
|
||||
</ButtonBase>
|
||||
</AppsNavBarRight>
|
||||
)}
|
||||
|
||||
<Menu
|
||||
id="navbar-more-mobile"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
"aria-labelledby": "basic-button",
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
backgroundColor: "var(--bg-primary)",
|
||||
color: "#fff",
|
||||
width: "148px",
|
||||
borderRadius: "5px",
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
marginTop: "10px",
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
if (!selectedTab) return;
|
||||
|
||||
setSortablePinnedApps((prev) => {
|
||||
let updatedApps;
|
||||
|
||||
if (isSelectedAppPinned) {
|
||||
// Remove the selected app if it is pinned
|
||||
updatedApps = prev.filter(
|
||||
(item) =>
|
||||
!(
|
||||
item?.name === selectedTab?.name &&
|
||||
item?.service === selectedTab?.service
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Add the selected app if it is not pinned
|
||||
updatedApps = [
|
||||
...prev,
|
||||
{
|
||||
name: selectedTab?.name,
|
||||
service: selectedTab?.service,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
saveToLocalStorage(
|
||||
"ext_saved_settings",
|
||||
"sortablePinnedApps",
|
||||
updatedApps
|
||||
);
|
||||
return updatedApps;
|
||||
});
|
||||
setSettingsLocalLastUpdated(Date.now());
|
||||
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: "24px !important",
|
||||
marginRight: "5px",
|
||||
}}
|
||||
>
|
||||
<PushPinIcon
|
||||
height={20}
|
||||
sx={{
|
||||
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)",
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: isSelectedAppPinned ? "red" : "rgba(250, 250, 250, 0.5)",
|
||||
},
|
||||
}}
|
||||
primary={`${isSelectedAppPinned ? "Unpin app" : "Pin app"}`}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
executeEvent("refreshApp", {
|
||||
tabId: selectedTab?.tabId,
|
||||
});
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: "24px !important",
|
||||
marginRight: "5px",
|
||||
}}
|
||||
>
|
||||
<RefreshIcon
|
||||
height={20}
|
||||
sx={{
|
||||
color: "rgba(250, 250, 250, 0.5)",
|
||||
}}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
sx={{
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: "rgba(250, 250, 250, 0.5)",
|
||||
},
|
||||
}}
|
||||
primary="Refresh"
|
||||
/>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</AppsNavBarParent>
|
||||
);
|
||||
};
|
176
src/components/Apps/SortablePinnedApps.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { DndContext, closestCenter } from '@dnd-kit/core';
|
||||
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable';
|
||||
import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Avatar, ButtonBase } from '@mui/material';
|
||||
import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles';
|
||||
import { getBaseApiReact } from '../../App';
|
||||
import { executeEvent } from '../../utils/events';
|
||||
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { saveToLocalStorage } from './AppsNavBar';
|
||||
import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps';
|
||||
|
||||
const SortableItem = ({ id, name, app, isDesktop }) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
padding: '10px',
|
||||
border: '1px solid #ccc',
|
||||
marginBottom: '5px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#f9f9f9',
|
||||
cursor: 'grab',
|
||||
color: 'black'
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenuPinnedApps app={app} isMine={!!app?.isMine}>
|
||||
<ButtonBase
|
||||
ref={setNodeRef} {...attributes} {...listeners}
|
||||
sx={{
|
||||
height: "80px",
|
||||
width: "60px",
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
}}
|
||||
onClick={()=> {
|
||||
executeEvent("addTab", {
|
||||
data: app
|
||||
})
|
||||
}}
|
||||
>
|
||||
<AppCircleContainer sx={{
|
||||
border: "none",
|
||||
gap: isDesktop ? '10px': '5px'
|
||||
}}>
|
||||
<AppCircle
|
||||
sx={{
|
||||
border: "none",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "31px",
|
||||
width: "31px",
|
||||
'& img': {
|
||||
objectFit: 'fill',
|
||||
}
|
||||
}}
|
||||
alt={app?.metadata?.title || app?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "31px",
|
||||
height: "auto",
|
||||
}}
|
||||
// src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
</AppCircle>
|
||||
<AppCircleLabel>
|
||||
{app?.metadata?.title || app?.name}
|
||||
</AppCircleLabel>
|
||||
</AppCircleContainer>
|
||||
</ButtonBase>
|
||||
</ContextMenuPinnedApps>
|
||||
);
|
||||
};
|
||||
|
||||
export const SortablePinnedApps = ({ isDesktop, myWebsite, myApp, availableQapps = [] }) => {
|
||||
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
||||
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom);
|
||||
|
||||
const transformPinnedApps = useMemo(() => {
|
||||
|
||||
// Clone the existing pinned apps list
|
||||
let pinned = [...pinnedApps];
|
||||
|
||||
// Function to add or update `isMine` property
|
||||
const addOrUpdateIsMine = (pinnedList, appToCheck) => {
|
||||
if (!appToCheck) return pinnedList;
|
||||
|
||||
const existingIndex = pinnedList.findIndex(
|
||||
(item) => item?.service === appToCheck?.service && item?.name === appToCheck?.name
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// If the app is already in the list, update it with `isMine: true`
|
||||
pinnedList[existingIndex] = { ...pinnedList[existingIndex], isMine: true };
|
||||
} else {
|
||||
// If not in the list, add it with `isMine: true` at the beginning
|
||||
pinnedList.unshift({ ...appToCheck, isMine: true });
|
||||
}
|
||||
|
||||
return pinnedList;
|
||||
};
|
||||
|
||||
// Update or add `myWebsite` and `myApp` while preserving their positions
|
||||
pinned = addOrUpdateIsMine(pinned, myWebsite);
|
||||
pinned = addOrUpdateIsMine(pinned, myApp);
|
||||
|
||||
// Update pinned list based on availableQapps
|
||||
pinned = pinned.map((pin) => {
|
||||
const findIndex = availableQapps?.findIndex(
|
||||
(item) => item?.service === pin?.service && item?.name === pin?.name
|
||||
);
|
||||
if (findIndex !== -1) return {
|
||||
...availableQapps[findIndex],
|
||||
...pin
|
||||
}
|
||||
|
||||
return pin;
|
||||
});
|
||||
|
||||
return pinned;
|
||||
}, [myApp, myWebsite, pinnedApps, availableQapps]);
|
||||
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 10, // Set a distance to avoid triggering drag on small movements
|
||||
},
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
distance: 10, // Also apply to touch
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over) return; // Make sure the drop target exists
|
||||
|
||||
if (active.id !== over.id) {
|
||||
const oldIndex = transformPinnedApps.findIndex((item) => `${item?.service}-${item?.name}` === active.id);
|
||||
const newIndex = transformPinnedApps.findIndex((item) => `${item?.service}-${item?.name}` === over.id);
|
||||
|
||||
const newOrder = arrayMove(transformPinnedApps, oldIndex, newIndex);
|
||||
setPinnedApps(newOrder);
|
||||
saveToLocalStorage('ext_saved_settings','sortablePinnedApps', newOrder)
|
||||
setSettingsLocalLastUpdated(Date.now())
|
||||
}
|
||||
};
|
||||
return (
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<SortableContext items={transformPinnedApps.map((app) => `${app?.service}-${app?.name}`)}>
|
||||
{transformPinnedApps.map((app) => (
|
||||
<SortableItem isDesktop={isDesktop} key={`${app?.service}-${app?.name}`} id={`${app?.service}-${app?.name}`} name={app?.name} app={app} />
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
61
src/components/Apps/TabComponent.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import { TabParent } from './Apps-styles'
|
||||
import NavCloseTab from "../../assets/svgs/NavCloseTab.svg";
|
||||
import { getBaseApiReact } from '../../App';
|
||||
import { Avatar, ButtonBase } from '@mui/material';
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
import { executeEvent } from '../../utils/events';
|
||||
|
||||
const TabComponent = ({isSelected, app}) => {
|
||||
return (
|
||||
<ButtonBase onClick={()=> {
|
||||
if(isSelected){
|
||||
executeEvent('removeTab', {
|
||||
data: app
|
||||
})
|
||||
return
|
||||
}
|
||||
executeEvent('setSelectedTab', {
|
||||
data: app
|
||||
})
|
||||
}}>
|
||||
<TabParent sx={{
|
||||
border: isSelected && '1px solid #FFFFFF'
|
||||
}}>
|
||||
{isSelected && (
|
||||
|
||||
<img style={
|
||||
{
|
||||
position: 'absolute',
|
||||
top: '-5px',
|
||||
right: '-5px',
|
||||
zIndex: 1
|
||||
}
|
||||
} src={NavCloseTab}/>
|
||||
|
||||
) }
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "28px",
|
||||
width: "28px",
|
||||
}}
|
||||
alt={app?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
app?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
width: "28px",
|
||||
height: "auto",
|
||||
}}
|
||||
src={LogoSelected}
|
||||
alt="center-icon"
|
||||
/>
|
||||
</Avatar>
|
||||
</TabParent>
|
||||
</ButtonBase>
|
||||
)
|
||||
}
|
||||
|
||||
export default TabComponent
|
484
src/components/Apps/useQortalMessageListener.tsx
Normal file
@ -0,0 +1,484 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import FileSaver from 'file-saver';
|
||||
import { executeEvent } from '../../utils/events';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { navigationControllerAtom } from '../../atoms/global';
|
||||
class Semaphore {
|
||||
constructor(count) {
|
||||
this.count = count
|
||||
this.waiting = []
|
||||
}
|
||||
acquire() {
|
||||
return new Promise(resolve => {
|
||||
if (this.count > 0) {
|
||||
this.count--
|
||||
resolve()
|
||||
} else {
|
||||
this.waiting.push(resolve)
|
||||
}
|
||||
})
|
||||
}
|
||||
release() {
|
||||
if (this.waiting.length > 0) {
|
||||
const resolve = this.waiting.shift()
|
||||
resolve()
|
||||
} else {
|
||||
this.count++
|
||||
}
|
||||
}
|
||||
}
|
||||
let semaphore = new Semaphore(1)
|
||||
let reader = new FileReader()
|
||||
|
||||
const fileToBase64 = (file) => new Promise(async (resolve, reject) => {
|
||||
if (!reader) {
|
||||
reader = new FileReader()
|
||||
}
|
||||
await semaphore.acquire()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => {
|
||||
const dataUrl = reader.result
|
||||
if (typeof dataUrl === "string") {
|
||||
const base64String = dataUrl.split(',')[1]
|
||||
reader.onload = null
|
||||
reader.onerror = null
|
||||
resolve(base64String)
|
||||
} else {
|
||||
reader.onload = null
|
||||
reader.onerror = null
|
||||
reject(new Error('Invalid data URL'))
|
||||
}
|
||||
semaphore.release()
|
||||
}
|
||||
reader.onerror = (error) => {
|
||||
reader.onload = null
|
||||
reader.onerror = null
|
||||
reject(error)
|
||||
semaphore.release()
|
||||
}
|
||||
})
|
||||
|
||||
function openIndexedDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open("fileStorageDB", 1);
|
||||
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains("files")) {
|
||||
db.createObjectStore("files", { keyPath: "id" });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = function (event) {
|
||||
resolve(event.target.result);
|
||||
};
|
||||
|
||||
request.onerror = function () {
|
||||
reject("Error opening IndexedDB");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function handleGetFileFromIndexedDB(fileId, sendResponse) {
|
||||
try {
|
||||
const db = await openIndexedDB();
|
||||
const transaction = db.transaction(["files"], "readonly");
|
||||
const objectStore = transaction.objectStore("files");
|
||||
|
||||
const getRequest = objectStore.get(fileId);
|
||||
|
||||
getRequest.onsuccess = async function (event) {
|
||||
if (getRequest.result) {
|
||||
const file = getRequest.result.data;
|
||||
|
||||
try {
|
||||
const base64String = await fileToBase64(file);
|
||||
|
||||
// Create a new transaction to delete the file
|
||||
const deleteTransaction = db.transaction(["files"], "readwrite");
|
||||
const deleteObjectStore = deleteTransaction.objectStore("files");
|
||||
const deleteRequest = deleteObjectStore.delete(fileId);
|
||||
|
||||
deleteRequest.onsuccess = function () {
|
||||
try {
|
||||
sendResponse({ result: base64String });
|
||||
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
}
|
||||
};
|
||||
|
||||
deleteRequest.onerror = function () {
|
||||
console.error(`Error deleting file with ID ${fileId} from IndexedDB`);
|
||||
sendResponse({ result: null, error: "Failed to delete file from IndexedDB" });
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error converting file to Base64:", error);
|
||||
sendResponse({ result: null, error: "Failed to convert file to Base64" });
|
||||
}
|
||||
} else {
|
||||
console.error(`File with ID ${fileId} not found in IndexedDB`);
|
||||
sendResponse({ result: null, error: "File not found in IndexedDB" });
|
||||
}
|
||||
};
|
||||
|
||||
getRequest.onerror = function () {
|
||||
console.error(`Error retrieving file with ID ${fileId} from IndexedDB`);
|
||||
sendResponse({ result: null, error: "Error retrieving file from IndexedDB" });
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error opening IndexedDB:", error);
|
||||
sendResponse({ result: null, error: "Error opening IndexedDB" });
|
||||
}
|
||||
}
|
||||
|
||||
const UIQortalRequests = [
|
||||
'GET_USER_ACCOUNT', 'DECRYPT_DATA', 'SEND_COIN', 'GET_LIST_ITEMS',
|
||||
'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM', 'VOTE_ON_POLL', 'CREATE_POLL',
|
||||
'SEND_CHAT_MESSAGE', 'JOIN_GROUP', 'DEPLOY_AT', 'GET_USER_WALLET',
|
||||
'GET_WALLET_BALANCE', 'GET_USER_WALLET_INFO', 'GET_CROSSCHAIN_SERVER_INFO',
|
||||
'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE',
|
||||
'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER',
|
||||
'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY'
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
async function retrieveFileFromIndexedDB(fileId) {
|
||||
const db = await openIndexedDB();
|
||||
const transaction = db.transaction(["files"], "readwrite");
|
||||
const objectStore = transaction.objectStore("files");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const getRequest = objectStore.get(fileId);
|
||||
|
||||
getRequest.onsuccess = function (event) {
|
||||
if (getRequest.result) {
|
||||
// File found, resolve it and delete from IndexedDB
|
||||
const file = getRequest.result.data;
|
||||
objectStore.delete(fileId);
|
||||
resolve(file);
|
||||
} else {
|
||||
reject("File not found in IndexedDB");
|
||||
}
|
||||
};
|
||||
|
||||
getRequest.onerror = function () {
|
||||
reject("Error retrieving file from IndexedDB");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteQortalFilesFromIndexedDB() {
|
||||
try {
|
||||
const db = await openIndexedDB();
|
||||
const transaction = db.transaction(["files"], "readwrite");
|
||||
const objectStore = transaction.objectStore("files");
|
||||
|
||||
// Create a request to get all keys
|
||||
const getAllKeysRequest = objectStore.getAllKeys();
|
||||
|
||||
getAllKeysRequest.onsuccess = function (event) {
|
||||
const keys = event.target.result;
|
||||
|
||||
// Iterate through keys to find and delete those containing '_qortalfile'
|
||||
for (let key of keys) {
|
||||
if (key.includes("_qortalfile")) {
|
||||
const deleteRequest = objectStore.delete(key);
|
||||
|
||||
deleteRequest.onsuccess = function () {
|
||||
console.log(`File with key '${key}' has been deleted from IndexedDB`);
|
||||
};
|
||||
|
||||
deleteRequest.onerror = function () {
|
||||
console.error(`Failed to delete file with key '${key}' from IndexedDB`);
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getAllKeysRequest.onerror = function () {
|
||||
console.error("Failed to retrieve keys from IndexedDB");
|
||||
};
|
||||
|
||||
transaction.oncomplete = function () {
|
||||
console.log("Transaction complete for deleting files from IndexedDB");
|
||||
};
|
||||
|
||||
transaction.onerror = function () {
|
||||
console.error("Error occurred during transaction for deleting files");
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error opening IndexedDB:", error);
|
||||
}
|
||||
}
|
||||
|
||||
const showSaveFilePicker = async (data) => {
|
||||
let blob
|
||||
let fileName
|
||||
try {
|
||||
const {filename, mimeType, fileHandleOptions, fileId} = data
|
||||
blob = await retrieveFileFromIndexedDB(fileId)
|
||||
fileName = filename
|
||||
|
||||
const fileHandle = await window.showSaveFilePicker({
|
||||
suggestedName: filename,
|
||||
types: [
|
||||
{
|
||||
description: mimeType,
|
||||
...fileHandleOptions
|
||||
}
|
||||
]
|
||||
})
|
||||
const writeFile = async (fileHandle, contents) => {
|
||||
const writable = await fileHandle.createWritable()
|
||||
await writable.write(contents)
|
||||
await writable.close()
|
||||
}
|
||||
writeFile(fileHandle, blob).then(() => console.log("FILE SAVED"))
|
||||
} catch (error) {
|
||||
FileSaver.saveAs(blob, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
async function storeFilesInIndexedDB(obj) {
|
||||
// First delete any existing files in IndexedDB with '_qortalfile' in their ID
|
||||
await deleteQortalFilesFromIndexedDB();
|
||||
|
||||
// Open the IndexedDB
|
||||
const db = await openIndexedDB();
|
||||
const transaction = db.transaction(["files"], "readwrite");
|
||||
const objectStore = transaction.objectStore("files");
|
||||
|
||||
// Handle the obj.file if it exists and is a File instance
|
||||
if (obj.file) {
|
||||
const fileId = "objFile_qortalfile";
|
||||
|
||||
// Store the file in IndexedDB
|
||||
const fileData = {
|
||||
id: fileId,
|
||||
data: obj.file,
|
||||
};
|
||||
objectStore.put(fileData);
|
||||
|
||||
// Replace the file object with the file ID in the original object
|
||||
obj.fileId = fileId;
|
||||
delete obj.file;
|
||||
}
|
||||
if (obj.blob) {
|
||||
const fileId = "objFile_qortalfile";
|
||||
|
||||
// Store the file in IndexedDB
|
||||
const fileData = {
|
||||
id: fileId,
|
||||
data: obj.blob,
|
||||
};
|
||||
objectStore.put(fileData);
|
||||
|
||||
// Replace the file object with the file ID in the original object
|
||||
let blobObj = {
|
||||
type: obj.blob?.type
|
||||
}
|
||||
obj.fileId = fileId;
|
||||
delete obj.blob;
|
||||
obj.blob = blobObj
|
||||
}
|
||||
|
||||
// Iterate through resources to find files and save them to IndexedDB
|
||||
for (let resource of (obj?.resources || [])) {
|
||||
if (resource.file) {
|
||||
const fileId = resource.identifier + "_qortalfile";
|
||||
|
||||
// Store the file in IndexedDB
|
||||
const fileData = {
|
||||
id: fileId,
|
||||
data: resource.file,
|
||||
};
|
||||
objectStore.put(fileData);
|
||||
|
||||
// Replace the file object with the file ID in the original object
|
||||
resource.fileId = fileId;
|
||||
delete resource.file;
|
||||
}
|
||||
}
|
||||
|
||||
// Set transaction completion handlers
|
||||
transaction.oncomplete = function () {
|
||||
console.log("Files saved successfully to IndexedDB");
|
||||
};
|
||||
|
||||
transaction.onerror = function () {
|
||||
console.error("Error saving files to IndexedDB");
|
||||
};
|
||||
|
||||
return obj; // Updated object with references to stored files
|
||||
}
|
||||
|
||||
export const useQortalMessageListener = (frameWindow, iframeRef, tabId) => {
|
||||
const [path, setPath] = useState('')
|
||||
const [history, setHistory] = useState({
|
||||
customQDNHistoryPaths: [],
|
||||
currentIndex: -1,
|
||||
isDOMContentLoaded: false
|
||||
})
|
||||
const setHasSettingsChangedAtom = useSetRecoilState(navigationControllerAtom);
|
||||
|
||||
|
||||
useEffect(()=> {
|
||||
if(tabId && !isNaN(history?.currentIndex)){
|
||||
setHasSettingsChangedAtom((prev)=> {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
hasBack: history?.currentIndex > 0,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [history?.currentIndex, tabId])
|
||||
|
||||
|
||||
const changeCurrentIndex = useCallback((value)=> {
|
||||
setHistory((prev)=> {
|
||||
return {
|
||||
...prev,
|
||||
currentIndex: value
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const resetHistory = useCallback(()=> {
|
||||
setHistory({
|
||||
customQDNHistoryPaths: [],
|
||||
currentIndex: -1,
|
||||
isManualNavigation: true,
|
||||
isDOMContentLoaded: false
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const listener = async (event) => {
|
||||
console.log('eventreactt', event)
|
||||
// event.preventDefault(); // Prevent default behavior
|
||||
// event.stopImmediatePropagation(); // Stop other listeners from firing
|
||||
|
||||
if (event?.data?.requestedHandler !== 'UI') return;
|
||||
|
||||
const sendMessageToRuntime = (message, eventPort) => {
|
||||
chrome?.runtime?.sendMessage(message, (response) => {
|
||||
if (response.error) {
|
||||
eventPort.postMessage({
|
||||
result: null,
|
||||
error: response,
|
||||
});
|
||||
} else {
|
||||
eventPort.postMessage({
|
||||
result: response,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Check if action is included in the predefined list of UI requests
|
||||
if (UIQortalRequests.includes(event.data.action)) {
|
||||
sendMessageToRuntime(
|
||||
{ action: event.data.action, type: 'qortalRequest', payload: event.data, isExtension: true },
|
||||
event.ports[0]
|
||||
);
|
||||
} else if (
|
||||
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
|
||||
event?.data?.action === 'PUBLISH_QDN_RESOURCE' ||
|
||||
event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'SAVE_FILE'
|
||||
|
||||
) {
|
||||
let data;
|
||||
try {
|
||||
data = await storeFilesInIndexedDB(event.data);
|
||||
} catch (error) {
|
||||
console.error('Error storing files in IndexedDB:', error);
|
||||
event.ports[0].postMessage({
|
||||
result: null,
|
||||
error: 'Failed to store files in IndexedDB',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (data) {
|
||||
sendMessageToRuntime(
|
||||
{ action: event.data.action, type: 'qortalRequest', payload: data, isExtension: true },
|
||||
event.ports[0]
|
||||
);
|
||||
} else {
|
||||
event.ports[0].postMessage({
|
||||
result: null,
|
||||
error: 'Failed to prepare data for publishing',
|
||||
});
|
||||
}
|
||||
} else if(event?.data?.action === 'LINK_TO_QDN_RESOURCE' ||
|
||||
event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){
|
||||
const pathUrl = event?.data?.path != null ? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path : null
|
||||
setPath(pathUrl)
|
||||
} else if(event?.data?.action === 'NAVIGATION_HISTORY'){
|
||||
if(event?.data?.payload?.isDOMContentLoaded){
|
||||
setHistory((prev)=> {
|
||||
const copyPrev = {...prev}
|
||||
if((copyPrev?.customQDNHistoryPaths || []).at(-1) === (event?.data?.payload?.customQDNHistoryPaths || []).at(-1)) {
|
||||
console.log('customQDNHistoryPaths.length', prev?.customQDNHistoryPaths.length)
|
||||
return {
|
||||
...prev,
|
||||
currentIndex: prev.customQDNHistoryPaths.length - 1 === -1 ? 0 : prev.customQDNHistoryPaths.length - 1
|
||||
}
|
||||
}
|
||||
const copyHistory = {...prev}
|
||||
const paths = [...(copyHistory?.customQDNHistoryPaths || []), ...(event?.data?.payload?.customQDNHistoryPaths || [])]
|
||||
console.log('paths', paths)
|
||||
return {
|
||||
...prev,
|
||||
customQDNHistoryPaths: paths,
|
||||
currentIndex: paths.length - 1
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setHistory(event?.data?.payload)
|
||||
|
||||
}
|
||||
} else if(event?.data?.action === 'SET_TAB'){
|
||||
executeEvent("addTab", {
|
||||
data: event?.data?.payload
|
||||
})
|
||||
iframeRef.current.contentWindow.postMessage(
|
||||
{ action: 'SET_TAB_SUCCESS', requestedHandler: 'UI',payload: {
|
||||
name: event?.data?.payload?.name
|
||||
} }, '*'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Add the listener for messages coming from the frameWindow
|
||||
frameWindow.addEventListener('message', listener);
|
||||
|
||||
// Cleanup function to remove the event listener when the component is unmounted
|
||||
return () => {
|
||||
frameWindow.removeEventListener('message', listener);
|
||||
};
|
||||
|
||||
|
||||
}, []); // Empty dependency array to run once when the component mounts
|
||||
|
||||
chrome.runtime?.onMessage.addListener( function (message, sender, sendResponse) {
|
||||
if(message.action === "SHOW_SAVE_FILE_PICKER"){
|
||||
showSaveFilePicker(message?.data)
|
||||
}
|
||||
|
||||
else if (message.action === "getFileFromIndexedDB") {
|
||||
handleGetFileFromIndexedDB(message.fileId, sendResponse);
|
||||
return true; // Keep the message channel open for async response
|
||||
}
|
||||
});
|
||||
|
||||
return {path, history, resetHistory, changeCurrentIndex}
|
||||
};
|
||||
|
@ -255,7 +255,7 @@ export const AnnouncementDiscussion = ({
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: isMobile ? '100%' : "100vh",
|
||||
height: isMobile ? '100%' : "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
|
@ -11,7 +11,7 @@ import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
|
||||
import { getNameInfo } from '../Group/Group';
|
||||
import { Spacer } from '../../common/Spacer';
|
||||
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
|
||||
import { getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App';
|
||||
import { getBaseApiReact, getBaseApiReactSocket, isMobile, pauseAllQueues, resumeAllQueues } from '../../App';
|
||||
import { getPublicKey } from '../../background';
|
||||
import { useMessageQueue } from '../../MessageQueueContext';
|
||||
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
|
||||
@ -77,9 +77,28 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
}, [selectedDirect?.address])
|
||||
|
||||
|
||||
|
||||
const middletierFunc = async (data: any, selectedDirectAddress: string, myAddress: string) => {
|
||||
try {
|
||||
if (hasInitialized.current) {
|
||||
decryptMessages(data, true);
|
||||
return;
|
||||
}
|
||||
hasInitialized.current = true;
|
||||
const url = `${getBaseApiReact()}/chat/messages?involving=${selectedDirectAddress}&involving=${myAddress}&encoding=BASE64&limit=0&reverse=false`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
decryptMessages(responseData, false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const decryptMessages = (encryptedMessages: any[])=> {
|
||||
const decryptMessages = (encryptedMessages: any[], isInitiated: boolean)=> {
|
||||
try {
|
||||
return new Promise((res, rej)=> {
|
||||
chrome?.runtime?.sendMessage({ action: "decryptDirect", payload: {
|
||||
@ -92,7 +111,7 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
processWithNewMessages(response, selectedDirect?.address)
|
||||
|
||||
res(response)
|
||||
if(hasInitialized.current){
|
||||
if(isInitiated){
|
||||
|
||||
const formatted = response.map((item: any)=> {
|
||||
return {
|
||||
@ -127,7 +146,6 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
|
||||
const forceCloseWebSocket = () => {
|
||||
if (socketRef.current) {
|
||||
console.log('Force closing the WebSocket');
|
||||
clearTimeout(timeoutIdRef.current);
|
||||
clearTimeout(groupSocketTimeoutRef.current);
|
||||
socketRef.current.close(1000, 'forced');
|
||||
@ -161,7 +179,6 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
socketRef.current = new WebSocket(socketLink);
|
||||
|
||||
socketRef.current.onopen = () => {
|
||||
console.log('WebSocket connection opened');
|
||||
setTimeout(pingWebSocket, 50); // Initial ping
|
||||
};
|
||||
|
||||
@ -171,7 +188,8 @@ export const ChatDirect = ({ myAddress, isNewChat, selectedDirect, setSelectedDi
|
||||
clearTimeout(timeoutIdRef.current);
|
||||
groupSocketTimeoutRef.current = setTimeout(pingWebSocket, 45000); // Ping every 45 seconds
|
||||
} else {
|
||||
decryptMessages(JSON.parse(e.data));
|
||||
middletierFunc(JSON.parse(e.data), selectedDirect?.address, myAddress)
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
|
@ -97,27 +97,28 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
}
|
||||
|
||||
const middletierFunc = async (data: any, groupId: string) => {
|
||||
try {
|
||||
if (hasInitialized.current) {
|
||||
decryptMessages(data, true);
|
||||
return;
|
||||
}
|
||||
hasInitialized.current = true;
|
||||
const url = `${getBaseApiReact()}/chat/messages?txGroupId=${groupId}&encoding=BASE64&limit=0&reverse=false`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
decryptMessages(responseData, false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
try {
|
||||
if (hasInitialized.current) {
|
||||
decryptMessages(data, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
hasInitialized.current = true;
|
||||
const url = `${getBaseApiReact()}/chat/messages?txGroupId=${groupId}&encoding=BASE64&limit=0&reverse=false`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
decryptMessages(responseData, false);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
const decryptMessages = ( encryptedMessages: any[], isInitiated: boolean )=> {
|
||||
|
||||
const decryptMessages = (encryptedMessages: any[], isInitiated: boolean )=> {
|
||||
try {
|
||||
if(!secretKeyRef.current){
|
||||
checkForFirstSecretKeyNotification(encryptedMessages)
|
||||
@ -231,6 +232,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
|
||||
}
|
||||
} )
|
||||
setMessages(formatted)
|
||||
|
||||
setChatReferences((prev) => {
|
||||
let organizedChatReferences = { ...prev };
|
||||
|
||||
|
@ -1,21 +1,33 @@
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { MessageItem } from './MessageItem';
|
||||
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events';
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
|
||||
export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, handleReaction, chatReferences, tempChatReferences }) => {
|
||||
|
||||
const virtuosoRef = useRef();
|
||||
const parentRef = useRef();
|
||||
const [messages, setMessages] = useState(initialMessages);
|
||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||
const hasLoadedInitialRef = useRef(false);
|
||||
const isAtBottomRef = useRef(true); //
|
||||
const isAtBottomRef = useRef(true);
|
||||
// const [ref, inView] = useInView({
|
||||
// threshold: 0.7
|
||||
// })
|
||||
|
||||
// useEffect(() => {
|
||||
// if (inView) {
|
||||
|
||||
// }
|
||||
// }, [inView])
|
||||
// Update message list with unique signatures and tempMessages
|
||||
useEffect(() => {
|
||||
let uniqueInitialMessagesMap = new Map();
|
||||
|
||||
// Only add a message if it doesn't already exist in the Map
|
||||
initialMessages.forEach((message) => {
|
||||
uniqueInitialMessagesMap.set(message.signature, message);
|
||||
if (!uniqueInitialMessagesMap.has(message.signature)) {
|
||||
uniqueInitialMessagesMap.set(message.signature, message);
|
||||
}
|
||||
});
|
||||
|
||||
const uniqueInitialMessages = Array.from(uniqueInitialMessagesMap.values()).sort(
|
||||
@ -29,22 +41,14 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
|
||||
setTimeout(() => {
|
||||
const hasUnreadMessages = totalMessages.some((msg) => msg.unread && !msg?.chatReference);
|
||||
|
||||
if (virtuosoRef.current) {
|
||||
|
||||
|
||||
if (virtuosoRef.current && !isAtBottomRef.current && hasUnreadMessages) {
|
||||
|
||||
|
||||
|
||||
|
||||
setShowScrollButton(hasUnreadMessages);
|
||||
if (parentRef.current) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = parentRef.current;
|
||||
const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed
|
||||
if (!atBottom && hasUnreadMessages) {
|
||||
setShowScrollButton(hasUnreadMessages);
|
||||
} else {
|
||||
handleMessageSeen();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if (!hasLoadedInitialRef.current) {
|
||||
scrollToBottom(totalMessages);
|
||||
@ -53,7 +57,14 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
}, 500);
|
||||
}, [initialMessages, tempMessages]);
|
||||
|
||||
|
||||
const scrollToBottom = (initialMsgs) => {
|
||||
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1;
|
||||
if (rowVirtualizer) {
|
||||
rowVirtualizer.scrollToIndex(index, { align: 'end' });
|
||||
}
|
||||
handleMessageSeen()
|
||||
};
|
||||
|
||||
|
||||
const handleMessageSeen = useCallback(() => {
|
||||
setMessages((prevMessages) =>
|
||||
@ -62,34 +73,16 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
unread: false,
|
||||
}))
|
||||
);
|
||||
setShowScrollButton(false)
|
||||
}, []);
|
||||
|
||||
const scrollToItem = useCallback((index) => {
|
||||
if (virtuosoRef.current) {
|
||||
virtuosoRef.current.scrollToIndex({ index, behavior: 'smooth' });
|
||||
}
|
||||
}, []);
|
||||
// const scrollToBottom = (initialMsgs) => {
|
||||
// const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1;
|
||||
// if (parentRef.current) {
|
||||
// parentRef.current.scrollToIndex(index);
|
||||
// }
|
||||
// };
|
||||
|
||||
const scrollToBottom = (initialMsgs) => {
|
||||
|
||||
const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1
|
||||
if (virtuosoRef.current) {
|
||||
virtuosoRef.current.scrollToIndex({ index});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleScroll = (scrollState) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollState;
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50;
|
||||
const hasUnreadMessages = messages.some((msg) => msg.unread);
|
||||
|
||||
if (isAtBottom) {
|
||||
handleMessageSeen();
|
||||
}
|
||||
|
||||
setShowScrollButton(!isAtBottom && hasUnreadMessages);
|
||||
};
|
||||
|
||||
const sentNewMessageGroupFunc = useCallback(() => {
|
||||
scrollToBottom();
|
||||
@ -102,96 +95,134 @@ export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onR
|
||||
};
|
||||
}, [sentNewMessageGroupFunc]);
|
||||
|
||||
const rowRenderer = (index) => {
|
||||
let message = messages[index];
|
||||
let replyIndex = messages.findIndex((msg)=> msg?.signature === message?.repliedTo)
|
||||
let reply
|
||||
let reactions = null
|
||||
if(message?.repliedTo && replyIndex !== -1){
|
||||
reply = messages[replyIndex]
|
||||
}
|
||||
if(message?.message && message?.groupDirectId){
|
||||
replyIndex = messages.findIndex((msg)=> msg?.signature === message?.message?.repliedTo)
|
||||
reply
|
||||
if(message?.message?.repliedTo && replyIndex !== -1){
|
||||
reply = messages[replyIndex]
|
||||
}
|
||||
message = {
|
||||
...(message?.message || {}),
|
||||
isTemp: true,
|
||||
unread: false
|
||||
const lastSignature = useMemo(()=> {
|
||||
if(!messages || messages?.length === 0) return null
|
||||
const lastIndex = messages.length - 1
|
||||
return messages[lastIndex]?.signature
|
||||
}, [messages])
|
||||
|
||||
|
||||
// Initialize the virtualizer
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: messages.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed
|
||||
overscan: 10, // Number of items to render outside the visible area to improve smoothness
|
||||
measureElement:
|
||||
typeof window !== 'undefined' &&
|
||||
navigator.userAgent.indexOf('Firefox') === -1
|
||||
? element => {
|
||||
return element?.getBoundingClientRect().height
|
||||
}
|
||||
}
|
||||
|
||||
if(chatReferences && chatReferences[message?.signature]){
|
||||
if(chatReferences[message.signature]?.reactions){
|
||||
reactions = chatReferences[message.signature]?.reactions
|
||||
}
|
||||
}
|
||||
let isUpdating = false
|
||||
if(tempChatReferences && tempChatReferences?.find((item)=> item?.chatReference === message?.signature)){
|
||||
isUpdating = true
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '10px 0', display: 'flex', justifyContent: 'center', width: '100%', minHeight: '50px' , overscrollBehavior: "none"}}>
|
||||
<MessageItem
|
||||
isLast={index === messages.length - 1}
|
||||
message={message}
|
||||
onSeen={handleMessageSeen}
|
||||
isTemp={!!message?.isTemp}
|
||||
myAddress={myAddress}
|
||||
onReply={onReply}
|
||||
reply={reply}
|
||||
replyIndex={replyIndex}
|
||||
scrollToItem={scrollToItem}
|
||||
handleReaction={handleReaction}
|
||||
reactions={reactions}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleAtBottomStateChange = (atBottom) => {
|
||||
isAtBottomRef.current = atBottom;
|
||||
if(atBottom){
|
||||
handleMessageSeen();
|
||||
setShowScrollButton(false)
|
||||
}
|
||||
};
|
||||
: undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
data={messages}
|
||||
itemContent={rowRenderer}
|
||||
atBottomThreshold={50}
|
||||
followOutput="smooth"
|
||||
atBottomStateChange={handleAtBottomStateChange} // Detect bottom status
|
||||
increaseViewportBy={3000}
|
||||
|
||||
/>
|
||||
<>
|
||||
<div ref={parentRef} style={{ height: '100%', overflow: 'auto', position: 'relative', display: 'flex' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center', // Center items horizontally
|
||||
gap: '10px', // Add gap between items
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const index = virtualRow.index;
|
||||
let message = messages[index];
|
||||
let replyIndex = messages.findIndex((msg) => msg?.signature === message?.repliedTo);
|
||||
let reply;
|
||||
let reactions = null;
|
||||
|
||||
{showScrollButton && (
|
||||
<button
|
||||
onClick={()=> scrollToBottom()}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
backgroundColor: '#ff5a5f',
|
||||
color: 'white',
|
||||
padding: '10px 20px',
|
||||
borderRadius: '20px',
|
||||
cursor: 'pointer',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
Scroll to Unread Messages
|
||||
</button>
|
||||
)}
|
||||
if (message?.repliedTo && replyIndex !== -1) {
|
||||
reply = messages[replyIndex];
|
||||
}
|
||||
|
||||
if (message?.message && message?.groupDirectId) {
|
||||
replyIndex = messages.findIndex((msg) => msg?.signature === message?.message?.repliedTo);
|
||||
if (message?.message?.repliedTo && replyIndex !== -1) {
|
||||
reply = messages[replyIndex];
|
||||
}
|
||||
message = {
|
||||
...(message?.message || {}),
|
||||
isTemp: true,
|
||||
unread: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (chatReferences && chatReferences[message?.signature]) {
|
||||
if (chatReferences[message.signature]?.reactions) {
|
||||
reactions = chatReferences[message.signature]?.reactions;
|
||||
}
|
||||
}
|
||||
|
||||
let isUpdating = false;
|
||||
if (tempChatReferences && tempChatReferences?.find((item) => item?.chatReference === message?.signature)) {
|
||||
isUpdating = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-index={virtualRow.index} //needed for dynamic row height measurement
|
||||
ref={node => rowVirtualizer.measureElement(node)} //measure dynamic row height
|
||||
key={message.signature}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '50%', // Move to the center horizontally
|
||||
transform: `translateY(${virtualRow.start}px) translateX(-50%)`, // Adjust for centering
|
||||
width: '100%', // Control width (90% of the parent)
|
||||
padding: '10px 0',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
overscrollBehavior: 'none',
|
||||
}}
|
||||
>
|
||||
<MessageItem
|
||||
isLast={index === messages.length - 1}
|
||||
lastSignature={lastSignature}
|
||||
message={message}
|
||||
onSeen={handleMessageSeen}
|
||||
isTemp={!!message?.isTemp}
|
||||
myAddress={myAddress}
|
||||
onReply={onReply}
|
||||
reply={reply}
|
||||
replyIndex={replyIndex}
|
||||
scrollToItem={(idx) => rowVirtualizer.scrollToIndex(idx)}
|
||||
handleReaction={handleReaction}
|
||||
reactions={reactions}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{showScrollButton && (
|
||||
<button
|
||||
onClick={() => scrollToBottom()}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
backgroundColor: '#ff5a5f',
|
||||
color: 'white',
|
||||
padding: '10px 20px',
|
||||
borderRadius: '20px',
|
||||
cursor: 'pointer',
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
Scroll to Unread Messages
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
@ -29,7 +29,8 @@ export const MessageItem = ({
|
||||
scrollToItem,
|
||||
handleReaction,
|
||||
reactions,
|
||||
isUpdating
|
||||
isUpdating,
|
||||
lastSignature
|
||||
}) => {
|
||||
const { ref, inView } = useInView({
|
||||
threshold: 0.7, // Fully visible
|
||||
@ -42,9 +43,10 @@ export const MessageItem = ({
|
||||
}
|
||||
}, [inView, message.id, message.unread, onSeen]);
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={isLast ? ref : null}
|
||||
ref={lastSignature === message?.signature ? ref : null}
|
||||
style={{
|
||||
padding: "10px",
|
||||
backgroundColor: "#232428",
|
||||
|
144
src/components/ContextMenuPinnedApps.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material';
|
||||
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||
import { saveToLocalStorage } from './Apps/AppsNavBar';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { sortablePinnedAppsAtom } from '../atoms/global';
|
||||
|
||||
const CustomStyledMenu = styled(Menu)(({ theme }) => ({
|
||||
'& .MuiPaper-root': {
|
||||
backgroundColor: '#f9f9f9',
|
||||
borderRadius: '12px',
|
||||
padding: theme.spacing(1),
|
||||
boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
|
||||
},
|
||||
'& .MuiMenuItem-root': {
|
||||
fontSize: '14px',
|
||||
color: '#444',
|
||||
transition: '0.3s background-color',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f0f0f0',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const ContextMenuPinnedApps = ({ children, app, isMine }) => {
|
||||
const [menuPosition, setMenuPosition] = useState(null);
|
||||
const longPressTimeout = useRef(null);
|
||||
const maxHoldTimeout = useRef(null);
|
||||
const preventClick = useRef(false);
|
||||
const startTouchPosition = useRef({ x: 0, y: 0 }); // Track initial touch position
|
||||
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
||||
|
||||
const handleContextMenu = (event) => {
|
||||
if(isMine) return
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
preventClick.current = true;
|
||||
setMenuPosition({
|
||||
mouseX: event.clientX,
|
||||
mouseY: event.clientY,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTouchStart = (event) => {
|
||||
if(isMine) return
|
||||
|
||||
const { clientX, clientY } = event.touches[0];
|
||||
startTouchPosition.current = { x: clientX, y: clientY };
|
||||
|
||||
longPressTimeout.current = setTimeout(() => {
|
||||
preventClick.current = true;
|
||||
|
||||
event.stopPropagation();
|
||||
setMenuPosition({
|
||||
mouseX: clientX,
|
||||
mouseY: clientY,
|
||||
});
|
||||
}, 500);
|
||||
|
||||
// Set a maximum hold duration (e.g., 1.5 seconds)
|
||||
maxHoldTimeout.current = setTimeout(() => {
|
||||
clearTimeout(longPressTimeout.current);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleTouchMove = (event) => {
|
||||
if(isMine) return
|
||||
|
||||
const { clientX, clientY } = event.touches[0];
|
||||
const { x, y } = startTouchPosition.current;
|
||||
|
||||
// Determine if the touch has moved beyond a small threshold (e.g., 10px)
|
||||
const movedEnough = Math.abs(clientX - x) > 10 || Math.abs(clientY - y) > 10;
|
||||
|
||||
if (movedEnough) {
|
||||
clearTimeout(longPressTimeout.current);
|
||||
clearTimeout(maxHoldTimeout.current);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = (event) => {
|
||||
if(isMine) return
|
||||
|
||||
clearTimeout(longPressTimeout.current);
|
||||
clearTimeout(maxHoldTimeout.current);
|
||||
if (preventClick.current) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
preventClick.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (e) => {
|
||||
if(isMine) return
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setMenuPosition(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onContextMenu={handleContextMenu}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
style={{ touchAction: 'none' }}
|
||||
>
|
||||
{children}
|
||||
<CustomStyledMenu
|
||||
disableAutoFocusItem
|
||||
open={!!menuPosition}
|
||||
onClose={handleClose}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={
|
||||
menuPosition
|
||||
? { top: menuPosition.mouseY, left: menuPosition.mouseX }
|
||||
: undefined
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={(e) => {
|
||||
handleClose(e);
|
||||
setSortablePinnedApps((prev) => {
|
||||
const updatedApps = prev.filter(
|
||||
(item) => !(item?.name === app?.name && item?.service === app?.service)
|
||||
);
|
||||
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps);
|
||||
return updatedApps;
|
||||
});
|
||||
}}>
|
||||
<ListItemIcon sx={{ minWidth: '32px' }}>
|
||||
<PushPinIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<Typography variant="inherit" sx={{ fontSize: '14px' }}>
|
||||
Unpin app
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</CustomStyledMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -13,21 +13,24 @@ import { WalletIcon } from "../../assets/Icons/WalletIcon";
|
||||
import { HubsIcon } from "../../assets/Icons/HubsIcon";
|
||||
import { TradingIcon } from "../../assets/Icons/TradingIcon";
|
||||
import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
|
||||
import { HomeIcon } from "../../assets/Icons/HomeIcon";
|
||||
import AppIcon from "../../assets/svgs/AppIcon.svg";
|
||||
|
||||
const IconWrapper = ({ children, label, color, selected }) => {
|
||||
import { HomeIcon } from "../../assets/Icons/HomeIcon";
|
||||
import { Save } from "../Save/Save";
|
||||
|
||||
export const IconWrapper = ({ children, label, color, selected }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
gap: "5px",
|
||||
flexDirection: "column",
|
||||
height: '89px',
|
||||
width: '89px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: selected ? 'rgba(28, 29, 32, 1)' : 'transparent'
|
||||
height: "89px",
|
||||
width: "89px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: selected ? "rgba(28, 29, 32, 1)" : "transparent",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@ -69,9 +72,17 @@ export const DesktopFooter = ({
|
||||
isHome,
|
||||
isGroups,
|
||||
isDirects,
|
||||
setDesktopSideView
|
||||
setDesktopSideView,
|
||||
isApps,
|
||||
setDesktopViewMode,
|
||||
desktopViewMode,
|
||||
hide,
|
||||
setIsOpenSideViewDirects,
|
||||
setIsOpenSideViewGroups
|
||||
|
||||
}) => {
|
||||
const [value, setValue] = React.useState(0);
|
||||
|
||||
if(hide) return
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -82,37 +93,93 @@ export const DesktopFooter = ({
|
||||
alignItems: "center",
|
||||
height: "100px", // Footer height
|
||||
zIndex: 1,
|
||||
justifyContent: 'center'
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
gap: '20px'
|
||||
}}>
|
||||
<ButtonBase onClick={()=> {
|
||||
goToHome()
|
||||
}}>
|
||||
<IconWrapper color="rgba(250, 250, 250, 0.5)" label="Home" selected={isHome}>
|
||||
<HomeIcon height={30} color={isHome ? "white" : "rgba(250, 250, 250, 0.5)"} />
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<ButtonBase onClick={()=> {
|
||||
setDesktopSideView('groups')
|
||||
}}>
|
||||
<IconWrapper color="rgba(250, 250, 250, 0.5)" label="Hubs" selected={isGroups}>
|
||||
<HubsIcon height={30} color={hasUnreadGroups ? "var(--unread)" : isGroups ? 'white' : "rgba(250, 250, 250, 0.5)"} />
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<ButtonBase onClick={()=> {
|
||||
setDesktopSideView('directs')
|
||||
}}>
|
||||
|
||||
<IconWrapper color="rgba(250, 250, 250, 0.5)" label="Messaging" selected={isDirects}>
|
||||
<MessagingIcon height={30} color={hasUnreadDirects ? "var(--unread)" : isDirects ? 'white' : "rgba(250, 250, 250, 0.5)"} />
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
goToHome();
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
label="Home"
|
||||
selected={isHome}
|
||||
>
|
||||
<HomeIcon
|
||||
height={30}
|
||||
color={isHome ? "white" : "rgba(250, 250, 250, 0.5)"}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopViewMode('apps')
|
||||
setIsOpenSideViewDirects(false)
|
||||
setIsOpenSideViewGroups(false)
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
label="Apps"
|
||||
selected={isApps}
|
||||
>
|
||||
<img src={AppIcon} />
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView("groups");
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
label="Hubs"
|
||||
selected={isGroups}
|
||||
>
|
||||
<HubsIcon
|
||||
height={30}
|
||||
color={
|
||||
hasUnreadGroups
|
||||
? "var(--unread)"
|
||||
: isGroups
|
||||
? "white"
|
||||
: "rgba(250, 250, 250, 0.5)"
|
||||
}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setDesktopSideView("directs");
|
||||
}}
|
||||
>
|
||||
<IconWrapper
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
label="Messaging"
|
||||
selected={isDirects}
|
||||
>
|
||||
<MessagingIcon
|
||||
height={30}
|
||||
color={
|
||||
hasUnreadDirects
|
||||
? "var(--unread)"
|
||||
: isDirects
|
||||
? "white"
|
||||
: "rgba(250, 250, 250, 0.5)"
|
||||
}
|
||||
/>
|
||||
</IconWrapper>
|
||||
</ButtonBase>
|
||||
|
||||
<Save isDesktop />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import ListItemText from '@mui/material/ListItemText';
|
||||
import InboxIcon from '@mui/icons-material/MoveToInbox';
|
||||
import MailIcon from '@mui/icons-material/Mail';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { isMobile } from '../../App';
|
||||
export const DrawerComponent = ({open, setOpen, children}) => {
|
||||
|
||||
const toggleDrawer = (newOpen: boolean) => () => {
|
||||
@ -21,7 +22,7 @@ export const DrawerComponent = ({open, setOpen, children}) => {
|
||||
return (
|
||||
<div>
|
||||
<Drawer open={open} onClose={toggleDrawer(false)}>
|
||||
<Box sx={{ width: 400, height: '100%' }} role="presentation">
|
||||
<Box sx={{ width: isMobile ? '100vw' : '400px', height: '100%' }} role="presentation">
|
||||
|
||||
{children}
|
||||
</Box>
|
||||
|
@ -536,10 +536,14 @@ export const GroupMail = ({
|
||||
});
|
||||
|
||||
// Convert the map back to an array and sort by "created" timestamp in descending order
|
||||
const sortedList = Array.from(uniqueItems.values()).sort((a, b) => b.threadData?.createdAt - a.threadData?.createdAt);
|
||||
const sortedList = Array.from(uniqueItems.values()).sort((a, b) =>
|
||||
filterMode === 'Oldest'
|
||||
? a.threadData?.createdAt - b.threadData?.createdAt
|
||||
: b.threadData?.createdAt - a.threadData?.createdAt
|
||||
);
|
||||
|
||||
return sortedList;
|
||||
}, [tempPublishedList, listOfThreadsToDisplay]);
|
||||
}, [tempPublishedList, listOfThreadsToDisplay, filterMode]);
|
||||
|
||||
if (currentThread)
|
||||
return (
|
||||
|
@ -754,29 +754,7 @@ export const GroupContainer = styled(Box)`
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-track:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
height: 10px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #838eee;
|
||||
border-radius: 8px;
|
||||
background-clip: content-box;
|
||||
border: 4px solid transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #6270f0;
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
|
@ -88,6 +88,9 @@ import { ExitIcon } from "../../assets/Icons/ExitIcon";
|
||||
import { HomeDesktop } from "./HomeDesktop";
|
||||
import { DesktopFooter } from "../Desktop/DesktopFooter";
|
||||
import { DesktopHeader } from "../Desktop/DesktopHeader";
|
||||
import { Apps } from "../Apps/Apps";
|
||||
import { AppsNavBar } from "../Apps/AppsNavBar";
|
||||
import { AppsDesktop } from "../Apps/AppsDesktop";
|
||||
|
||||
// let touchStartY = 0;
|
||||
// let disablePullToRefresh = false;
|
||||
@ -373,7 +376,11 @@ export const Group = ({
|
||||
isOpenDrawerProfile,
|
||||
setIsOpenDrawerProfile,
|
||||
logoutFunc,
|
||||
setDesktopViewMode,
|
||||
desktopViewMode
|
||||
}: GroupProps) => {
|
||||
const [desktopSideView, setDesktopSideView] = useState('groups')
|
||||
|
||||
const [secretKey, setSecretKey] = useState(null);
|
||||
const [secretKeyPublishDate, setSecretKeyPublishDate] = useState(null);
|
||||
const lastFetchedSecretKey = useRef(null);
|
||||
@ -418,7 +425,6 @@ export const Group = ({
|
||||
const [mutedGroups, setMutedGroups] = useState([]);
|
||||
const [mobileViewMode, setMobileViewMode] = useState("home");
|
||||
const [mobileViewModeKeepOpen, setMobileViewModeKeepOpen] = useState("");
|
||||
const [desktopSideView, setDesktopSideView] = useState('groups')
|
||||
const isFocusedRef = useRef(true);
|
||||
const timestampEnterDataRef = useRef({});
|
||||
const selectedGroupRef = useRef(null);
|
||||
@ -431,7 +437,22 @@ export const Group = ({
|
||||
const { clearStatesMessageQueueProvider } = useMessageQueue();
|
||||
const initiatedGetMembers = useRef(false);
|
||||
const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({});
|
||||
const [appsMode, setAppsMode] = useState('home')
|
||||
const [isOpenSideViewDirects, setIsOpenSideViewDirects] = useState(false)
|
||||
const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false)
|
||||
|
||||
const toggleSideViewDirects = ()=> {
|
||||
if(isOpenSideViewGroups){
|
||||
setIsOpenSideViewGroups(false)
|
||||
}
|
||||
setIsOpenSideViewDirects((prev)=> !prev)
|
||||
}
|
||||
const toggleSideViewGroups = ()=> {
|
||||
if(isOpenSideViewDirects){
|
||||
setIsOpenSideViewDirects(false)
|
||||
}
|
||||
setIsOpenSideViewGroups((prev)=> !prev)
|
||||
}
|
||||
useEffect(()=> {
|
||||
timestampEnterDataRef.current = timestampEnterData
|
||||
}, [timestampEnterData])
|
||||
@ -821,98 +842,7 @@ export const Group = ({
|
||||
}
|
||||
}, [selectedGroup]);
|
||||
|
||||
// const handleNotification = async (data)=> {
|
||||
// try {
|
||||
// if(isFocusedRef.current){
|
||||
// throw new Error('isFocused')
|
||||
// }
|
||||
// const newActiveChats= data
|
||||
// const oldActiveChats = await new Promise((res, rej) => {
|
||||
// chrome?.runtime?.sendMessage(
|
||||
// {
|
||||
// action: "getChatHeads",
|
||||
// },
|
||||
// (response) => {
|
||||
// console.log({ response });
|
||||
// if (!response?.error) {
|
||||
// res(response);
|
||||
// }
|
||||
// rej(response.error);
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
|
||||
// let results = []
|
||||
// newActiveChats?.groups?.forEach(newChat => {
|
||||
// let isNewer = true;
|
||||
// oldActiveChats?.data?.groups?.forEach(oldChat => {
|
||||
// if (newChat?.timestamp <= oldChat?.timestamp) {
|
||||
// isNewer = false;
|
||||
// }
|
||||
// });
|
||||
// if (isNewer) {
|
||||
// results.push(newChat)
|
||||
// console.log('This newChat is newer than all oldChats:', newChat);
|
||||
// }
|
||||
// });
|
||||
|
||||
// if(results?.length > 0){
|
||||
// if (!lastGroupNotification.current || (Date.now() - lastGroupNotification.current >= 60000)) {
|
||||
// console.log((Date.now() - lastGroupNotification.current >= 60000), lastGroupNotification.current)
|
||||
// chrome?.runtime?.sendMessage(
|
||||
// {
|
||||
// action: "notification",
|
||||
// payload: {
|
||||
// },
|
||||
// },
|
||||
// (response) => {
|
||||
// console.log({ response });
|
||||
// if (!response?.error) {
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
// );
|
||||
// audio.play();
|
||||
// lastGroupNotification.current = Date.now()
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
// } catch (error) {
|
||||
// console.log('error not', error)
|
||||
// if(!isFocusedRef.current){
|
||||
// chrome?.runtime?.sendMessage(
|
||||
// {
|
||||
// action: "notification",
|
||||
// payload: {
|
||||
// },
|
||||
// },
|
||||
// (response) => {
|
||||
// console.log({ response });
|
||||
// if (!response?.error) {
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
// );
|
||||
// audio.play();
|
||||
// lastGroupNotification.current = Date.now()
|
||||
// }
|
||||
|
||||
// } finally {
|
||||
|
||||
// chrome?.runtime?.sendMessage(
|
||||
// {
|
||||
// action: "setChatHeads",
|
||||
// payload: {
|
||||
// data,
|
||||
// },
|
||||
// }
|
||||
// );
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
const getAdmins = async (groupId) => {
|
||||
try {
|
||||
@ -1176,6 +1106,7 @@ export const Group = ({
|
||||
if (findDirect) {
|
||||
if(!isMobile){
|
||||
setDesktopSideView("directs");
|
||||
setDesktopViewMode('home')
|
||||
} else {
|
||||
setMobileViewModeKeepOpen("messaging");
|
||||
}
|
||||
@ -1213,6 +1144,7 @@ export const Group = ({
|
||||
if (findDirect) {
|
||||
if(!isMobile){
|
||||
setDesktopSideView("directs");
|
||||
setDesktopViewMode('home')
|
||||
} else {
|
||||
setMobileViewModeKeepOpen("messaging");
|
||||
}
|
||||
@ -1236,6 +1168,7 @@ export const Group = ({
|
||||
} else {
|
||||
if(!isMobile){
|
||||
setDesktopSideView("directs");
|
||||
setDesktopViewMode('home')
|
||||
} else {
|
||||
setMobileViewModeKeepOpen("messaging");
|
||||
}
|
||||
@ -1402,6 +1335,8 @@ export const Group = ({
|
||||
setTimeout(() => {
|
||||
setSelectedGroup(findGroup);
|
||||
setMobileViewMode("group");
|
||||
setDesktopSideView('groups')
|
||||
setDesktopViewMode('home')
|
||||
getTimestampEnterChat();
|
||||
isLoadingOpenSectionFromNotification.current = false;
|
||||
}, 200);
|
||||
@ -1449,7 +1384,8 @@ export const Group = ({
|
||||
setTimeout(() => {
|
||||
setSelectedGroup(findGroup);
|
||||
setMobileViewMode("group");
|
||||
|
||||
setDesktopSideView('groups')
|
||||
setDesktopViewMode('home')
|
||||
getGroupAnnouncements();
|
||||
}, 200);
|
||||
}
|
||||
@ -1504,6 +1440,8 @@ export const Group = ({
|
||||
setTimeout(() => {
|
||||
setSelectedGroup(findGroup);
|
||||
setMobileViewMode("group");
|
||||
setDesktopSideView('groups')
|
||||
setDesktopViewMode('home')
|
||||
getGroupAnnouncements();
|
||||
}, 200);
|
||||
}
|
||||
@ -1527,6 +1465,8 @@ export const Group = ({
|
||||
}
|
||||
if (!isMobile) {
|
||||
}
|
||||
setDesktopViewMode('home')
|
||||
|
||||
setGroupSection("default");
|
||||
clearAllQueues();
|
||||
await new Promise((res) => {
|
||||
@ -1550,6 +1490,8 @@ export const Group = ({
|
||||
setMemberCountFromSecretKeyData(null);
|
||||
setTriedToFetchSecretKey(false);
|
||||
setFirstSecretKeyInCreation(false);
|
||||
setIsOpenSideViewDirects(false)
|
||||
setIsOpenSideViewGroups(false)
|
||||
};
|
||||
|
||||
const goToAnnouncements = async () => {
|
||||
@ -2025,6 +1967,8 @@ export const Group = ({
|
||||
// }
|
||||
onClick={() => {
|
||||
setMobileViewMode("group");
|
||||
setDesktopSideView('groups')
|
||||
setDesktopViewMode('home')
|
||||
initiatedGetMembers.current = false;
|
||||
clearAllQueues();
|
||||
setSelectedDirect(null);
|
||||
@ -2228,7 +2172,7 @@ export const Group = ({
|
||||
isThin={
|
||||
mobileViewMode === "groups" ||
|
||||
mobileViewMode === "group" ||
|
||||
mobileViewModeKeepOpen === "messaging"
|
||||
mobileViewModeKeepOpen === "messaging" || (mobileViewMode === "apps" && appsMode !== 'home')
|
||||
}
|
||||
logoutFunc={logoutFunc}
|
||||
goToHome={goToHome}
|
||||
@ -2253,8 +2197,8 @@ export const Group = ({
|
||||
alignItems: "flex-start",
|
||||
}}
|
||||
>
|
||||
{!isMobile && desktopSideView === 'groups' && renderGroups()}
|
||||
{!isMobile && desktopSideView === 'directs' && renderDirects()}
|
||||
{!isMobile && ((desktopSideView === 'groups' && desktopViewMode !== 'apps') || isOpenSideViewGroups) && renderGroups()}
|
||||
{!isMobile && ((desktopSideView === 'directs' && desktopViewMode !== 'apps') || isOpenSideViewDirects) && renderDirects()}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
@ -2711,10 +2655,16 @@ export const Group = ({
|
||||
groupsAnnHasUnread}
|
||||
hasUnreadDirects={directChatHasUnread}
|
||||
myName={userInfo?.name || null}
|
||||
isHome={groupSection === "home"}
|
||||
isGroups={desktopSideView === 'groups'}
|
||||
isDirects={desktopSideView === 'directs'}
|
||||
isHome={groupSection === "home" && desktopViewMode === 'home'}
|
||||
isGroups={desktopSideView === 'groups' && desktopViewMode !== 'apps'}
|
||||
isDirects={desktopSideView === 'directs' && desktopViewMode !== 'apps'}
|
||||
setDesktopViewMode={setDesktopViewMode}
|
||||
isApps={desktopViewMode === 'apps'}
|
||||
setDesktopSideView={setDesktopSideView}
|
||||
desktopViewMode={desktopViewMode}
|
||||
hide={desktopViewMode === 'apps'}
|
||||
setIsOpenSideViewDirects={setIsOpenSideViewDirects}
|
||||
setIsOpenSideViewGroups={setIsOpenSideViewGroups}
|
||||
/>
|
||||
)}
|
||||
{isMobile && mobileViewMode === "home" && (
|
||||
@ -2733,11 +2683,19 @@ export const Group = ({
|
||||
setMobileViewMode={setMobileViewMode}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
!isMobile && !selectedGroup &&
|
||||
groupSection === "home" && (
|
||||
|
||||
<HomeDesktop
|
||||
{isMobile && (
|
||||
<Apps mode={appsMode} setMode={setAppsMode} show={mobileViewMode === "apps"} myName={userInfo?.name} />
|
||||
)}
|
||||
{!isMobile && (
|
||||
<AppsDesktop toggleSideViewGroups={toggleSideViewGroups} toggleSideViewDirects={toggleSideViewDirects} goToHome={goToHome} mode={appsMode} setMode={setAppsMode} setDesktopSideView={setDesktopSideView} hasUnreadDirects={directChatHasUnread} show={desktopViewMode === "apps"} myName={userInfo?.name} isGroups={isOpenSideViewGroups}
|
||||
isDirects={isOpenSideViewDirects} hasUnreadGroups={groupChatHasUnread ||
|
||||
groupsAnnHasUnread} />
|
||||
)}
|
||||
|
||||
|
||||
{!isMobile && !selectedGroup &&
|
||||
groupSection === "home" && desktopViewMode !== "apps" && (
|
||||
<HomeDesktop
|
||||
refreshHomeDataFunc={refreshHomeDataFunc}
|
||||
myAddress={myAddress}
|
||||
isLoadingGroups={isLoadingGroups}
|
||||
@ -2751,7 +2709,9 @@ export const Group = ({
|
||||
setOpenAddGroup={setOpenAddGroup}
|
||||
setMobileViewMode={setMobileViewMode}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
|
||||
|
||||
</Box>
|
||||
<AuthenticatedContainerInnerRight
|
||||
sx={{
|
||||
@ -2759,188 +2719,10 @@ export const Group = ({
|
||||
width: "31px",
|
||||
// minWidth: "135px",
|
||||
padding: "5px",
|
||||
display: isMobile ? "none" : "flex",
|
||||
display: (isMobile || desktopViewMode === 'apps') ? "none" : "flex",
|
||||
}}
|
||||
>
|
||||
{/* <Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "3px",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={goToHome}
|
||||
>
|
||||
<HomeIcon
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: groupSection === "home" ? "#1444c7" : "white",
|
||||
opacity: groupSection === "home" ? 1 : 0.4,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: groupSection === "home" ? "#1444c7" : "white",
|
||||
opacity: groupSection === "home" ? 1 : 0.4,
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</Typography>
|
||||
</Box>
|
||||
{selectedGroup && (
|
||||
<>
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "3px",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={goToAnnouncements}
|
||||
>
|
||||
<CampaignIcon
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: isUnread
|
||||
? "red"
|
||||
: groupSection === "announcement"
|
||||
? "#1444c7"
|
||||
: "white",
|
||||
opacity: groupSection === "announcement" ? 1 : 0.4,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: isUnread
|
||||
? "red"
|
||||
: groupSection === "announcement"
|
||||
? "#1444c7"
|
||||
: "white",
|
||||
opacity: groupSection === "announcement" ? 1 : 0.4,
|
||||
}}
|
||||
>
|
||||
Announcements
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "3px",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={goToChat}
|
||||
>
|
||||
<ChatIcon
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: isUnreadChat
|
||||
? "red"
|
||||
: groupSection === "chat"
|
||||
? "#1444c7"
|
||||
: "white",
|
||||
opacity: groupSection === "chat" ? 1 : 0.4,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: isUnreadChat
|
||||
? "red"
|
||||
: groupSection === "chat"
|
||||
? "#1444c7"
|
||||
: "white",
|
||||
opacity: groupSection === "chat" ? 1 : 0.4,
|
||||
}}
|
||||
>
|
||||
Chat
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "3px",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
setGroupSection("forum");
|
||||
setSelectedDirect(null);
|
||||
setNewChat(false);
|
||||
}}
|
||||
>
|
||||
<ForumIcon
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: groupSection === "forum" ? "#1444c7" : "white",
|
||||
opacity: groupSection === "forum" ? 1 : 0.4,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: groupSection === "forum" ? "#1444c7" : "white",
|
||||
opacity: groupSection === "forum" ? 1 : 0.4,
|
||||
}}
|
||||
>
|
||||
Forum
|
||||
</Typography>
|
||||
</Box>
|
||||
<Spacer height="20px" />
|
||||
<Box
|
||||
onClick={() => setOpenManageMembers(true)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "3px",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<PeopleIcon
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: "white",
|
||||
opacity: 0.4,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
color: "white",
|
||||
opacity: 0.4,
|
||||
}}
|
||||
>
|
||||
Members
|
||||
</Typography>
|
||||
</Box>
|
||||
<Spacer height="20px" />
|
||||
</>
|
||||
)} */}
|
||||
|
||||
{/* <SettingsIcon
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
color: "white",
|
||||
}}
|
||||
/> */}
|
||||
|
||||
</AuthenticatedContainerInnerRight>
|
||||
<LoadingSnackbar
|
||||
open={isLoadingGroup}
|
||||
@ -2958,7 +2740,7 @@ export const Group = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isMobile && mobileViewMode === "home" && !mobileViewModeKeepOpen && (
|
||||
{(isMobile && mobileViewMode === "home" || (isMobile && mobileViewMode === "apps" && appsMode === 'home')) && !mobileViewModeKeepOpen && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
@ -3000,240 +2782,13 @@ export const Group = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{(isMobile && mobileViewMode === "apps" && appsMode !== 'home') && !mobileViewModeKeepOpen && (
|
||||
<>
|
||||
<AppsNavBar />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// {isMobile && (
|
||||
// <Box
|
||||
// sx={{
|
||||
// display: "flex",
|
||||
// alignItems: "center",
|
||||
// justifyContent: "center",
|
||||
// flexDirection: "column",
|
||||
// width: "100%",
|
||||
// height: "75px", // Keep the height at 75px
|
||||
// background: "rgba(0, 0, 0, 0.1)",
|
||||
// padding: "0px", // Remove unnecessary padding
|
||||
// }}
|
||||
// >
|
||||
// <Grid
|
||||
// container
|
||||
// spacing={0.5}
|
||||
// sx={{ width: "100%", justifyContent: "space-around" }}
|
||||
// >
|
||||
// {selectedGroup && (
|
||||
// <>
|
||||
// <Grid item xs={4} sx={{
|
||||
// display: 'flex'
|
||||
// }}>
|
||||
// <Button
|
||||
// fullWidth
|
||||
// size="small"
|
||||
// variant="contained"
|
||||
// startIcon={<AnnouncementsIcon />}
|
||||
// sx={{
|
||||
// padding: "4px 6px",
|
||||
// color:
|
||||
// groupSection === "announcement" ? "black" : "white",
|
||||
// backgroundColor: isUnread
|
||||
// ? "red"
|
||||
// : groupSection === "announcement"
|
||||
// ? "white"
|
||||
// : "black",
|
||||
// "&:hover": {
|
||||
// backgroundColor: isUnread
|
||||
// ? "red"
|
||||
// : groupSection === "announcement"
|
||||
// ? "white"
|
||||
// : "black",
|
||||
// },
|
||||
// "&:active": {
|
||||
// backgroundColor: isUnread
|
||||
// ? "red"
|
||||
// : groupSection === "announcement"
|
||||
// ? "white"
|
||||
// : "black",
|
||||
// },
|
||||
// "&:focus": {
|
||||
// backgroundColor: isUnread
|
||||
// ? "red"
|
||||
// : groupSection === "announcement"
|
||||
// ? "white"
|
||||
// : "black",
|
||||
// },
|
||||
// }}
|
||||
// onClick={goToAnnouncements}
|
||||
// >
|
||||
// ANN
|
||||
// </Button>
|
||||
// </Grid>
|
||||
// <Grid item xs={4} sx={{
|
||||
// display: 'flex'
|
||||
// }}>
|
||||
// <Button
|
||||
// fullWidth
|
||||
// size="small"
|
||||
// variant="contained"
|
||||
// startIcon={<ChatIcon />}
|
||||
// sx={{
|
||||
// padding: "4px 6px",
|
||||
// color: groupSection === "chat" ? "black" : "white",
|
||||
// backgroundColor: isUnreadChat
|
||||
// ? "red"
|
||||
// : groupSection === "chat"
|
||||
// ? "white"
|
||||
// : "black",
|
||||
// "&:hover": {
|
||||
// backgroundColor: isUnreadChat
|
||||
// ? "red"
|
||||
// : groupSection === "chat"
|
||||
// ? "white"
|
||||
// : "black", // Same logic for hover
|
||||
// },
|
||||
// "&:active": {
|
||||
// backgroundColor: isUnreadChat
|
||||
// ? "red"
|
||||
// : groupSection === "chat"
|
||||
// ? "white"
|
||||
// : "black", // Same logic for active
|
||||
// },
|
||||
// "&:focus": {
|
||||
// backgroundColor: isUnreadChat
|
||||
// ? "red"
|
||||
// : groupSection === "chat"
|
||||
// ? "white"
|
||||
// : "black", // Same logic for focus
|
||||
// },
|
||||
// }}
|
||||
// onClick={goToChat}
|
||||
// >
|
||||
// Chat
|
||||
// </Button>
|
||||
// </Grid>
|
||||
// <Grid item xs={4} sx={{
|
||||
// display: 'flex'
|
||||
// }}>
|
||||
// <Button
|
||||
// fullWidth
|
||||
// size="small"
|
||||
// variant="contained"
|
||||
// startIcon={<ForumIcon />}
|
||||
// sx={{
|
||||
// padding: "4px 6px",
|
||||
// color: groupSection === "forum" ? "black" : "white",
|
||||
// backgroundColor:
|
||||
// groupSection === "forum" ? "white" : "black",
|
||||
// "&:hover": {
|
||||
// backgroundColor: groupSection === "forum" ? "white" : "black", // Hover state
|
||||
// },
|
||||
// "&:active": {
|
||||
// backgroundColor: groupSection === "forum" ? "white" : "black", // Active state
|
||||
// },
|
||||
// "&:focus": {
|
||||
// backgroundColor: groupSection === "forum" ? "white" : "black", // Focus state
|
||||
// },
|
||||
// }}
|
||||
// onClick={() => {
|
||||
// setSelectedDirect(null);
|
||||
// setNewChat(false)
|
||||
// setGroupSection("forum")
|
||||
// } }
|
||||
// >
|
||||
// Forum
|
||||
// </Button>
|
||||
// </Grid>
|
||||
// <Grid item xs={4} sx={{
|
||||
// display: 'flex'
|
||||
// }}>
|
||||
// <Button
|
||||
// fullWidth
|
||||
// size="small"
|
||||
// variant="contained"
|
||||
// startIcon={<GroupIcon />}
|
||||
// sx={{ padding: "4px 6px", backgroundColor: "black", "&:hover": {
|
||||
// backgroundColor: "black", // Hover state
|
||||
// },
|
||||
// "&:active": {
|
||||
// backgroundColor: "black", // Active state
|
||||
// },
|
||||
// "&:focus": {
|
||||
// backgroundColor: "black", // Focus state
|
||||
// }, }}
|
||||
// onClick={() => setOpenManageMembers(true)}
|
||||
// >
|
||||
// Members
|
||||
// </Button>
|
||||
// </Grid>
|
||||
// </>
|
||||
// )}
|
||||
|
||||
// {/* Second row: Groups, Home, Profile */}
|
||||
// <Grid item xs={4} sx={{
|
||||
// display: 'flex',
|
||||
// }}>
|
||||
// <Button
|
||||
// fullWidth
|
||||
// size="small"
|
||||
// variant="contained"
|
||||
// startIcon={<GroupIcon />}
|
||||
// sx={{
|
||||
// padding: "2px 4px",
|
||||
// backgroundColor:
|
||||
// groupChatHasUnread ||
|
||||
// groupsAnnHasUnread ||
|
||||
// directChatHasUnread
|
||||
// ? "red"
|
||||
// : "black",
|
||||
// "&:hover": {
|
||||
// backgroundColor:
|
||||
// groupChatHasUnread || groupsAnnHasUnread || directChatHasUnread
|
||||
// ? "red"
|
||||
// : "black", // Hover state follows the same logic
|
||||
// },
|
||||
// "&:active": {
|
||||
// backgroundColor:
|
||||
// groupChatHasUnread || groupsAnnHasUnread || directChatHasUnread
|
||||
// ? "red"
|
||||
// : "black", // Active state follows the same logic
|
||||
// },
|
||||
// "&:focus": {
|
||||
// backgroundColor:
|
||||
// groupChatHasUnread || groupsAnnHasUnread || directChatHasUnread
|
||||
// ? "red"
|
||||
// : "black", // Focus state follows the same logic
|
||||
// },
|
||||
// }}
|
||||
// onClick={() => {
|
||||
// setIsOpenDrawer(true);
|
||||
// setDrawerMode("groups");
|
||||
// }}
|
||||
// >
|
||||
// {chatMode === "groups" ? "Groups" : "Direct"}
|
||||
// </Button>
|
||||
// </Grid>
|
||||
// <Grid item xs={2} sx={{
|
||||
// display: 'flex',
|
||||
// justifyContent: 'center'
|
||||
// }}>
|
||||
// <IconButton
|
||||
// sx={{ padding: "0", color: "white" }} // Reduce padding for icons
|
||||
// onClick={goToHome}
|
||||
// >
|
||||
// <HomeIcon />
|
||||
// </IconButton>
|
||||
// </Grid>
|
||||
// <Grid item xs={2} sx={{
|
||||
// display: 'flex',
|
||||
// justifyContent: 'center'
|
||||
// }}>
|
||||
// <IconButton
|
||||
// sx={{ padding: "0", color: "white" }} // Reduce padding for icons
|
||||
// onClick={() => setIsOpenDrawerProfile(true)}
|
||||
// >
|
||||
// <PersonIcon />
|
||||
// </IconButton>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// </Box>
|
||||
// )}
|
||||
|
@ -48,20 +48,7 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
|
||||
return true
|
||||
})
|
||||
|
||||
// const getJoinGroupRequests = groupsAsAdmin.map(async (group)=> {
|
||||
// console.log('getJoinGroupRequests', group)
|
||||
// const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> {
|
||||
// return fetch(
|
||||
// `${getBaseApiReact()}/groups/joinrequests/${group.groupId}`
|
||||
// );
|
||||
// })
|
||||
|
||||
// const joinRequestData = await joinRequestResponse.json()
|
||||
// return {
|
||||
// group,
|
||||
// data: joinRequestData
|
||||
// }
|
||||
// })
|
||||
|
||||
await Promise.all(getAllGroupsAsAdmin)
|
||||
const res = await Promise.all(groupsAsAdmin.map(async (group)=> {
|
||||
|
||||
|
@ -134,6 +134,7 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers,
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: hasUnreadChat ? "var(--unread)" :"#fff"
|
||||
},
|
||||
}} primary="Chat" />
|
||||
</MenuItem>
|
||||
@ -153,6 +154,7 @@ export const GroupMenu = ({ setGroupSection, groupSection, setOpenManageMembers,
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: hasUnreadAnnouncements ? "var(--unread)" :"#fff"
|
||||
},
|
||||
}} primary="Announcements" />
|
||||
</MenuItem>
|
||||
|
@ -2,11 +2,14 @@ import * as React from "react";
|
||||
import {
|
||||
BottomNavigation,
|
||||
BottomNavigationAction,
|
||||
ButtonBase,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Home, Groups, Message, ShowChart } from "@mui/icons-material";
|
||||
import Box from "@mui/material/Box";
|
||||
import BottomLogo from "../../assets/svgs/BottomLogo5.svg";
|
||||
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
|
||||
|
||||
import { CustomSvg } from "../../common/CustomSvg";
|
||||
import { WalletIcon } from "../../assets/Icons/WalletIcon";
|
||||
import { HubsIcon } from "../../assets/Icons/HubsIcon";
|
||||
@ -132,6 +135,15 @@ export const MobileFooter = ({
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
<ButtonBase onClick={()=> {
|
||||
if(mobileViewMode === 'home'){
|
||||
setMobileViewMode('apps')
|
||||
|
||||
} else {
|
||||
setMobileViewMode('home')
|
||||
|
||||
}
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "49px", // Slightly smaller inner circle
|
||||
@ -144,8 +156,9 @@ export const MobileFooter = ({
|
||||
}}
|
||||
>
|
||||
{/* Custom Center Icon */}
|
||||
<img src={BottomLogo} alt="center-icon" />
|
||||
<img src={mobileViewMode === 'apps' ? LogoSelected : BottomLogo} alt="center-icon" />
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
|
||||
<BottomNavigation
|
||||
|
@ -19,6 +19,11 @@ import { ArrowDownIcon } from "../../assets/Icons/ArrowDownIcon";
|
||||
import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
|
||||
import { MessagingIcon2 } from "../../assets/Icons/MessagingIcon2";
|
||||
import { HubsIcon } from "../../assets/Icons/HubsIcon";
|
||||
import { Save } from "../Save/Save";
|
||||
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
|
||||
import { useRecoilState } from "recoil";
|
||||
import { fullScreenAtom, hasSettingsChangedAtom } from "../../atoms/global";
|
||||
import { useAppFullScreen } from "../../useAppFullscreen";
|
||||
|
||||
const Header = ({
|
||||
logoutFunc,
|
||||
@ -32,16 +37,11 @@ const Header = ({
|
||||
myName,
|
||||
setSelectedDirect,
|
||||
setNewChat
|
||||
// selectedGroup,
|
||||
// onHomeClick,
|
||||
// onLogoutClick,
|
||||
// onGroupChange,
|
||||
// onWalletClick,
|
||||
// onNotificationClick,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const [fullScreen, setFullScreen] = useRecoilState(fullScreenAtom);
|
||||
const {exitFullScreen} = useAppFullScreen(setFullScreen)
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
@ -76,10 +76,10 @@ const Header = ({
|
||||
width: "75px",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="home"
|
||||
<ButtonBase
|
||||
|
||||
|
||||
|
||||
onClick={() => {
|
||||
setMobileViewModeKeepOpen("");
|
||||
goToHome();
|
||||
@ -87,15 +87,24 @@ const Header = ({
|
||||
// onClick={onHomeClick}
|
||||
>
|
||||
<HomeIcon height={20} width={27} color="rgba(145, 145, 147, 1)" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="home"
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
|
||||
onClick={handleClick}
|
||||
>
|
||||
<NotificationIcon height={20} width={21} color={hasUnreadDirects || hasUnreadGroups ? "var(--unread)" : "rgba(145, 145, 147, 1)"} />
|
||||
</IconButton>
|
||||
</ButtonBase>
|
||||
{fullScreen && (
|
||||
<ButtonBase onClick={()=> {
|
||||
exitFullScreen()
|
||||
setFullScreen(false)
|
||||
}}>
|
||||
<CloseFullscreenIcon sx={{
|
||||
color: 'rgba(145, 145, 147, 1)'
|
||||
}} />
|
||||
</ButtonBase>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
{/* Center Title */}
|
||||
@ -121,34 +130,25 @@ const Header = ({
|
||||
>
|
||||
{/* Right Logout Icon */}
|
||||
|
||||
<IconButton
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setMobileViewModeKeepOpen("messaging");
|
||||
}}
|
||||
edge="end"
|
||||
color="inherit"
|
||||
aria-label="logout"
|
||||
|
||||
// onClick={onLogoutClick}
|
||||
>
|
||||
<MessagingIcon2 height={20} color={hasUnreadDirects ? "var(--unread)" : "rgba(145, 145, 147, 1)"}
|
||||
|
||||
/>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
</ButtonBase>
|
||||
<Save />
|
||||
<ButtonBase
|
||||
onClick={logoutFunc}
|
||||
edge="end"
|
||||
color="inherit"
|
||||
aria-label="logout"
|
||||
|
||||
// onClick={onLogoutClick}
|
||||
>
|
||||
<LogoutIcon
|
||||
height={20}
|
||||
width={21}
|
||||
color="rgba(145, 145, 147, 1)"
|
||||
/>
|
||||
</IconButton>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
<Menu
|
||||
@ -203,7 +203,7 @@ const Header = ({
|
||||
"& .MuiTypography-root": {
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: hasUnreadDirects ? "var(--unread)" :"rgba(250, 250, 250, 0.5)"
|
||||
color: hasUnreadGroups ? "var(--unread)" :"rgba(250, 250, 250, 0.5)"
|
||||
},
|
||||
}} primary="Hubs" />
|
||||
</MenuItem>
|
||||
@ -247,16 +247,32 @@ const Header = ({
|
||||
}}
|
||||
>
|
||||
{/* Left Home Icon */}
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="home"
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "18px",
|
||||
width: "75px",
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
|
||||
onClick={goToHome}
|
||||
// onClick={onHomeClick}
|
||||
>
|
||||
<HomeIcon color="rgba(145, 145, 147, 1)" />
|
||||
</IconButton>
|
||||
|
||||
</ButtonBase>
|
||||
{fullScreen && (
|
||||
<ButtonBase onClick={()=> {
|
||||
exitFullScreen()
|
||||
setFullScreen(false)
|
||||
}}>
|
||||
<CloseFullscreenIcon sx={{
|
||||
color: 'rgba(145, 145, 147, 1)'
|
||||
}} />
|
||||
</ButtonBase>
|
||||
)}
|
||||
</Box>
|
||||
{/* Center Title */}
|
||||
<Typography
|
||||
variant="h6"
|
||||
@ -269,18 +285,26 @@ const Header = ({
|
||||
>
|
||||
QORTAL
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "30px",
|
||||
width: "75px",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
{/* Right Logout Icon */}
|
||||
<IconButton
|
||||
<Save />
|
||||
<ButtonBase
|
||||
onClick={logoutFunc}
|
||||
edge="end"
|
||||
color="inherit"
|
||||
aria-label="logout"
|
||||
|
||||
|
||||
// onClick={onLogoutClick}
|
||||
>
|
||||
<LogoutIcon color="rgba(145, 145, 147, 1)" />
|
||||
</IconButton>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
|
161
src/components/Save/Save.tsx
Normal file
@ -0,0 +1,161 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import isEqual from 'lodash/isEqual'; // Import deep comparison utility
|
||||
import { canSaveSettingToQdnAtom, hasSettingsChangedAtom, oldPinnedAppsAtom, settingsLocalLastUpdatedAtom, settingsQDNLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global';
|
||||
import { ButtonBase } from '@mui/material';
|
||||
import { objectToBase64 } from '../../qdn/encryption/group-encryption';
|
||||
import { MyContext } from '../../App';
|
||||
import { getFee } from '../../background';
|
||||
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
|
||||
import { SaveIcon } from '../../assets/svgs/SaveIcon';
|
||||
import { IconWrapper } from '../Desktop/DesktopFooter';
|
||||
export const Save = ({isDesktop}) => {
|
||||
const [pinnedApps, setPinnedApps] = useRecoilState(sortablePinnedAppsAtom);
|
||||
const [settingsQdnLastUpdated, setSettingsQdnLastUpdated] = useRecoilState(settingsQDNLastUpdatedAtom);
|
||||
const [settingsLocalLastUpdated] = useRecoilState(settingsLocalLastUpdatedAtom);
|
||||
const setHasSettingsChangedAtom = useSetRecoilState(hasSettingsChangedAtom);
|
||||
|
||||
const [canSave] = useRecoilState(canSaveSettingToQdnAtom);
|
||||
const [openSnack, setOpenSnack] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [infoSnack, setInfoSnack] = useState(null);
|
||||
const [oldPinnedApps, setOldPinnedApps] = useRecoilState(oldPinnedAppsAtom)
|
||||
|
||||
const { show } = useContext(MyContext);
|
||||
|
||||
const hasChanged = useMemo(()=> {
|
||||
const newChanges = {
|
||||
sortablePinnedApps: pinnedApps.map((item)=> {
|
||||
return {
|
||||
name: item?.name,
|
||||
service: item?.service
|
||||
}
|
||||
})
|
||||
}
|
||||
const oldChanges = {
|
||||
sortablePinnedApps: oldPinnedApps.map((item)=> {
|
||||
return {
|
||||
name: item?.name,
|
||||
service: item?.service
|
||||
}
|
||||
})
|
||||
}
|
||||
if(settingsQdnLastUpdated === -100) return false
|
||||
return !isEqual(oldChanges, newChanges) && settingsQdnLastUpdated < settingsLocalLastUpdated
|
||||
}, [oldPinnedApps, pinnedApps, settingsQdnLastUpdated, settingsLocalLastUpdated])
|
||||
|
||||
useEffect(()=> {
|
||||
setHasSettingsChangedAtom(hasChanged)
|
||||
}, [hasChanged])
|
||||
|
||||
const saveToQdn = async ()=> {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const data64 = await objectToBase64({
|
||||
sortablePinnedApps: pinnedApps.map((item)=> {
|
||||
return {
|
||||
name: item?.name,
|
||||
service: item?.service
|
||||
}
|
||||
})
|
||||
})
|
||||
const encryptData = await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "ENCRYPT_DATA",
|
||||
type: "qortalRequest",
|
||||
payload: {
|
||||
data64
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
if (response.error) {
|
||||
rej(response?.message);
|
||||
return;
|
||||
} else {
|
||||
res(response);
|
||||
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
if(encryptData && !encryptData?.error){
|
||||
const fee = await getFee('ARBITRARY')
|
||||
|
||||
await show({
|
||||
message: "Would you like to publish your settings to QDN (encrypted) ?" ,
|
||||
publishFee: fee.fee + ' QORT'
|
||||
})
|
||||
const response = await new Promise((res, rej) => {
|
||||
chrome?.runtime?.sendMessage(
|
||||
{
|
||||
action: "publishOnQDN",
|
||||
payload: {
|
||||
data: encryptData,
|
||||
identifier: "ext_saved_settings",
|
||||
service: 'DOCUMENT_PRIVATE'
|
||||
},
|
||||
},
|
||||
(response) => {
|
||||
|
||||
if (!response?.error) {
|
||||
res(response);
|
||||
return
|
||||
}
|
||||
rej(response.error);
|
||||
}
|
||||
);
|
||||
});
|
||||
if(response?.identifier){
|
||||
setOldPinnedApps(pinnedApps)
|
||||
setSettingsQdnLastUpdated(Date.now())
|
||||
setInfoSnack({
|
||||
type: "success",
|
||||
message:
|
||||
"Sucessfully published to QDN",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setInfoSnack({
|
||||
type: "error",
|
||||
message:
|
||||
error?.message || "Unable to save to QDN",
|
||||
});
|
||||
setOpenSnack(true);
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<ButtonBase onClick={saveToQdn} disabled={!hasChanged || !canSave || isLoading || settingsQdnLastUpdated === -100}>
|
||||
{isDesktop ? (
|
||||
<IconWrapper
|
||||
color="rgba(250, 250, 250, 0.5)"
|
||||
label="Save"
|
||||
selected={false}
|
||||
>
|
||||
<SaveIcon
|
||||
color={settingsQdnLastUpdated === -100 ? '#8F8F91' : (hasChanged && !isLoading) ? '#5EB049' : '#8F8F91'}
|
||||
/>
|
||||
</IconWrapper>
|
||||
) : (
|
||||
<SaveIcon
|
||||
color={settingsQdnLastUpdated === -100 ? '#8F8F91' : (hasChanged && !isLoading) ? '#5EB049' : '#8F8F91'}
|
||||
/>
|
||||
)}
|
||||
|
||||
</ButtonBase>
|
||||
<CustomizedSnackbars
|
||||
duration={3500}
|
||||
open={openSnack}
|
||||
setOpen={setOpenSnack}
|
||||
info={infoSnack}
|
||||
setInfo={setInfoSnack}
|
||||
/>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
@ -3,7 +3,7 @@ import Button from '@mui/material/Button';
|
||||
import Snackbar, { SnackbarCloseReason } from '@mui/material/Snackbar';
|
||||
import Alert from '@mui/material/Alert';
|
||||
|
||||
export const CustomizedSnackbars = ({open, setOpen, info, setInfo}) => {
|
||||
export const CustomizedSnackbars = ({open, setOpen, info, setInfo, duration}) => {
|
||||
|
||||
|
||||
|
||||
@ -19,9 +19,10 @@ export const CustomizedSnackbars = ({open, setOpen, info, setInfo}) => {
|
||||
setInfo(null)
|
||||
};
|
||||
|
||||
if(!open) return null
|
||||
return (
|
||||
<div>
|
||||
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={6000} onClose={handleClose}>
|
||||
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={duration || 6000} onClose={handleClose}>
|
||||
<Alert
|
||||
|
||||
|
||||
|
@ -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%;
|
||||
}
|
@ -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>,
|
||||
|
@ -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)
|
||||
}
|
@ -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
@ -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
73
src/transactions/CreatePollTransaction.ts
Normal 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
|
||||
}
|
||||
}
|
78
src/transactions/DeployAtTransaction.ts
Normal 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
|
||||
}
|
||||
}
|
38
src/transactions/VoteOnPollTransaction.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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
@ -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 };
|
||||
};
|
||||
|
||||
|
94
src/useQortalGetSaveSettings.tsx
Normal 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])
|
||||
|
||||
}
|
35
src/useRetrieveDataLocalStorage.tsx
Normal 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
@ -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",
|
||||
}
|